@antelopejs/dms-ai 0.0.5 → 0.1.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 (218) hide show
  1. package/README.md +53 -2
  2. package/dist/routes/settings.d.ts +7 -0
  3. package/dist/routes/settings.js +2 -0
  4. package/dist/routes/settings.js.map +1 -1
  5. package/frontend-vue/app/components/SettingsView.vue +85 -3
  6. package/frontend-vue/app/plugins/ai.client.ts +17 -5
  7. package/frontend-vue/app/runtime/session-gate.ts +22 -0
  8. package/frontend-vue/tests/session-gate.test.ts +39 -0
  9. package/frontend-vue/tests/sidecar-guest.test.ts +53 -19
  10. package/package.json +11 -1
  11. package/sidecar/chatbox/dist/assets/index-CEZ1NLp9.js +52 -0
  12. package/sidecar/chatbox/dist/assets/{index-pP_KOHIn.css → index-D_51Vqqk.css} +1 -1
  13. package/sidecar/chatbox/dist/index.html +2 -2
  14. package/sidecar/dist/agent/attachment-files.d.ts +5 -0
  15. package/sidecar/dist/agent/attachment-files.js +29 -0
  16. package/sidecar/dist/agent/attachment-files.js.map +1 -0
  17. package/sidecar/dist/agent/provider.d.ts +40 -0
  18. package/sidecar/dist/agent/provider.js +2 -0
  19. package/sidecar/dist/agent/provider.js.map +1 -0
  20. package/sidecar/dist/agent/{claude-runner.d.ts → runner.d.ts} +8 -11
  21. package/sidecar/dist/agent/runner.js +71 -0
  22. package/sidecar/dist/agent/runner.js.map +1 -0
  23. package/sidecar/dist/agent/session.d.ts +22 -0
  24. package/sidecar/dist/agent/{claude-session.js → session.js} +39 -47
  25. package/sidecar/dist/agent/session.js.map +1 -0
  26. package/sidecar/dist/agent/switching-runner.d.ts +6 -0
  27. package/sidecar/dist/agent/switching-runner.js +58 -0
  28. package/sidecar/dist/agent/switching-runner.js.map +1 -0
  29. package/sidecar/dist/constants/agent.d.ts +2 -12
  30. package/sidecar/dist/constants/agent.js +2 -11
  31. package/sidecar/dist/constants/agent.js.map +1 -1
  32. package/sidecar/dist/constants/claude.d.ts +10 -0
  33. package/sidecar/dist/constants/claude.js +10 -0
  34. package/sidecar/dist/constants/claude.js.map +1 -0
  35. package/sidecar/dist/constants/codex.d.ts +85 -0
  36. package/sidecar/dist/constants/codex.js +110 -0
  37. package/sidecar/dist/constants/codex.js.map +1 -0
  38. package/sidecar/dist/constants/http.d.ts +1 -0
  39. package/sidecar/dist/constants/http.js +1 -0
  40. package/sidecar/dist/constants/http.js.map +1 -1
  41. package/sidecar/dist/constants/mcp.d.ts +6 -0
  42. package/sidecar/dist/constants/mcp.js +11 -0
  43. package/sidecar/dist/constants/mcp.js.map +1 -1
  44. package/sidecar/dist/constants/paths.d.ts +5 -0
  45. package/sidecar/dist/constants/paths.js +5 -0
  46. package/sidecar/dist/constants/paths.js.map +1 -1
  47. package/sidecar/dist/constants/providers.d.ts +16 -0
  48. package/sidecar/dist/constants/providers.js +17 -0
  49. package/sidecar/dist/constants/providers.js.map +1 -0
  50. package/sidecar/dist/constants/settings.d.ts +1 -4
  51. package/sidecar/dist/constants/settings.js +2 -12
  52. package/sidecar/dist/constants/settings.js.map +1 -1
  53. package/sidecar/dist/constants/skills.d.ts +1 -0
  54. package/sidecar/dist/constants/skills.js +2 -0
  55. package/sidecar/dist/constants/skills.js.map +1 -0
  56. package/sidecar/dist/constants/timing.d.ts +1 -0
  57. package/sidecar/dist/constants/timing.js +1 -0
  58. package/sidecar/dist/constants/timing.js.map +1 -1
  59. package/sidecar/dist/constants/tool-lexicon.d.ts +7 -0
  60. package/sidecar/dist/constants/tool-lexicon.js +8 -0
  61. package/sidecar/dist/constants/tool-lexicon.js.map +1 -0
  62. package/sidecar/dist/index.js +28 -3
  63. package/sidecar/dist/index.js.map +1 -1
  64. package/sidecar/dist/mcp/define-tool.d.ts +23 -0
  65. package/sidecar/dist/mcp/define-tool.js +4 -0
  66. package/sidecar/dist/mcp/define-tool.js.map +1 -0
  67. package/sidecar/dist/mcp/http-binding.d.ts +9 -0
  68. package/sidecar/dist/mcp/http-binding.js +123 -0
  69. package/sidecar/dist/mcp/http-binding.js.map +1 -0
  70. package/sidecar/dist/mcp/sdk-binding.js +14 -0
  71. package/sidecar/dist/mcp/sdk-binding.js.map +1 -0
  72. package/sidecar/dist/mcp/tool-definitions.d.ts +3 -0
  73. package/sidecar/dist/mcp/{server.js → tool-definitions.js} +2 -11
  74. package/sidecar/dist/mcp/tool-definitions.js.map +1 -0
  75. package/sidecar/dist/mcp/tools/ask-user.d.ts +1 -1
  76. package/sidecar/dist/mcp/tools/ask-user.js +2 -2
  77. package/sidecar/dist/mcp/tools/ask-user.js.map +1 -1
  78. package/sidecar/dist/mcp/tools/builder.d.ts +19 -19
  79. package/sidecar/dist/mcp/tools/builder.js +26 -26
  80. package/sidecar/dist/mcp/tools/builder.js.map +1 -1
  81. package/sidecar/dist/mcp/tools/find-pages-using.d.ts +1 -1
  82. package/sidecar/dist/mcp/tools/find-pages-using.js +2 -2
  83. package/sidecar/dist/mcp/tools/find-pages-using.js.map +1 -1
  84. package/sidecar/dist/mcp/tools/get-current-page.d.ts +1 -1
  85. package/sidecar/dist/mcp/tools/get-current-page.js +2 -2
  86. package/sidecar/dist/mcp/tools/get-current-page.js.map +1 -1
  87. package/sidecar/dist/mcp/tools/list-pages.d.ts +1 -1
  88. package/sidecar/dist/mcp/tools/list-pages.js +2 -2
  89. package/sidecar/dist/mcp/tools/list-pages.js.map +1 -1
  90. package/sidecar/dist/mcp/tools/navigate-to-page.d.ts +1 -1
  91. package/sidecar/dist/mcp/tools/navigate-to-page.js +2 -2
  92. package/sidecar/dist/mcp/tools/navigate-to-page.js.map +1 -1
  93. package/sidecar/dist/mcp/tools/query-logs.d.ts +1 -1
  94. package/sidecar/dist/mcp/tools/query-logs.js +2 -2
  95. package/sidecar/dist/mcp/tools/query-logs.js.map +1 -1
  96. package/sidecar/dist/mcp/tools/typecheck.d.ts +1 -1
  97. package/sidecar/dist/mcp/tools/typecheck.js +2 -2
  98. package/sidecar/dist/mcp/tools/typecheck.js.map +1 -1
  99. package/sidecar/dist/mcp/types.d.ts +6 -2
  100. package/sidecar/dist/protocol/events.d.ts +37 -0
  101. package/sidecar/dist/protocol/events.js +8 -0
  102. package/sidecar/dist/protocol/events.js.map +1 -1
  103. package/sidecar/dist/protocol/messages.d.ts +8 -0
  104. package/sidecar/dist/protocol/messages.js +2 -0
  105. package/sidecar/dist/protocol/messages.js.map +1 -1
  106. package/sidecar/dist/providers/claude/adapter.d.ts +5 -0
  107. package/sidecar/dist/{agent/sdk-adapter.js → providers/claude/adapter.js} +20 -1
  108. package/sidecar/dist/providers/claude/adapter.js.map +1 -0
  109. package/sidecar/dist/{agent → providers/claude}/attachments.d.ts +1 -1
  110. package/sidecar/dist/{agent → providers/claude}/attachments.js +3 -27
  111. package/sidecar/dist/providers/claude/attachments.js.map +1 -0
  112. package/sidecar/dist/providers/claude/config.d.ts +4 -0
  113. package/sidecar/dist/providers/claude/config.js +13 -0
  114. package/sidecar/dist/providers/claude/config.js.map +1 -0
  115. package/sidecar/dist/providers/claude/input-queue.js.map +1 -0
  116. package/sidecar/dist/providers/claude/module.d.ts +2 -0
  117. package/sidecar/dist/providers/claude/module.js +25 -0
  118. package/sidecar/dist/providers/claude/module.js.map +1 -0
  119. package/sidecar/dist/providers/claude/provider.d.ts +11 -0
  120. package/sidecar/dist/{agent/claude-runner.js → providers/claude/provider.js} +118 -96
  121. package/sidecar/dist/providers/claude/provider.js.map +1 -0
  122. package/sidecar/dist/{agent/resolve-claude-binary.js → providers/claude/resolve-binary.js} +3 -3
  123. package/sidecar/dist/{agent/resolve-claude-binary.js.map → providers/claude/resolve-binary.js.map} +1 -1
  124. package/sidecar/dist/providers/claude/sdk-loader.js +16 -0
  125. package/sidecar/dist/providers/claude/sdk-loader.js.map +1 -0
  126. package/sidecar/dist/providers/codex/adapter.d.ts +7 -0
  127. package/sidecar/dist/providers/codex/adapter.js +147 -0
  128. package/sidecar/dist/providers/codex/adapter.js.map +1 -0
  129. package/sidecar/dist/providers/codex/attachments.d.ts +7 -0
  130. package/sidecar/dist/providers/codex/attachments.js +73 -0
  131. package/sidecar/dist/providers/codex/attachments.js.map +1 -0
  132. package/sidecar/dist/providers/codex/client.d.ts +34 -0
  133. package/sidecar/dist/providers/codex/client.js +128 -0
  134. package/sidecar/dist/providers/codex/client.js.map +1 -0
  135. package/sidecar/dist/providers/codex/config.d.ts +34 -0
  136. package/sidecar/dist/providers/codex/config.js +111 -0
  137. package/sidecar/dist/providers/codex/config.js.map +1 -0
  138. package/sidecar/dist/providers/codex/event-stream.d.ts +8 -0
  139. package/sidecar/dist/providers/codex/event-stream.js +53 -0
  140. package/sidecar/dist/providers/codex/event-stream.js.map +1 -0
  141. package/sidecar/dist/providers/codex/module.d.ts +2 -0
  142. package/sidecar/dist/providers/codex/module.js +54 -0
  143. package/sidecar/dist/providers/codex/module.js.map +1 -0
  144. package/sidecar/dist/providers/codex/permissions.d.ts +21 -0
  145. package/sidecar/dist/providers/codex/permissions.js +77 -0
  146. package/sidecar/dist/providers/codex/permissions.js.map +1 -0
  147. package/sidecar/dist/providers/codex/process.d.ts +23 -0
  148. package/sidecar/dist/providers/codex/process.js +208 -0
  149. package/sidecar/dist/providers/codex/process.js.map +1 -0
  150. package/sidecar/dist/providers/codex/protocol/common.d.ts +59 -0
  151. package/sidecar/dist/providers/codex/protocol/common.js +2 -0
  152. package/sidecar/dist/providers/codex/protocol/common.js.map +1 -0
  153. package/sidecar/dist/providers/codex/protocol/index.d.ts +3 -0
  154. package/sidecar/dist/providers/codex/protocol/index.js +3 -0
  155. package/sidecar/dist/providers/codex/protocol/index.js.map +1 -0
  156. package/sidecar/dist/providers/codex/protocol/serde_json.d.ts +3 -0
  157. package/sidecar/dist/providers/codex/protocol/serde_json.js +2 -0
  158. package/sidecar/dist/providers/codex/protocol/serde_json.js.map +1 -0
  159. package/sidecar/dist/providers/codex/protocol/v2.d.ts +481 -0
  160. package/sidecar/dist/providers/codex/protocol/v2.js +2 -0
  161. package/sidecar/dist/providers/codex/protocol/v2.js.map +1 -0
  162. package/sidecar/dist/providers/codex/provider.d.ts +14 -0
  163. package/sidecar/dist/providers/codex/provider.js +225 -0
  164. package/sidecar/dist/providers/codex/provider.js.map +1 -0
  165. package/sidecar/dist/providers/codex/resolve-binary.d.ts +10 -0
  166. package/sidecar/dist/providers/codex/resolve-binary.js +80 -0
  167. package/sidecar/dist/providers/codex/resolve-binary.js.map +1 -0
  168. package/sidecar/dist/providers/codex/skills.d.ts +10 -0
  169. package/sidecar/dist/providers/codex/skills.js +42 -0
  170. package/sidecar/dist/providers/codex/skills.js.map +1 -0
  171. package/sidecar/dist/providers/registry.d.ts +7 -0
  172. package/sidecar/dist/providers/registry.js +31 -0
  173. package/sidecar/dist/providers/registry.js.map +1 -0
  174. package/sidecar/dist/providers/types.d.ts +25 -0
  175. package/sidecar/dist/providers/types.js +5 -0
  176. package/sidecar/dist/providers/types.js.map +1 -0
  177. package/sidecar/dist/server/http.d.ts +2 -0
  178. package/sidecar/dist/server/http.js +26 -12
  179. package/sidecar/dist/server/http.js.map +1 -1
  180. package/sidecar/dist/server/routing.d.ts +4 -4
  181. package/sidecar/dist/server/routing.js +7 -1
  182. package/sidecar/dist/server/routing.js.map +1 -1
  183. package/sidecar/dist/server/ws.d.ts +2 -0
  184. package/sidecar/dist/server/ws.js +40 -17
  185. package/sidecar/dist/server/ws.js.map +1 -1
  186. package/sidecar/dist/skills/build-catalog.d.ts +2 -0
  187. package/sidecar/dist/skills/build-catalog.js +12 -1
  188. package/sidecar/dist/skills/build-catalog.js.map +1 -1
  189. package/sidecar/dist/state/conversations.d.ts +3 -1
  190. package/sidecar/dist/state/conversations.js +20 -0
  191. package/sidecar/dist/state/conversations.js.map +1 -1
  192. package/sidecar/dist/state/safe-segment.d.ts +1 -0
  193. package/sidecar/dist/state/safe-segment.js +17 -0
  194. package/sidecar/dist/state/safe-segment.js.map +1 -0
  195. package/sidecar/dist/state/settings-store.js +7 -0
  196. package/sidecar/dist/state/settings-store.js.map +1 -1
  197. package/sidecar/dist/state/settings-types.d.ts +2 -0
  198. package/sidecar/dist/state/settings-types.js.map +1 -1
  199. package/sidecar/dist/state/types.d.ts +11 -0
  200. package/sidecar/dist/state/types.js +7 -0
  201. package/sidecar/dist/state/types.js.map +1 -1
  202. package/sidecar/package.json +3 -0
  203. package/sidecar/chatbox/dist/assets/index-Cy2BwTG0.js +0 -50
  204. package/sidecar/dist/agent/attachments.js.map +0 -1
  205. package/sidecar/dist/agent/claude-runner.js.map +0 -1
  206. package/sidecar/dist/agent/claude-session.d.ts +0 -18
  207. package/sidecar/dist/agent/claude-session.js.map +0 -1
  208. package/sidecar/dist/agent/input-queue.js.map +0 -1
  209. package/sidecar/dist/agent/sdk-adapter.d.ts +0 -3
  210. package/sidecar/dist/agent/sdk-adapter.js.map +0 -1
  211. package/sidecar/dist/agent/sdk-loader.js +0 -14
  212. package/sidecar/dist/agent/sdk-loader.js.map +0 -1
  213. package/sidecar/dist/mcp/server.js.map +0 -1
  214. /package/sidecar/dist/mcp/{server.d.ts → sdk-binding.d.ts} +0 -0
  215. /package/sidecar/dist/{agent → providers/claude}/input-queue.d.ts +0 -0
  216. /package/sidecar/dist/{agent → providers/claude}/input-queue.js +0 -0
  217. /package/sidecar/dist/{agent/resolve-claude-binary.d.ts → providers/claude/resolve-binary.d.ts} +0 -0
  218. /package/sidecar/dist/{agent → providers/claude}/sdk-loader.d.ts +0 -0
