mixdog 1.1.0 → 1.1.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 (141) hide show
  1. package/README.md +13 -6
  2. package/package.json +1 -1
  3. package/scripts/latest-guard.mjs +63 -0
  4. package/scripts/lib/run-node-tests.mjs +103 -27
  5. package/scripts/lib/test-failure-summary.mjs +7 -2
  6. package/scripts/merge-mac-update-feed.mjs +107 -0
  7. package/scripts/mobile-release.mjs +210 -0
  8. package/scripts/post-release-check.mjs +353 -0
  9. package/scripts/publish-asset-release.mjs +43 -0
  10. package/scripts/release-guard.mjs +63 -0
  11. package/scripts/test.mjs +29 -7
  12. package/src/defaults/shipped-history.json +2 -0
  13. package/src/defaults/skills/computer-use/SKILL.md +1 -1
  14. package/src/defaults/skills/setup/references/actions.md +1 -0
  15. package/src/lib/keychain-cjs.cjs +582 -17
  16. package/src/runtime/agent/orchestrator/config-storage.mjs +32 -4
  17. package/src/runtime/agent/orchestrator/config.mjs +18 -0
  18. package/src/runtime/agent/orchestrator/context/instruction-files.mjs +130 -0
  19. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +4 -0
  20. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +11 -0
  21. package/src/runtime/agent/orchestrator/providers/lib/open-login-url.mjs +9 -0
  22. package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +254 -0
  23. package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +218 -0
  24. package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +239 -0
  25. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +233 -0
  26. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-login.mjs +247 -0
  27. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +225 -0
  28. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +542 -0
  29. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +25 -0
  30. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +4 -1
  31. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +13 -0
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +14 -6
  33. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +99 -0
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +4 -8
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -7
  36. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +8 -0
  37. package/src/runtime/agent/orchestrator/providers/registry.mjs +4 -0
  38. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +2 -0
  39. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +15 -1
  40. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -1
  41. package/src/runtime/agent/orchestrator/session/loop/tool-call-ids.mjs +90 -0
  42. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +19 -11
  43. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +1 -0
  44. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +28 -0
  45. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +8 -0
  46. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +3 -0
  47. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +3 -0
  48. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +2 -0
  49. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +2 -0
  50. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +23 -0
  51. package/src/runtime/agent/orchestrator/session/turn-effort.mjs +18 -0
  52. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +11 -8
  53. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +9 -4
  54. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +245 -0
  55. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/command-preflight.mjs +13 -1
  56. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +3 -1
  57. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
  58. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +49 -0
  59. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +3 -3
  60. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -7
  61. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +61 -8
  62. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +28 -3
  63. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +73 -1
  64. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +2 -2
  65. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +3 -1
  66. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +17 -6
  67. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +23 -5
  68. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +93 -43
  69. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +32 -6
  70. package/src/runtime/agent/orchestrator/tools/builtin.mjs +9 -2
  71. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +3 -1
  72. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +12 -15
  73. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +2 -2
  74. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +4 -2
  75. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -81
  76. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +19 -4
  77. package/src/runtime/computer-bridge/client.mjs +25 -1
  78. package/src/runtime/computer-bridge/linux-action-capabilities.mjs +84 -0
  79. package/src/runtime/computer-bridge/linux-readiness.d.mts +52 -0
  80. package/src/runtime/computer-bridge/linux-readiness.mjs +411 -0
  81. package/src/runtime/computer-bridge/test-linux-display.mjs +5 -0
  82. package/src/runtime/effort-judge/compact-tokenizer.mjs +13 -8
  83. package/src/runtime/effort-judge/judge-input.mjs +5 -2
  84. package/src/runtime/effort-judge/model-install.mjs +9 -3
  85. package/src/runtime/effort-judge/model-manifest.json +7 -7
  86. package/src/runtime/memory/lib/embedding-worker.mjs +9 -3
  87. package/src/runtime/memory/lib/pg/supervisor.mjs +3 -0
  88. package/src/runtime/shared/config.mjs +50 -2
  89. package/src/runtime/shared/developer-options.mjs +7 -0
  90. package/src/runtime/shared/instruction-files-mode.mjs +9 -0
  91. package/src/runtime/shared/llm/model-catalog.mjs +2 -1
  92. package/src/runtime/shared/llm/openai-daybreak.mjs +79 -0
  93. package/src/runtime/shared/pristine-execution-contract.json +9 -0
  94. package/src/runtime/shared/pristine-execution.mjs +1 -1
  95. package/src/runtime/shared/provider-accounts.mjs +2 -0
  96. package/src/runtime/shared/resource-admission.mjs +10 -3
  97. package/src/runtime/shared/schedule-model-ref.mjs +11 -0
  98. package/src/runtime/shared/session-approval-wait.mjs +35 -0
  99. package/src/runtime/shared/shell-display.mjs +4 -2
  100. package/src/runtime/shared/turn-worktree-snapshot.mjs +11 -10
  101. package/src/session-runtime/boot/apis.mjs +5 -0
  102. package/src/session-runtime/model-recency.mjs +3 -0
  103. package/src/session-runtime/provider-auth/login.mjs +10 -10
  104. package/src/session-runtime/provider-auth/setup.mjs +22 -2
  105. package/src/session-runtime/provider-models/row-hydration.mjs +12 -4
  106. package/src/session-runtime/provider-readiness.mjs +4 -0
  107. package/src/session-runtime/runtime-facade.mjs +5 -0
  108. package/src/session-runtime/schedule-session-run.mjs +3 -1
  109. package/src/session-runtime/services/agent-tool/index-write-queue.mjs +55 -14
  110. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +15 -3
  111. package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +2 -0
  112. package/src/session-runtime/services/agent-tool/worker-index/approval-wait.mjs +97 -0
  113. package/src/session-runtime/services/agent-tool/worker-index/mutation-batch.mjs +4 -2
  114. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -2
  115. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +3 -0
  116. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +58 -2
  117. package/src/session-runtime/services/agent-tool/worker-index.mjs +69 -15
  118. package/src/session-runtime/services/agent-tool/worker-rows.mjs +2 -0
  119. package/src/session-runtime/services/channel-admin.mjs +4 -2
  120. package/src/session-runtime/services/provider-admin.mjs +80 -17
  121. package/src/session-runtime/services/seeds.mjs +7 -10
  122. package/src/session-runtime/services/usage-dashboard-model.mjs +6 -0
  123. package/src/session-runtime/settings-builtin-tools-api.mjs +15 -0
  124. package/src/session-runtime/setup-tool/executor.mjs +5 -0
  125. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  126. package/src/session-runtime/tool-policy-refresh.mjs +3 -0
  127. package/src/session-runtime/webhook-session-run.mjs +3 -1
  128. package/src/standalone/session-protocol.mjs +1 -0
  129. package/src/tui/app/model-options.mjs +13 -1
  130. package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +10 -4
  131. package/src/tui/app/provider-setup/oauth-actions.mjs +5 -0
  132. package/src/tui/dist/index.mjs +84 -34
  133. package/src/tui/session/context-state.mjs +2 -0
  134. package/src/tui/session/live-share/owner.mjs +17 -0
  135. package/src/tui/session/oauth-flows.mjs +2 -0
  136. package/src/tui/session/session-api/integrations.mjs +8 -8
  137. package/src/tui/session/session-api/routes.mjs +2 -2
  138. package/src/tui/session/session-api/settings.mjs +1 -0
  139. package/src/tui/session/tool-card-results/grouped-result.mjs +11 -2
  140. package/src/tui/session/transcript-spill/spill-dir.mjs +22 -6
  141. package/src/tui/spinner-meta.mjs +26 -10
