mixdog 0.7.16 → 0.7.18

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 (41) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/UNINSTALL.md +7 -4
  4. package/bin/statusline-lib.mjs +29 -6
  5. package/bin/statusline-route.mjs +273 -0
  6. package/bin/statusline.mjs +31 -8
  7. package/commands/model.md +61 -0
  8. package/hooks/session-start.cjs +15 -1
  9. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  10. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  11. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  12. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  13. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  14. package/package.json +1 -1
  15. package/rules/lead/01-general.md +3 -0
  16. package/scripts/gateway-model.mjs +596 -0
  17. package/scripts/lib/gateway-inventory.mjs +178 -0
  18. package/scripts/lib/gateway-settings.mjs +78 -0
  19. package/scripts/run-mcp.mjs +26 -1
  20. package/scripts/statusline-launcher-smoke.mjs +155 -2
  21. package/scripts/uninstall.mjs +44 -7
  22. package/server-main.mjs +252 -11
  23. package/setup/setup.html +1 -1
  24. package/src/agent/index.mjs +96 -5
  25. package/src/agent/orchestrator/bridge-trace.mjs +18 -0
  26. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +4 -1
  27. package/src/agent/orchestrator/providers/anthropic.mjs +6 -2
  28. package/src/agent/orchestrator/session/loop.mjs +145 -4
  29. package/src/agent/orchestrator/session/trim.mjs +1 -1
  30. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +62 -6
  31. package/src/agent/orchestrator/tools/graph-manifest.json +11 -11
  32. package/src/agent/orchestrator/tools/patch-manifest.json +7 -7
  33. package/src/agent/tool-defs.mjs +10 -3
  34. package/src/channels/lib/runtime-paths.mjs +39 -4
  35. package/src/gateway/claude-current.mjs +255 -0
  36. package/src/gateway/oauth-usage.mjs +598 -0
  37. package/src/gateway/route-meta.mjs +629 -0
  38. package/src/gateway/server.mjs +713 -0
  39. package/src/shared/llm/cost.mjs +1 -1
  40. package/src/shared/seed.mjs +25 -0
  41. package/tools.json +21 -3
