convex 1.34.0 → 1.34.1

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 (243) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser.bundle.js +6 -9
  3. package/dist/browser.bundle.js.map +2 -2
  4. package/dist/cjs/browser/sync/authentication_manager.js +4 -1
  5. package/dist/cjs/browser/sync/authentication_manager.js.map +2 -2
  6. package/dist/cjs/browser/sync/web_socket_manager.js +1 -7
  7. package/dist/cjs/browser/sync/web_socket_manager.js.map +2 -2
  8. package/dist/cjs/cli/aiFiles.js +15 -14
  9. package/dist/cjs/cli/aiFiles.js.map +2 -2
  10. package/dist/cjs/cli/configure.js +15 -10
  11. package/dist/cjs/cli/configure.js.map +2 -2
  12. package/dist/cjs/cli/lib/aiFiles/agentsmd.js +69 -0
  13. package/dist/cjs/cli/lib/aiFiles/agentsmd.js.map +7 -0
  14. package/dist/cjs/cli/lib/aiFiles/claudemd.js +69 -0
  15. package/dist/cjs/cli/lib/aiFiles/claudemd.js.map +7 -0
  16. package/dist/cjs/cli/lib/{ai → aiFiles}/config.js +73 -46
  17. package/dist/cjs/cli/lib/aiFiles/config.js.map +7 -0
  18. package/dist/cjs/cli/lib/aiFiles/cursorrules.js +48 -0
  19. package/dist/cjs/cli/lib/aiFiles/cursorrules.js.map +7 -0
  20. package/dist/cjs/cli/lib/aiFiles/guidelinesmd.js +51 -0
  21. package/dist/cjs/cli/lib/aiFiles/guidelinesmd.js.map +7 -0
  22. package/dist/cjs/cli/lib/aiFiles/index.js +231 -0
  23. package/dist/cjs/cli/lib/aiFiles/index.js.map +7 -0
  24. package/dist/cjs/cli/lib/aiFiles/paths.js.map +7 -0
  25. package/dist/cjs/cli/lib/aiFiles/skills.js +180 -0
  26. package/dist/cjs/cli/lib/aiFiles/skills.js.map +7 -0
  27. package/dist/cjs/cli/lib/aiFiles/status.js +195 -0
  28. package/dist/cjs/cli/lib/aiFiles/status.js.map +7 -0
  29. package/dist/cjs/cli/lib/aiFiles/utils.js +111 -0
  30. package/dist/cjs/cli/lib/aiFiles/utils.js.map +7 -0
  31. package/dist/cjs/cli/lib/command.js +6 -1
  32. package/dist/cjs/cli/lib/command.js.map +2 -2
  33. package/dist/cjs/cli/lib/config.js +3 -4
  34. package/dist/cjs/cli/lib/config.js.map +2 -2
  35. package/dist/cjs/cli/lib/localDeployment/anonymous.js +2 -2
  36. package/dist/cjs/cli/lib/localDeployment/anonymous.js.map +2 -2
  37. package/dist/cjs/cli/lib/updates.js +8 -8
  38. package/dist/cjs/cli/lib/updates.js.map +2 -2
  39. package/dist/cjs/cli/lib/versionApi.js +7 -4
  40. package/dist/cjs/cli/lib/versionApi.js.map +2 -2
  41. package/dist/cjs/cli/lib/workos/workos.js +4 -6
  42. package/dist/cjs/cli/lib/workos/workos.js.map +2 -2
  43. package/dist/cjs/index.js +1 -1
  44. package/dist/cjs/index.js.map +1 -1
  45. package/dist/cjs-types/browser/sync/authentication_manager.d.ts.map +1 -1
  46. package/dist/cjs-types/browser/sync/web_socket_manager.d.ts.map +1 -1
  47. package/dist/cjs-types/cli/aiFiles.d.ts.map +1 -1
  48. package/dist/cjs-types/cli/configure.d.ts.map +1 -1
  49. package/dist/cjs-types/cli/lib/aiFiles/agentsmd.d.ts +19 -0
  50. package/dist/cjs-types/cli/lib/aiFiles/agentsmd.d.ts.map +1 -0
  51. package/dist/cjs-types/cli/lib/aiFiles/agentsmd.test.d.ts +2 -0
  52. package/dist/cjs-types/cli/lib/aiFiles/agentsmd.test.d.ts.map +1 -0
  53. package/dist/cjs-types/cli/lib/aiFiles/claudemd.d.ts +19 -0
  54. package/dist/cjs-types/cli/lib/aiFiles/claudemd.d.ts.map +1 -0
  55. package/dist/cjs-types/cli/lib/aiFiles/claudemd.test.d.ts +2 -0
  56. package/dist/cjs-types/cli/lib/aiFiles/claudemd.test.d.ts.map +1 -0
  57. package/dist/cjs-types/cli/lib/aiFiles/config.d.ts +46 -0
  58. package/dist/cjs-types/cli/lib/aiFiles/config.d.ts.map +1 -0
  59. package/dist/cjs-types/cli/lib/aiFiles/config.test.d.ts.map +1 -0
  60. package/dist/cjs-types/cli/lib/aiFiles/cursorrules.d.ts +10 -0
  61. package/dist/cjs-types/cli/lib/aiFiles/cursorrules.d.ts.map +1 -0
  62. package/dist/cjs-types/cli/lib/aiFiles/guidelinesmd.d.ts +12 -0
  63. package/dist/cjs-types/cli/lib/aiFiles/guidelinesmd.d.ts.map +1 -0
  64. package/dist/cjs-types/cli/lib/aiFiles/guidelinesmd.test.d.ts +2 -0
  65. package/dist/cjs-types/cli/lib/aiFiles/guidelinesmd.test.d.ts.map +1 -0
  66. package/dist/cjs-types/cli/lib/aiFiles/index.d.ts +40 -0
  67. package/dist/cjs-types/cli/lib/aiFiles/index.d.ts.map +1 -0
  68. package/dist/cjs-types/cli/lib/aiFiles/index.test.d.ts.map +1 -0
  69. package/dist/cjs-types/cli/lib/aiFiles/integration.test.d.ts.map +1 -0
  70. package/dist/cjs-types/cli/lib/{ai → aiFiles}/paths.d.ts +4 -0
  71. package/dist/cjs-types/cli/lib/aiFiles/paths.d.ts.map +1 -0
  72. package/dist/cjs-types/cli/lib/aiFiles/prompt.test.d.ts.map +1 -0
  73. package/dist/cjs-types/cli/lib/aiFiles/skills.d.ts +18 -0
  74. package/dist/cjs-types/cli/lib/aiFiles/skills.d.ts.map +1 -0
  75. package/dist/cjs-types/cli/lib/aiFiles/status.d.ts +3 -0
  76. package/dist/cjs-types/cli/lib/aiFiles/status.d.ts.map +1 -0
  77. package/dist/cjs-types/cli/lib/aiFiles/utils.d.ts +46 -0
  78. package/dist/cjs-types/cli/lib/aiFiles/utils.d.ts.map +1 -0
  79. package/dist/cjs-types/cli/lib/config.d.ts +1 -0
  80. package/dist/cjs-types/cli/lib/config.d.ts.map +1 -1
  81. package/dist/cjs-types/cli/lib/versionApi.d.ts +7 -1
  82. package/dist/cjs-types/cli/lib/versionApi.d.ts.map +1 -1
  83. package/dist/cjs-types/cli/lib/workos/workos.d.ts.map +1 -1
  84. package/dist/cjs-types/index.d.ts +1 -1
  85. package/dist/cli.bundle.cjs +1605 -1548
  86. package/dist/cli.bundle.cjs.map +4 -4
  87. package/dist/esm/browser/sync/authentication_manager.js +4 -1
  88. package/dist/esm/browser/sync/authentication_manager.js.map +2 -2
  89. package/dist/esm/browser/sync/web_socket_manager.js +1 -7
  90. package/dist/esm/browser/sync/web_socket_manager.js.map +2 -2
  91. package/dist/esm/cli/aiFiles.js +17 -17
  92. package/dist/esm/cli/aiFiles.js.map +2 -2
  93. package/dist/esm/cli/configure.js +15 -10
  94. package/dist/esm/cli/configure.js.map +2 -2
  95. package/dist/esm/cli/lib/aiFiles/agentsmd.js +52 -0
  96. package/dist/esm/cli/lib/aiFiles/agentsmd.js.map +7 -0
  97. package/dist/esm/cli/lib/aiFiles/claudemd.js +52 -0
  98. package/dist/esm/cli/lib/aiFiles/claudemd.js.map +7 -0
  99. package/dist/esm/cli/lib/{ai → aiFiles}/config.js +71 -45
  100. package/dist/esm/cli/lib/aiFiles/config.js.map +7 -0
  101. package/dist/esm/cli/lib/aiFiles/cursorrules.js +16 -0
  102. package/dist/esm/cli/lib/aiFiles/cursorrules.js.map +7 -0
  103. package/dist/esm/cli/lib/aiFiles/guidelinesmd.js +28 -0
  104. package/dist/esm/cli/lib/aiFiles/guidelinesmd.js.map +7 -0
  105. package/dist/esm/cli/lib/aiFiles/index.js +210 -0
  106. package/dist/esm/cli/lib/aiFiles/index.js.map +7 -0
  107. package/dist/esm/cli/lib/aiFiles/paths.js.map +7 -0
  108. package/dist/esm/cli/lib/aiFiles/skills.js +147 -0
  109. package/dist/esm/cli/lib/aiFiles/skills.js.map +7 -0
  110. package/dist/esm/cli/lib/aiFiles/status.js +175 -0
  111. package/dist/esm/cli/lib/aiFiles/status.js.map +7 -0
  112. package/dist/esm/cli/lib/aiFiles/utils.js +82 -0
  113. package/dist/esm/cli/lib/aiFiles/utils.js.map +7 -0
  114. package/dist/esm/cli/lib/command.js +6 -1
  115. package/dist/esm/cli/lib/command.js.map +2 -2
  116. package/dist/esm/cli/lib/config.js +3 -4
  117. package/dist/esm/cli/lib/config.js.map +2 -2
  118. package/dist/esm/cli/lib/localDeployment/anonymous.js +2 -2
  119. package/dist/esm/cli/lib/localDeployment/anonymous.js.map +2 -2
  120. package/dist/esm/cli/lib/updates.js +8 -8
  121. package/dist/esm/cli/lib/updates.js.map +2 -2
  122. package/dist/esm/cli/lib/versionApi.js +7 -4
  123. package/dist/esm/cli/lib/versionApi.js.map +2 -2
  124. package/dist/esm/cli/lib/workos/workos.js +4 -6
  125. package/dist/esm/cli/lib/workos/workos.js.map +2 -2
  126. package/dist/esm/index.js +1 -1
  127. package/dist/esm/index.js.map +1 -1
  128. package/dist/esm-types/browser/sync/authentication_manager.d.ts.map +1 -1
  129. package/dist/esm-types/browser/sync/web_socket_manager.d.ts.map +1 -1
  130. package/dist/esm-types/cli/aiFiles.d.ts.map +1 -1
  131. package/dist/esm-types/cli/configure.d.ts.map +1 -1
  132. package/dist/esm-types/cli/lib/aiFiles/agentsmd.d.ts +19 -0
  133. package/dist/esm-types/cli/lib/aiFiles/agentsmd.d.ts.map +1 -0
  134. package/dist/esm-types/cli/lib/aiFiles/agentsmd.test.d.ts +2 -0
  135. package/dist/esm-types/cli/lib/aiFiles/agentsmd.test.d.ts.map +1 -0
  136. package/dist/esm-types/cli/lib/aiFiles/claudemd.d.ts +19 -0
  137. package/dist/esm-types/cli/lib/aiFiles/claudemd.d.ts.map +1 -0
  138. package/dist/esm-types/cli/lib/aiFiles/claudemd.test.d.ts +2 -0
  139. package/dist/esm-types/cli/lib/aiFiles/claudemd.test.d.ts.map +1 -0
  140. package/dist/esm-types/cli/lib/aiFiles/config.d.ts +46 -0
  141. package/dist/esm-types/cli/lib/aiFiles/config.d.ts.map +1 -0
  142. package/dist/esm-types/cli/lib/aiFiles/config.test.d.ts.map +1 -0
  143. package/dist/esm-types/cli/lib/aiFiles/cursorrules.d.ts +10 -0
  144. package/dist/esm-types/cli/lib/aiFiles/cursorrules.d.ts.map +1 -0
  145. package/dist/esm-types/cli/lib/aiFiles/guidelinesmd.d.ts +12 -0
  146. package/dist/esm-types/cli/lib/aiFiles/guidelinesmd.d.ts.map +1 -0
  147. package/dist/esm-types/cli/lib/aiFiles/guidelinesmd.test.d.ts +2 -0
  148. package/dist/esm-types/cli/lib/aiFiles/guidelinesmd.test.d.ts.map +1 -0
  149. package/dist/esm-types/cli/lib/aiFiles/index.d.ts +40 -0
  150. package/dist/esm-types/cli/lib/aiFiles/index.d.ts.map +1 -0
  151. package/dist/esm-types/cli/lib/aiFiles/index.test.d.ts.map +1 -0
  152. package/dist/esm-types/cli/lib/aiFiles/integration.test.d.ts.map +1 -0
  153. package/dist/esm-types/cli/lib/{ai → aiFiles}/paths.d.ts +4 -0
  154. package/dist/esm-types/cli/lib/aiFiles/paths.d.ts.map +1 -0
  155. package/dist/esm-types/cli/lib/aiFiles/prompt.test.d.ts.map +1 -0
  156. package/dist/esm-types/cli/lib/aiFiles/skills.d.ts +18 -0
  157. package/dist/esm-types/cli/lib/aiFiles/skills.d.ts.map +1 -0
  158. package/dist/esm-types/cli/lib/aiFiles/status.d.ts +3 -0
  159. package/dist/esm-types/cli/lib/aiFiles/status.d.ts.map +1 -0
  160. package/dist/esm-types/cli/lib/aiFiles/utils.d.ts +46 -0
  161. package/dist/esm-types/cli/lib/aiFiles/utils.d.ts.map +1 -0
  162. package/dist/esm-types/cli/lib/config.d.ts +1 -0
  163. package/dist/esm-types/cli/lib/config.d.ts.map +1 -1
  164. package/dist/esm-types/cli/lib/versionApi.d.ts +7 -1
  165. package/dist/esm-types/cli/lib/versionApi.d.ts.map +1 -1
  166. package/dist/esm-types/cli/lib/workos/workos.d.ts.map +1 -1
  167. package/dist/esm-types/index.d.ts +1 -1
  168. package/dist/react.bundle.js +6 -9
  169. package/dist/react.bundle.js.map +2 -2
  170. package/package.json +1 -1
  171. package/schemas/convex.schema.json +7 -1
  172. package/src/browser/sync/authentication_manager.ts +9 -4
  173. package/src/browser/sync/client_node.test.ts +125 -0
  174. package/src/browser/sync/web_socket_manager.ts +1 -7
  175. package/src/cli/aiFiles.ts +20 -27
  176. package/src/cli/configure.ts +17 -11
  177. package/src/cli/deploymentSelection.test.ts +56 -2
  178. package/src/cli/lib/{ai → aiFiles}/MANUAL_TESTING.md +6 -2
  179. package/src/cli/lib/aiFiles/agentsmd.test.ts +133 -0
  180. package/src/cli/lib/aiFiles/agentsmd.ts +77 -0
  181. package/src/cli/lib/aiFiles/claudemd.test.ts +92 -0
  182. package/src/cli/lib/aiFiles/claudemd.ts +77 -0
  183. package/src/cli/lib/{ai → aiFiles}/config.test.ts +181 -59
  184. package/src/cli/lib/{ai → aiFiles}/config.ts +92 -63
  185. package/src/cli/lib/aiFiles/cursorrules.ts +25 -0
  186. package/src/cli/lib/aiFiles/guidelinesmd.test.ts +40 -0
  187. package/src/cli/lib/aiFiles/guidelinesmd.ts +41 -0
  188. package/src/cli/lib/{ai → aiFiles}/index.test.ts +200 -339
  189. package/src/cli/lib/aiFiles/index.ts +303 -0
  190. package/src/cli/lib/{ai → aiFiles}/integration.test.ts +117 -147
  191. package/src/cli/lib/{ai → aiFiles}/paths.ts +5 -0
  192. package/src/cli/lib/{ai → aiFiles}/prompt.test.ts +78 -30
  193. package/src/cli/lib/aiFiles/skills.ts +213 -0
  194. package/src/cli/lib/aiFiles/status.ts +240 -0
  195. package/src/cli/lib/aiFiles/utils.ts +163 -0
  196. package/src/cli/lib/command.ts +6 -1
  197. package/src/cli/lib/config.test.ts +1 -1
  198. package/src/cli/lib/config.ts +6 -5
  199. package/src/cli/lib/localDeployment/anonymous.ts +2 -2
  200. package/src/cli/lib/updates.test.ts +40 -30
  201. package/src/cli/lib/updates.ts +8 -8
  202. package/src/cli/lib/versionApi.test.ts +13 -10
  203. package/src/cli/lib/versionApi.ts +13 -5
  204. package/src/cli/lib/workos/workos.ts +4 -5
  205. package/src/index.ts +1 -1
  206. package/src/values/.claude/settings.local.json +10 -0
  207. package/dist/cjs/cli/lib/ai/config.js.map +0 -7
  208. package/dist/cjs/cli/lib/ai/index.js +0 -704
  209. package/dist/cjs/cli/lib/ai/index.js.map +0 -7
  210. package/dist/cjs/cli/lib/ai/paths.js.map +0 -7
  211. package/dist/cjs-types/cli/lib/ai/config.d.ts +0 -50
  212. package/dist/cjs-types/cli/lib/ai/config.d.ts.map +0 -1
  213. package/dist/cjs-types/cli/lib/ai/config.test.d.ts.map +0 -1
  214. package/dist/cjs-types/cli/lib/ai/index.d.ts +0 -56
  215. package/dist/cjs-types/cli/lib/ai/index.d.ts.map +0 -1
  216. package/dist/cjs-types/cli/lib/ai/index.test.d.ts.map +0 -1
  217. package/dist/cjs-types/cli/lib/ai/integration.test.d.ts.map +0 -1
  218. package/dist/cjs-types/cli/lib/ai/paths.d.ts.map +0 -1
  219. package/dist/cjs-types/cli/lib/ai/prompt.test.d.ts.map +0 -1
  220. package/dist/esm/cli/lib/ai/config.js.map +0 -7
  221. package/dist/esm/cli/lib/ai/index.js +0 -684
  222. package/dist/esm/cli/lib/ai/index.js.map +0 -7
  223. package/dist/esm/cli/lib/ai/paths.js.map +0 -7
  224. package/dist/esm-types/cli/lib/ai/config.d.ts +0 -50
  225. package/dist/esm-types/cli/lib/ai/config.d.ts.map +0 -1
  226. package/dist/esm-types/cli/lib/ai/config.test.d.ts.map +0 -1
  227. package/dist/esm-types/cli/lib/ai/index.d.ts +0 -56
  228. package/dist/esm-types/cli/lib/ai/index.d.ts.map +0 -1
  229. package/dist/esm-types/cli/lib/ai/index.test.d.ts.map +0 -1
  230. package/dist/esm-types/cli/lib/ai/integration.test.d.ts.map +0 -1
  231. package/dist/esm-types/cli/lib/ai/paths.d.ts.map +0 -1
  232. package/dist/esm-types/cli/lib/ai/prompt.test.d.ts.map +0 -1
  233. package/src/cli/lib/ai/index.ts +0 -1006
  234. /package/dist/cjs/cli/lib/{ai → aiFiles}/paths.js +0 -0
  235. /package/dist/cjs-types/cli/lib/{ai → aiFiles}/config.test.d.ts +0 -0
  236. /package/dist/cjs-types/cli/lib/{ai → aiFiles}/index.test.d.ts +0 -0
  237. /package/dist/cjs-types/cli/lib/{ai → aiFiles}/integration.test.d.ts +0 -0
  238. /package/dist/cjs-types/cli/lib/{ai → aiFiles}/prompt.test.d.ts +0 -0
  239. /package/dist/esm/cli/lib/{ai → aiFiles}/paths.js +0 -0
  240. /package/dist/esm-types/cli/lib/{ai → aiFiles}/config.test.d.ts +0 -0
  241. /package/dist/esm-types/cli/lib/{ai → aiFiles}/index.test.d.ts +0 -0
  242. /package/dist/esm-types/cli/lib/{ai → aiFiles}/integration.test.d.ts +0 -0
  243. /package/dist/esm-types/cli/lib/{ai → aiFiles}/prompt.test.d.ts +0 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "convex",
