mixdog 1.1.0 → 1.1.2

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 (174) hide show
  1. package/README.md +13 -6
  2. package/package.json +1 -2
  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/release-paths.mjs +39 -2
  12. package/scripts/test.mjs +29 -7
  13. package/src/defaults/shipped-history.json +2 -0
  14. package/src/defaults/skills/computer-use/SKILL.md +1 -1
  15. package/src/defaults/skills/setup/references/actions.md +1 -0
  16. package/src/headless-role.mjs +2 -2
  17. package/src/lib/keychain-cjs.cjs +582 -17
  18. package/src/lib/rules-builder.cjs +0 -23
  19. package/src/rules/lead/LEAD.md +2 -2
  20. package/src/runtime/agent/orchestrator/config-storage.mjs +32 -4
  21. package/src/runtime/agent/orchestrator/config.mjs +18 -0
  22. package/src/runtime/agent/orchestrator/context/instruction-files.mjs +130 -0
  23. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +4 -0
  24. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +11 -0
  25. package/src/runtime/agent/orchestrator/providers/lib/open-login-url.mjs +9 -0
  26. package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +254 -0
  27. package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +218 -0
  28. package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +239 -0
  29. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +233 -0
  30. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-login.mjs +247 -0
  31. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +225 -0
  32. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +542 -0
  33. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +25 -0
  34. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +4 -1
  35. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +13 -0
  36. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +14 -6
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +99 -0
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +4 -8
  39. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -7
  40. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +8 -0
  41. package/src/runtime/agent/orchestrator/providers/registry.mjs +4 -0
  42. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +2 -0
  43. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +15 -1
  44. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +1 -5
  45. package/src/runtime/agent/orchestrator/session/context-utils.mjs +12 -14
  46. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -1
  47. package/src/runtime/agent/orchestrator/session/loop/tool-call-ids.mjs +90 -0
  48. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +19 -11
  49. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +1 -0
  50. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +28 -0
  51. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +8 -0
  52. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +3 -0
  53. package/src/runtime/agent/orchestrator/session/manager.mjs +1 -5
  54. package/src/runtime/agent/orchestrator/session/read-dedup.mjs +0 -1
  55. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +3 -0
  56. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +2 -0
  57. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +2 -0
  58. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +23 -0
  59. package/src/runtime/agent/orchestrator/session/turn-effort.mjs +18 -0
  60. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +11 -8
  61. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +9 -4
  62. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +245 -0
  63. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/command-preflight.mjs +13 -1
  64. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +3 -1
  65. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
  66. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +49 -0
  67. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +3 -3
  68. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -7
  69. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +61 -8
  70. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +28 -3
  71. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +73 -1
  72. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +2 -2
  73. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +3 -1
  74. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +17 -6
  75. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +23 -5
  76. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +93 -43
  77. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +32 -6
  78. package/src/runtime/agent/orchestrator/tools/builtin.mjs +9 -2
  79. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +3 -1
  80. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +12 -15
  81. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +2 -2
  82. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +4 -2
  83. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -91
  84. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +19 -4
  85. package/src/runtime/channels/index.mjs +1 -5
  86. package/src/runtime/channels/lib/runtime-paths.mjs +8 -23
  87. package/src/runtime/channels/lib/tool-dispatch.mjs +1 -13
  88. package/src/runtime/channels/lib/worker-ipc.mjs +2 -2
  89. package/src/runtime/channels/lib/worker-main.mjs +4 -34
  90. package/src/runtime/computer-bridge/client.mjs +25 -1
  91. package/src/runtime/computer-bridge/linux-action-capabilities.mjs +84 -0
  92. package/src/runtime/computer-bridge/linux-readiness.d.mts +52 -0
  93. package/src/runtime/computer-bridge/linux-readiness.mjs +411 -0
  94. package/src/runtime/computer-bridge/test-linux-display.mjs +5 -0
  95. package/src/runtime/effort-judge/compact-tokenizer.mjs +13 -8
  96. package/src/runtime/effort-judge/judge-input.mjs +5 -2
  97. package/src/runtime/effort-judge/model-install.mjs +9 -3
  98. package/src/runtime/effort-judge/model-manifest.json +7 -7
  99. package/src/runtime/memory/lib/embedding-worker.mjs +9 -3
  100. package/src/runtime/memory/lib/pg/supervisor.mjs +3 -0
  101. package/src/runtime/shared/config.mjs +50 -2
  102. package/src/runtime/shared/developer-options.mjs +7 -0
  103. package/src/runtime/shared/instruction-files-mode.mjs +9 -0
  104. package/src/runtime/shared/llm/model-catalog.mjs +2 -1
  105. package/src/runtime/shared/llm/openai-daybreak.mjs +79 -0
  106. package/src/runtime/shared/pristine-execution-contract.json +9 -0
  107. package/src/runtime/shared/pristine-execution.mjs +1 -1
  108. package/src/runtime/shared/provider-accounts.mjs +2 -0
  109. package/src/runtime/shared/resource-admission.mjs +10 -3
  110. package/src/runtime/shared/schedule-model-ref.mjs +11 -0
  111. package/src/runtime/shared/semver.d.mts +1 -0
  112. package/src/runtime/shared/semver.mjs +57 -0
  113. package/src/runtime/shared/session-approval-wait.mjs +35 -0
  114. package/src/runtime/shared/shell-display.mjs +4 -2
  115. package/src/runtime/shared/tool-surface.mjs +2 -2
  116. package/src/runtime/shared/turn-worktree-snapshot.mjs +11 -10
  117. package/src/runtime/shared/update-checker.mjs +2 -31
  118. package/src/session-runtime/boot/apis.mjs +5 -0
  119. package/src/session-runtime/boot/begin.mjs +0 -2
  120. package/src/session-runtime/boot/tools.mjs +5 -4
  121. package/src/session-runtime/model-recency.mjs +3 -0
  122. package/src/session-runtime/provider-auth/login.mjs +10 -10
  123. package/src/session-runtime/provider-auth/setup.mjs +22 -2
  124. package/src/session-runtime/provider-models/row-hydration.mjs +12 -4
  125. package/src/session-runtime/provider-readiness.mjs +4 -0
  126. package/src/session-runtime/resource-mcp-api.mjs +7 -3
  127. package/src/session-runtime/runtime-bootstrap.mjs +0 -4
  128. package/src/session-runtime/runtime-facade.mjs +5 -0
  129. package/src/session-runtime/runtime-paths.mjs +0 -1
  130. package/src/session-runtime/schedule-session-run.mjs +3 -1
  131. package/src/session-runtime/services/agent-tool/index-write-queue.mjs +55 -14
  132. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +15 -3
  133. package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +2 -0
  134. package/src/session-runtime/services/agent-tool/worker-index/approval-wait.mjs +97 -0
  135. package/src/session-runtime/services/agent-tool/worker-index/mutation-batch.mjs +4 -2
  136. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -2
  137. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +3 -0
  138. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +58 -2
  139. package/src/session-runtime/services/agent-tool/worker-index.mjs +69 -15
  140. package/src/session-runtime/services/agent-tool/worker-rows.mjs +2 -0
  141. package/src/session-runtime/services/channel-admin.mjs +4 -2
  142. package/src/session-runtime/services/channel-worker.mjs +1 -1
  143. package/src/session-runtime/services/provider-admin.mjs +80 -17
  144. package/src/session-runtime/services/seeds.mjs +7 -10
  145. package/src/session-runtime/services/usage-dashboard-model.mjs +6 -0
  146. package/src/session-runtime/settings-builtin-tools-api.mjs +15 -0
  147. package/src/session-runtime/settings-profile-api.mjs +27 -2
  148. package/src/session-runtime/setup-tool/executor.mjs +5 -0
  149. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  150. package/src/session-runtime/starter-guide-state.mjs +62 -0
  151. package/src/session-runtime/tool-policy-refresh.mjs +3 -0
  152. package/src/session-runtime/webhook-session-run.mjs +3 -1
  153. package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -2
  154. package/src/standalone/channel-binding.mjs +1 -2
  155. package/src/standalone/channel-transport/call-dispatch.mjs +4 -10
  156. package/src/standalone/daemon-channels-loader.mjs +2 -3
  157. package/src/standalone/local-session-runtime.mjs +2 -78
  158. package/src/standalone/session-protocol.mjs +2 -0
  159. package/src/tui/app/model-options.mjs +13 -1
  160. package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +10 -4
  161. package/src/tui/app/provider-setup/oauth-actions.mjs +5 -0
  162. package/src/tui/dist/index.mjs +84 -34
  163. package/src/tui/session/context-state.mjs +2 -0
  164. package/src/tui/session/live-share/owner.mjs +17 -0
  165. package/src/tui/session/oauth-flows.mjs +2 -0
  166. package/src/tui/session/session-api/extensions.mjs +12 -8
  167. package/src/tui/session/session-api/integrations.mjs +9 -8
  168. package/src/tui/session/session-api/routes.mjs +2 -2
  169. package/src/tui/session/session-api/settings.mjs +1 -0
  170. package/src/tui/session/session-api/shared.mjs +10 -6
  171. package/src/tui/session/tool-card-results/grouped-result.mjs +11 -2
  172. package/src/tui/session/transcript-spill/spill-dir.mjs +22 -6
  173. package/src/tui/spinner-meta.mjs +26 -10
  174. package/src/runtime/channels/tool-defs.mjs +0 -17
