mixdog 1.3.4 → 1.3.5

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 (197) hide show
  1. package/LICENSES/oh-my-pi-MIT.txt +23 -0
  2. package/NOTICE.md +15 -0
  3. package/README.md +11 -0
  4. package/package.json +2 -1
  5. package/scripts/embed-google-oauth-client.mjs +49 -0
  6. package/scripts/lib/test-spawn-ledger.mjs +46 -1
  7. package/scripts/test.mjs +5 -0
  8. package/src/defaults/shipped-history.json +8 -0
  9. package/src/defaults/skills/browser-use/SKILL.md +21 -9
  10. package/src/defaults/skills/setup/SKILL.md +7 -2
  11. package/src/defaults/skills/setup/references/actions.md +82 -27
  12. package/src/defaults/skills/setup/references/surfaces.md +67 -34
  13. package/src/defaults/skills/setup-guide/SKILL.md +3 -3
  14. package/src/lib/keychain-cjs.cjs +359 -29
  15. package/src/rules/agent-mode/AGENT_MODE.md +10 -2
  16. package/src/rules/agent-mode/MIXY.md +12 -0
  17. package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
  18. package/src/runtime/agent/orchestrator/config.mjs +18 -0
  19. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  21. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
  22. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
  23. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
  24. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
  25. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
  26. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
  27. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
  28. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
  29. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
  30. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
  32. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
  33. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
  35. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
  36. package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
  37. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
  41. package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
  42. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
  43. package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
  44. package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
  45. package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
  46. package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
  47. package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
  48. package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
  49. package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
  50. package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
  51. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
  52. package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
  53. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
  54. package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
  55. package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
  56. package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
  57. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
  58. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
  60. package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
  61. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
  63. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
  64. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
  65. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
  66. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  67. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
  69. package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
  70. package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
  71. package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
  72. package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
  73. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
  75. package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
  76. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  77. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
  78. package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
  79. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
  80. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
  81. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
  82. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
  83. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
  84. package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
  85. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
  86. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
  87. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
  88. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
  89. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
  90. package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
  91. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
  92. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
  93. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
  94. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
  96. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
  97. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
  98. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
  99. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
  100. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
  101. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
  102. package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
  103. package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
  104. package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
  105. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
  106. package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
  107. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
  108. package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
  109. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
  110. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
  111. package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
  112. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
  113. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
  114. package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
  115. package/src/runtime/channels/lib/scheduler.mjs +109 -14
  116. package/src/runtime/channels/lib/schedules-db.mjs +49 -0
  117. package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
  118. package/src/runtime/channels/lib/worker-main.mjs +9 -0
  119. package/src/runtime/office/com/com-adapter.mjs +17 -1
  120. package/src/runtime/office/com/office-com-host.ps1 +51 -1
  121. package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
  122. package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
  123. package/src/runtime/office/pdf/document-preview.mjs +326 -8
  124. package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
  125. package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
  126. package/src/runtime/office/portable/portable-soffice.mjs +45 -5
  127. package/src/runtime/shared/bot-hops.mjs +62 -2
  128. package/src/runtime/shared/config.mjs +49 -22
  129. package/src/runtime/shared/connector-catalog.d.mts +4 -2
  130. package/src/runtime/shared/connector-catalog.mjs +16 -8
  131. package/src/runtime/shared/developer-options.mjs +32 -1
  132. package/src/runtime/shared/google-oauth-client.generated.json +4 -0
  133. package/src/runtime/shared/google-oauth-client.mjs +34 -9
  134. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
  135. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
  136. package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
  137. package/src/runtime/shared/llm/well-formed.mjs +66 -0
  138. package/src/runtime/shared/provider-accounts.mjs +224 -58
  139. package/src/runtime/shared/provider-api-key.mjs +19 -8
  140. package/src/runtime/shared/routine-trigger.mjs +42 -0
  141. package/src/runtime/shared/tool-execution-contract.mjs +2 -0
  142. package/src/runtime/shared/transcript-metadata.mjs +38 -0
  143. package/src/runtime/shared/user-data-guard.mjs +30 -2
  144. package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
  145. package/src/session-runtime/agent-home-delivery.mjs +57 -13
  146. package/src/session-runtime/agent-room-delivery.mjs +28 -1
  147. package/src/session-runtime/agent-task-sessions.mjs +292 -79
  148. package/src/session-runtime/boot/begin.mjs +3 -2
  149. package/src/session-runtime/boot/tools.mjs +2 -0
  150. package/src/session-runtime/bot-chain.mjs +32 -0
  151. package/src/session-runtime/bot-status.mjs +7 -4
  152. package/src/session-runtime/channel-config-api.mjs +63 -17
  153. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
  154. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
  155. package/src/session-runtime/internal-tool-executor.mjs +4 -0
  156. package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
  157. package/src/session-runtime/mixy-heartbeat.mjs +807 -0
  158. package/src/session-runtime/model-route/route-tuning.mjs +10 -6
  159. package/src/session-runtime/owned-automation-run.mjs +33 -12
  160. package/src/session-runtime/provider-auth/login.mjs +7 -0
  161. package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
  162. package/src/session-runtime/routine-home-turn.mjs +33 -6
  163. package/src/session-runtime/schedule-session-run.mjs +7 -3
  164. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
  165. package/src/session-runtime/services/channel-admin.mjs +66 -18
  166. package/src/session-runtime/services/channel-worker.mjs +1 -1
  167. package/src/session-runtime/services/provider-admin.mjs +128 -25
  168. package/src/session-runtime/services/schedule-access.mjs +14 -0
  169. package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
  170. package/src/session-runtime/session-create/session-options.mjs +7 -4
  171. package/src/session-runtime/settings-system-api.mjs +16 -1
  172. package/src/session-runtime/setup-tool/executor.mjs +49 -14
  173. package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
  174. package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
  175. package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
  176. package/src/session-runtime/task-session-route.mjs +99 -0
  177. package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
  178. package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
  179. package/src/session-runtime/tool-surface.mjs +4 -0
  180. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
  181. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
  182. package/src/session-runtime/workspace/transitions.mjs +10 -2
  183. package/src/standalone/daemon-session-control.mjs +88 -20
  184. package/src/standalone/daemon.mjs +8 -0
  185. package/src/standalone/session-protocol.mjs +5 -0
  186. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
  187. package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
  188. package/src/standalone/session-service.mjs +13 -0
  189. package/src/standalone/session-tool-output.mjs +8 -5
  190. package/src/tui/app/model-options.mjs +2 -0
  191. package/src/tui/dist/index.mjs +24 -2
  192. package/src/tui/session/notice-surface.mjs +17 -1
  193. package/src/tui/session/session-api/integrations.mjs +1 -0
  194. package/src/tui/session/session-api/lifecycle.mjs +8 -0
  195. package/src/tui/session/session-api/settings.mjs +14 -2
  196. package/src/tui/session/turn-stream.mjs +17 -1
  197. package/src/tui/session/turn.mjs +22 -2