3
3
  "description": "Client for the Convex Cloud",
4
- "version": "1.34.0",
4
+ "version": "1.34.1",
5
5
  "author": "Convex, Inc. <no-reply@convex.dev>",
6
6
  "homepage": "https://convex.dev",
7
7
  "repository": {
@@ -189,9 +189,15 @@
189
189
  "type": "object",
190
190
  "description": "User-configurable settings for Convex AI files behavior.",
191
191
  "properties": {
192
+ "enabled": {
193
+ "type": "boolean",
194
+ "description": "When false, disables all Convex AI files prompts and staleness messages in `npx convex dev`. Use `npx convex ai-files enable` to re-enable.",
195
+ "default": true
196
+ },
192
197
  "disableStalenessMessage": {
193
198
  "type": "boolean",
194
- "description": "When true, suppresses Convex AI files install/staleness nags in `npx convex dev`.",
199
+ "description": "Deprecated. Use `enabled` instead.",
200
+ "deprecated": true,
195
201
  "default": false
196
202
  }
197
203
  },
@@ -174,6 +174,14 @@ export class AuthenticationManager {
174
174
  return;
175
175
  }
176
176
 
177
+ this._logVerbose(
178
+ `auth state is ${this.authState.state} when handling transition`,
179
+ );
180
+
181
+ // This transition advanced the auth version, which means the token used was valid
182
+ // and the client and server auth states are in sync.
183
+ this.syncState.markAuthCompletion();
184
+
177
185
  if (this.authState.state === "waitingForServerConfirmationOfCachedToken") {
178
186
  this._logVerbose("server confirmed auth token is valid");
179
187
  void this.refetchToken();
@@ -455,11 +463,8 @@ export class AuthenticationManager {
455
463
  }
456
464
  }
457
465
  if (this.authState.state === "waitingForScheduledRefetch") {
466
+ // TODO: this side-effect would be better situated with scheduling refetch
458
467
  clearTimeout(this.authState.refetchTokenTimeoutId);
459
-
460
- // The waitingForScheduledRefetch state is the most quiesced authed state.
461
- // Let the syncState know that auth is in a good state, so it can reset failure backoffs
462
- this.syncState.markAuthCompletion();
463
468
  }
464
469
  this.authState = newAuth;
465
470
  }