@@ -0,0 +1,542 @@
1
+ /**
2
+ * Muse Code subscription provider (Meta Muse Spark models).
3
+ *
4
+ * Inference authenticates with the subscription-minted Model API key (never
5
+ * the Meta account token) against https://api.meta.ai/v1 over the Responses
6
+ * wire, delegated to the shared OpenAI-compatible transport. The key never
7
+ * expires and cannot be refreshed: a 401 means the user must sign in again.
8
+ */
9
+ import { createHash } from 'node:crypto';
10
+ import { noteErrorUsage } from './lib/note-error-usage.mjs';
11
+ import { fileMtimeMs } from './lib/oauth-token-utils.mjs';
12
+ import { OpenAICompatProvider, preloadOpenAICompatRuntime } from './openai-compat.mjs';
13
+ import {
14
+ API_VERSION,
15
+ MODEL_API_BASE_URL,
16
+ getOwnTokenPath,
17
+ loadTokens,
18
+ requestMuseCodeKey,
19
+ scrubMuseSecrets,
20
+ } from './muse-code-oauth-tokens.mjs';
21
+
22
+ export {
23
+ hasMuseCodeOAuthCredentials,
24
+ describeMuseCodeOAuthCredentials,
25
+ forgetMuseCodeOAuthCredentials,
26
+ } from './muse-code-oauth-tokens.mjs';
27
+ export { beginOAuthLogin, loginOAuth } from './muse-code-oauth-login.mjs';
28
+
29
+ export const DEFAULT_MODEL = 'muse-spark-1.3';
30
+ const FIVE_LEVELS = Object.freeze(['minimal', 'low', 'medium', 'high', 'xhigh']);
31
+ const SIX_LEVELS = Object.freeze([...FIVE_LEVELS, 'max']);
32
+ const effortOptions = (levels) => [{ type: 'effort', values: [...levels] }];
33
+ // Image and voice generation ids are served by the same catalog but are not chat models.
34
+ const NON_CHAT_MODEL_RE = /^muse-(image|voice)-/i;
35
+ const SPARK_CONTEXT = 1_048_576;
36
+ const SPARK_MAX_OUTPUT = 131_072;
37
+ const STANDARD_COST = { inputCostPerM: 1.25, outputCostPerM: 4.25, cacheReadCostPerM: 0.15, cacheWriteCostPerM: 0 };
38
+ const CONTRIBUTOR_COST = { inputCostPerM: 0.1, outputCostPerM: 0.2, cacheReadCostPerM: 0.002, cacheWriteCostPerM: 0 };
39
+
40
+ // Seeded roster: the discovery endpoint lists bare ids, so limits, pricing and
41
+ // the effort ladder come from here. Only the 1.3 revisions accept `max`.
42
+ const SEEDS = Object.freeze({
43
+ 'muse-spark-1.1': { name: 'Muse Spark 1.1', levels: FIVE_LEVELS, cost: STANDARD_COST },
44
+ 'muse-spark-1.2': { name: 'Muse Spark 1.2', levels: FIVE_LEVELS, cost: STANDARD_COST },
45
+ 'muse-spark-1.2-contributor': { name: 'Muse Spark 1.2 (C)', levels: FIVE_LEVELS, cost: CONTRIBUTOR_COST },
46
+ 'muse-spark-1.3': { name: 'Muse Spark 1.3', levels: SIX_LEVELS, cost: STANDARD_COST },
47
+ 'muse-spark-1.3-contributor': { name: 'Muse Spark 1.3 (C)', levels: SIX_LEVELS, cost: CONTRIBUTOR_COST },
48
+ });
49
+ const SEED_MODELS = Object.freeze(Object.keys(SEEDS));
50
+
51
+ function seedRow(id) {
52
+ const seed = SEEDS[id];
53
+ return {
54
+ id,
55
+ name: seed.name,
56
+ provider: 'muse-code-oauth',
57
+ family: 'muse-spark',
58
+ contextWindow: SPARK_CONTEXT,
59
+ outputTokens: SPARK_MAX_OUTPUT,
60
+ supportsReasoning: true,
61
+ supportsVision: true,
62
+ supportsFunctionCalling: true,
63
+ reasoningLevels: [...seed.levels],
64
+ reasoningOptions: effortOptions(seed.levels),
65
+ ...seed.cost,
66
+ };
67
+ }
68
+
69
+ // Revisioned Spark ids the seed does not list inherit the newest seeded
70
+ // revision of their tier (limits, vision, pricing) but take the five-level
71
+ // ladder: only reviewed seeds advertise `max`.
72
+ const SPARK_ID_RE = /^muse-spark-(\d+)\.(\d+)(?:\.(\d+))?(-contributor)?$/i;
73
+
74
+ function revisionOf(id) {
75
+ const m = SPARK_ID_RE.exec(id);
76
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3] || 0)] : null;
77
+ }
78
+
79
+ function compareRevision(a, b) {
80
+ for (let i = 0; i < 3; i += 1) if (a[i] !== b[i]) return a[i] - b[i];
81
+ return 0;
82
+ }
83
+
84
+ function lineageRow(id) {
85
+ if (SEEDS[id]) return null;
86
+ const match = SPARK_ID_RE.exec(id);
87
+ if (!match) return null;
88
+ const contributor = Boolean(match[4]);
89
+ let template = null;
90
+ let templateRevision = null;
91
+ for (const seedId of SEED_MODELS) {
92
+ if (SPARK_ID_RE.exec(seedId)[4] ? !contributor : contributor) continue;
93
+ const candidate = revisionOf(seedId);
94
+ if (!templateRevision || compareRevision(candidate, templateRevision) > 0) {
95
+ template = seedId;
96
+ templateRevision = candidate;
97
+ }
98
+ }
99
+ if (!template) return null;
100
+ const revision =
101
+ match[3] && Number(match[3]) !== 0 ? `${match[1]}.${match[2]}.${match[3]}` : `${match[1]}.${match[2]}`;
102
+ return {
103
+ ...seedRow(template),
104
+ id,
105
+ name: `Muse Spark ${revision}${contributor ? ' (C)' : ''}`,
106
+ reasoningLevels: [...FIVE_LEVELS],
107
+ reasoningOptions: effortOptions(FIVE_LEVELS),
108
+ };
109
+ }
110
+
111
+ // Seeds win for the ids they cover; lineage rows yield to discovered metadata.
112
+ // A name supplied by discovery is always kept. Other discovered rows keep
113
+ // their own metadata and only gain the default effort ladder when they report none.
114
+ function museRow(id, discovered) {
115
+ const discoveredName =
116
+ typeof discovered?.name === 'string' && discovered.name && discovered.name !== id ? discovered.name : '';
117
+ if (SEEDS[id]) return { ...(discovered || {}), ...seedRow(id), ...(discoveredName ? { name: discoveredName } : {}) };
118
+ const lineage = lineageRow(id);
119
+ const row = {
120
+ id,
121
+ name: id,
122
+ provider: 'muse-code-oauth',
123
+ family: 'muse-spark',
124
+ ...(lineage || {}),
125
+ ...(discovered || {}),
126
+ };
127
+ if (discoveredName) row.name = discoveredName;
128
+ else if (lineage) row.name = lineage.name;
129
+ if (!row.reasoningOptions?.length) {
130
+ row.reasoningOptions = effortOptions(FIVE_LEVELS);
131
+ row.reasoningLevels = [...FIVE_LEVELS];
132
+ }
133
+ return row;
134
+ }
135
+
136
+ const EFFORT_DOWNGRADE = { max: 'xhigh' };
137
+ const SUMMARY_MODES = new Set(['auto', 'detailed', 'concise']);
138
+
139
+ // Cache identities beyond the wire limit become a bounded hash identity.
140
+ function normalizeCacheKey(value) {
141
+ const text = String(value ?? '').trim();
142
+ if (!text) return '';
143
+ const wellFormed = text.toWellFormed();
144
+ if (wellFormed.length <= 64) return wellFormed;
145
+ return `pc_${createHash('sha256').update(wellFormed).digest('hex').slice(0, 32)}`;
146
+ }
147
+
148
+ // Final request shaping on the Responses wire (installed through the shared
149
+ // transport's opt-in params hook).
150
+ function shapeResponsesParams(params, { opts, modelInfo }, config = {}) {
151
+ delete params.parallel_tool_calls;
152
+ const modelMax = Number(modelInfo?.outputTokens) > 0 ? Number(modelInfo.outputTokens) : SPARK_MAX_OUTPUT;
153
+ if (params.max_output_tokens) params.max_output_tokens = Math.min(params.max_output_tokens, modelMax);
154
+
155
+ delete params.reasoning;
156
+ const requested = String(opts.effort ?? config.reasoningEffort ?? '')
157
+ .trim()
158
+ .toLowerCase();
159
+ const allowed = modelInfo?.reasoningOptions?.find((o) => o?.type === 'effort')?.values || FIVE_LEVELS;
160
+ const forcedOff = opts.forceReasoningOff === true || requested === 'none' || requested === 'off';
161
+ if (forcedOff) {
162
+ // Explicit off: the only disable level the wire defines, with no encrypted replay.
163
+ params.reasoning = { effort: 'none' };
164
+ } else {
165
+ if (opts.disableReasoning === true) {
166
+ // Disabling without a request selects the model's lowest supported effort.
167
+ if (!requested && allowed.length) params.reasoning = { effort: allowed[0] };
168
+ } else if (requested || opts.reasoningSummary !== undefined) {
169
+ // A summary request alone still engages reasoning at the default effort.
170
+ const wanted = requested || 'medium';
171
+ const effort = allowed.includes(wanted) ? wanted : EFFORT_DOWNGRADE[wanted];
172
+ if (effort && allowed.includes(effort)) {
173
+ params.reasoning = { effort };
174
+ const summary = opts.reasoningSummary === undefined ? 'auto' : opts.reasoningSummary;
175
+ if (summary !== null) params.reasoning.summary = SUMMARY_MODES.has(summary) ? summary : 'auto';
176
+ }
177
+ }
178
+ }
179
+
180
+ // Caller includes survive (copied, deduped); encrypted reasoning is added
181
+ // only when reasoning is not forced off and the model and caller allow it.
182
+ const include = Array.isArray(opts.include) ? opts.include.filter((v) => typeof v === 'string' && v) : [];
183
+ if (!forcedOff && modelInfo?.supportsReasoning !== false && opts.includeEncryptedReasoning !== false) {
184
+ include.push('reasoning.encrypted_content');
185
+ }
186
+ if (include.length) params.include = [...new Set(include)];
187
+ else delete params.include;
188
+
189
+ for (const [field, value] of [
190
+ ['temperature', opts.temperature],
191
+ ['top_p', opts.topP ?? opts.top_p],
192
+ ['top_k', opts.topK ?? opts.top_k],
193
+ ['min_p', opts.minP ?? opts.min_p],
194
+ ['presence_penalty', opts.presencePenalty ?? opts.presence_penalty],
195
+ ['repetition_penalty', opts.repetitionPenalty ?? opts.repetition_penalty],
196
+ ]) {
197
+ if (typeof value === 'number' && Number.isFinite(value)) params[field] = value;
198
+ }
199
+
200
+ if (opts.cacheRetention !== 'none') {
201
+ const key = normalizeCacheKey(opts.promptCacheKey ?? opts.sessionId);
202
+ if (key) params.prompt_cache_key = key;
203
+ }
204
+
205
+ if (opts.extraBody && typeof opts.extraBody === 'object' && !Array.isArray(opts.extraBody)) {
206
+ Object.assign(params, opts.extraBody);
207
+ }
208
+ }
209
+
210
+ function resetAtMs(value) {
211
+ if (typeof value === 'string') {
212
+ const ms = Date.parse(value);
213
+ return Number.isFinite(ms) ? ms : 0;
214
+ }
215
+ const n = Number(value);
216
+ if (!Number.isFinite(n) || n <= 0) return 0;
217
+ return n < 1e12 ? n * 1000 : n;
218
+ }
219
+
220
+ const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
221
+
222
+ function usageStatus(usedFraction) {
223
+ if (usedFraction >= 1) return 'exhausted';
224
+ if (usedFraction >= 0.9) return 'warning';
225
+ return 'ok';
226
+ }
227
+
228
+ function usageWindow({ id, label, longLabel, durationMs }, entry, tier) {
229
+ const used = entry?.used_percent;
230
+ if (!entry || typeof used !== 'number' || !Number.isFinite(used) || used < 0) return null;
231
+ const resetAt = resetAtMs(entry.resets_at);
232
+ const usedPct = Math.min(100, used);
233
+ const usedFraction = usedPct / 100;
234
+ return {
235
+ id,
236
+ label,
237
+ longLabel,
238
+ source: 'muse-code-key',
239
+ usedPct: Math.round(usedPct * 100) / 100,
240
+ usedFraction,
241
+ remainingFraction: 1 - usedFraction,
242
+ status: usageStatus(usedFraction),
243
+ ...(durationMs ? { durationMs } : {}),
244
+ ...(tier ? { tier } : {}),
245
+ ...(resetAt ? { resetAt } : {}),
246
+ };
247
+ }
248
+
249
+ function rollingWindowSpec(minutes) {
250
+ const m = Number(minutes);
251
+ if (!Number.isFinite(m) || m <= 0) return { id: 'rolling', label: 'W', longLabel: 'Rolling Window' };
252
+ const rounded = Math.round(m);
253
+ const hours = rounded / 60;
254
+ const whole = rounded % 60 === 0;
255
+ const count = whole ? hours : rounded;
256
+ const unit = whole ? 'Hour' : 'Minute';
257
+ return {
258
+ id: `${rounded}m`,
259
+ label: whole ? `${hours}H` : `${rounded}M`,
260
+ longLabel: `${count} ${count === 1 ? unit : `${unit}s`}`,
261
+ durationMs: m * 60_000,
262
+ };
263
+ }
264
+
265
+ /** Map the key endpoint's `subs_usage` onto the shared quota-window shape. */
266
+ export function quotaWindowsFromSubsUsage(subsUsage, tier) {
267
+ const windows = [];
268
+ const rolling = usageWindow(rollingWindowSpec(subsUsage?.window?.window_duration_mins), subsUsage?.window, tier);
269
+ if (rolling) windows.push(rolling);
270
+ const weekly = usageWindow(
271
+ { id: '1w', label: '7D', longLabel: 'Weekly', durationMs: WEEK_MS },
272
+ subsUsage?.weekly,
273
+ tier
274
+ );
275
+ if (weekly) windows.push(weekly);
276
+ return windows;
277
+ }
278
+
279
+ function reauthError(detail, status = 401) {
280
+ return Object.assign(new Error(`[muse-code-oauth] ${detail} — open /providers in mixdog to sign in again`), {
281
+ reauthRequired: true,
282
+ status,
283
+ });
284
+ }
285
+
286
+ const USAGE_FAILURE_BACKOFF_MS = 5 * 60_000;
287
+
288
+ // Scrub the key from errors where the transport first surfaces them, so the
289
+ // shared retry reporter (which logs the original error) never sees it.
290
+ // Exact known secret values only: normal stream output must pass through unchanged.
291
+ function replaceExact(text, secrets) {
292
+ let out = text;
293
+ for (const s of secrets) if (s) out = out.split(s).join('[REDACTED]');
294
+ return out;
295
+ }
296
+
297
+ // `exact` true replaces only the known secret values; false additionally applies
298
+ // the generic field-pattern redaction (error objects and messages).
299
+ function scrubDeep(value, secrets, exact, seen = new WeakSet()) {
300
+ if (typeof value === 'string') return exact ? replaceExact(value, secrets) : scrubMuseSecrets(value, secrets);
301
+ if (!value || typeof value !== 'object' || seen.has(value)) return value;
302
+ seen.add(value);
303
+ if (value instanceof Error && typeof value.message === 'string') {
304
+ value.message = scrubMuseSecrets(value.message, secrets);
305
+ }
306
+ for (const k of Object.keys(value)) {
307
+ try {
308
+ value[k] = scrubDeep(value[k], secrets, exact, seen);
309
+ } catch {
310
+ /* read-only field */
311
+ }
312
+ }
313
+ return value;
314
+ }
315
+
316
+ function redactClientErrors(client, apiKey, accessToken) {
317
+ const responses = client?.responses;
318
+ const secrets = [apiKey, accessToken].filter(Boolean);
319
+ const marker = secrets.join('|');
320
+ if (!responses || responses.create?.museRedacting === marker) return;
321
+ const create = responses.create.bind(responses);
322
+ const redact = (err) => scrubDeep(err, secrets, false);
323
+ const wrapped = async (...args) => {
324
+ try {
325
+ const stream = await create(...args);
326
+ if (stream && typeof stream[Symbol.asyncIterator] === 'function') {
327
+ const iterate = stream[Symbol.asyncIterator].bind(stream);
328
+ stream[Symbol.asyncIterator] = () => {
329
+ const it = iterate();
330
+ return {
331
+ next: async (...a) => {
332
+ try {
333
+ const step = await it.next(...a);
334
+ // Any event payload can become an exception downstream; scrub every
335
+ // string field at the source rather than by event name.
336
+ if (step && !step.done) scrubDeep(step.value, secrets, true);
337
+ return step;
338
+ } catch (err) {
339
+ throw redact(err);
340
+ }
341
+ },
342
+ return: (...a) => it.return?.(...a),
343
+ };
344
+ };
345
+ }
346
+ return stream;
347
+ } catch (err) {
348
+ throw redact(err);
349
+ }
350
+ };
351
+ wrapped.museRedacting = marker;
352
+ responses.create = wrapped;
353
+ }
354
+
355
+ export class MuseCodeOAuthProvider {
356
+ // No document block on this wire: a PDF is lowered to text before the request is built.
357
+ nativePdf = false;
358
+ static inputExcludesCache = false;
359
+ name = 'muse-code-oauth';
360
+ config;
361
+ tokens = null;
362
+ _inner = null;
363
+ _innerKey = null;
364
+ _lastDiskScan = 0;
365
+ _usageFailure = null;
366
+ _usageLastGood = null;
367
+
368
+ _clearUsageState() {
369
+ this._usageFailure = null;
370
+ this._usageLastGood = null;
371
+ }
372
+
373
+ constructor(config) {
374
+ this.config = config || {};
375
+ this.tokens = loadTokens();
376
+ try {
377
+ preloadOpenAICompatRuntime();
378
+ } catch {
379
+ /* authoritative send surfaces load failures */
380
+ }
381
+ }
382
+
383
+ // The stored key is the whole credential; pick up a newer login from disk
384
+ // but never mint or refresh anything here.
385
+ async ensureAuth() {
386
+ const modified = fileMtimeMs(getOwnTokenPath());
387
+ if (modified === 0) {
388
+ // A missing credential file is signed out, whatever was cached.
389
+ if (this.tokens) this._clearUsageState();
390
+ this.tokens = null;
391
+ this._lastDiskScan = 0;
392
+ } else if (!this.tokens || modified !== this._lastDiskScan) {
393
+ this.tokens = loadTokens();
394
+ this._lastDiskScan = modified;
395
+ }
396
+ if (!this.tokens?.apiKey) {
397
+ throw new Error('[muse-code-oauth] credentials not found — open /providers in mixdog to sign in first');
398
+ }
399
+ return this.tokens;
400
+ }
401
+
402
+ _ensureInner(apiKey) {
403
+ if (this._inner && this._innerKey === apiKey) return this._inner;
404
+ this._inner = new OpenAICompatProvider('muse-code-oauth', {
405
+ ...this.config,
406
+ apiKey,
407
+ baseURL: MODEL_API_BASE_URL,
408
+ // JSON accept for discovery; streaming requests override it per call.
409
+ extraHeaders: { Accept: 'application/json', 'x-api-version': API_VERSION },
410
+ // Never inherit OPENAI_ORG_ID / OPENAI_PROJECT_ID from the environment.
411
+ clientOptions: { organization: null, project: null },
412
+ responsesSystemAsInstructions: true,
413
+ responsesParamsHook: (params, ctx) => shapeResponsesParams(params, ctx, this.config),
414
+ });
415
+ this._innerKey = apiKey;
416
+ // The Responses builder reads limits and the effort ladder from the inner
417
+ // provider's cached model row; seeds fill in what discovery omits.
418
+ const innerRow = this._inner.getCachedModelInfo.bind(this._inner);
419
+ this._inner.getCachedModelInfo = (model) => {
420
+ const discovered = innerRow(model);
421
+ return discovered || SEEDS[model] || lineageRow(model) ? museRow(model, discovered) : null;
422
+ };
423
+ return this._inner;
424
+ }
425
+
426
+ async send(messages, model, tools, sendOpts) {
427
+ const { apiKey, accessToken } = await this.ensureAuth();
428
+ const inner = this._ensureInner(apiKey);
429
+ redactClientErrors(inner.client, apiKey, accessToken);
430
+ // The endpoint accepts only tool_choice "auto"; the field is omitted entirely.
431
+ const { toolChoice: _unsupported, ...opts } = sendOpts || {};
432
+ try {
433
+ // _doSend bypasses the compat 401 handler, which re-reads a static config key.
434
+ return await inner._doSend(messages, model || DEFAULT_MODEL, tools, {
435
+ ...opts,
436
+ compatWireApi: 'responses',
437
+ requestHeaders: { Accept: 'text/event-stream', ...(opts.requestHeaders || {}) },
438
+ });
439
+ } catch (err) {
440
+ noteErrorUsage(err);
441
+ if (Number(err?.httpStatus || err?.status) === 401) {
442
+ const detail = scrubMuseSecrets(String(err?.message || ''), [apiKey])
443
+ .trim()
444
+ .slice(0, 300);
445
+ throw reauthError(`Model API key rejected${detail ? ` (${detail})` : ''}`);
446
+ }
447
+ if (err && typeof err.message === 'string') err.message = scrubMuseSecrets(err.message, [apiKey]);
448
+ throw err;
449
+ }
450
+ }
451
+
452
+ async listModels() {
453
+ const { apiKey } = await this.ensureAuth();
454
+ const inner = this._ensureInner(apiKey);
455
+ let items;
456
+ try {
457
+ items = await inner._fetchModelItems();
458
+ } catch {
459
+ // Discovery failed: fall back to the seeded roster.
460
+ return SEED_MODELS.map((id) => museRow(id));
461
+ }
462
+ // A successful roster is authoritative, even when it holds no chat models.
463
+ const rows = (Array.isArray(items) ? items : [])
464
+ .filter((m) => typeof m?.id === 'string' && m.id && !NON_CHAT_MODEL_RE.test(m.id))
465
+ .map((m) => {
466
+ const contextWindow = Number(m.context_window ?? m.max_context_window ?? m.context_length ?? 0);
467
+ const outputTokens = Number(m.max_output_tokens ?? m.output_tokens ?? 0);
468
+ const name = [m.name, m.display_name].find((v) => typeof v === 'string' && v.trim());
469
+ return museRow(m.id, {
470
+ ...(name ? { name: name.trim() } : {}),
471
+ ...(contextWindow > 0 ? { contextWindow } : {}),
472
+ ...(outputTokens > 0 ? { outputTokens } : {}),
473
+ ...(typeof m.created === 'number' ? { created: m.created } : {}),
474
+ });
475
+ });
476
+ inner._enrichedModels = rows;
477
+ return rows;
478
+ }
479
+
480
+ getCachedModelInfo(model) {
481
+ return this._inner?.getCachedModelInfo?.(model) || (SEEDS[model] || lineageRow(model) ? museRow(model) : null);
482
+ }
483
+
484
+ /** Subscription window/weekly usage, read from the key endpoint without onboarding. */
485
+ async getUsageSnapshot() {
486
+ const { accessToken, accountId, email: storedEmail } = await this.ensureAuth();
487
+ if (!accessToken) return null;
488
+ // State is bound to the credential: a replaced login never sees another account's cooldown.
489
+ const identity = `${accountId || ''}:${createHash('sha256').update(accessToken).digest('hex').slice(0, 16)}`;
490
+ if (this._usageFailure && this._usageFailure.identity !== identity) this._clearUsageState();
491
+ if (this._usageLastGood && this._usageLastGood.identity !== identity) this._usageLastGood = null;
492
+ const lastGood = () => this._usageLastGood?.snapshot ?? null;
493
+ // A recent failure suppresses re-querying the rate-limited endpoint.
494
+ if (this._usageFailure && Date.now() < this._usageFailure.until) {
495
+ if (this._usageFailure.error) throw this._usageFailure.error;
496
+ return lastGood();
497
+ }
498
+ const fail = (error) => {
499
+ // ±25% jitter keeps accounts from retrying in lockstep.
500
+ const cooldown = USAGE_FAILURE_BACKOFF_MS * (1 + (Math.random() * 0.5 - 0.25));
501
+ this._usageFailure = { identity, until: Date.now() + cooldown, error };
502
+ // A rejected credential must not keep showing stale quota.
503
+ if (error) this._usageLastGood = null;
504
+ return error;
505
+ };
506
+ let payload;
507
+ try {
508
+ payload = await requestMuseCodeKey(accessToken);
509
+ } catch (err) {
510
+ if (err?.status === 401 || err?.status === 403) {
511
+ throw fail(reauthError(`account session rejected (${err.status})`, err.status));
512
+ }
513
+ fail(null);
514
+ return lastGood();
515
+ }
516
+ if (payload.is_subs_active === false) throw fail(reauthError('Muse Code subscription is inactive', 403));
517
+ this._usageFailure = null;
518
+ const tier = payload.subs_tier_name?.trim() || payload.subs_tier_id?.trim() || undefined;
519
+ const quotaWindows = quotaWindowsFromSubsUsage(payload.subs_usage, tier);
520
+ if (!quotaWindows.length) return null;
521
+ const email = payload.user_email?.trim().toLowerCase() || storedEmail || undefined;
522
+ const { api_key: _apiKey, ...raw } = payload;
523
+ const snapshot = {
524
+ provider: 'muse-code-oauth',
525
+ model: null,
526
+ source: 'muse-code-key',
527
+ fetchedAt: Date.now(),
528
+ quotaWindows,
529
+ ...(tier ? { tier } : {}),
530
+ ...(email ? { email } : {}),
531
+ accountId: payload.user_id?.trim() || accountId || email || undefined,
532
+ raw,
533
+ rawKeys: Object.keys(raw).sort(),
534
+ };
535
+ this._usageLastGood = { identity, snapshot };
536
+ return snapshot;
537
+ }
538
+
539
+ async isAvailable() {
540
+ return Boolean(this.tokens?.apiKey || loadTokens()?.apiKey);
541
+ }
542
+ }
@@ -4,6 +4,7 @@ import { join } from 'node:path';
4
4
  import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