@@ -0,0 +1,409 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, unlinkSync } from 'node:fs';
3
+ import { createServer } from 'node:http';
4
+ import { join, resolve } from 'node:path';
5
+ import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
6
+ import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
7
+ import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
8
+ import { modelProxyClientMode } from '../../../shared/model-proxy-mode.mjs';
9
+ import { openLoginUrl } from './lib/open-login-url.mjs';
10
+ import { OAUTH_PAGE_CONTENT_TYPE, oauthErrorHtml, oauthSuccessHtml } from './lib/oauth-page.mjs';
11
+ import { createOAuthPkce, parseOAuthCodeInput } from './lib/oauth-pkce.mjs';
12
+ import {
13
+ accountIdentityFields,
14
+ decodeJwtPayload,
15
+ expiryFromAccessToken,
16
+ normalizeAccountIdentity,
17
+ } from './lib/oauth-token-utils.mjs';
18
+
19
+ const AUTHORIZE_URL = 'https://app.devin.ai/auth/cli/continue';
20
+ const TOKEN_URL = 'https://api.devin.ai/auth/cli/token';
21
+ const CALLBACK_HOST = '127.0.0.1';
22
+ const CALLBACK_PORT = 59653;
23
+ const CALLBACK_PATH = '/callback';
24
+ const REDIRECT_URI = `http://${CALLBACK_HOST}:${CALLBACK_PORT}${CALLBACK_PATH}`;
25
+ const LOGIN_TIMEOUT_MS = 5 * 60_000;
26
+ const EXCHANGE_TIMEOUT_MS = 30_000;
27
+ // The login token is the only credential and cannot be refreshed. A token
28
+ // without a readable `exp` claim is assumed to live for a year.
29
+ const DEFAULT_TOKEN_LIFETIME_MS = 365 * 24 * 60 * 60_000;
30
+ export const REAUTH_GUIDANCE = 'Open /providers in Mixdog to sign in again.';
31
+
32
+ function reauthError(message) {
33
+ return Object.assign(new Error(message), { reauthRequired: true });
34
+ }
35
+
36
+ // Sign-in failures the desktop words in the user's language: the message leads
37
+ // with `DEVIN_OAUTH_<CODE>:` and apps/desktop/src/renderer/oauth-failure-copy.ts
38
+ // maps each code to its cause and remedy (like GMAIL_APP_PASSWORD_<CODE>).
39
+ export const DEVIN_OAUTH_FAILURE_CODES = Object.freeze([
40
+ 'DEVIN_OAUTH_CALLBACK_PORT_IN_USE',
41
+ 'DEVIN_OAUTH_CALLBACK_SERVER_FAILED',
42
+ 'DEVIN_OAUTH_LOGIN_TIMED_OUT',
43
+ 'DEVIN_OAUTH_AUTHORIZATION_DENIED',
44
+ 'DEVIN_OAUTH_STATE_MISMATCH',
45
+ 'DEVIN_OAUTH_CODE_REQUIRED',
46
+ 'DEVIN_OAUTH_TOKEN_EXCHANGE_FAILED',
47
+ ]);
48
+
49
+ function loginFailure(code, text, props = {}) {
50
+ const failureCode = `DEVIN_OAUTH_${code}`;
51
+ return Object.assign(new Error(`${failureCode}: ${text}`), { failureCode }, props);
52
+ }
53
+
54
+ function credentialsPath() {
55
+ const bound = boundProviderAuthPath('devin-oauth');
56
+ if (bound) return resolve(bound);
57
+ const explicit = process.env.DEVIN_OAUTH_CREDENTIALS_PATH;
58
+ if (explicit) return resolve(explicit);
59
+ const dir = resolvePluginData();
60
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
61
+ return join(dir, 'devin-oauth.json');
62
+ }
63
+
64
+ export function devinTokenExpiry(token) {
65
+ return expiryFromAccessToken(token);
66
+ }
67
+
68
+ // The login token is a JWT whose `sub` is the Devin user; no request needed.
69
+ export function devinTokenIdentity(token) {
70
+ const payload = decodeJwtPayload(token);
71
+ return normalizeAccountIdentity({ id: payload?.sub, email: payload?.email });
72
+ }
73
+
74
+ function loadStoredCredentials() {
75
+ if (modelProxyClientMode()) return null;
76
+ const path = credentialsPath();
77
+ if (!existsSync(path)) return null;
78
+ try {
79
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
80
+ if (!raw?.access_token) return null;
81
+ return {
82
+ ...raw,
83
+ expires_at: Number(raw.expires_at) || devinTokenExpiry(raw.access_token),
84
+ identity: normalizeAccountIdentity(raw.identity) || devinTokenIdentity(raw.access_token),
85
+ source: 'Mixdog token store',
86
+ };
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ function saveCredentials(tokens) {
93
+ writeJsonAtomicSync(credentialsPath(), tokens, {
94
+ lock: true,
95
+ fsyncDir: true,
96
+ mode: 0o600,
97
+ secret: true,
98
+ });
99
+ }
100
+
101
+ function tokenRecord(accessToken) {
102
+ const identity = devinTokenIdentity(accessToken);
103
+ return {
104
+ access_token: accessToken,
105
+ refresh_token: null,
106
+ expires_at: devinTokenExpiry(accessToken) || Date.now() + DEFAULT_TOKEN_LIFETIME_MS,
107
+ ...(identity ? { identity } : {}),
108
+ };
109
+ }
110
+
111
+ export function hasDevinOAuthCredentials() {
112
+ const tokens = loadStoredCredentials();
113
+ if (!tokens?.access_token) return false;
114
+ const expiresAt = Number(tokens.expires_at) || 0;
115
+ return expiresAt === 0 || expiresAt > Date.now();
116
+ }
117
+
118
+ export function describeDevinOAuthCredentials() {
119
+ const tokens = loadStoredCredentials();
120
+ if (!tokens?.access_token) {
121
+ return {
122
+ authenticated: false,
123
+ usable: false,
124
+ refreshable: false,
125
+ reauthRequired: false,
126
+ status: 'Not Set',
127
+ detail: 'Mixdog token store',
128
+ };
129
+ }
130
+ const expiresAt = Number(tokens.expires_at) || 0;
131
+ const expired = expiresAt > 0 && expiresAt <= Date.now();
132
+ return {
133
+ authenticated: !expired,
134
+ usable: !expired,
135
+ refreshable: false,
136
+ reauthRequired: expired,
137
+ status: expired ? 'Reauth Required' : 'Access Only',
138
+ detail: tokens.source || 'Devin OAuth',
139
+ expiresAt: expiresAt || null,
140
+ ...accountIdentityFields(tokens.identity),
141
+ };
142
+ }
143
+
144
+ export function forgetDevinOAuthCredentials() {
145
+ const path = credentialsPath();
146
+ if (!existsSync(path)) return { removed: false };
147
+ unlinkSync(path);
148
+ return { removed: true };
149
+ }
150
+
151
+ export function generateDevinOAuthParams() {
152
+ const { verifier, challenge } = createOAuthPkce();
153
+ const state = randomUUID();
154
+ const params = new URLSearchParams({
155
+ response_type: 'code',
156
+ redirect_uri: REDIRECT_URI,
157
+ code_challenge: challenge,
158
+ code_challenge_method: 'S256',
159
+ state,
160
+ prompt: 'select_account',
161
+ });
162
+ return { verifier, challenge, state, loginUrl: `${AUTHORIZE_URL}?${params}` };
163
+ }
164
+
165
+ export async function exchangeDevinCode(code, verifier, { fetchFn = fetch, signal } = {}) {
166
+ const timeout = AbortSignal.timeout(EXCHANGE_TIMEOUT_MS);
167
+ let response;
168
+ try {
169
+ response = await fetchFn(TOKEN_URL, {
170
+ method: 'POST',
171
+ headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
172
+ body: JSON.stringify({ code, code_verifier: verifier }),
173
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
174
+ redirect: 'error',
175
+ });
176
+ } catch (error) {
177
+ // A cancelled login keeps its own abort; any other failure to get an
178
+ // answer (network, timeout) is a coded exchange failure. The cause is not
179
+ // quoted: only its kind is.
180
+ if (signal?.aborted) throw error;
181
+ throw loginFailure(
182
+ 'TOKEN_EXCHANGE_FAILED',
183
+ error?.name === 'TimeoutError' ? 'Devin token exchange timed out' : 'Devin token exchange request failed'
184
+ );
185
+ }
186
+ // Response bodies may carry the login token itself, so none is ever quoted
187
+ // in an error: only the status is reported.
188
+ if (!response.ok) {
189
+ throw loginFailure('TOKEN_EXCHANGE_FAILED', `Devin token exchange failed (${response.status})`, {
190
+ status: response.status,
191
+ httpStatus: response.status,
192
+ });
193
+ }
194
+ const data = await response.json().catch(() => null);
195
+ if (typeof data?.token !== 'string' || !data.token) {
196
+ throw loginFailure('TOKEN_EXCHANGE_FAILED', 'Devin token exchange returned no token');
197
+ }
198
+ return tokenRecord(data.token);
199
+ }
200
+
201
+ // The callback port is fixed, so at most one login can own it. A newer login
202
+ // in this process takes the port over from the one it supersedes.
203
+ let activeLogin = null;
204
+
205
+ function listenOnCallbackPort(server) {
206
+ return new Promise((resolveListen, rejectListen) => {
207
+ server.once('error', rejectListen);
208
+ server.listen(CALLBACK_PORT, CALLBACK_HOST, () => {
209
+ server.off('error', rejectListen);
210
+ resolveListen();
211
+ });
212
+ });
213
+ }
214
+
215
+ // Logins start one at a time, in call order: ownership of the port is decided
216
+ // only once the previous start has fully settled, so overlapping calls
217
+ // supersede each other instead of racing for the bind.
218
+ let startQueue = Promise.resolve();
219
+
220
+ export function beginDevinOAuthLogin(options) {
221
+ const started = startQueue.then(() => startDevinOAuthLogin(options));
222
+ startQueue = started.catch(() => {});
223
+ return started;
224
+ }
225
+
226
+ async function startDevinOAuthLogin({ openBrowser = true, timeoutMs = LOGIN_TIMEOUT_MS, fetchFn = fetch } = {}) {
227
+ activeLogin?.cancel();
228
+ const params = generateDevinOAuthParams();
229
+ const controller = new AbortController();
230
+ const server = createServer();
231
+ let timer = null;
232
+ let exchangePromise = null;
233
+ let settled = false;
234
+ let resolveLogin;
235
+ let rejectLogin;
236
+ const waitForCallback = new Promise((resolvePromise, rejectPromise) => {
237
+ resolveLogin = resolvePromise;
238
+ rejectLogin = rejectPromise;
239
+ });
240
+ // A caller may only ever cancel; that rejection must not go unhandled.
241
+ waitForCallback.catch(() => {});
242
+
243
+ const login = {
244
+ provider: 'devin-oauth',
245
+ url: params.loginUrl,
246
+ manualUrl: null,
247
+ waitForCallback,
248
+ // A remote client signs in on its own machine, where the redirect to this
249
+ // host's loopback listener cannot land: it pastes the final address (or
250
+ // code) back, and the state it carries must be this login's own.
251
+ completeCode: async (input) => {
252
+ const text = String(input || '').trim();
253
+ // An address carries its code and state as query parameters only: one
254
+ // without a code is not a bare code (the shared parser would take the
255
+ // whole address for one and spend it on the exchange).
256
+ const address = URL.canParse(text) ? new URL(text) : null;
257
+ const pasted = address
258
+ ? { code: address.searchParams.get('code') || '', state: address.searchParams.get('state') || '' }
259
+ : parseOAuthCodeInput(text, { allowHashState: true });
260
+ if (pasted.state && pasted.state !== params.state) throw loginFailure('STATE_MISMATCH', 'OAuth state mismatch');
261
+ if (!pasted.code) throw loginFailure('CODE_REQUIRED', 'Authorization code is required');
262
+ return await acceptCode(pasted.code);
263
+ },
264
+ cancel: () => finish(null, new Error('Devin OAuth cancelled')),
265
+ };
266
+
267
+ function finish(value, error = null) {
268
+ if (settled) return;
269
+ settled = true;
270
+ if (activeLogin === login) activeLogin = null;
271
+ // Work arriving after the login settled (or was cancelled) is rejected.
272
+ controller.abort();
273
+ clearTimeout(timer);
274
+ try {
275
+ server.close();
276
+ server.closeIdleConnections?.();
277
+ } catch {
278
+ /* already closed */
279
+ }
280
+ if (error) rejectLogin(error);
281
+ else resolveLogin(value);
282
+ }
283
+
284
+ // The authorization code is spent once: a duplicate callback joins the first
285
+ // exchange instead of failing the login.
286
+ const acceptCode = (code) => {
287
+ if (exchangePromise) return exchangePromise;
288
+ controller.signal.throwIfAborted();
289
+ // The browser deadline ends once a code arrives; the exchange has its own.
290
+ clearTimeout(timer);
291
+ exchangePromise = exchangeDevinCode(code, params.verifier, { fetchFn, signal: controller.signal }).then(
292
+ (tokens) => {
293
+ // A cancelled login never stores the account it was exchanging.
294
+ controller.signal.throwIfAborted();
295
+ saveCredentials(tokens);
296
+ return tokens;
297
+ }
298
+ );
299
+ exchangePromise.then(
300
+ (tokens) => finish(tokens),
301
+ (error) => finish(null, error instanceof Error ? error : new Error(String(error)))
302
+ );
303
+ return exchangePromise;
304
+ };
305
+
306
+ const reply = (res, status, html) => {
307
+ res.writeHead(status, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE, Connection: 'close' });
308
+ res.end(html);
309
+ };
310
+
311
+ const handleRequest = async (req, res) => {
312
+ const target = new URL(req.url || '/', REDIRECT_URI);
313
+ if (req.method !== 'GET' || target.pathname !== CALLBACK_PATH) {
314
+ res.writeHead(404);
315
+ res.end();
316
+ return;
317
+ }
318
+ // A request that is not this login's own is refused; the real callback
319
+ // may still arrive.
320
+ if (target.searchParams.get('state') !== params.state) {
321
+ reply(res, 400, oauthErrorHtml('The sign-in callback did not match this login.'));
322
+ return;
323
+ }
324
+ const authorizationError = target.searchParams.get('error');
325
+ if (authorizationError) {
326
+ // Only a plain OAuth error code is named; free text (error_description)
327
+ // may carry anything, secrets included, so it never reaches the message.
328
+ const code = /^[A-Za-z0-9_.-]{1,64}$/.test(authorizationError) ? ` (${authorizationError})` : '';
329
+ reply(res, 400, oauthErrorHtml('Devin did not authorize this sign-in.'));
330
+ // A late denial must not cancel an already accepted code.
331
+ if (!exchangePromise) {
332
+ finish(null, loginFailure('AUTHORIZATION_DENIED', `Authorization failed${code}`));
333
+ }
334
+ return;
335
+ }
336
+ const code = target.searchParams.get('code')?.trim();
337
+ if (!code) {
338
+ reply(res, 400, oauthErrorHtml('The sign-in callback carried no authorization code.'));
339
+ return;
340
+ }
341
+ try {
342
+ await acceptCode(code);
343
+ reply(res, 200, oauthSuccessHtml('Devin'));
344
+ } catch {
345
+ // The shared exchange reports its error through waitForCallback.
346
+ reply(res, 500, oauthErrorHtml('Devin could not complete the token exchange.'));
347
+ }
348
+ };
349
+
350
+ // Whatever a stray local client sends (a malformed target included) is
351
+ // answered here and never escapes the listener.
352
+ server.on('request', async (req, res) => {
353
+ try {
354
+ await handleRequest(req, res);
355
+ } catch {
356
+ if (res.headersSent) res.destroy();
357
+ else reply(res, 400, oauthErrorHtml('The sign-in callback was malformed.'));
358
+ }
359
+ });
360
+
361
+ try {
362
+ await listenOnCallbackPort(server);
363
+ } catch (error) {
364
+ const failure =
365
+ error?.code === 'EADDRINUSE'
366
+ ? loginFailure(
367
+ 'CALLBACK_PORT_IN_USE',
368
+ `Callback port ${CALLBACK_HOST}:${CALLBACK_PORT} is already in use. Close the program using it (or another Devin sign-in) and try again.`,
369
+ { code: 'EADDRINUSE' }
370
+ )
371
+ : loginFailure(
372
+ 'CALLBACK_SERVER_FAILED',
373
+ `Callback server failed on ${CALLBACK_HOST}:${CALLBACK_PORT}: ${error?.message || error}`
374
+ );
375
+ finish(null, failure);
376
+ throw failure;
377
+ }
378
+ // Once bound, a runtime fault on the listener ends the login.
379
+ server.on('error', (error) =>
380
+ finish(null, loginFailure('CALLBACK_SERVER_FAILED', `Callback server failed: ${error?.message || error}`))
381
+ );
382
+ timer = setTimeout(
383
+ () => finish(null, loginFailure('LOGIN_TIMED_OUT', `Browser authentication timed out after ${timeoutMs}ms`)),
384
+ timeoutMs
385
+ );
386
+ timer.unref?.();
387
+ activeLogin = login;
388
+
389
+ process.stderr.write(`\n[devin-oauth] Open the Devin sign-in page:\n${params.loginUrl}\n\n`);
390
+ if (openBrowser) await openLoginUrl('devin-oauth', params.loginUrl);
391
+ return login;
392
+ }
393
+
394
+ export async function loginDevinOAuth() {
395
+ const login = await beginDevinOAuthLogin();
396
+ return await login.waitForCallback;
397
+ }
398
+
399
+ // The login token cannot be refreshed: once it expires the user signs in again.
400
+ export async function resolveDevinOAuthAccessToken() {
401
+ const tokens = loadStoredCredentials();
402
+ if (!tokens?.access_token) {
403
+ throw reauthError(`Devin OAuth is not connected. ${REAUTH_GUIDANCE}`);
404
+ }
405
+ if (tokens.expires_at > 0 && tokens.expires_at <= Date.now()) {
406
+ throw reauthError(`Devin login token expired and cannot be refreshed. ${REAUTH_GUIDANCE}`);
407
+ }
408
+ return tokens.access_token;
409
+ }
@@ -0,0 +1,187 @@
1
+ // Reviewed Devin model families: logical models the Cascade catalog serves as
2
+ // one wire uid per effort tier. Each family lists its member uids in default
3
+ // priority order, the uid each effort routes to ('off' = thinking disabled),
4
+ // and its effort ladder. Families the server also declares through
5
+ // `modelFamilyMetadata` are collapsed from that live metadata first; this
6
+ // table then covers accounts and models that ship none.
7
+
8
+ const SUFFIX_ROUTE = Object.freeze({
9
+ none: 'off',
10
+ minimal: 'minimal',
11
+ low: 'low',
12
+ medium: 'medium',
13
+ high: 'high',
14
+ xhigh: 'xhigh',
15
+ max: 'max',
16
+ });
17
+
18
+ // `${base}-${tier}${lane}` members for every tier, routed by their tier.
19
+ function ladder(base, tiers, lane = '') {
20
+ const members = tiers.map((tier) => `${base}-${tier}${lane}`);
21
+ const routing = Object.fromEntries(tiers.map((tier, index) => [SUFFIX_ROUTE[tier], members[index]]));
22
+ return { members, routing };
23
+ }
24
+
25
+ // `${PREFIX}_${TIER}` enum-style members (e.g. MODEL_GPT_5_2_HIGH).
26
+ function enumLadder(prefix, tiers) {
27
+ const members = tiers.map((tier) => `${prefix}_${tier.toUpperCase()}`);
28
+ const routing = Object.fromEntries(tiers.map((tier, index) => [SUFFIX_ROUTE[tier], members[index]]));
29
+ return { members, routing };
30
+ }
31
+
32
+ function family(id, name, { members, routing }, { efforts, requiresEffort = false, defaultLevel, defaultMember } = {}) {
33
+ return Object.freeze({
34
+ id,
35
+ name,
36
+ members,
37
+ routing,
38
+ ...(defaultMember ? { defaultMember } : {}),
39
+ thinking: {
40
+ mode: 'effort',
41
+ efforts,
42
+ ...(defaultLevel ? { defaultLevel } : {}),
43
+ ...(requiresEffort ? { requiresEffort: true } : {}),
44
+ },
45
+ });
46
+ }
47
+
48
+ const CLAUDE_TIERS = ['low', 'medium', 'high', 'xhigh', 'max'];
49
+ const CLAUDE = { efforts: CLAUDE_TIERS, requiresEffort: true };
50
+ const GPT_TIERS = ['none', 'low', 'medium', 'high', 'xhigh'];
51
+ const GPT = { efforts: ['low', 'medium', 'high', 'xhigh'] };
52
+ const GPT_REQUIRED = { efforts: ['low', 'medium', 'high', 'xhigh'], requiresEffort: true };
53
+ const GPT_56_TIERS = ['none', 'low', 'medium', 'high', 'xhigh', 'max'];
54
+ const GPT_56 = { efforts: ['low', 'medium', 'high', 'xhigh', 'max'] };
55
+ const FLASH_TIERS = ['minimal', 'low', 'medium', 'high'];
56
+ const FLASH = { efforts: FLASH_TIERS, requiresEffort: true };
57
+
58
+ export const DEVIN_REVIEWED_FAMILIES = Object.freeze([
59
+ family('claude-opus-5', 'Claude Opus 5', ladder('claude-opus-5', CLAUDE_TIERS), CLAUDE),
60
+ family('claude-opus-5-fast', 'Claude Opus 5 Fast', ladder('claude-opus-5', CLAUDE_TIERS, '-fast'), CLAUDE),
61
+ family('claude-fable-5', 'Claude Fable 5', ladder('claude-5-fable', CLAUDE_TIERS), CLAUDE),
62
+ family('claude-sonnet-5', 'Claude Sonnet 5', ladder('claude-sonnet-5', CLAUDE_TIERS), CLAUDE),
63
+ family('claude-opus-4-7', 'Claude Opus 4.7', ladder('claude-opus-4-7', CLAUDE_TIERS), CLAUDE),
64
+ family('claude-opus-4-7-fast', 'Claude Opus 4.7 Fast', ladder('claude-opus-4-7', CLAUDE_TIERS, '-fast'), CLAUDE),
65
+ family('claude-opus-4-8', 'Claude Opus 4.8', ladder('claude-opus-4-8', CLAUDE_TIERS), CLAUDE),
66
+ family('claude-opus-4-8-fast', 'Claude Opus 4.8 Fast', ladder('claude-opus-4-8', CLAUDE_TIERS, '-fast'), CLAUDE),
67
+ family('gpt-5-2', 'GPT-5.2', enumLadder('MODEL_GPT_5_2', GPT_TIERS), GPT),
68
+ family('gpt-5-3-codex', 'GPT-5.3 Codex', ladder('gpt-5-3-codex', GPT_TIERS.slice(1)), GPT_REQUIRED),
69
+ family(
70
+ 'gpt-5-3-codex-fast',
71
+ 'GPT-5.3 Codex Fast',
72
+ ladder('gpt-5-3-codex', GPT_TIERS.slice(1), '-priority'),
73
+ GPT_REQUIRED
74
+ ),
75
+ family('gpt-5-4', 'GPT-5.4', ladder('gpt-5-4', GPT_TIERS), GPT),
76
+ family('gpt-5-4-fast', 'GPT-5.4 Fast', ladder('gpt-5-4', GPT_TIERS, '-priority'), GPT),
77
+ family('gpt-5-4-mini', 'GPT-5.4 Mini', ladder('gpt-5-4-mini', GPT_TIERS.slice(1)), GPT_REQUIRED),
78
+ family('gpt-5-5', 'GPT-5.5', ladder('gpt-5-5', GPT_TIERS), GPT),
79
+ family('gpt-5-5-fast', 'GPT-5.5 Fast', ladder('gpt-5-5', GPT_TIERS, '-priority'), GPT),
80
+ family('gpt-5-6-luna', 'GPT-5.6 Luna', ladder('gpt-5-6-luna', GPT_56_TIERS), GPT_56),
81
+ family('gpt-5-6-luna-fast', 'GPT-5.6 Luna Fast', ladder('gpt-5-6-luna', GPT_56_TIERS, '-priority'), GPT_56),
82
+ family('gpt-5-6-sol', 'GPT-5.6 Sol', ladder('gpt-5-6-sol', GPT_56_TIERS), GPT_56),
83
+ family('gpt-5-6-sol-fast', 'GPT-5.6 Sol Fast', ladder('gpt-5-6-sol', GPT_56_TIERS, '-priority'), GPT_56),
84
+ family('gpt-5-6-terra', 'GPT-5.6 Terra', ladder('gpt-5-6-terra', GPT_56_TIERS), GPT_56),
85
+ family('gpt-5-6-terra-fast', 'GPT-5.6 Terra Fast', ladder('gpt-5-6-terra', GPT_56_TIERS, '-priority'), GPT_56),
86
+ family('kimi-k3', 'Kimi K3', ladder('kimi-k3', ['low', 'high', 'max']), {
87
+ efforts: ['low', 'high', 'max'],
88
+ requiresEffort: true,
89
+ }),
90
+ family(
91
+ 'swe-1-7',
92
+ 'SWE-1.7',
93
+ { members: ['swe-1-7-medium', 'swe-1-7'], routing: { medium: 'swe-1-7-medium', max: 'swe-1-7' } },
94
+ { efforts: ['medium', 'max'], requiresEffort: true }
95
+ ),
96
+ family('grok-4-5', 'Grok 4.5', ladder('grok-4-5', ['low', 'medium', 'high']), {
97
+ efforts: ['low', 'medium', 'high'],
98
+ requiresEffort: true,
99
+ }),
100
+ family('inkling', 'Inkling', ladder('inkling', GPT_56_TIERS), GPT_56),
101
+ family('gemini-3-1-pro', 'Gemini 3.1 Pro', ladder('gemini-3-1-pro', ['low', 'high']), {
102
+ efforts: ['low', 'high'],
103
+ requiresEffort: true,
104
+ }),
105
+ family('gemini-3-5-flash', 'Gemini 3.5 Flash', ladder('gemini-3-5-flash', FLASH_TIERS), FLASH),
106
+ family('gemini-3-6-flash', 'Gemini 3.6 Flash', ladder('gemini-3-6-flash', FLASH_TIERS), FLASH),
107
+ family('gemini-3-flash', 'Gemini 3 Flash', enumLadder('MODEL_GOOGLE_GEMINI_3_0_FLASH', FLASH_TIERS), FLASH),
108
+ family(
109
+ 'glm-5-2',
110
+ 'GLM-5.2',
111
+ { members: ['glm-5-2', 'glm-5-2-none', 'glm-5-2-max'], routing: { high: 'glm-5-2', xhigh: 'glm-5-2' } },
112
+ { efforts: ['high', 'xhigh'], requiresEffort: true }
113
+ ),
114
+ family(
115
+ 'glm-5-2-1m',
116
+ 'GLM-5.2 1M',
117
+ {
118
+ members: ['glm-5-2-none-1m', 'glm-5-2-1m', 'glm-5-2-max-1m'],
119
+ routing: { off: 'glm-5-2-none-1m', high: 'glm-5-2-1m', xhigh: 'glm-5-2-max-1m' },
120
+ },
121
+ { efforts: ['high', 'xhigh'] }
122
+ ),
123
+ family(
124
+ 'gemini-3-7-flash',
125
+ 'Gemini 3.7 Flash',
126
+ {
127
+ members: ['gemini-3-7-flash-medium', 'gemini-3-7-flash-minimal', 'gemini-3-7-flash-low', 'gemini-3-7-flash-high'],
128
+ routing: ladder('gemini-3-7-flash', FLASH_TIERS).routing,
129
+ },
130
+ { ...FLASH, defaultLevel: 'medium', defaultMember: 'gemini-3-7-flash-medium' }
131
+ ),
132
+ family(
133
+ 'swe-1-7-lightning',
134
+ 'SWE-1.7 Lightning',
135
+ {
136
+ members: ['swe-1-7-lightning-medium', 'swe-1-7-lightning'],
137
+ routing: { medium: 'swe-1-7-lightning-medium', max: 'swe-1-7-lightning' },
138
+ },
139
+ {
140
+ efforts: ['medium', 'max'],
141
+ requiresEffort: true,
142
+ defaultLevel: 'medium',
143
+ defaultMember: 'swe-1-7-lightning-medium',
144
+ }
145
+ ),
146
+ family(
147
+ 'grok-4-6',
148
+ 'Grok 4.6',
149
+ {
150
+ members: ['grok-4-6-medium', 'grok-4-6-low', 'grok-4-6-high', 'grok-4-6-xhigh'],
151
+ routing: ladder('grok-4-6', ['low', 'medium', 'high', 'xhigh']).routing,
152
+ },
153
+ {
154
+ efforts: ['low', 'medium', 'high', 'xhigh'],
155
+ requiresEffort: true,
156
+ defaultLevel: 'medium',
157
+ defaultMember: 'grok-4-6-medium',
158
+ }
159
+ ),
160
+ ...['flash', 'pro'].map((size) =>
161
+ family(
162
+ `deepseek-v4-${size}`,
163
+ `DeepSeek V4 ${size === 'pro' ? 'Pro' : 'Flash'}`,
164
+ {
165
+ members: [`deepseek-v4-${size}-high`, `deepseek-v4-${size}-low`, `deepseek-v4-${size}-max`],
166
+ routing: ladder(`deepseek-v4-${size}`, ['low', 'high', 'max']).routing,
167
+ },
168
+ {
169
+ efforts: ['low', 'high', 'max'],
170
+ requiresEffort: true,
171
+ defaultLevel: 'high',
172
+ defaultMember: `deepseek-v4-${size}-high`,
173
+ }
174
+ )
175
+ ),
176
+ family(
177
+ 'nemotron-3-ultra',
178
+ 'Nemotron 3 Ultra',
179
+ {
180
+ members: ['nemotron-3-ultra-high', 'nemotron-3-ultra-none', 'nemotron-3-ultra-medium'],
181
+ routing: ladder('nemotron-3-ultra', ['none', 'medium', 'high']).routing,
182
+ },
183
+ { efforts: ['medium', 'high'], defaultLevel: 'high', defaultMember: 'nemotron-3-ultra-high' }
184
+ ),
185
+ // A single-uid rename: no effort surface.
186
+ Object.freeze({ id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5', members: ['MODEL_PRIVATE_11'], routing: {} }),
187
+ ]);