@@ -0,0 +1,255 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+
5
+ export const CLAUDE_CURRENT_MODE = 'claude-current';
6
+ export const CLAUDE_CURRENT_CHOICE_ID = 'claude-current';
7
+
8
+ const DEFAULT_FAMILY_MODEL = Object.freeze({
9
+ opus: 'claude-opus-4-8',
10
+ sonnet: 'claude-sonnet-4-6',
11
+ haiku: 'claude-haiku-4-5-20251001',
12
+ });
13
+
14
+ const EFFORT_BUDGET = Object.freeze({
15
+ low: 1024,
16
+ medium: 4096,
17
+ high: 16384,
18
+ xhigh: 32768,
19
+ max: 32768,
20
+ });
21
+
22
+ function cleanString(v) {
23
+ return typeof v === 'string' && v.trim() ? v.trim() : '';
24
+ }
25
+
26
+ function num(v, fallback = null) {
27
+ if (v === null || v === undefined || v === '') return fallback;
28
+ const n = Number(v);
29
+ return Number.isFinite(n) ? n : fallback;
30
+ }
31
+
32
+ function cleanBool(v) {
33
+ if (v === true || v === false) return v;
34
+ if (typeof v !== 'string') return null;
35
+ const s = v.trim().toLowerCase();
36
+ if (['1', 'true', 'yes', 'on', 'fast', 'priority'].includes(s)) return true;
37
+ if (['0', 'false', 'no', 'off', 'standard'].includes(s)) return false;
38
+ return null;
39
+ }
40
+
41
+ function claudeConfigDir() {
42
+ return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
43
+ }
44
+
45
+ function pluginDataDir() {
46
+ return process.env.CLAUDE_PLUGIN_DATA || path.join(claudeConfigDir(), 'plugins', 'data', 'mixdog-trib-plugin');
47
+ }
48
+
49
+ function readJsonFile(file) {
50
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
51
+ catch { return null; }
52
+ }
53
+
54
+ function readFreshJsonFile(file, maxAgeMs) {
55
+ try {
56
+ const st = fs.statSync(file);
57
+ if (Number.isFinite(maxAgeMs) && maxAgeMs > 0 && Date.now() - st.mtimeMs > maxAgeMs) return null;
58
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ function familyDefaultModel(family) {
65
+ const f = String(family || '').toLowerCase();
66
+ if (!f) return null;
67
+ const envName = `ANTHROPIC_DEFAULT_${f.toUpperCase()}_MODEL`;
68
+ return cleanString(process.env[envName]) || DEFAULT_FAMILY_MODEL[f] || null;
69
+ }
70
+
71
+ export function stripClaudeContextSuffix(model) {
72
+ return cleanString(model).replace(/\[[^\]]+\]\s*$/i, '');
73
+ }
74
+
75
+ function contextWindowFromRawModel(raw, resolved) {
76
+ const s = cleanString(raw).toLowerCase();
77
+ if (/\[1m\]/.test(s)) return 1000000;
78
+ if (/^claude-(opus|sonnet)-4-(6|7|8)(?:$|-)/i.test(resolved || '')) return 1000000;
79
+ if (/^claude-haiku-4-5/i.test(resolved || '')) return 200000;
80
+ return null;
81
+ }
82
+
83
+ export function resolveClaudeModelAlias(model) {
84
+ const raw = cleanString(model);
85
+ if (!raw) return null;
86
+ const stripped = stripClaudeContextSuffix(raw);
87
+ if (/^claude-/i.test(stripped)) return stripped;
88
+ const family = stripped.match(/^(opus|sonnet|haiku)(?:[-_].*)?$/i)?.[1]?.toLowerCase();
89
+ if (family) return familyDefaultModel(family);
90
+ return null;
91
+ }
92
+
93
+ export function displayClaudeModel(model, raw = '', display = '') {
94
+ const explicit = cleanString(display);
95
+ if (explicit) return explicit;
96
+ const id = cleanString(model) || resolveClaudeModelAlias(raw) || cleanString(raw);
97
+ const m = id.match(/^claude-(opus|sonnet|haiku)-(\d+)-(\d+)(?:-\d{8})?$/i);
98
+ if (m) {
99
+ const family = `${m[1][0].toUpperCase()}${m[1].slice(1).toLowerCase()}`;
100
+ const suffix = /\[1m\]/i.test(raw) || contextWindowFromRawModel(raw, id) === 1000000 ? ' (1M context)' : '';
101
+ return `${family} ${m[2]}.${m[3]}${suffix}`;
102
+ }
103
+ return id || raw || '';
104
+ }
105
+
106
+ function normalizeEffort(v) {
107
+ const s = cleanString(v).toLowerCase();
108
+ if (!s) return null;
109
+ if (s === 'extra-high' || s === 'very-high') return 'xhigh';
110
+ if (['low', 'medium', 'high', 'xhigh', 'max'].includes(s)) return s;
111
+ return null;
112
+ }
113
+
114
+ function effortFromThinking(thinking) {
115
+ if (!thinking || typeof thinking !== 'object') return null;
116
+ const t = cleanString(thinking.type).toLowerCase();
117
+ if (t && t !== 'enabled') return null;
118
+ const budget = num(thinking.budget_tokens ?? thinking.budgetTokens, null);
119
+ if (!(budget > 0)) return null;
120
+ let best = null;
121
+ let bestDelta = Infinity;
122
+ for (const [effort, value] of Object.entries(EFFORT_BUDGET)) {
123
+ const delta = Math.abs(value - budget);
124
+ if (delta < bestDelta) { best = effort; bestDelta = delta; }
125
+ }
126
+ return best;
127
+ }
128
+
129
+ function fastFromPayload(payload) {
130
+ const direct = cleanBool(payload?.fast_mode)
131
+ ?? cleanBool(payload?.fastMode)
132
+ ?? cleanBool(payload?.fast);
133
+ if (direct !== null) return direct;
134
+ const speed = cleanString(payload?.speed || payload?.service_tier || payload?.serviceTier).toLowerCase();
135
+ if (speed === 'fast' || speed === 'priority') return true;
136
+ if (speed === 'standard' || speed === 'default') return false;
137
+ return null;
138
+ }
139
+
140
+ function routeFromPayload(payload, source) {
141
+ if (!payload || typeof payload !== 'object') return null;
142
+ const modelObj = payload.model && typeof payload.model === 'object' ? payload.model : null;
143
+ const rawModel = cleanString(modelObj?.id)
144
+ || cleanString(payload.model)
145
+ || cleanString(payload.request?.model)
146
+ || cleanString(payload.message?.model);
147
+ const model = resolveClaudeModelAlias(rawModel);
148
+ const effort = normalizeEffort(payload.effort?.level)
149
+ || normalizeEffort(payload.effortLevel)
150
+ || effortFromThinking(payload.thinking);
151
+ const fast = fastFromPayload(payload);
152
+ const thinkingBudgetTokens = num(payload.thinking?.budget_tokens ?? payload.thinking?.budgetTokens, null);
153
+ const contextWindow = num(payload.context_window?.context_window_size, null)
154
+ || contextWindowFromRawModel(rawModel, model);
155
+ if (!model && !effort && fast === null && !(thinkingBudgetTokens > 0)) return null;
156
+ return {
157
+ mode: CLAUDE_CURRENT_MODE,
158
+ source,
159
+ provider: 'anthropic-oauth',
160
+ model,
161
+ requestedModel: rawModel || null,
162
+ modelDisplay: model ? displayClaudeModel(model, rawModel, modelObj?.display_name) : null,
163
+ effort,
164
+ fast,
165
+ thinkingBudgetTokens: thinkingBudgetTokens > 0 ? thinkingBudgetTokens : null,
166
+ contextWindow: contextWindow > 0 ? contextWindow : null,
167
+ at: Date.now(),
168
+ };
169
+ }
170
+
171
+ function mergeRoute(base, next) {
172
+ if (!next) return base || null;
173
+ if (!base) return { ...next };
174
+ return {
175
+ ...base,
176
+ ...next,
177
+ provider: next.provider || base.provider,
178
+ model: next.model || base.model,
179
+ requestedModel: next.requestedModel || base.requestedModel,
180
+ modelDisplay: next.modelDisplay || base.modelDisplay,
181
+ effort: next.effort || base.effort,
182
+ fast: next.fast !== null && next.fast !== undefined ? next.fast : base.fast,
183
+ thinkingBudgetTokens: next.thinkingBudgetTokens || base.thinkingBudgetTokens,
184
+ contextWindow: next.contextWindow || base.contextWindow,
185
+ source: next.source || base.source,
186
+ };
187
+ }
188
+
189
+ function routeFromSettings() {
190
+ const settings = readJsonFile(path.join(claudeConfigDir(), 'settings.json'));
191
+ if (!settings || typeof settings !== 'object') return null;
192
+ const route = routeFromPayload({
193
+ model: cleanString(settings.model),
194
+ effortLevel: settings.effortLevel,
195
+ fast_mode: settings.fastMode ?? settings.fast_mode ?? settings.fast,
196
+ }, 'settings');
197
+ if (!route) return null;
198
+ route.thinkingEnabled = settings.alwaysThinkingEnabled === true || !!route.effort;
199
+ return route;
200
+ }
201
+
202
+ function routeFromPersistedStatus(maxAgeMs) {
203
+ const own = readFreshJsonFile(path.join(pluginDataDir(), 'claude-current-model.json'), maxAgeMs);
204
+ if (own && own.mode === CLAUDE_CURRENT_MODE) return own;
205
+ const last = readFreshJsonFile(path.join(claudeConfigDir(), 'cc-statusline-last.json'), maxAgeMs);
206
+ return routeFromPayload(last, 'cc-statusline-last');
207
+ }
208
+
209
+ export function readClaudeCodeCurrentRoute({ request = null, maxAgeMs = 10 * 60 * 1000 } = {}) {
210
+ let route = routeFromSettings();
211
+ route = mergeRoute(route, routeFromPersistedStatus(maxAgeMs));
212
+ route = mergeRoute(route, routeFromPayload(request, 'request'));
213
+ if (!route) {
214
+ const model = familyDefaultModel('opus');
215
+ route = {
216
+ mode: CLAUDE_CURRENT_MODE,
217
+ source: 'default',
218
+ provider: 'anthropic-oauth',
219
+ model,
220
+ requestedModel: 'opus',
221
+ modelDisplay: displayClaudeModel(model, 'opus[1m]'),
222
+ effort: null,
223
+ fast: false,
224
+ thinkingBudgetTokens: null,
225
+ contextWindow: 1000000,
226
+ at: Date.now(),
227
+ };
228
+ }
229
+ route.provider = route.provider || 'anthropic-oauth';
230
+ route.modelDisplay = route.modelDisplay || displayClaudeModel(route.model, route.requestedModel || route.model);
231
+ route.contextWindow = route.contextWindow || contextWindowFromRawModel(route.requestedModel || route.model, route.model);
232
+ if (route.fast === null || route.fast === undefined) route.fast = false;
233
+ return route;
234
+ }
235
+
236
+ export function writeClaudeCodeCurrentSnapshot(ccJsonInput) {
237
+ if (!ccJsonInput) return null;
238
+ let payload = null;
239
+ try { payload = JSON.parse(String(ccJsonInput)); } catch { return null; }
240
+ const route = routeFromPayload(payload, 'statusline');
241
+ if (!route?.model) return null;
242
+ const file = path.join(pluginDataDir(), 'claude-current-model.json');
243
+ try {
244
+ fs.mkdirSync(path.dirname(file), { recursive: true });
245
+ fs.writeFileSync(file, JSON.stringify(route, null, 2) + '\n');
246
+ } catch { /* best-effort statusline cache */ }
247
+ return route;
248
+ }
249
+
250
+ export function formatClaudeCurrentChoiceLabel(route = readClaudeCodeCurrentRoute()) {
251
+ const parts = [`Claude Code current: ${route.modelDisplay || route.model || 'current model'}`];
252
+ if (route.effort) parts.push(String(route.effort).toUpperCase());
253
+ if (route.fast === true) parts.push('fast');
254
+ return parts.join(' · ');
255
+ }