5
5
  import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
6
6
  import { cursorTokenExpiry } from './cursor-auth.mjs';
7
+ import { isValidMuseCredentialDocument } from './muse-code-oauth-tokens.mjs';
7
8
  import { developerOptionEnabled, developerOptionForProvider } from '../../../shared/developer-options.mjs';
8
9
 
9
10
  const ANTHROPIC_DEFAULT_CREDENTIALS_PATH = join(resolvePluginData(), 'anthropic-oauth-credentials.json');
@@ -166,6 +167,28 @@ function grokOAuthState() {
166
167
  );
167
168
  }
168
169
 
170
+ function mimoOAuthState() {
171
+ const paths = [
172
+ boundProviderAuthPath('mimo-oauth') ||
173
+ process.env.MIMO_OAUTH_CREDENTIALS_PATH ||
174
+ join(resolvePluginData(), 'mimo-oauth.json'),
175
+ ];
176
+ return memoProbe('mimo-oauth', paths, () =>
177
+ resolveProbeState(paths, (docs) => docs.some((own) => !!own?.apiKey))
178
+ );
179
+ }
180
+
181
+ function museCodeOAuthState() {
182
+ const paths = [
183
+ boundProviderAuthPath('muse-code-oauth') ||
184
+ process.env.MUSE_CODE_OAUTH_CREDENTIALS_PATH ||
185
+ join(resolvePluginData(), 'muse-code-oauth.json'),
186
+ ];
187
+ return memoProbe('muse-code-oauth', paths, () =>
188
+ resolveProbeState(paths, (docs) => docs.some(isValidMuseCredentialDocument))
189
+ );
190
+ }
191
+
169
192
  // Antigravity needs a resolved Cloud project alongside the tokens: without it