@@ -2,6 +2,7 @@ import child_process from "child_process";
2
2
 
3
3
  import { test, expect } from "vitest";
4
4
  import { Long } from "../../vendor/long.js";
5
+ import { createHmac } from "crypto";
5
6
 
6
7
  import { BaseConvexClient } from "./client.js";
7
8
  import {
@@ -1026,3 +1027,127 @@ test("Backoff does not reset for idle client until server re-confirms queries",
1026
1027
  await client.close();
1027
1028
  });
1028
1029
  });
1030
+
1031
+ // We had an issue where fixing a bug related to having the client do proper exponential backoff
1032
+ // resulted in revealing that if a client was authenticated and had a WebSocket disconnect, the
1033
+ // count of retries would never get reset upon reconnect. This test covers that scenario.
1034
+ test("Retries do not increase across connections for auth'd clients", async () => {
1035
+ await withInMemoryWebSocket(async ({ address, receive, send, close }) => {
1036
+ const client = new BaseConvexClient(address, () => null, {
1037
+ webSocketConstructor: nodeWebSocket,
1038
+ unsavedChangesWarning: false,
1039
+ logger: true,
1040
+ verbose: true,
1041
+ });
1042
+
1043
+ // Setup auth
1044
+ const iat = Math.floor(Date.now() / 1000);
1045
+ const exp = iat + 3600;
1046
+ const token = encodeJwt({ sub: "user_123", exp, iat }, "test-secret");
1047
+ client.setAuth(
1048
+ async () => token,
1049
+ (_) => {},
1050
+ );
1051
+
1052
+ // Verify client handshake on "server" side.
1053
+ expect((await receive()).type).toEqual("Connect");
1054
+ expect((await receive()).type).toEqual("Authenticate");
1055
+ expect((await receive()).type).toEqual("ModifyQuerySet");
1056
+
1057
+ // Respond to the handshake noting that the identity was accepted (increased version).
1058
+ send({
1059
+ type: "Transition",
1060
+ startVersion: { querySet: 0, identity: 0, ts: Long.fromNumber(0) },
1061
+ endVersion: { querySet: 0, identity: 1, ts: Long.fromNumber(100) },
1062
+ modifications: [],
1063
+ });
1064
+
1065
+ // Subscribe to a query and verify the "server" receives it.
1066
+ client.subscribe("queries:myQuery", {});
1067
+ expect((await receive()).type).toEqual("ModifyQuerySet");
1068
+
1069
+ // Respond to the subscription with a result.
1070
+ send({
1071
+ type: "Transition",
1072
+ startVersion: { querySet: 0, identity: 1, ts: Long.fromNumber(100) },
1073
+ endVersion: { querySet: 1, identity: 1, ts: Long.fromNumber(200) },
1074
+ modifications: [
1075
+ {
1076
+ type: "QueryUpdated",
1077
+ queryId: 0,
1078
+ value: "result",
1079
+ logLines: [],
1080
+ journal: null,
1081
+ },
1082
+ ],
1083
+ });
1084
+
1085
+ // Verify the client gets the subscription result.
1086
+ for (let i = 0; i < 20; i++) {
1087
+ if (client.localQueryResult("queries:myQuery", {}) === "result") break;
1088
+ await new Promise((r) => setTimeout(r, 50));
1089
+ }
1090
+ expect(client.localQueryResult("queries:myQuery", {})).toEqual("result");
1091
+
1092
+ // Unceremoniously drop the connection.
1093
+ close();
1094
+
1095
+ // The "server" should see the client handshake for a reconnect.
1096
+ expect((await receive()).type).toEqual("Connect");
1097
+ expect((await receive()).type).toEqual("Authenticate");
1098
+ expect((await receive()).type).toEqual("ModifyQuerySet");
1099
+
1100
+ // The client should record that it has begun a retry.
1101
+ expect(client.connectionState().connectionRetries).toBe(1);
1102
+
1103
+ // Respond to the handshake noting that the identity was accepted (increased version).
1104
+ send({
1105
+ type: "Transition",
1106
+ startVersion: { querySet: 0, identity: 0, ts: Long.fromNumber(0) },
1107
+ endVersion: { querySet: 0, identity: 1, ts: Long.fromNumber(100) },
1108
+ modifications: [],
1109
+ });
1110
+ // Let client handle the transition.
1111
+ await new Promise((r) => setTimeout(r, 100));
1112
+
1113
+ // Respond to the re-subscription with a result.
1114
+ send({
1115
+ type: "Transition",
1116
+ startVersion: { querySet: 0, identity: 1, ts: Long.fromNumber(100) },
1117
+ endVersion: { querySet: 1, identity: 1, ts: Long.fromNumber(200) },
1118
+ modifications: [
1119
+ {
1120
+ type: "QueryUpdated",
1121
+ queryId: 0,
1122
+ value: "updated result",
1123
+ logLines: [],
1124
+ journal: null,
1125
+ },
1126
+ ],
1127
+ });
1128
+
1129
+ for (let i = 0; i < 20; i++) {
1130
+ if (client.localQueryResult("queries:myQuery", {}) === "updated result")
1131
+ break;
1132
+ await new Promise((r) => setTimeout(r, 50));
1133
+ }
1134
+ // Verify the client gets the subscription result.
1135
+ expect(client.localQueryResult("queries:myQuery", {})).toEqual(
1136
+ "updated result",
1137
+ );
1138
+
1139
+ // Client has resynced - should be at 0 retries
1140
+ expect(client.connectionState().connectionRetries).toBe(0);
1141
+ });
1142
+ });
1143
+
1144
+ function encodeJwt(payload: object, secret: string): string {
1145
+ const header = Buffer.from(
1146
+ JSON.stringify({ alg: "HS256", typ: "JWT" }),
1147
+ ).toString("base64url");
1148
+ const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
1149
+ const sig = createHmac("sha256", secret)
1150
+ .update(`${header}.${body}`)
1151
+ .digest("base64url");
1152
+ return `${header}.${body}.${sig}`;
1153
+ }
@@ -126,12 +126,6 @@ const serverDisconnectErrors = {
126
126
  VectorIndexesUnavailable: { timeout: 1000 },
127
127
  SearchIndexesUnavailable: { timeout: 1000 },
128
128
  TableSummariesUnavailable: { timeout: 1000 },
129
- // ErrorMetadata::service_unavailable() when backend/conductor is unreachable
130
- ServiceUnavailable: { timeout: 3000 },
131
- // ErrorMetadata::rejected_before_execution() when funrun workers are unavailable
132
- WorkerOverloaded: { timeout: 3000 },
133
- IsolateNotClean: { timeout: 3000 },
134
- InitialPermitTimeoutError: { timeout: 3000 },
135
129
  // More ErrorMetadata::overloaded()
136
130
  VectorIndexTooLarge: { timeout: 3000 },
137
131
  SearchIndexTooLarge: { timeout: 3000 },
@@ -235,7 +229,7 @@ export class WebSocketManager {
235
229
 
236
230
  // backoff for unknown errors
237
231
  this.defaultInitialBackoff = 1000;
238
- this.maxBackoff = 64000;
232
+ this.maxBackoff = 16000;
239
233
  this.retries = 0;
240
234
 
241
235
  // Ping messages (sync protocol Pings, not WebSocket protocol Pings) are
@@ -4,12 +4,12 @@ import { oneoffContext } from "../bundler/context.js";
4
4
  import { readProjectConfig } from "./lib/config.js";
5
5
  import { functionsDir } from "./lib/utils/utils.js";
6
6
  import {
7
- updateAiFiles,
7
+ installAiFiles,
8
8
  enableAiFiles,
9
9
  removeAiFiles,
10
- disableAiFiles,
11
- statusAiFiles,
12
- } from "./lib/ai/index.js";
10
+ safelyAttemptToDisableAiFiles,
11
+ } from "./lib/aiFiles/index.js";
12
+ import { statusAiFiles } from "./lib/aiFiles/status.js";
13
13
 
14
14
  async function resolveProjectPaths() {
15
15
  const ctx = await oneoffContext({});
@@ -26,53 +26,46 @@ const aiInstall = new Command("install")
26
26
  " - convex/_generated/ai/guidelines.md\n" +
27
27
  " - AGENTS.md (Convex section only)\n" +
28
28
  " - CLAUDE.md (Convex section only)\n" +
29
- " - Agent skills (installed to each coding agent's native path)\n\n" +
30
- "Files you have edited yourself are detected via content hashing and skipped\n" +
31
- "rather than silently overwritten.",
29
+ " - Agent skills (installed to each coding agent's native path)",
32
30
  )
33
31
  .allowExcessArguments(false)
34
32
  .action(async () => {
35
33
  const { projectDir, convexDir } = await resolveProjectPaths();
36
- await updateAiFiles(projectDir, convexDir);
34
+ await installAiFiles({ projectDir, convexDir });
37
35
  });
38
36
 
39
37
  const aiEnable = new Command("enable")
40
- .summary("Enable Convex AI file staleness/install messages")
38
+ .summary("Enable Convex AI files")
41
39
  .description(
42
- "Re-enables AI file staleness/install messages by writing\n" +
43
- "`aiFiles.disableStalenessMessage: false` to `convex.json`, then installs\n" +
44
- "or refreshes the managed AI files.",
40
+ "Re-enables Convex AI files by writing `aiFiles.enabled: true` to\n" +
41
+ "`convex.json`, then installs or refreshes the managed AI files.",
45
42
  )
46
43
  .allowExcessArguments(false)
47
44
  .action(async () => {
48
45
  const { projectDir, convexDir } = await resolveProjectPaths();
49
- await enableAiFiles(projectDir, convexDir);
46
+ await enableAiFiles({ projectDir, convexDir });
50
47
  });
51
48
 
52
49
  const aiUpdate = new Command("update")
53
50
  .summary("Update Convex AI files to the latest version")
54
51
  .description(
55
- "Updates the following files if they exist and have not been locally modified:\n" +
52
+ "Updates the following to their latest versions:\n" +
56
53
  " - convex/_generated/ai/guidelines.md\n" +
57
54
  " - AGENTS.md (Convex section only)\n" +
58
55
  " - CLAUDE.md (Convex section only)\n" +
59
- " - Agent skills (installed to each coding agent's native path)\n\n" +
60
- "Files you have edited yourself are detected via content hashing and skipped\n" +
61
- "rather than silently overwritten.\n\n" +
62
- "Does not change `aiFiles.disableStalenessMessage` in `convex.json`.\n" +
63
- "Run `npx convex ai-files enable` to enable staleness/install messages.",
56
+ " - Agent skills (installed to each coding agent's native path)\n\n",
64
57
  )
65
58
  .allowExcessArguments(false)
66
59
  .action(async () => {
67
60
  const { projectDir, convexDir } = await resolveProjectPaths();
68
- await updateAiFiles(projectDir, convexDir);
61
+ await installAiFiles({ projectDir, convexDir });
69
62
  });
70
63
 
71
64
  const aiDisable = new Command("disable")
72
- .summary("Suppress AI file staleness/install messages without removing files")
65
+ .summary("Disable Convex AI files without removing them")
73
66
  .description(
74
- "Writes a suppression marker into `convex.json` (`aiFiles.disableStalenessMessage`) so\n" +
75
- "`npx convex dev` stops suggesting you install AI files.\n\n" +
67
+ "Writes `aiFiles.enabled: false` to `convex.json` so `npx convex dev`\n" +
68
+ "stops prompting to install AI files and suppresses staleness messages.\n\n" +
76
69
  "Files already installed are left untouched - use `npx convex ai-files remove`\n" +
77
70
  "if you also want to delete them.\n\n" +
78
71
  "Run `npx convex ai-files enable` to re-enable at any time.",
@@ -80,7 +73,7 @@ const aiDisable = new Command("disable")
80
73
  .allowExcessArguments(false)
81
74
  .action(async () => {
82
75
  const { projectDir } = await resolveProjectPaths();
83
- await disableAiFiles(projectDir);
76
+ await safelyAttemptToDisableAiFiles(projectDir);
84
77
  });
85
78
 
86
79
  const aiStatus = new Command("status")
@@ -98,7 +91,7 @@ const aiStatus = new Command("status")
98
91
  .allowExcessArguments(false)
99
92
  .action(async () => {
100
93
  const { projectDir, convexDir } = await resolveProjectPaths();
101
- await statusAiFiles(projectDir, convexDir);
94
+ await statusAiFiles({ projectDir, convexDir });
102
95
  });
103
96
 
104
97
  const aiRemove = new Command("remove")
@@ -112,12 +105,12 @@ const aiRemove = new Command("remove")
112
105
  "empty file is deleted. Otherwise the rest of the file is kept.\n\n" +
113
106
  "Skills installed from other sources are not affected.\n\n" +
114
107
  "Note: after `remove`, `npx convex dev` will suggest reinstalling AI files.\n" +
115
- "Use `npx convex ai-files disable` to suppress that prompt without deleting files.",
108
+ "Use `npx convex ai-files disable` to opt out entirely without deleting files.",
116
109
  )
117
110
  .allowExcessArguments(false)
118
111
  .action(async () => {
119
112
  const { projectDir, convexDir } = await resolveProjectPaths();
120
- await removeAiFiles(projectDir, convexDir);
113
+ await removeAiFiles({ projectDir, convexDir });
121
114
  });
122
115
 
123
116
  export const aiFiles = new Command("ai-files")
@@ -44,7 +44,7 @@ import {
44
44
  promptYesNo,
45
45
  } from "./lib/utils/prompts.js";
46
46
  import { readGlobalConfig } from "./lib/utils/globalConfig.js";
47
- import { maybeSetupAiFiles } from "./lib/ai/index.js";
47
+ import { maybeSetupAiFiles } from "./lib/aiFiles/index.js";
48
48
  import {
49
49
  DeploymentSelection,
50
50
  deploymentNameFromSelection,
@@ -382,14 +382,20 @@ async function handleDeploymentWithinProject(
382
382
  selectedDeployment.deploymentFields !== null &&
383
383
  selectedDeployment.deploymentFields.deploymentType === "local"
384
384
  ) {
385
- // Start running the local backend
386
- await handleLocalDeployment(ctx, {
385
+ // Start running the local backend, which may bind to different ports
386
+ // than what was saved from a previous run.
387
+ const localDeployment = await handleLocalDeployment(ctx, {
387
388
  teamSlug: selectedDeployment.deploymentFields.teamSlug!,
388
389
  projectSlug: selectedDeployment.deploymentFields.projectSlug!,
389
390
  forceUpgrade: cmdOptions.localOptions.forceUpgrade,
390
391
  ports: cmdOptions.localOptions.ports,
391
392
  backendVersion: cmdOptions.localOptions.backendVersion,
392
393
  });
394
+ return {
395
+ url: localDeployment.deploymentUrl,
396
+ adminKey: localDeployment.adminKey,
397
+ deploymentFields: selectedDeployment.deploymentFields,
398
+ };
393
399
  }
394
400
  return {
395
401
  url: selectedDeployment.url,
@@ -619,11 +625,11 @@ async function selectNewProject(
619
625
  await doInitConvexFolder(ctx);
620
626
  const { configPath, projectConfig } = await readProjectConfig(ctx);
621
627
  const folder = functionsDir(configPath, projectConfig);
622
- await maybeSetupAiFiles(
628
+ await maybeSetupAiFiles({
623
629
  ctx,
624
- path.resolve(folder),
625
- path.resolve(path.dirname(configPath)),
626
- );
630
+ convexDir: path.resolve(folder),
631
+ projectDir: path.resolve(path.dirname(configPath)),
632
+ });
627
633
  return { teamSlug, projectSlug, devDeployment };
628
634
  }
629
635
 
@@ -679,11 +685,11 @@ async function selectExistingProject(
679
685
 
680
686
  const { configPath, projectConfig } = await readProjectConfig(ctx);
681
687
  const folder = functionsDir(configPath, projectConfig);
682
- await maybeSetupAiFiles(
688
+ await maybeSetupAiFiles({
683
689
  ctx,
684
- path.resolve(folder),
685
- path.resolve(path.dirname(configPath)),
686
- );
690
+ convexDir: path.resolve(folder),
691
+ projectDir: path.resolve(path.dirname(configPath)),
692
+ });
687
693
 
688
694
  return { teamSlug, projectSlug, devDeployment };
689
695
  }
@@ -161,8 +161,9 @@ vi.mock("./lib/login.js", async (importOriginal) => {
161
161
  return { ...actual, ensureLoggedIn: vi.fn() };
162
162
  });
163
163
 
164
- vi.mock("./lib/ai/index.js", async (importOriginal) => {
165
- const actual = await importOriginal<typeof import("./lib/ai/index.js")>();
164
+ vi.mock("./lib/aiFiles/index.js", async (importOriginal) => {
165
+ const actual =
166
+ await importOriginal<typeof import("./lib/aiFiles/index.js")>();
166
167
  return { ...actual, maybeSetupAiFiles: vi.fn() };
167
168
  });
168
169
 
@@ -1555,6 +1556,59 @@ describe("deployment selection flows", () => {
1555
1556
  expect(bigBrainAPIMaybeThrows).not.toHaveBeenCalled();
1556
1557
  });
1557
1558
 
1559
+ it("dev with CONVEX_DEPLOYMENT=local:... uses fresh credentials from handleLocalDeployment", async () => {
1560
+ process.env.CONVEX_DEPLOYMENT = "local:my-local-deployment";
1561
+ vi.mocked(readGlobalConfig).mockReturnValue({
1562
+ accessToken: "test-token",
1563
+ });
1564
+
1565
+ // loadLocalDeploymentCredentials returns stale saved config (e.g. from a
1566
+ // previous run on a different port).
1567
+ vi.mocked(loadLocalDeploymentCredentials).mockResolvedValue({
1568
+ deploymentName: "my-local-deployment",
1569
+ deploymentUrl: "http://127.0.0.1:3212",
1570
+ adminKey: "stale|admin|key",
1571
+ });
1572
+
1573
+ // handleLocalDeployment starts a new backend, potentially on different
1574
+ // ports, and returns the actual credentials.
1575
+ vi.mocked(handleLocalDeployment).mockResolvedValue({
1576
+ deploymentName: "my-local-deployment",
1577
+ deploymentUrl: "http://127.0.0.1:3210",
1578
+ adminKey: "fresh|admin|key",
1579
+ onActivity: async () => {},
1580
+ } as any);
1581
+
1582
+ setupBigBrainRoutes({
1583
+ "deployment/my-local-deployment/team_and_project": () => ({
1584
+ team: "my-team",
1585
+ project: "my-project",
1586
+ teamId: 1,
1587
+ projectId: 1,
1588
+ }),
1589
+ });
1590
+
1591
+ await dev.parseAsync([], { from: "user" });
1592
+
1593
+ expect(handleLocalDeployment).toHaveBeenCalledWith(
1594
+ expect.anything(),
1595
+ expect.objectContaining({
1596
+ teamSlug: "my-team",
1597
+ projectSlug: "my-project",
1598
+ }),
1599
+ );
1600
+ // Must use the fresh credentials from handleLocalDeployment, not the
1601
+ // stale ones from loadLocalDeploymentCredentials.
1602
+ expect(devAgainstDeployment).toHaveBeenCalledWith(
1603
+ expect.anything(),
1604
+ expect.objectContaining({
1605
+ url: "http://127.0.0.1:3210",
1606
+ adminKey: "fresh|admin|key",
1607
+ }),
1608
+ expect.anything(),
1609
+ );
1610
+ });
1611
+
1558
1612
  it("dev --local crashes when local deployments are globally disabled", async () => {
1559
1613
  vi.mocked(readGlobalConfig).mockReturnValue({
1560
1614
  accessToken: "test-token",
@@ -27,8 +27,12 @@ After `npx convex ai-files install` (or `update`) verify:
27
27
  - `.cursor/skills/` (Cursor symlinks)
28
28
  - `.claude/skills/` (Claude Code symlinks)
29
29
 
30
- Expected skill set: `auth-setup`, `components-guide`, `convex-helpers-guide`,
31
- `convex-quickstart`, `function-creator`, `migration-helper`, `schema-builder`.
30
+ Expected skill set (as of Mar 2026): `convex-create-component`,
31
+ `convex-migration-helper`, `convex-performance-audit`, `convex-quickstart`,
32
+ `convex-setup-auth`.
33
+
34
+ Note: the live skill set is fetched remotely and may change. Check the current
35
+ list against what `npx convex ai-files status` reports after install.
32
36
 
33
37
  **Why manual:** these paths and symlink behaviors are environment-dependent and
34
38
  are mocked in automated tests.
@@ -0,0 +1,133 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "vitest";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { injectAgentsMdSection, hasAgentsMdInstalled } from "./agentsmd.js";
6
+ import {
7
+ AGENTS_MD_START_MARKER,
8
+ AGENTS_MD_END_MARKER,
9
+ } from "../../codegen_templates/agentsmd.js";
10
+
11
+ describe("injectAgentsMdSection", () => {
12
+ let tmpDir: string;
13
+
14
+ beforeEach(() => {
15
+ tmpDir = fs.mkdtempSync(`${os.tmpdir()}${path.sep}`);
16
+ });
17
+
18
+ afterEach(() => {
19
+ fs.rmSync(tmpDir, { recursive: true, force: true });
20
+ });
21
+
22
+ const section = `${AGENTS_MD_START_MARKER}\n## Convex\nRead guidelines.\n${AGENTS_MD_END_MARKER}`;
23
+
24
+ test("creates AGENTS.md when it does not exist", async () => {
25
+ await injectAgentsMdSection({ section, projectDir: tmpDir });
26
+
27
+ const content = fs.readFileSync(path.join(tmpDir, "AGENTS.md"), "utf8");
28
+ expect(content).toContain(AGENTS_MD_START_MARKER);
29
+ expect(content).toContain(AGENTS_MD_END_MARKER);
30
+ expect(content).toContain("## Convex");
31
+ });
32
+
33
+ test("appends to an existing AGENTS.md that has no Convex section", async () => {
34
+ const existing = "# My project\n\nSome existing content.\n";
35
+ fs.writeFileSync(path.join(tmpDir, "AGENTS.md"), existing);
36
+
37
+ await injectAgentsMdSection({ section, projectDir: tmpDir });
38
+
39
+ const content = fs.readFileSync(path.join(tmpDir, "AGENTS.md"), "utf8");
40
+ expect(content).toContain("# My project");
41
+ expect(content).toContain("Some existing content.");
42
+ expect(content).toContain(AGENTS_MD_START_MARKER);
43
+ expect(content).toContain("## Convex");
44
+ });
45
+
46
+ test("replaces an existing Convex section when markers are present", async () => {
47
+ const oldSection = `${AGENTS_MD_START_MARKER}\n## Convex\nOld content.\n${AGENTS_MD_END_MARKER}`;
48
+ const existing = `# My project\n\n${oldSection}\n`;
49
+ fs.writeFileSync(path.join(tmpDir, "AGENTS.md"), existing);
50
+
51
+ const newSection = `${AGENTS_MD_START_MARKER}\n## Convex\nNew content.\n${AGENTS_MD_END_MARKER}`;
52
+ await injectAgentsMdSection({ section: newSection, projectDir: tmpDir });
53
+
54
+ const content = fs.readFileSync(path.join(tmpDir, "AGENTS.md"), "utf8");
55
+ expect(content).toContain("New content.");
56
+ expect(content).not.toContain("Old content.");
57
+ expect(content.split(AGENTS_MD_START_MARKER).length - 1).toBe(1);
58
+ });
59
+
60
+ test("preserves content before and after an existing Convex section", async () => {
61
+ const oldSection = `${AGENTS_MD_START_MARKER}\n## Convex\nOld.\n${AGENTS_MD_END_MARKER}`;
62
+ const existing = `# Before\n\n${oldSection}\n\n# After\n`;
63
+ fs.writeFileSync(path.join(tmpDir, "AGENTS.md"), existing);
64
+
65
+ await injectAgentsMdSection({ section, projectDir: tmpDir });
66
+
67
+ const content = fs.readFileSync(path.join(tmpDir, "AGENTS.md"), "utf8");
68
+ expect(content).toContain("# Before");
69
+ expect(content).toContain("# After");
70
+ });
71
+
72
+ test("returns a non-null hash of the written content", async () => {
73
+ const result = await injectAgentsMdSection({ section, projectDir: tmpDir });
74
+ expect(typeof result.sectionHash).toBe("string");
75
+ expect(result.sectionHash.length).toBeGreaterThan(0);
76
+ expect(result.didWrite).toBe(true);
77
+ });
78
+
79
+ test("returns hash of the section content, not the entire file", async () => {
80
+ fs.writeFileSync(
81
+ path.join(tmpDir, "AGENTS.md"),
82
+ "# My project\n\nExisting content.\n",
83
+ );
84
+
85
+ const result = await injectAgentsMdSection({ section, projectDir: tmpDir });
86
+
87
+ const { hashSha256 } = await import("../utils/hash.js");
88
+ expect(result.sectionHash).toBe(hashSha256(section));
89
+ });
90
+
91
+ test("does not write when content is unchanged", async () => {
92
+ await injectAgentsMdSection({ section, projectDir: tmpDir });
93
+ const result = await injectAgentsMdSection({ section, projectDir: tmpDir });
94
+ expect(result.didWrite).toBe(false);
95
+ });
96
+ });
97
+
98
+ describe("hasAgentsMdInstalled", () => {
99
+ let tmpDir: string;
100
+
101
+ beforeEach(() => {
102
+ tmpDir = fs.mkdtempSync(`${os.tmpdir()}${path.sep}`);
103
+ });
104
+
105
+ afterEach(() => {
106
+ fs.rmSync(tmpDir, { recursive: true, force: true });
107
+ });
108
+
109
+ test("returns false when AGENTS.md does not exist", async () => {
110
+ expect(await hasAgentsMdInstalled(tmpDir)).toBe(false);
111
+ });
112
+
113
+ test("returns false when AGENTS.md exists but has no managed markers", async () => {
114
+ fs.writeFileSync(path.join(tmpDir, "AGENTS.md"), "User content only\n");
115
+ expect(await hasAgentsMdInstalled(tmpDir)).toBe(false);
116
+ });
117
+
118
+ test("returns false when only the start marker is present", async () => {
119
+ fs.writeFileSync(
120
+ path.join(tmpDir, "AGENTS.md"),
121
+ `${AGENTS_MD_START_MARKER}\npartial content\n`,
122
+ );
123
+ expect(await hasAgentsMdInstalled(tmpDir)).toBe(false);
124
+ });
125
+
126
+ test("returns true when both markers are present", async () => {
127
+ fs.writeFileSync(
128
+ path.join(tmpDir, "AGENTS.md"),
129
+ `# Project\n\n${AGENTS_MD_START_MARKER}\n## Convex\n${AGENTS_MD_END_MARKER}\n`,
130
+ );
131
+ expect(await hasAgentsMdInstalled(tmpDir)).toBe(true);
132
+ });
133
+ });