@@ -0,0 +1,218 @@
1
+ // MiMo wire profile: the opt-in hooks the shared Chat Completions sender reads
2
+ // from `config.chatProfile`, plus the model roster helpers and gateway error
3
+ // shaping. Nothing here applies to any other provider.
4
+ import { markProviderRecoveryExhausted } from './retry-classifier.mjs';
5
+ import { scrubOAuthSecrets } from './lib/oauth-token-utils.mjs';
6
+ import { PROVIDER_MAX_BEFORE_WARN_MS, STALL_ABORT_S, STALL_TICK_MS } from '../stall-policy.mjs';
7
+ import { PACKAGE_VERSION } from '../../../web-search/lib/package-version.mjs';
8
+ import { DEFAULT_MODEL } from './mimo-oauth-tokens.mjs';
9
+
10
+ const MIMO_EFFORT_LEVELS = Object.freeze(['minimal', 'low', 'medium', 'high']);
11
+ const MIMO_DEFAULT_TEMPERATURE = 1.0;
12
+ const MIMO_DEFAULT_MAX_OUTPUT_TOKENS = 128_000;
13
+ // Slow reasoning can stay silent for minutes: wait for the first byte as long
14
+ // as the agent stall watchdog allows, and tolerate long gaps between events.
15
+ export const MIMO_FIRST_BYTE_TIMEOUT_MS = Math.min(300_000, PROVIDER_MAX_BEFORE_WARN_MS);
16
+ export const MIMO_SEMANTIC_IDLE_TIMEOUT_MS = Math.min(480_000, Math.max(10_000, STALL_ABORT_S * 1000 - STALL_TICK_MS));
17
+
18
+ // Local roster used when /models fails or comes back empty.
19
+ const SEED_CONTEXT = 1_048_576;
20
+ const SEED_OUTPUT = 131_072;
21
+ export const SEED_MODELS = Object.freeze(
22
+ [
23
+ ['mimo-v2.6-pro', 'MiMo-V2.6-Pro', SEED_CONTEXT],
24
+ ['mimo-v2.6-flash', 'MiMo-V2.6-Flash', SEED_CONTEXT],
25
+ ['mimo-v2.6-pro-ultraspeed', 'MiMo-V2.6-Pro-Ultraspeed', SEED_CONTEXT],
26
+ ['mimo-v2.5-pro', 'MiMo-V2.5-Pro', SEED_CONTEXT],
27
+ ['mimo-v2.5', 'MiMo-V2.5', SEED_CONTEXT],
28
+ ['mimo-v2-pro', 'MiMo-V2-Pro', SEED_CONTEXT],
29
+ ['mimo-v2-omni', 'MiMo-V2-Omni', 262_144],
30
+ ].map(([id, display_name, context_window]) =>
31
+ Object.freeze({ id, display_name, context_window, max_output_tokens: SEED_OUTPUT })
32
+ )
33
+ );
34
+
35
+ export function seedModelItems() {
36
+ return SEED_MODELS.map((item) => ({ ...item }));
37
+ }
38
+
39
+ // Speech/transcription ids cannot serve a text chat turn.
40
+ export function isChatModelId(id) {
41
+ const text = String(id || '').toLowerCase();
42
+ return Boolean(text) && !text.includes('-tts') && !text.includes('-asr');
43
+ }
44
+
45
+ // Prefer the flagship only when this credential actually serves it; otherwise
46
+ // the first chat model the roster lists, and the seed default for an empty one.
47
+ export function chooseDefaultModel(ids) {
48
+ const chat = (ids || []).filter(isChatModelId);
49
+ if (chat.includes(DEFAULT_MODEL)) return DEFAULT_MODEL;
50
+ return chat[0] || DEFAULT_MODEL;
51
+ }
52
+
53
+ // Exact seed id, else the longest seed whose id is a lineage prefix of the
54
+ // model (dated or suffixed variants of a seeded model).
55
+ function seedRow(model) {
56
+ const id = String(model || '').toLowerCase();
57
+ let seed = SEED_MODELS.find((item) => item.id === id) || null;
58
+ if (!seed) {
59
+ for (const item of SEED_MODELS) {
60
+ if (id.startsWith(`${item.id}-`) && (!seed || item.id.length > seed.id.length)) seed = item;
61
+ }
62
+ }
63
+ if (!seed) return null;
64
+ return {
65
+ id: String(model),
66
+ contextWindow: seed.context_window,
67
+ outputTokens: seed.max_output_tokens,
68
+ reasoning: true,
69
+ vision: true,
70
+ inputModalities: ['text', 'image'],
71
+ };
72
+ }
73
+
74
+ // Fill the gaps a /models row leaves (names, limits, effort levels).
75
+ export function decorateModelRows(rows, items) {
76
+ const display = new Map();
77
+ for (const item of items || []) {
78
+ if (typeof item?.id === 'string' && typeof item.display_name === 'string' && item.display_name.trim()) {
79
+ display.set(item.id, item.display_name.trim());
80
+ }
81
+ }
82
+ return rows.map((row) => {
83
+ const seed = seedRow(row.id);
84
+ return {
85
+ ...row,
86
+ name: display.get(row.id) || row.name,
87
+ contextWindow: row.contextWindow || seed?.contextWindow || 0,
88
+ outputTokens: row.outputTokens || seed?.outputTokens || null,
89
+ ...(seed && row.reasoning === undefined ? { reasoning: seed.reasoning } : {}),
90
+ ...(seed && row.vision === undefined ? { vision: seed.vision } : {}),
91
+ ...(seed && row.inputModalities === undefined ? { inputModalities: seed.inputModalities } : {}),
92
+ reasoningLevels: [...MIMO_EFFORT_LEVELS],
93
+ reasoningContentField: 'reasoning_content',
94
+ };
95
+ });
96
+ }
97
+
98
+ // Mixdog effort → MiMo `reasoning_effort`; the ladder's top edges fold to high.
99
+ function mimoEffort(value) {
100
+ const effort = String(value ?? '')
101
+ .trim()
102
+ .toLowerCase();
103
+ if (MIMO_EFFORT_LEVELS.includes(effort)) return effort;
104
+ return ['xhigh', 'max', 'ultra'].includes(effort) ? 'high' : null;
105
+ }
106
+
107
+ function thinkingDisabled(thinking, effort) {
108
+ const mode = String(thinking?.type ?? thinking ?? '')
109
+ .trim()
110
+ .toLowerCase();
111
+ return thinking === false || mode === 'disabled' || mode === 'off' || mode === 'none' || effort === 'none';
112
+ }
113
+
114
+ function namedToolChoice(choice) {
115
+ if (!choice || typeof choice !== 'object') return null;
116
+ const name = choice.function?.name || choice.name;
117
+ return typeof name === 'string' && name ? name : null;
118
+ }
119
+
120
+ function applyMimoParams(params, { opts = {}, config = {}, modelInfo = null } = {}) {
121
+ const temperature = Number(opts.temperature);
122
+ params.temperature = Number.isFinite(temperature) && temperature >= 0 ? temperature : MIMO_DEFAULT_TEMPERATURE;
123
+
124
+ for (const [key, wire] of [
125
+ ['topP', 'top_p'],
126
+ ['topK', 'top_k'],
127
+ ]) {
128
+ const value = Number(opts[key] ?? config?.[key]);
129
+ if ((opts[key] ?? config?.[key]) != null && Number.isFinite(value) && value >= 0) params[wire] = value;
130
+ }
131
+
132
+ const limit = Number(modelInfo?.outputTokens);
133
+ const requested = params.max_tokens || MIMO_DEFAULT_MAX_OUTPUT_TOKENS;
134
+ params.max_tokens = Number.isFinite(limit) && limit > 0 ? Math.min(requested, limit) : requested;
135
+
136
+ const thinking = opts.thinking ?? config?.thinking;
137
+ const rawEffort = opts.effort ?? config?.reasoningEffort;
138
+ const effort = String(rawEffort ?? '')
139
+ .trim()
140
+ .toLowerCase();
141
+ if (thinkingDisabled(thinking, effort)) {
142
+ params.thinking = { type: 'disabled' };
143
+ } else if (thinking !== undefined || effort) {
144
+ params.thinking = { type: 'enabled' };
145
+ const level = mimoEffort(effort);
146
+ if (level) params.reasoning_effort = level;
147
+ }
148
+
149
+ if (params.tools?.length) {
150
+ if (opts.toolChoice === 'required') params.tool_choice = 'required';
151
+ else {
152
+ const name = namedToolChoice(opts.toolChoice);
153
+ if (name) params.tool_choice = { type: 'function', function: { name } };
154
+ }
155
+ }
156
+ return params;
157
+ }
158
+
159
+ // Session affinity and client identity, sent per request.
160
+ export function mimoRequestHeaders(opts = {}) {
161
+ const headers = { 'User-Agent': `mimocode/${PACKAGE_VERSION}` };
162
+ if (opts.ephemeral === true) return headers;
163
+ const sessionId = String(opts.sessionId || opts.session?.id || '').trim();
164
+ if (sessionId) headers['x-session-affinity'] = sessionId;
165
+ const parentId = String(opts.parentSessionId || opts.session?.parentSessionId || '').trim();
166
+ if (sessionId && parentId) headers['x-parent-session-id'] = parentId;
167
+ return headers;
168
+ }
169
+
170
+ const FRIENDLY_GATEWAY_CODES = Object.freeze({
171
+ 421: 'Request blocked by content moderation',
172
+ 441: 'Request blocked by risk control',
173
+ });
174
+
175
+ // The gateway body is `{ error: { code, message, param } }`; the SDK exposes it
176
+ // as err.error (already unwrapped) or leaves it wrapped for stream events.
177
+ function gatewayError(err) {
178
+ const body = err?.error;
179
+ if (!body || typeof body !== 'object') return null;
180
+ return body.error && typeof body.error === 'object' ? body.error : body;
181
+ }
182
+
183
+ // Readable failure text: the friendly label for known block codes, otherwise
184
+ // the gateway message, followed by `param` when it carries the real reason.
185
+ export function mimoErrorDetail(err, secrets = []) {
186
+ const gw = gatewayError(err);
187
+ let text = '';
188
+ if (gw) {
189
+ const base = FRIENDLY_GATEWAY_CODES[String(gw.code)] || (typeof gw.message === 'string' ? gw.message : '');
190
+ if (base) text = typeof gw.param === 'string' && gw.param && gw.param !== base ? `${base}: ${gw.param}` : base;
191
+ }
192
+ return scrubOAuthSecrets(text || String(err?.message || err || ''), secrets).trim();
193
+ }
194
+
195
+ // Keep gateway detail on the error; a quota refusal can never succeed on replay.
196
+ function refineMimoError(err) {
197
+ if (!err || typeof err !== 'object') return err;
198
+ const gw = gatewayError(err);
199
+ if (!gw) return err;
200
+ const detail = mimoErrorDetail(err);
201
+ if (detail && detail !== err.message) err.message = detail;
202
+ if (gw.code !== undefined && gw.code !== null) err.mimoGatewayCode = String(gw.code);
203
+ if (typeof gw.param === 'string' && gw.param) err.mimoGatewayParam = gw.param;
204
+ if (String(gw.code || gw.type || '').toLowerCase() === 'insufficient_quota') {
205
+ err.code = 'insufficient_quota';
206
+ markProviderRecoveryExhausted(err, { owner: 'mimo-oauth' });
207
+ }
208
+ return err;
209
+ }
210
+
211
+ export const MIMO_CHAT_PROFILE = Object.freeze({
212
+ replaysReasoningContent: true,
213
+ firstByteTimeoutMs: MIMO_FIRST_BYTE_TIMEOUT_MS,
214
+ semanticIdleTimeoutMs: MIMO_SEMANTIC_IDLE_TIMEOUT_MS,
215
+ resolveModelInfo: (provider, model) => provider.getCachedModelInfo(model) || seedRow(model),
216
+ applyParams: applyMimoParams,
217
+ refineError: refineMimoError,
218
+ });
@@ -0,0 +1,239 @@
1
+ // MiMo subscription credentials: a long-lived API key minted by the MiMo
2
+ // platform authorize flow. There is no refresh grant — a 401 means the user
3
+ // has to sign in again. Stored in Mixdog's own token file (mimo-oauth.json).
4
+ import { randomBytes } from 'node:crypto';
5
+ import { readFileSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
6
+ import { join, resolve } from 'node:path';
7
+ import { getPluginData } from '../config.mjs';
8
+ import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
9
+ import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
10
+ import { accountIdentityFields, fileMtimeMs, scrubOAuthSecrets } from './lib/oauth-token-utils.mjs';
11
+
12
+ const DEFAULT_PLATFORM_ORIGIN = 'https://platform.xiaomimimo.com';
13
+ const DEFAULT_BASE_URL = 'https://api.xiaomimimo.com/v1';
14
+ export const DEFAULT_MODEL = 'mimo-v2.6-pro';
15
+ export const SOURCE_HEADER = Object.freeze({ 'X-Mimo-Source': 'mimocode-cli' });
16
+ export const LOGIN_TIMEOUT_MS = 5 * 60_000;
17
+ const TOKEN_PLAN_KEY_PREFIX = 'tp-';
18
+ // Probe order for a Token Plan key that arrives without a gateway URL.
19
+ export const TOKEN_PLAN_BASE_URLS = Object.freeze(
20
+ ['sgp', 'ams', 'cn'].map((region) => `https://token-plan-${region}.xiaomimimo.com/v1`)
21
+ );
22
+ const TOKEN_PLAN_PROBE_TIMEOUT_MS = 15_000;
23
+ const TOKEN_PLAN_PROBE_MODEL = 'mimo-v2.5';
24
+ // The only hosts an API key may be sent to: the default gateway and the Token
25
+ // Plan regional gateways. Derived from the constants above so they cannot drift.
26
+ const MIMO_API_HOSTS = new Set([DEFAULT_BASE_URL, ...TOKEN_PLAN_BASE_URLS].map((url) => new URL(url).hostname));
27
+ const MAX_BASE_URL_LENGTH = 2048;
28
+ const SAFE_BASE_PATH = /^(?:\/[A-Za-z0-9._~-]+)*\/?$/;
29
+
30
+ /**
31
+ * A gateway URL from the authorization payload, validated against the Xiaomi
32
+ * MiMo API host allowlist: HTTPS, no credentials, default port, no query or
33
+ * fragment. Returns it without a trailing slash; throws otherwise.
34
+ */
35
+ export function normalizeMimoBaseUrl(value) {
36
+ const reject = () =>
37
+ new Error('[mimo-oauth] authorization payload carried a gateway URL outside the Xiaomi MiMo API hosts');
38
+ if (typeof value !== 'string' || value.length > MAX_BASE_URL_LENGTH) throw reject();
39
+ let url;
40
+ try {
41
+ url = new URL(value);
42
+ } catch {
43
+ throw reject();
44
+ }
45
+ if (
46
+ url.protocol !== 'https:' ||
47
+ !MIMO_API_HOSTS.has(url.hostname) ||
48
+ url.username ||
49
+ url.password ||
50
+ url.port ||
51
+ url.search ||
52
+ url.hash ||
53
+ !SAFE_BASE_PATH.test(url.pathname)
54
+ ) {
55
+ throw reject();
56
+ }
57
+ return `${url.origin}${url.pathname.replace(/\/+$/, '')}`;
58
+ }
59
+
60
+ // Read per call so MIMO_PLATFORM_URL can point every login URL at another host.
61
+ function platformOrigin() {
62
+ const override = String(process.env.MIMO_PLATFORM_URL || '')
63
+ .trim()
64
+ .replace(/\/+$/, '');
65
+ return override || DEFAULT_PLATFORM_ORIGIN;
66
+ }
67
+ export const authorizeEndpoint = () => `${platformOrigin()}/authorize`;
68
+ export const manualRedirectUri = () => `${platformOrigin()}/authorize/code/callback`;
69
+ export const resultUrl = () => `${platformOrigin()}/authorize/callback`;
70
+
71
+ function dataDir() {
72
+ const dir = getPluginData();
73
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
74
+ return dir;
75
+ }
76
+
77
+ export function getOwnTokenPath() {
78
+ const bound = boundProviderAuthPath('mimo-oauth');
79
+ if (bound) return resolve(bound);
80
+ const explicit = process.env.MIMO_OAUTH_CREDENTIALS_PATH;
81
+ if (explicit) return resolve(explicit);
82
+ return join(dataDir(), 'mimo-oauth.json');
83
+ }
84
+
85
+ // Key name shown on the MiMo platform. Generated once, then reused so repeated
86
+ // sign-ins replace one key instead of piling up new ones.
87
+ export function getKeyName() {
88
+ const path = join(dataDir(), 'mimo-oauth-key-name.json');
89
+ try {
90
+ const saved = JSON.parse(readFileSync(path, 'utf-8'));
91
+ if (typeof saved?.keyName === 'string' && saved.keyName) return saved.keyName;
92
+ } catch {
93
+ /* generate below */
94
+ }
95
+ const keyName = `mimo-code-cli-key-${randomBytes(4).toString('hex')}`;
96
+ writeJsonAtomicSync(path, { keyName }, { lock: true, mode: 0o600 });
97
+ return keyName;
98
+ }
99
+
100
+ export function loadTokens() {
101
+ const path = getOwnTokenPath();
102
+ if (!existsSync(path)) return null;
103
+ try {
104
+ const raw = JSON.parse(readFileSync(path, 'utf-8'));
105
+ if (!raw?.apiKey) return null;
106
+ return {
107
+ apiKey: String(raw.apiKey),
108
+ uid: raw.uid ? String(raw.uid) : '',
109
+ baseUrl: raw.baseUrl || DEFAULT_BASE_URL,
110
+ source: 'own',
111
+ mtimeMs: fileMtimeMs(path),
112
+ };
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ export function saveTokens({ apiKey, uid, baseUrl }) {
119
+ const tokens = { apiKey, uid: uid ? String(uid) : '', baseUrl: baseUrl || DEFAULT_BASE_URL };
120
+ writeJsonAtomicSync(getOwnTokenPath(), tokens, { lock: true, fsyncDir: true, mode: 0o600, secret: true });
121
+ return tokens;
122
+ }
123
+
124
+ // Find the regional gateway that accepts a Token Plan key. Each endpoint gets
125
+ // its own fresh timeout so one slow region cannot starve the next; a 401 or a
126
+ // failure of any kind moves on (only caller cancellation stops); the last error
127
+ // is reported when every region fails.
128
+ export async function probeTokenPlanBaseUrl(apiKey, { fetchImpl = fetch, signal = null } = {}) {
129
+ let lastError = null;
130
+ let allRejected = true;
131
+ for (const baseUrl of TOKEN_PLAN_BASE_URLS) {
132
+ signal?.throwIfAborted();
133
+ const timeout = AbortSignal.timeout(TOKEN_PLAN_PROBE_TIMEOUT_MS);
134
+ try {
135
+ const res = await fetchImpl(`${baseUrl}/chat/completions`, {
136
+ method: 'POST',
137
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, ...SOURCE_HEADER },
138
+ body: JSON.stringify({
139
+ model: TOKEN_PLAN_PROBE_MODEL,
140
+ max_tokens: 1,
141
+ messages: [{ role: 'user', content: 'ping' }],
142
+ }),
143
+ redirect: 'error',
144
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
145
+ });
146
+ if (res.ok) return baseUrl;
147
+ const detail = scrubOAuthSecrets(await res.text().catch(() => ''), [apiKey])
148
+ .trim()
149
+ .slice(0, 300);
150
+ lastError = Object.assign(
151
+ new Error(`[mimo-oauth] Token Plan check failed (${res.status})${detail ? `: ${detail}` : ''}`),
152
+ { status: res.status, httpStatus: res.status, detail }
153
+ );
154
+ if (res.status !== 401) allRejected = false;
155
+ } catch (err) {
156
+ if (signal?.aborted) throw err;
157
+ allRejected = false;
158
+ lastError = err instanceof Error ? err : new Error(String(err));
159
+ }
160
+ }
161
+ if (lastError && allRejected) throw mimoReauthError(lastError.detail);
162
+ throw lastError || new Error('[mimo-oauth] Token Plan check failed');
163
+ }
164
+
165
+ // A rejected key needs a fresh sign-in; shared by the probe and the send path.
166
+ export function mimoReauthError(detail = '') {
167
+ return Object.assign(
168
+ new Error(
169
+ `[mimo-oauth] API key rejected (401) — open /providers in mixdog to sign in again${detail ? `: ${detail}` : ''}`
170
+ ),
171
+ { httpStatus: 401, status: 401, reauthRequired: true }
172
+ );
173
+ }
174
+
175
+ // A Token Plan key whose gateway URL is unknown is resolved once and persisted.
176
+ export async function ensureTokenPlanBaseUrl(tokens, options = {}) {
177
+ const needsProbe =
178
+ tokens.apiKey.startsWith(TOKEN_PLAN_KEY_PREFIX) && (!tokens.baseUrl || tokens.baseUrl === DEFAULT_BASE_URL);
179
+ if (!needsProbe) return tokens;
180
+ const { persist = true, ...probeOptions } = options;
181
+ const baseUrl = await probeTokenPlanBaseUrl(tokens.apiKey, probeOptions);
182
+ // The probe awaited the network; only write back if the stored credential is
183
+ // still the one that was probed (a newer sign-in must not be overwritten).
184
+ const current = persist ? loadTokens() : null;
185
+ if (current && current.apiKey === tokens.apiKey && current.uid === (tokens.uid || '')) {
186
+ saveTokens({ apiKey: tokens.apiKey, uid: tokens.uid, baseUrl });
187
+ }
188
+ return { ...tokens, baseUrl };
189
+ }
190
+
191
+ export function hasMimoOAuthCredentials() {
192
+ try {
193
+ return Boolean(loadTokens()?.apiKey);
194
+ } catch {
195
+ return false;
196
+ }
197
+ }
198
+
199
+ export function describeMimoOAuthCredentials() {
200
+ try {
201
+ const tokens = loadTokens();
202
+ if (!tokens) {
203
+ return {
204
+ authenticated: false,
205
+ usable: false,
206
+ refreshable: false,
207
+ reauthRequired: false,
208
+ status: 'Not Set',
209
+ detail: 'Mixdog token store',
210
+ };
211
+ }
212
+ return {
213
+ authenticated: true,
214
+ usable: true,
215
+ refreshable: false,
216
+ reauthRequired: false,
217
+ status: 'Valid',
218
+ detail: 'Mixdog token store; API key',
219
+ expiresAt: 0,
220
+ ...accountIdentityFields({ id: tokens.uid }),
221
+ };
222
+ } catch (err) {
223
+ return {
224
+ authenticated: false,
225
+ usable: false,
226
+ refreshable: false,
227
+ reauthRequired: false,
228
+ status: 'Error',
229
+ detail: String(err?.message || err).slice(0, 200),
230
+ };
231
+ }
232
+ }
233
+
234
+ export function forgetMimoOAuthCredentials() {
235
+ const path = getOwnTokenPath();
236
+ if (!existsSync(path)) return { removed: false };
237
+ unlinkSync(path);
238
+ return { removed: true };
239
+ }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Xiaomi MiMo subscription provider.
3
+ *
4
+ * Credentials are a platform-issued API key (mimo-oauth-tokens.mjs). Requests
5
+ * go through an inner OpenAI-compatible Chat Completions client pointed at the
6
+ * stored base URL. The key never expires and has no refresh grant: a 401 means
7
+ * the user must sign in again.
8
+ */
9
+ import { noteErrorUsage } from './lib/note-error-usage.mjs';
10
+ import { fileMtimeMs, scrubOAuthSecrets } from './lib/oauth-token-utils.mjs';
11
+ import { OpenAICompatProvider, preloadOpenAICompatRuntime } from './openai-compat.mjs';
12
+ import { fetchCompatModelItems, listCompatModels } from './openai-compat-models.mjs';
13
+ import {
14
+ SOURCE_HEADER,
15
+ ensureTokenPlanBaseUrl,
16
+ getOwnTokenPath,
17
+ loadTokens,
18
+ mimoReauthError,
19
+ } from './mimo-oauth-tokens.mjs';
20
+ import {
21
+ MIMO_CHAT_PROFILE,
22
+ chooseDefaultModel,
23
+ decorateModelRows,
24
+ isChatModelId,
25
+ mimoErrorDetail,
26
+ mimoRequestHeaders,
27
+ seedModelItems,
28
+ } from './mimo-oauth-profile.mjs';
29
+
30
+ const isUnauthorized = (err) => Number(err?.httpStatus || err?.status) === 401;
31
+
32
+ // Strip the key from an error (message plus any echoed gateway body).
33
+ function scrubErrorSecrets(err, apiKey) {
34
+ if (!err || typeof err !== 'object' || !apiKey) return err;
35
+ const seen = new Set();
36
+ const walk = (obj, depth) => {
37
+ if (!obj || typeof obj !== 'object' || seen.has(obj) || depth > 4) return;
38
+ seen.add(obj);
39
+ for (const k of Object.keys(obj)) {
40
+ const v = obj[k];
41
+ if (typeof v === 'string') {
42
+ if (v.includes(apiKey)) obj[k] = scrubOAuthSecrets(v, [apiKey]);
43
+ } else walk(v, depth + 1);
44
+ }
45
+ };
46
+ if (typeof err.message === 'string') err.message = scrubOAuthSecrets(err.message, [apiKey]);
47
+ walk(err.error, 0);
48
+ walk(err.body, 0);
49
+ return err;
50
+ }
51
+
52
+ // Redact where the transport first surfaces errors (request open and stream
53
+ // iteration), so the shared retry reporter never logs the key.
54
+ function redactClientErrors(client, apiKey) {
55
+ const completions = client?.chat?.completions;
56
+ if (!completions || !apiKey || completions.create?.mimoRedacting === apiKey) return;
57
+ const create = completions.create.bind(completions);
58
+ const wrapped = async (...args) => {
59
+ try {
60
+ const stream = await create(...args);
61
+ if (stream && typeof stream[Symbol.asyncIterator] === 'function') {
62
+ const iterate = stream[Symbol.asyncIterator].bind(stream);
63
+ stream[Symbol.asyncIterator] = () => {
64
+ const it = iterate();
65
+ return {
66
+ next: async (...a) => {
67
+ try {
68
+ return await it.next(...a);
69
+ } catch (err) {
70
+ throw scrubErrorSecrets(err, apiKey);
71
+ }
72
+ },
73
+ return: (...a) => it.return?.(...a),
74
+ };
75
+ };
76
+ }
77
+ return stream;
78
+ } catch (err) {
79
+ throw scrubErrorSecrets(err, apiKey);
80
+ }
81
+ };
82
+ wrapped.mimoRedacting = apiKey;
83
+ completions.create = wrapped;
84
+ }
85
+
86
+ export {
87
+ hasMimoOAuthCredentials,
88
+ describeMimoOAuthCredentials,
89
+ forgetMimoOAuthCredentials,
90
+ } from './mimo-oauth-tokens.mjs';
91
+ export { beginOAuthLogin, loginOAuth } from './mimo-oauth-login.mjs';
92
+
93
+ export class MimoOAuthProvider {
94
+ // No document block on this wire: a PDF is lowered to text before the request is built.
95
+ nativePdf = false;
96
+ // OpenAI-compatible usage: prompt_tokens includes cached. See registry.mjs.
97
+ static inputExcludesCache = false;
98
+ name = 'mimo-oauth';
99
+ config;
100
+ tokens = null;
101
+ _inner = null;
102
+ _innerKey = null;
103
+ _rosterItems = [];
104
+ _defaultModelKey = null;
105
+ _defaultModel = null;
106
+
107
+ constructor(config) {
108
+ this.config = config || {};
109
+ this.tokens = loadTokens();
110
+ try {
111
+ preloadOpenAICompatRuntime();
112
+ } catch {
113
+ /* authoritative send surfaces load failures */
114
+ }
115
+ }
116
+
117
+ ensureAuth() {
118
+ // Re-read when the token file changed (sign-in from another process).
119
+ const mtime = fileMtimeMs(getOwnTokenPath());
120
+ if (!mtime) {
121
+ // File gone (signed out / forgotten): drop every cached credential.
122
+ this.tokens = null;
123
+ this._inner = null;
124
+ this._innerKey = null;
125
+ }
126
+ if (!this.tokens || mtime > (this.tokens.mtimeMs || 0)) this.tokens = loadTokens() || this.tokens;
127
+ if (!this.tokens?.apiKey) {
128
+ throw new Error('[mimo-oauth] credentials not found — open /providers in mixdog to sign in first');
129
+ }
130
+ return this.tokens;
131
+ }
132
+
133
+ _ensureInner() {
134
+ const { apiKey, baseUrl } = this.ensureAuth();
135
+ const key = `${baseUrl}:${apiKey}`;
136
+ if (this._inner && this._innerKey === key) return this._inner;
137
+ const inner = new OpenAICompatProvider('mimo-oauth', {
138
+ ...this.config,
139
+ apiKey,
140
+ baseURL: baseUrl,
141
+ chatProfile: MIMO_CHAT_PROFILE,
142
+ // Explicit nulls keep OPENAI_ORG_ID / OPENAI_PROJECT_ID out of the requests.
143
+ clientOptions: { organization: null, project: null },
144
+ extraHeaders: { ...SOURCE_HEADER, Accept: 'application/json' },
145
+ });
146
+ inner._fetchModelItems = (args) => this._fetchRoster(inner, args);
147
+ redactClientErrors(inner.client, apiKey);
148
+ this._inner = inner;
149
+ this._innerKey = key;
150
+ return inner;
151
+ }
152
+
153
+ // Chat models from /models; a failed or empty listing falls back to the local
154
+ // seed roster. Only a rejected key is surfaced.
155
+ async _fetchRoster(inner, args) {
156
+ let items = [];
157
+ try {
158
+ items = (await fetchCompatModelItems(inner, args)).filter((item) => isChatModelId(item?.id));
159
+ } catch (err) {
160
+ if (isUnauthorized(err)) throw err;
161
+ }
162
+ if (!items.length) items = seedModelItems();
163
+ this._rosterItems = items;
164
+ return items;
165
+ }
166
+
167
+ // Shared send/list error shaping, also applied to credential setup failures.
168
+ // A rejected key needs a fresh sign-in; the gateway's own detail stays attached.
169
+ _shapeError(err, apiKey) {
170
+ noteErrorUsage(err);
171
+ if (isUnauthorized(err) && !err?.reauthRequired) return mimoReauthError(mimoErrorDetail(err, [apiKey]));
172
+ return err;
173
+ }
174
+
175
+ async _readyInner(signal = null) {
176
+ this.tokens = await ensureTokenPlanBaseUrl(this.ensureAuth(), { fetchImpl: this.config.fetchImpl, signal });
177
+ return this._ensureInner();
178
+ }
179
+
180
+ // Configured model wins; otherwise pick from this credential's own roster.
181
+ async _pickDefaultModel(inner) {
182
+ if (this._defaultModel && this._defaultModelKey === this._innerKey) return this._defaultModel;
183
+ await this._listRows(inner);
184
+ this._defaultModel = chooseDefaultModel(this._enrichedIds(inner));
185
+ this._defaultModelKey = this._innerKey;
186
+ return this._defaultModel;
187
+ }
188
+
189
+ _enrichedIds(inner) {
190
+ return (inner._enrichedModels || []).map((row) => row.id);
191
+ }
192
+
193
+ async _listRows(inner) {
194
+ const rows = decorateModelRows(await listCompatModels(inner, { throwOnError: true }), this._rosterItems);
195
+ inner._enrichedModels = rows;
196
+ return rows;
197
+ }
198
+
199
+ async send(messages, model, tools, sendOpts) {
200
+ let inner;
201
+ try {
202
+ inner = await this._readyInner(sendOpts?.signal || null);
203
+ } catch (err) {
204
+ throw this._shapeError(err, this.tokens?.apiKey);
205
+ }
206
+ const apiKey = this.tokens.apiKey;
207
+ // This wire is Chat Completions only; a caller cannot move it to Responses.
208
+ const { compatWireApi: _wire, responsesTransport: _transport, ...rest } = sendOpts || {};
209
+ const opts = { ...rest, requestHeaders: { ...mimoRequestHeaders(rest), ...rest.requestHeaders } };
210
+ try {
211
+ const useModel = model || (await this._pickDefaultModel(inner));
212
+ // _doSend directly: the compat wrapper's 401 handler reloads a static
213
+ // config key, which does not apply to this credential.
214
+ return await inner._doSend(messages, useModel, tools, opts);
215
+ } catch (err) {
216
+ throw this._shapeError(err, apiKey);
217
+ }
218
+ }
219
+
220
+ async listModels() {
221
+ let inner;
222
+ try {
223
+ inner = await this._readyInner();
224
+ return await this._listRows(inner);
225
+ } catch (err) {
226
+ throw this._shapeError(err, this.tokens?.apiKey);
227
+ }
228
+ }
229
+
230
+ async isAvailable() {
231
+ return loadTokens() !== null;
232
+ }
233
+ }