170
193
  // every request 400s, so a half-finished login must not enable the provider.
171
194
  function antigravityOAuthState() {
@@ -228,6 +251,8 @@ const OAUTH_PROBE_STATES = new Map([
228
251
  ['anthropic-oauth', anthropicOAuthState],
229
252
  ['openai-oauth', openAIOAuthState],
230
253
  ['grok-oauth', grokOAuthState],
254
+ ['muse-code-oauth', museCodeOAuthState],
255
+ ['mimo-oauth', mimoOAuthState],
231
256
  ['cursor-oauth', cursorOAuthState],
232
257
  ['antigravity-oauth', antigravityOAuthState],
233
258
  ]);
@@ -865,7 +865,10 @@ async function measureUsage(provider, providerObj, routeInfo) {
865
865
  if (provider === 'anthropic-oauth') return await fetchAnthropicUsage(providerObj);
866
866
  if (provider === 'grok-oauth') return await fetchGrokUsage(providerObj, routeInfo);
867
867
  if (
868
- (provider === 'cursor-oauth' || provider === 'cursor-api' || provider === 'antigravity-oauth') &&
868
+ (provider === 'cursor-oauth' ||
869
+ provider === 'cursor-api' ||
870
+ provider === 'antigravity-oauth' ||
871
+ provider === 'muse-code-oauth') &&
869
872
  typeof providerObj?.getUsageSnapshot === 'function'
870
873
  ) {
871
874
  return await providerObj.getUsageSnapshot();
@@ -6,6 +6,7 @@
6
6
  * per-family "latest" pass. No module state; openai-oauth.mjs re-exports
7
7
  * _displayCodexModel for existing importers.
8
8
  */
9
+ import { accessProgramOnly, catalogAccessPrograms } from '../../../shared/llm/openai-daybreak.mjs';
9
10
 
10
11
  // OAuth catalog returns dated ids (gpt-5.4-mini-2026-03-17). Strip the trailing
11
12
  // -YYYY-MM-DD to get the version alias (gpt-5.4-mini). Unknown shapes pass
@@ -59,6 +60,9 @@ export function _normalizeCodexModel(m) {
59
60
  ? m.additional_speed_tiers.map((t) => String(t || '').trim()).filter(Boolean)
60
61
  : [];
61
62
  const reasoningLevels = (m?.supported_reasoning_levels || []).map((r) => r.effort);
63
+ // Explicit access programs this caller may select (Daybreak Blue); absent
64
+ // when the catalog does not say, which is not the same as an empty list.
65
+ const accessPrograms = catalogAccessPrograms(m?.available_access_programs);
62
66
  // Catalog ids are version aliases without separate display dating.
63
67
  return {
64
68
  id,
@@ -97,6 +101,15 @@ export function _normalizeCodexModel(m) {
97
101
  serviceTiers,
98
102
  defaultServiceTier: m?.default_service_tier || null,
99
103
  additionalSpeedTiers,
104
+ ...(accessPrograms ? { accessPrograms } : {}),
105
+ // The catalog's own replacement for a model being retired.
106
+ ...(m?.upgrade?.model
107
+ ? { upgrade: { model: String(m.upgrade.model), retirementAt: m.upgrade.retirement_at || null } }
108
+ : {}),
109
+ // Out of the pickers: the catalog hides it, or it is a standalone
110
+ // access-program variant that a route toggle on a regular model replaces.
111
+ // Saved routes on it keep resolving.
112
+ ...(m?.visibility === 'hide' || accessProgramOnly({ accessPrograms }) ? { hidden: true } : {}),
100
113
  };
101
114
  }
102
115
 
@@ -55,7 +55,7 @@ function resolveReasoning(provider, useModel, opts) {
55
55
 
56
56
  /** The request body: lowered transcript plus the per-turn knobs (output cap,
57
57
  * tools and tool choice, reasoning effort). */
58
- function buildCompatResponsesParams({ provider, useModel, input, previousResponseId, tools, opts }) {
58
+ function buildCompatResponsesParams({ provider, useModel, input, previousResponseId, tools, opts, instructions }) {
59
59
  const params = {
60
60
  model: useModel,
61
61
  input,
@@ -73,6 +73,8 @@ function buildCompatResponsesParams({ provider, useModel, input, previousRespons
73
73
  applyCompatToolChoice(params, opts);
74
74
  const reasoning = resolveReasoning(provider, useModel, opts);
75
75
  if (reasoning) params.reasoning = reasoning;
76
+ // Opt-in system-as-instructions: set before the hook so the hook can override it.
77
+ if (instructions) params.instructions = instructions;
76
78
  // Opt-in per-provider final shaping (a provider passes the hook in its config).
77
79
  const hook = provider.config?.responsesParamsHook;
78
80
  if (typeof hook === 'function') {
@@ -163,11 +165,17 @@ export async function sendCompatResponses(provider, messages, useModel, tools, o
163
165
  // Opt-in: system prompts travel as top-level `instructions`, not input items.
164
166
  includeSystem: provider.config?.responsesSystemAsInstructions !== true,
165
167
  });
166
- const params = buildCompatResponsesParams({ provider, useModel, input, previousResponseId, tools, opts });
167
- if (provider.config?.responsesSystemAsInstructions === true) {
168
- const instructions = xaiSystemInstructions(messages);
169
- if (instructions) params.instructions = instructions;
170
- }
168
+ const instructions =
169
+ provider.config?.responsesSystemAsInstructions === true ? xaiSystemInstructions(messages) : '';
170
+ const params = buildCompatResponsesParams({
171
+ provider,
172
+ useModel,
173
+ input,
174
+ previousResponseId,
175
+ tools,
176
+ opts,
177
+ instructions,
178
+ });
171
179
 
172
180
  try {
173
181
  opts.onStageChange?.('requesting');