package/README.md CHANGED
@@ -7,8 +7,9 @@
7
7
  </div>
8
8
 
9
9
  An AntelopeJS DMS development assistant. It adds an owner-only AI workspace to the dashboard and
10
- runs Claude Code in a local sidecar that can inspect the loaded modules and their declared skills,
11
- edit the host project, and stream activity back to the dashboard.
10
+ runs a coding agent in a local sidecar that can inspect the loaded modules and their declared skills,
11
+ edit the host project, and stream activity back to the dashboard. Claude Code drives it by default;
12
+ OpenAI's Codex is selectable once its CLI is installed.
12
13
 
13
14
  ## Installation
14
15
 
@@ -22,6 +23,55 @@ The module starts its bundled sidecar with the project as its working directory.
22
23
  set `DMS_AI=0` before starting the backend to disable the sidecar. If
23
24
  `@antelopejs/dms-builder` is installed, the assistant also exposes its builder integration.
24
25
 
26
+ ## Agent providers
27
+
28
+ The assistant runs on either Claude Code or Codex, picked in the AI settings page. Both go through
29
+ the same seam, so the chat, the tool calls, the permission prompts and the file-change animation
30
+ behave the same either way. `GET /settings` reports which ones this install can actually drive:
31
+
32
+ ```json
33
+ { "provider": "claude", "providers": { "claude": { "available": true }, "codex": { "available": false, "reason": "Install @openai/codex, at the exact version dms-ai pins, to enable this provider." } } }
34
+ ```
35
+
36
+ `claude` is the default *choice*, not a fallback. A provider that this install cannot drive — its
37
+ package uninstalled, its API key gone from the environment — fails the turn with that reason in the
38
+ chat, and the conversation is never quietly handed to the other backend: someone who picked OpenAI
39
+ may have picked it precisely so their code does not reach Anthropic.
40
+
41
+ Switching provider disposes the live sessions of the previous one — an open conversation loses its
42
+ in-agent context, its transcript is kept.
43
+
44
+ ### Enabling Codex
45
+
46
+ 1. **Install the `codex` CLI.** It is an *optional* peer dependency, so it is never installed for
47
+ you: the platform packages weigh ~324 MB unpacked, and only the installs that want Codex should
48
+ pay for them. Use the exact version this module pins, in `peerDependencies["@openai/codex"]`:
49
+
50
+ ```bash
51
+ pnpm add @openai/codex@0.154.0
52
+ ```
53
+
54
+ The app-server protocol is versioned by binary and OpenAI publishes roughly ten versions a
55
+ month. The sidecar ships protocol types generated from the pinned version and checks
56
+ `codex --version` against it at spawn, refusing to start on a mismatch rather than speaking a
57
+ protocol its types do not describe. The refusal names both versions, so an upgrade says what to
58
+ pin back to. To run a newer binary anyway — at the risk of a protocol its types do not
59
+ describe — set `DMS_AI_CODEX_ALLOW_VERSION_MISMATCH=1`; the drift is then logged once, at startup, instead of
60
+ blocking the provider.
61
+
62
+ 2. **Provide an API key.** Export `OPENAI_API_KEY` in the host process environment. The binary does
63
+ not read that variable itself: the sidecar writes it into an `auth.json` (mode `0600`) inside a
64
+ `CODEX_HOME` dedicated to the conversation. On Windows that mode is not enforced by the
65
+ filesystem, so the file is only as private as the directory it sits in. ChatGPT login is not
66
+ supported here — it shares a token refresh with the user's own `codex` install.
67
+
68
+ 3. **Restart the sidecar**, then pick *OpenAI (Codex)* in the AI settings page.
69
+
70
+ Linux, macOS and Windows are all supported. One app-server process runs per conversation, and the
71
+ sidecar records its pid so a previous run's orphans are killed at startup — through procfs on
72
+ Linux, `ps` on macOS and `taskkill /T` on Windows, which is also what takes the sandboxed children
73
+ of the app-server with it.
74
+
25
75
  ## Vue frontend
26
76
 
27
77
  The module registers `frontend-vue` through `AddFrontendModule` with the Vue 3 renderer. The host DMS supplies authentication and shared state.
@@ -33,6 +83,7 @@ Install dependencies and run the backend and sidecar checks from the repository
33
83
  ```bash
34
84
  pnpm install
35
85
  pnpm build
86
+ pnpm build:sidecar
36
87
  pnpm test
37
88
  ```
38
89
 
@@ -1,9 +1,16 @@
1
+ type ProviderName = "claude" | "codex";
2
+ interface ProviderAvailability {
3
+ available: boolean;
4
+ reason?: string;
5
+ }
1
6
  interface AiSettings {
7
+ provider: ProviderName;
2
8
  mode: "normal" | "acceptEdits" | "plan" | "auto";
3
9
  thinking: "off" | "low" | "medium" | "high";
4
10
  generationMode: "safe" | "vibe";
5
11
  allowLocalSkills: boolean;
6
12
  builderAvailable: boolean;
13
+ providers: Record<ProviderName, ProviderAvailability>;
7
14
  }
8
15
  declare const AISettingsController_base: import("@antelopejs/interface-api").ControllerClass<object>;
9
16
  export declare class AISettingsController extends AISettingsController_base {
@@ -18,11 +18,13 @@ const auth_1 = require("@antelopejs/interface-dms/auth");
18
18
  const module_1 = require("../constants/module");
19
19
  const proxy_1 = require("./proxy");
20
20
  const DEFAULT_SETTINGS = {
21
+ provider: "claude",
21
22
  mode: "normal",
22
23
  thinking: "medium",
23
24
  generationMode: "safe",
24
25
  allowLocalSkills: false,
25
26
  builderAvailable: false,
27
+ providers: { claude: { available: true }, codex: { available: false } },
26
28
  };
27
29
  let AISettingsController = class AISettingsController extends (0, interface_api_1.Controller)(module_1.ROUTE_PREFIX) {
28
30
  getSettings() {
@@ -1 +1 @@
1
- {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/routes/settings.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,6DAA2E;AAC3E,yDAA+D;AAC/D,gDAAmD;AACnD,mCAAiD;AAYjD,MAAM,gBAAgB,GAAe;IACnC,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE,QAAQ;IAClB,cAAc,EAAE,MAAM;IACtB,gBAAgB,EAAE,KAAK;IACvB,gBAAgB,EAAE,KAAK;CACxB,CAAC;AAGK,IAAM,oBAAoB,GAA1B,MAAM,oBAAqB,SAAQ,IAAA,0BAAU,EAAC,qBAAY,CAAC;IAEhE,WAAW;QACT,OAAO,IAAA,kBAAU,EAAC,WAAW,EAAE,gBAAgB,CAAC,CAAC;IACnD,CAAC;IAGD,cAAc,CAAa,IAAa;QACtC,OAAO,IAAA,kBAAU,EAAC,WAAW,EAAE,IAAI,EAAE,gBAAgB,CAAC,CAAC;IACzD,CAAC;CACF,CAAA;AAVY,oDAAoB;AAE/B;IADC,IAAA,mBAAG,EAAC,WAAW,CAAC;;;;uDAGhB;AAGD;IADC,IAAA,mBAAG,EAAC,WAAW,CAAC;IACD,WAAA,IAAA,wBAAQ,GAAE,CAAA;;;;0DAEzB;+BATU,oBAAoB;IADhC,IAAA,oBAAa,GAAE;GACH,oBAAoB,CAUhC"}
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/routes/settings.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,6DAA2E;AAC3E,yDAA+D;AAC/D,gDAAmD;AACnD,mCAAiD;AAwBjD,MAAM,gBAAgB,GAAe;IACnC,QAAQ,EAAE,QAAQ;IAClB,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE,QAAQ;IAClB,cAAc,EAAE,MAAM;IACtB,gBAAgB,EAAE,KAAK;IACvB,gBAAgB,EAAE,KAAK;IACvB,SAAS,EAAE,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE;CACxE,CAAC;AAGK,IAAM,oBAAoB,GAA1B,MAAM,oBAAqB,SAAQ,IAAA,0BAAU,EAAC,qBAAY,CAAC;IAEhE,WAAW;QACT,OAAO,IAAA,kBAAU,EAAC,WAAW,EAAE,gBAAgB,CAAC,CAAC;IACnD,CAAC;IAGD,cAAc,CAAa,IAAa;QACtC,OAAO,IAAA,kBAAU,EAAC,WAAW,EAAE,IAAI,EAAE,gBAAgB,CAAC,CAAC;IACzD,CAAC;CACF,CAAA;AAVY,oDAAoB;AAE/B;IADC,IAAA,mBAAG,EAAC,WAAW,CAAC;;;;uDAGhB;AAGD;IADC,IAAA,mBAAG,EAAC,WAAW,CAAC;IACD,WAAA,IAAA,wBAAQ,GAAE,CAAA;;;;0DAEzB;+BATU,oBAAoB;IADhC,IAAA,oBAAa,GAAE;GACH,oBAAoB,CAUhC"}
@@ -4,23 +4,33 @@ import { computed, onMounted, ref } from 'vue'
4
4
  type Mode = 'normal' | 'acceptEdits' | 'plan' | 'auto'
5
5
  type Thinking = 'off' | 'low' | 'medium' | 'high'
6
6
  type GenerationMode = 'safe' | 'vibe'
7
+ type ProviderName = 'claude' | 'codex'
8
+
9
+ interface ProviderAvailability {
10
+ available: boolean
11
+ reason?: string
12
+ }
7
13
 
8
14
  interface AiSettings {
15
+ provider: ProviderName
9
16
  mode: Mode
10
17
  thinking: Thinking
11
18
  generationMode: GenerationMode
12
19
  allowLocalSkills: boolean
13
20
  builderAvailable: boolean
21
+ providers: Record<ProviderName, ProviderAvailability>
14
22
  }
15
23
 
16
24
  const { $authFetch } = useAuthFetch()
17
25
 
18
26
  const settings = ref<AiSettings>({
27
+ provider: 'claude',
19
28
  mode: 'normal',
20
29
  thinking: 'medium',
21
30
  generationMode: 'safe',
22
31
  allowLocalSkills: false,
23
32
  builderAvailable: false,
33
+ providers: { claude: { available: true }, codex: { available: false } },
24
34
  })
25
35
  const loading = ref(false)
26
36
  const saving = ref(false)
@@ -37,6 +47,23 @@ const MODE_OPTIONS: Array<{ value: Mode; label: string; hint: string }> = [
37
47
  { value: 'auto', label: 'Auto', hint: 'Auto-approve every tool action' },
38
48
  ]
39
49
 
50
+ const PROVIDER_OPTIONS: Array<{
51
+ value: ProviderName
52
+ label: string
53
+ model: string
54
+ }> = [
55
+ { value: 'claude', label: 'Anthropic (Claude)', model: 'Claude Opus 4.x' },
56
+ { value: 'codex', label: 'OpenAI (Codex)', model: 'GPT-5.x (Codex default)' },
57
+ ]
58
+
59
+ // One control, two meanings: a token budget on Claude, a reasoning effort level
60
+ // on Codex, which has no "off" and maps it to its lowest level.
61
+ const THINKING_HINTS: Record<ProviderName, string> = {
62
+ claude: 'Thinking budget handed to the model.',
63
+ codex:
64
+ 'Reasoning effort handed to the model. Codex has no "off": it maps to the lowest level.',
65
+ }
66
+
40
67
  const THINKING_OPTIONS: Array<{ value: Thinking; label: string }> = [
41
68
  { value: 'off', label: 'Off' },
42
69
  { value: 'low', label: 'Low' },
@@ -71,6 +98,39 @@ async function persist(): Promise<void> {
71
98
  }
72
99
  }
73
100
 
101
+ function availabilityOf(name: ProviderName): ProviderAvailability {
102
+ return settings.value.providers?.[name] ?? { available: false }
103
+ }
104
+
105
+ function providerLabel(option: (typeof PROVIDER_OPTIONS)[number]): string {
106
+ return availabilityOf(option.value).available
107
+ ? option.label
108
+ : `${option.label} — unavailable`
109
+ }
110
+
111
+ const activeProvider = computed(() => settings.value.provider)
112
+
113
+ const providerReason = computed(
114
+ () => availabilityOf(settings.value.provider).reason ?? null,
115
+ )
116
+
117
+ const activeModel = computed(
118
+ () =>
119
+ PROVIDER_OPTIONS.find((o) => o.value === settings.value.provider)?.model ??
120
+ '',
121
+ )
122
+
123
+ function onProvider(event: Event): void {
124
+ const next = (event.target as HTMLSelectElement).value as ProviderName
125
+ if (!availabilityOf(next).available) {
126
+ // The option is disabled, so this only fires on a stale DOM; snap back.
127
+ ;(event.target as HTMLSelectElement).value = settings.value.provider
128
+ return
129
+ }
130
+ settings.value.provider = next
131
+ void persist()
132
+ }
133
+
74
134
  function onMode(event: Event): void {
75
135
  settings.value.mode = (event.target as HTMLSelectElement).value as Mode
76
136
  void persist()
@@ -123,14 +183,33 @@ const SELECT_CLASS =
123
183
  <div class="flex flex-col gap-4 p-5">
124
184
  <div>
125
185
  <label class="mb-1.5 block text-xs font-medium text-toned">Provider</label>
126
- <select :class="SELECT_CLASS" disabled>
127
- <option>Anthropic (Claude)</option>
186
+ <select
187
+ :class="SELECT_CLASS"
188
+ :value="activeProvider"
189
+ :disabled="loading || saving"
190
+ @change="onProvider"
191
+ >
192
+ <option
193
+ v-for="option in PROVIDER_OPTIONS"
194
+ :key="option.value"
195
+ :value="option.value"
196
+ :disabled="!availabilityOf(option.value).available"
197
+ >
198
+ {{ providerLabel(option) }}
199
+ </option>
128
200
  </select>
201
+ <p v-if="providerReason" class="mt-1.5 text-xs text-error">
202
+ {{ providerReason }}
203
+ </p>
204
+ <p class="mt-1.5 text-xs text-dimmed">
205
+ Switching provider ends the live context of open conversations;
206
+ their transcripts are kept.
207
+ </p>
129
208
  </div>
130
209
  <div>
131
210
  <label class="mb-1.5 block text-xs font-medium text-toned">Model</label>
132
211
  <select :class="SELECT_CLASS" disabled>
133
- <option>Claude Opus 4.x</option>
212
+ <option>{{ activeModel }}</option>
134
213
  </select>
135
214
  </div>
136
215
  </div>
@@ -181,6 +260,9 @@ const SELECT_CLASS =
181
260
  {{ option.label }}
182
261
  </option>
183
262
  </select>
263
+ <p class="mt-1.5 text-xs text-dimmed">
264
+ {{ THINKING_HINTS[settings.provider] }}
265
+ </p>
184
266
  </div>
185
267
  <div>
186
268
  <label class="flex items-start gap-3">
@@ -1,4 +1,8 @@
1
- import { defineDmsPlugin, useDmsRouter } from '#dms/frontend-module'
1
+ import {
2
+ type DmsAppContext,
3
+ defineDmsPlugin,
4
+ useDmsRouter,
5
+ } from '#dms/frontend-module'
2
6
  import {
3
7
  SIDECAR_HOST_NAME,
4
8
  SIDECAR_INFO_PATH,
@@ -14,6 +18,7 @@ import { registerLauncherAction } from '../runtime/header-action'
14
18
  import { createHostCommandDispatcher } from '../runtime/host-commands'
15
19
  import { installNavigationCompleteEmitter } from '../runtime/navigation-complete'
16
20
  import { injectOverlay } from '../runtime/overlay'
21
+ import { runWhenLoggedIn } from '../runtime/session-gate'
17
22
  import { installToggleShortcut } from '../runtime/shortcuts'
18
23
  import {
19
24
  createSidecarStatusController,
@@ -30,10 +35,7 @@ function buildIframeUrl(port: number, token: string): string {
30
35
  return `${SIDECAR_PROBE_HOST}:${port}/?theme=${mode}#token=${encodeURIComponent(token)}`
31
36
  }
32
37
 
33
- export default defineDmsPlugin(async ({ vueApp }) => {
34
- if (!import.meta.env.DEV) return
35
- const { loggedIn } = useUserSession()
36
- if (!loggedIn.value) return
38
+ async function startAssistant({ vueApp }: DmsAppContext): Promise<void> {
37
39
  const { $authFetch } = useAuthFetch()
38
40
  const controller = createSidecarStatusController(() =>
39
41
  $authFetch(SIDECAR_INFO_PATH),
@@ -106,4 +108,14 @@ export default defineDmsPlugin(async ({ vueApp }) => {
106
108
  }
107
109
  vueApp.onUnmount(stop)
108
110
  import.meta.hot?.dispose(stop)
111
+ }
112
+
113
+ export default defineDmsPlugin((context) => {
114
+ if (!import.meta.env.DEV) return
115
+ const { loggedIn } = useUserSession()
116
+ const stopGate = runWhenLoggedIn(
117
+ () => loggedIn.value,
118
+ () => void context.runWithContext(() => startAssistant(context)),
119
+ )
120
+ context.vueApp.onUnmount(stopGate)
109
121
  })
@@ -0,0 +1,22 @@
1
+ import { watch } from 'vue'
2
+
3
+ // Plugins run once, when the Vue app boots, on whatever page the browser landed
4
+ // on. Landing on the sign-in screen means there is no session yet, and signing
5
+ // in is a client-side navigation that never re-creates the app: a plugin that
6
+ // gave up there would stay silent until the next full page load. Wait for the
7
+ // session to appear instead, then run exactly once.
8
+ export function runWhenLoggedIn(
9
+ isLoggedIn: () => boolean,
10
+ start: () => void,
11
+ ): () => void {
12
+ if (isLoggedIn()) {
13
+ start()
14
+ return () => undefined
15
+ }
16
+ const stop = watch(isLoggedIn, (loggedIn) => {
17
+ if (!loggedIn) return
18
+ stop()
19
+ start()
20
+ })
21
+ return stop
22
+ }
@@ -0,0 +1,39 @@
1
+ import { nextTick, ref } from 'vue'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+ import { runWhenLoggedIn } from '../app/runtime/session-gate'
4
+
5
+ describe('session gate', () => {
6
+ it('starts immediately when the session is already hydrated', () => {
7
+ const start = vi.fn()
8
+ runWhenLoggedIn(() => true, start)
9
+ expect(start).toHaveBeenCalledTimes(1)
10
+ })
11
+
12
+ it('starts once the session appears, and only once', async () => {
13
+ const loggedIn = ref(false)
14
+ const start = vi.fn()
15
+ runWhenLoggedIn(() => loggedIn.value, start)
16
+ expect(start).not.toHaveBeenCalled()
17
+
18
+ loggedIn.value = true
19
+ await nextTick()
20
+ expect(start).toHaveBeenCalledTimes(1)
21
+
22
+ loggedIn.value = false
23
+ await nextTick()
24
+ loggedIn.value = true
25
+ await nextTick()
26
+ expect(start).toHaveBeenCalledTimes(1)
27
+ })
28
+
29
+ it('never starts once the gate is stopped', async () => {
30
+ const loggedIn = ref(false)
31
+ const start = vi.fn()
32
+ const stop = runWhenLoggedIn(() => loggedIn.value, start)
33
+
34
+ stop()
35
+ loggedIn.value = true
36
+ await nextTick()
37
+ expect(start).not.toHaveBeenCalled()
38
+ })
39
+ })
@@ -1,13 +1,29 @@
1
1
  import { readFileSync } from 'node:fs'
2
2
  import { resolve } from 'node:path'
3
3
  import ts from 'typescript'
4
+ import { nextTick, ref, type Ref } from 'vue'
4
5
  import { expect, it, vi } from 'vitest'
6
+ import { runWhenLoggedIn } from '../app/runtime/session-gate'
5
7
 
6
8
  interface PluginExports {
7
- default: (context: unknown) => Promise<void>
9
+ default: (context: unknown) => void | Promise<void>
8
10
  }
9
11
 
10
- it('does not invoke authenticated fetching or inject the assistant for a guest', async () => {
12
+ interface PluginHarness {
13
+ useAuthFetch: ReturnType<typeof vi.fn>
14
+ run: () => void | Promise<void>
15
+ }
16
+
17
+ const SESSION_GATE_MODULE = '../runtime/session-gate'
18
+ const SIDECAR_STATUS_MODULE = '../runtime/sidecar-status'
19
+
20
+ // A first probe reporting "no assistant here" stops the startup right after the
21
+ // authenticated fetch, which is all these tests need to observe.
22
+ const silentController = {
23
+ createSidecarStatusController: () => ({ init: async () => false }),
24
+ }
25
+
26
+ function loadPlugin(loggedIn: Ref<boolean>): PluginHarness {
11
27
  const source = readFileSync(
12
28
  resolve(__dirname, '../app/plugins/ai.client.ts'),
13
29
  'utf8',
@@ -16,30 +32,48 @@ it('does not invoke authenticated fetching or inject the assistant for a guest',
16
32
  source
17
33
  .replace('import.meta.env.DEV', 'true')
18
34
  .replace('import.meta.hot', 'undefined'),
19
- {
20
- compilerOptions: { module: ts.ModuleKind.CommonJS },
21
- },
35
+ { compilerOptions: { module: ts.ModuleKind.CommonJS } },
22
36
  )
23
37
  const exports = {} as PluginExports
24
- const useAuthFetch = vi.fn(() => {
25
- throw new Error('A guest must not enter session recovery')
26
- })
27
- const requireModule = (name: string) =>
28
- name === '#dms/frontend-module'
29
- ? { defineDmsPlugin: (plugin: unknown) => plugin }
30
- : {}
38
+ const useAuthFetch = vi.fn(() => ({ $authFetch: vi.fn() }))
39
+ const requireModule = (name: string) => {
40
+ if (name === '#dms/frontend-module') {
41
+ return { defineDmsPlugin: (plugin: unknown) => plugin }
42
+ }
43
+ if (name === SESSION_GATE_MODULE) return { runWhenLoggedIn }
44
+ if (name === SIDECAR_STATUS_MODULE) return silentController
45
+ return {}
46
+ }
31
47
  new Function(
32
48
  'require',
33
49
  'exports',
34
50
  'useUserSession',
35
51
  'useAuthFetch',
36
52
  compiled.outputText,
37
- )(
38
- requireModule,
39
- exports,
40
- () => ({ loggedIn: { value: false } }),
53
+ )(requireModule, exports, () => ({ loggedIn }), useAuthFetch)
54
+ return {
41
55
  useAuthFetch,
42
- )
43
- await exports.default({})
44
- expect(useAuthFetch).not.toHaveBeenCalled()
56
+ run: () =>
57
+ exports.default({
58
+ vueApp: { onUnmount: vi.fn() },
59
+ runWithContext: (callback: () => unknown) => callback(),
60
+ }),
61
+ }
62
+ }
63
+
64
+ it('does not invoke authenticated fetching or inject the assistant for a guest', async () => {
65
+ const plugin = loadPlugin(ref(false))
66
+ await plugin.run()
67
+ expect(plugin.useAuthFetch).not.toHaveBeenCalled()
68
+ })
69
+
70
+ it('starts the assistant when a guest signs in without reloading the page', async () => {
71
+ const loggedIn = ref(false)
72
+ const plugin = loadPlugin(loggedIn)
73
+ await plugin.run()
74
+ expect(plugin.useAuthFetch).not.toHaveBeenCalled()
75
+
76
+ loggedIn.value = true
77
+ await nextTick()
78
+ expect(plugin.useAuthFetch).toHaveBeenCalledTimes(1)
45
79
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-ai",
3
- "version": "0.0.5",
3
+ "version": "0.1.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,6 +38,7 @@
38
38
  "lint:fix": "oxlint --disable-nested-config --fix --max-warnings=1000000 && oxfmt --disable-nested-config . && cd frontend-vue && pnpm lint:fix && cd ../sidecar && pnpm lint:fix",
39
39
  "typecheck": "tsc --noEmit && cd frontend-vue && pnpm typecheck && cd ../sidecar && pnpm typecheck",
40
40
  "test": "cd sidecar && pnpm test && cd ../frontend-vue && pnpm test",
41
+ "test:sidecar-smoke": "pnpm run build:sidecar && node --test tests/sidecar-build.test.cjs",
41
42
  "test:frontend-registration": "pnpm run build && node --test tests/frontend-registration.test.cjs",
42
43
  "test:release-script": "node --test tests/release-script.test.cjs",
43
44
  "test:e2e": "cd sidecar && pnpm test:e2e",
@@ -52,6 +53,7 @@
52
53
  "@antelopejs/interface-dms": ">=0.0.1 <1.0.0",
53
54
  "@anthropic-ai/claude-agent-sdk": "^0.2.131",
54
55
  "@anthropic-ai/claude-code": "^2.1.131",
56
+ "@modelcontextprotocol/sdk": "1.29.0",
55
57
  "ws": "^8.18.0",
56
58
  "zod": "^4.0.0"
57
59
  },
@@ -67,6 +69,14 @@
67
69
  "rimraf": "^6.1.3",
68
70
  "typescript": "^5.9.3"
69
71
  },
72
+ "peerDependencies": {
73
+ "@openai/codex": "0.154.0"
74
+ },
75
+ "peerDependenciesMeta": {
76
+ "@openai/codex": {
77
+ "optional": true
78
+ }
79
+ },
70
80
  "optionalDependencies": {
71
81
  "@antelopejs/interface-dms-builder": ">=0.0.1 <1.0.0"
72
82
  },