mixdog 1.0.1 → 1.0.3

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 (58) hide show
  1. package/README.md +8 -0
  2. package/package.json +2 -2
  3. package/src/defaults/skills/browser-use/SKILL.md +14 -6
  4. package/src/defaults/skills/setup/references/actions.md +6 -3
  5. package/src/defaults/skills/setup/references/surfaces.md +8 -4
  6. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +0 -1
  7. package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +10 -1
  8. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +40 -99
  9. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +6 -1
  10. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +2 -0
  11. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +7 -3
  12. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +1 -1
  13. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +18 -38
  14. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-path-fanout.mjs +59 -32
  15. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +15 -10
  16. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-request.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/tools/builtin/lib/shared-native-scan.mjs +12 -0
  18. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +14 -0
  19. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  20. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +3 -1
  21. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +17 -9
  22. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -0
  23. package/src/runtime/agent/orchestrator/tools/patch/wave.mjs +4 -0
  24. package/src/runtime/browser-bridge/input-fields.mjs +1 -1
  25. package/src/runtime/browser-bridge/tool-defs.mjs +1 -1
  26. package/src/runtime/computer-bridge/actions.mjs +0 -13
  27. package/src/runtime/media/adapters/antigravity-image.mjs +7 -3
  28. package/src/runtime/media/adapters/codex-image.mjs +12 -1
  29. package/src/runtime/media/adapters/gemini-image.mjs +17 -6
  30. package/src/runtime/media/adapters/gemini-video.mjs +19 -9
  31. package/src/runtime/media/adapters/xai-media.mjs +20 -7
  32. package/src/runtime/media/jobs.mjs +35 -3
  33. package/src/runtime/media/media-usage.mjs +126 -0
  34. package/src/runtime/media/tool.mjs +16 -4
  35. package/src/runtime/shared/llm/cost.mjs +50 -3
  36. package/src/runtime/shared/llm/model-catalog-projection.mjs +7 -2
  37. package/src/runtime/shared/llm/model-catalog.mjs +2 -1
  38. package/src/runtime/shared/llm/model-pricing-rates.mjs +21 -0
  39. package/src/runtime/shared/llm/usage-accounting.mjs +25 -5
  40. package/src/runtime/shared/llm/usage-context.mjs +6 -0
  41. package/src/runtime/shared/llm/usage-ledger-import.mjs +1 -0
  42. package/src/session-runtime/cwd-plugins/plugin-status.mjs +24 -4
  43. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +9 -1
  44. package/src/session-runtime/services/agent-tool/index-write-queue.mjs +38 -6
  45. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +9 -2
  46. package/src/session-runtime/services/agent-tool/worker-index.mjs +6 -3
  47. package/src/session-runtime/services/hook-bus/event-runner.mjs +10 -2
  48. package/src/session-runtime/services/hook-bus/tool-gate.mjs +5 -2
  49. package/src/session-runtime/setup-tool/tool-defs.mjs +1 -1
  50. package/src/session-runtime/workflow-agents-api/agent-route.mjs +8 -5
  51. package/src/tui/app/doctor.mjs +78 -2
  52. package/src/tui/dist/index.mjs +7 -3
  53. package/scripts/lib/bench-sandbox.test.mjs +0 -54
  54. package/scripts/lib/cli-args.test.mjs +0 -64
  55. package/scripts/lib/embedding-model-bench-core.test.mjs +0 -47
  56. package/scripts/lib/smoke-loop-shared.test.mjs +0 -22
  57. package/scripts/lib/trace-row.test.mjs +0 -47
  58. package/scripts/lib/trace-stats.test.mjs +0 -62
@@ -1,6 +1,7 @@
1
1
  // cwd-plugins/plugin-status.mjs — the registered-plugin status list with its
2
2
  // per-root caches (manifest keyed on mtime, MCP discovery and skill-file count
3
3
  // on a short TTL).
4
+ import { readFileSync } from 'node:fs';
4
5
  import { discoverPluginMcp } from '../../runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs';
5
6
  import { pluginMetadata } from '../../runtime/shared/plugin-metadata.mjs';
6
7
 
@@ -35,11 +36,25 @@ export function createPluginStatus({
35
36
  }
36
37
  return key;
37
38
  }
39
+ // A manifest file that exists but is not a JSON object is breakage.
40
+ function manifestInvalid(root) {
41
+ for (const rel of ['.codex-plugin/plugin.json', 'plugin.json']) {
42
+ const file = resolve(root, rel);
43
+ if (!existsSync(file)) continue;
44
+ try {
45
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
46
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return true;
47
+ } catch {
48
+ return true;
49
+ }
50
+ }
51
+ return false;
52
+ }
38
53
  function cachedPluginData(root) {
39
54
  const key = manifestMtimeKey(root);
40
55
  const hit = pluginRootCache.get(root);
41
56
  if (hit && hit.key === key) return hit;
42
- const entry = { key, manifest: pluginManifest(root) };
57
+ const entry = { key, manifest: pluginManifest(root), invalid: manifestInvalid(root) };
43
58
  pluginRootCache.set(root, entry);
44
59
  return entry;
45
60
  }
@@ -65,8 +80,9 @@ export function createPluginStatus({
65
80
 
66
81
  function registeredPlugin(entry, configuredMcp) {
67
82
  const root = clean(entry.root);
68
- if (!root || !existsSync(root)) return null;
69
- const manifest = cachedPluginData(root).manifest;
83
+ if (!root) return null;
84
+ if (!existsSync(root)) return { missing: true, id: clean(entry.id) || clean(entry.name) || root, name: clean(entry.name) || clean(entry.id) || root.split(/[\\/]/).pop() || root };
85
+ const { manifest, invalid } = cachedPluginData(root);
70
86
  const name = clean(manifest.name) || clean(manifest.id) || clean(entry.name) || root.split(/[\\/]/).pop() || root;
71
87
  const { mcpScript, mcpInline } = cachedMcpDiscovery(root);
72
88
  const plugin = {
@@ -89,6 +105,7 @@ export function createPluginStatus({
89
105
  mcpScript,
90
106
  mcpInline,
91
107
  };
108
+ if (invalid) plugin.invalid = true;
92
109
  plugin.mcpServerName = pluginMcpServerName(plugin);
93
110
  plugin.mcpEnabled =
94
111
  Object.hasOwn(configuredMcp, plugin.mcpServerName) ||
@@ -101,9 +118,11 @@ export function createPluginStatus({
101
118
  const dataDir = cfgMod.getPluginData?.();
102
119
  const configuredMcp = config?.mcpServers && typeof config.mcpServers === 'object' ? config.mcpServers : {};
103
120
  const plugins = [];
121
+ const missing = [];
104
122
  for (const entry of listRegisteredPlugins({ dataDir })) {
105
123
  const plugin = registeredPlugin(entry, configuredMcp);
106
- if (plugin) plugins.push(plugin);
124
+ if (plugin?.missing) missing.push({ id: plugin.id, name: plugin.name, enabled: entry.enabled !== false });
125
+ else if (plugin) plugins.push(plugin);
107
126
  }
108
127
  plugins.sort((a, b) => {
109
128
  if (a.source !== b.source) return a.source.localeCompare(b.source);
@@ -113,6 +132,7 @@ export function createPluginStatus({
113
132
  return {
114
133
  count: plugins.length,
115
134
  plugins,
135
+ missing,
116
136
  roots: {
117
137
  registry: admin.registryPath,
118
138
  installed: admin.installRoot,
@@ -27,7 +27,15 @@ export function createFeatureToolHandlers({ rt, setupTool, officeToolsEnabled, m
27
27
  media: async (args, { callerCtx, callerCwd }) => {
28
28
  requireEnabled(callerCtx, mediaToolEnabled, 'media');
29
29
  const { executeMediaTool } = await import('../../runtime/media/tool.mjs');
30
- return await executeMediaTool(args, { cwd: callerCwd, signal: signalFor(callerCtx) });
30
+ const { getSession } = await import('../../runtime/agent/orchestrator/session/manager/session-crud.mjs');
31
+ // Generation spend is attributed to the calling session in the usage ledger.
32
+ const sessionId = sessionIdFor(callerCtx) || '';
33
+ return await executeMediaTool(args, {
34
+ cwd: callerCwd,
35
+ signal: signalFor(callerCtx),
36
+ sessionId,
37
+ sourceType: (sessionId && getSession(sessionId)?.sourceType) || '',
38
+ });
31
39
  },
32
40
  tidy: async (args, { callerCtx, callerCwd }) => {
33
41
  requireEnabled(callerCtx, tidyToolEnabled, 'tidy');
@@ -51,6 +51,38 @@ function recordOps(args, run) {
51
51
  });
52
52
  }
53
53
 
54
+ /**
55
+ * Collapse a sequence of op sets into one op set per argument slot with the
56
+ * same sequential outcome: every key ever deleted is deleted (so a foreign
57
+ * writer's key is still removed), then each surviving key is set once with its
58
+ * final value, ordered as sequential insertion would leave it (an overwrite
59
+ * keeps its position, a delete/reinsert moves to the end). Set adds are unioned.
60
+ */
61
+ function compactOps(batch) {
62
+ const slots = [];
63
+ for (const ops of batch) {
64
+ ops.forEach((op, index) => {
65
+ if (!op) return;
66
+ let slot = slots[index];
67
+ if (!slot) {
68
+ slot = slots[index] = op.add ? { add: new Set() } : { del: new Set(), final: new Map() };
69
+ }
70
+ if (slot.add) {
71
+ for (const value of op.add) slot.add.add(value);
72
+ return;
73
+ }
74
+ for (const key of op.del) {
75
+ slot.del.add(key);
76
+ slot.final.delete(key);
77
+ }
78
+ for (const [key, json] of op.set) slot.final.set(key, json);
79
+ });
80
+ }
81
+ return slots.map((slot) =>
82
+ !slot ? null : slot.add ? { add: slot.add } : { del: slot.del, set: slot.final }
83
+ );
84
+ }
85
+
54
86
  function replayOps(ops, args) {
55
87
  args.forEach((arg, index) => {
56
88
  const op = ops[index];
@@ -64,6 +96,10 @@ function replayOps(ops, args) {
64
96
  });
65
97
  }
66
98
 
99
+ function replayBatch(batch, args) {
100
+ replayOps(compactOps(batch), args);
101
+ }
102
+
67
103
  /**
68
104
  * @param {object} options
69
105
  * @param {string|null} options.file index file; null → every write is a no-op
@@ -99,9 +135,7 @@ export function createIndexWriteQueue({ file, rewrite, readDoc, onPersisted = ()
99
135
  await updateJsonAtomic(
100
136
  file,
101
137
  (cur) =>
102
- rewrite(cur, (...args) => {
103
- for (const ops of batch) replayOps(ops, args);
104
- }),
138
+ rewrite(cur, (...args) => replayBatch(batch, args)),
105
139
  { lock: true }
106
140
  );
107
141
  return;
@@ -153,9 +187,7 @@ export function createIndexWriteQueue({ file, rewrite, readDoc, onPersisted = ()
153
187
  updateJsonAtomicSync(
154
188
  file,
155
189
  (cur) =>
156
- rewrite(cur, (...args) => {
157
- for (const set of ops) replayOps(set, args);
158
- }),
190
+ rewrite(cur, (...args) => replayBatch(ops, args)),
159
191
  { lock: true, timeoutMs: EXIT_LOCK_TIMEOUT_MS }
160
192
  );
161
193
  } catch {
@@ -101,9 +101,15 @@ export function createWorkerRowStore(file) {
101
101
  return rows;
102
102
  }
103
103
 
104
+ // Rows and tombstones must come from the same read/projection. In
105
+ // particular, do not stat and reload the file between the two collections.
106
+ function readSnapshot() {
107
+ const rows = readAll();
108
+ return { rows, tombstones: (projectedView() || cache)?.tombstones || [] };
109
+ }
110
+
104
111
  function readTombstones() {
105
- readAll();
106
- return (projectedView() || cache)?.tombstones || [];
112
+ return readSnapshot().tombstones;
107
113
  }
108
114
 
109
115
  // Single writer path: the mutator runs now over keyed maps against the
@@ -113,6 +119,7 @@ export function createWorkerRowStore(file) {
113
119
 
114
120
  return {
115
121
  readAll,
122
+ readSnapshot,
116
123
  readTombstones,
117
124
  write,
118
125
  /** Resolves once every write so far is on disk. */
@@ -27,16 +27,19 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
27
27
  const activeWorkerKeys = new Set();
28
28
 
29
29
  function readWorkerRows(context = {}) {
30
- const rows = store.readAll();
30
+ const { rows, tombstones: savedTombstones } = store.readSnapshot();
31
31
  if (rows.length === 0) return rows;
32
- const tombstones = new Map(readAllTagTombstones().map((row) => [tagTombstoneKey(row), row]));
32
+ const tombstones = new Map(recoverTombstoneOwners(savedTombstones).map((row) => [tagTombstoneKey(row), row]));
33
33
  return rows.filter(
34
34
  (row) => rowMatchesContext(row, context) && !tombstoneBlocksWork(row, findTagTombstone(row, tombstones))
35
35
  );
36
36
  }
37
37
 
38
38
  function readAllTagTombstones() {
39
- const rows = store.readTombstones();
39
+ return recoverTombstoneOwners(store.readTombstones());
40
+ }
41
+
42
+ function recoverTombstoneOwners(rows) {
40
43
  if (!rows.some((row) => !clean(row.parentSessionId || row.ownerSessionId))) return rows;
41
44
  const sessions = typeof mgr.listSessions === 'function' ? mgr.listSessions({ includeClosed: true }) : [];
42
45
  // Resolve legacy ownership against all sessions, never just the caller's
@@ -11,8 +11,8 @@ import { parseHandlerOutput } from './handlers.mjs';
11
11
  import { createHandlerDispatch } from './handler-dispatch.mjs';
12
12
  import { handlerDedupeKey, shellCountFor } from './rules.mjs';
13
13
 
14
- // A failed run is reported as a hook:error and skipped; returns the message
15
- // or null when the run produced usable output.
14
+ // A failed run is reported as a hook:error and blocks PreToolUse; other events
15
+ // skip it. Returns the message or null when the run produced usable output.
16
16
  function runFailure(run, handler) {
17
17
  if (run.timedOut)
18
18
  return `hook ${shellCountFor(handler)} timed out: ${handler.command || handler.url || handler.type}`;
@@ -23,6 +23,12 @@ function runFailure(run, handler) {
23
23
  return null;
24
24
  }
25
25
 
26
+ function failClosed(agg, message) {
27
+ agg.blocked = true;
28
+ agg.reason = `PreToolUse policy check failed: ${message}`;
29
+ return agg;
30
+ }
31
+
26
32
  function foldParsedOutput(agg, parsed) {
27
33
  if (parsed.additionalContext) agg.additionalContext.push(parsed.additionalContext);
28
34
  if (parsed.updatedInput && !agg.updatedInput) agg.updatedInput = parsed.updatedInput;
@@ -90,12 +96,14 @@ export function createEventRunner({ loadConfig, emit, cursor, pluginData, prompt
90
96
  } catch (error) {
91
97
  throwIfAborted(signal);
92
98
  emit('hook:error', { name: payload.tool_name || eventName, error: error?.message || String(error) });
99
+ if (eventName === 'PreToolUse') return failClosed(agg, error?.message || String(error));
93
100
  continue;
94
101
  }
95
102
  if (!run) continue;
96
103
  const failure = runFailure(run, handler);
97
104
  if (failure) {
98
105
  emit('hook:error', { name: payload.tool_name || eventName, error: failure });
106
+ if (eventName === 'PreToolUse') return failClosed(agg, failure);
99
107
  continue;
100
108
  }
101
109
  foldParsedOutput(agg, parseHandlerOutput(run, eventName));
@@ -65,8 +65,11 @@ export function createToolGate({ loadConfig, loadRules, runEventHandlers, emit,
65
65
  return standardDecision(agg, input) ?? legacyDecision(cfg, input);
66
66
  } catch (error) {
67
67
  throwIfAborted(signal);
68
- emit('hook:error', { name: input.name || input.tool_name || 'tool', error: error?.message || String(error) });
69
- return null;
68
+ const message = error?.message || String(error);
69
+ emit('hook:error', { name: input.name || input.tool_name || 'tool', error: message });
70
+ const reason = `PreToolUse policy check failed: ${message}`;
71
+ emit('tool:deny', { ...identityOf(input), reason });
72
+ return { action: 'deny', reason };
70
73
  }
71
74
  };
72
75
  }
@@ -204,7 +204,7 @@ export const SETUP_TOOL_DEFS = Object.freeze([
204
204
  },
205
205
  name: {
206
206
  type: 'string',
207
- description: `set_builtin_enabled: ${SETUP_BUILTIN_TOGGLE_FEATURES.join('|')}; install_builtin also accepts memory. Memory toggles use set_memory_enabled. set_first_use_approval: browser|computer; else MCP/plugin/provider id.`,
207
+ description: `set_builtin_enabled: ${SETUP_BUILTIN_TOGGLE_FEATURES.join('|')}; install_builtin also accepts memory. Memory toggles use set_memory_enabled. set_first_use_approval: browser|computer; revoke_linked_device: client id from status connection, not its display name; else MCP/plugin/provider id.`,
208
208
  },
209
209
  modelId: {
210
210
  type: 'string',
@@ -68,8 +68,10 @@ export function createAgentRouteApi(deps) {
68
68
  const requested = { ...(next || {}) };
69
69
  const stored = agentRouteFromConfig(getConfig(), id) || {};
70
70
  // Off is an explicit state, stored apart from the route so the model the
71
- // user picked survives and comes back when the agent is switched on.
72
- if (requested.disabled === true) {
71
+ // user picked survives and comes back when the agent is switched on. An
72
+ // off request that also carries a model saves that model and stays off.
73
+ const keepOff = requested.disabled === true;
74
+ if (keepOff && !(clean(requested.provider) && clean(requested.model))) {
73
75
  saveDisabled(id, true);
74
76
  return { ...stored, id, disabled: true };
75
77
  }
@@ -77,6 +79,7 @@ export function createAgentRouteApi(deps) {
77
79
  saveDisabled(id, false);
78
80
  return { ...stored, id, disabled: false, inherited: !stored.provider };
79
81
  }
82
+ delete requested.disabled;
80
83
  // Tuning-only edits must preserve the selected agent model, not silently inherit Main.
81
84
  const current = stored.provider ? stored : normalizeWorkflowRoute(resolveRoute(getConfig(), {})) || {};
82
85
  if (!hasOwn(requested, 'provider')) requested.provider = current.provider;
@@ -87,11 +90,11 @@ export function createAgentRouteApi(deps) {
87
90
  if (!clean(requested.model)) throw new Error('agent route requires provider and model');
88
91
  const routeToSave = normalizeWorkflowRoute(await resolveSelectedRoute(requested, stored));
89
92
  if (!routeToSave) throw new Error('agent route requires provider and model');
90
- // Picking a model is also the "on" switch — the two states are exclusive.
91
- const nextConfig = withAgentDisabled(getConfig(), id, false);
93
+ // Picking a model is also the "on" switch unless the request keeps it off.
94
+ const nextConfig = withAgentDisabled(getConfig(), id, keepOff);
92
95
  nextConfig.agents = { ...(nextConfig.agents || {}), [id]: routeToSave };
93
96
  saveConfigAndAdopt(canonicalizeAgentRouteStorage(nextConfig));
94
- return routeToSave;
97
+ return keepOff ? { ...routeToSave, id, disabled: true } : routeToSave;
95
98
  }
96
99
 
97
100
  return { setAgentRoute };
@@ -15,6 +15,7 @@
15
15
  import { compareSemver } from '../../runtime/shared/update-checker.mjs';
16
16
  import { resolvePluginData } from '../../runtime/shared/plugin-paths.mjs';
17
17
  import { hasEnabledAutomation } from '../../runtime/shared/automation-presence.mjs';
18
+ import { builtinFeatureActive } from '../../runtime/agent/orchestrator/runtime-core/builtin-features.mjs';
18
19
  import { providerRowUsable } from './provider-usable.mjs';
19
20
  import { constants as fsConstants, readFileSync } from 'node:fs';
20
21
  import { access, readdir, readFile } from 'node:fs/promises';
@@ -247,16 +248,78 @@ function reportRegistry(label) {
247
248
  const broken = active.filter(
248
249
  (entry) => entry.broken || entry.error || entry.invalid || entry.dependencyIssues?.length
249
250
  );
251
+ const missing = Array.isArray(status.missing)
252
+ ? status.missing.filter((entry) => entry.enabled !== false)
253
+ : [];
250
254
  let detail = `${active.length}/${entries.length} active${scopeDetail}`;
251
- if (!broken.length) {
255
+ const issues = [...broken, ...(label === 'plugins' ? missing : [])];
256
+ if (!issues.length) {
252
257
  row('ok', detail);
253
258
  return;
254
259
  }
255
- detail += ` · issues: ${broken.map((entry) => entry.name || entry.id).join(', ')}`;
260
+ detail += ` · issues: ${issues.map((entry) => entry.name || entry.id).join(', ')}`;
256
261
  row('warn', detail, { command: `/${label}` });
257
262
  };
258
263
  }
259
264
 
265
+ function reportLocalProvider(local, row) {
266
+ if (typeof local.installed !== 'boolean') {
267
+ row('warn', 'status unavailable');
268
+ return;
269
+ }
270
+ if (!local.installed) {
271
+ row('ok', 'not installed');
272
+ return;
273
+ }
274
+ if (local.enabled !== true) {
275
+ row('ok', 'installed · disabled');
276
+ return;
277
+ }
278
+ const fix = { command: '/providers' };
279
+ const models = Array.isArray(local.models) ? local.models.filter((model) => model.installed === true).length : 0;
280
+ const jobs = Array.isArray(local.installations) ? local.installations : [];
281
+ const latest = jobs.reduce((a, b) => (a && (a.updatedAt || 0) > (b.updatedAt || 0) ? a : b), null);
282
+ if (latest?.state === 'failed') {
283
+ row('warn', `installed · enabled · ${models} models · latest installation failed`, fix);
284
+ } else if (!models) {
285
+ row('warn', 'installed · enabled · no model installed', fix);
286
+ } else row('ok', `installed · enabled · ${models} models installed`);
287
+ }
288
+
289
+ function reportBuiltins(settings, row) {
290
+ const groups = { enabled: [], disabled: [], 'not installed': [] };
291
+ for (const id of ['git', 'office', 'tidy']) {
292
+ const entry = settings[id];
293
+ if (!entry || typeof entry.installed !== 'boolean') {
294
+ row('warn', 'status unavailable');
295
+ return;
296
+ }
297
+ if (!entry.installed) groups['not installed'].push(id);
298
+ else groups[entry.enabled === false ? 'disabled' : 'enabled'].push(id);
299
+ }
300
+ const parts = Object.entries(groups)
301
+ .filter(([, ids]) => ids.length)
302
+ .map(([state, ids]) => `${state}: ${ids.join(', ')}`);
303
+ const bridge = (id) => (builtinFeatureActive(undefined, id) ? 'bridge active' : 'no bridge');
304
+ row('ok', `${parts.join(' · ')} · browser use ${bridge('browser')} · computer use ${bridge('computer')}`);
305
+ }
306
+
307
+ function reportVoice(voice, row) {
308
+ if (typeof voice.enabled !== 'boolean') {
309
+ row('warn', 'status unavailable');
310
+ return;
311
+ }
312
+ if (!voice.enabled) {
313
+ row('ok', 'disabled');
314
+ return;
315
+ }
316
+ if (voice.installed !== true || voice.components?.model === false) {
317
+ row('warn', 'enabled · runtime or model not installed', { hint: 'reinstall voice from Extensions' });
318
+ return;
319
+ }
320
+ row('ok', 'enabled · runtime and model installed');
321
+ }
322
+
260
323
  function reportHooks(hooks, row) {
261
324
  if (
262
325
  typeof hooks.enabled !== 'boolean' ||
@@ -333,6 +396,19 @@ export async function runDoctorChecks(runtime = {}, getState = () => ({}), optio
333
396
  ['providers', 'providers', () => runtime.getProviderSetup?.(), reportProviders(getState)],
334
397
  ['mcp', 'mcp', () => runtime.mcpStatus?.(), reportMcp],
335
398
  ['memory', 'memory', async () => (await runtime.getToolModuleSettings?.())?.memory, reportMemory(runtime)],
399
+ [
400
+ 'localProvider',
401
+ 'local provider',
402
+ async () => (await runtime.getToolModuleSettings?.())?.localProvider,
403
+ reportLocalProvider,
404
+ ],
405
+ ['builtins', 'built-ins', async () => runtime.getToolModuleSettings?.(), reportBuiltins],
406
+ [
407
+ 'voice',
408
+ 'voice',
409
+ async () => (runtime.getVoiceStatus || (await import('../lib/voice-setup.mjs')).getVoiceStatus)(),
410
+ reportVoice,
411
+ ],
336
412
  [
337
413
  'channels',
338
414
  'channels',
@@ -47669,7 +47669,11 @@ var LITELLM_NUMBER_FIELDS = [
47669
47669
  "input_cost_per_token",
47670
47670
  "output_cost_per_token",
47671
47671
  "cache_read_input_token_cost",
47672
- "cache_creation_input_token_cost"
47672
+ "cache_creation_input_token_cost",
47673
+ "output_cost_per_image",
47674
+ "output_cost_per_image_token",
47675
+ "output_cost_per_video_token",
47676
+ "output_cost_per_second"
47673
47677
  ];
47674
47678
  var LITELLM_FLAG_FIELDS = [
47675
47679
  "supports_vision",
@@ -47688,9 +47692,9 @@ function projectLitellmRow(row) {
47688
47692
  if (typeof value === "number" && Number.isFinite(value)) out[field] = value;
47689
47693
  }
47690
47694
  for (const [field, value] of Object.entries(row)) {
47691
- if (/^(?:input_cost_per_token|output_cost_per_token|cache_read_input_token_cost|cache_creation_input_token_cost)(?:_above_\d+k_tokens)?(?:_priority)?$/.test(
47695
+ if ((/^(?:input_cost_per_token|output_cost_per_token|cache_read_input_token_cost|cache_creation_input_token_cost)(?:_above_\d+k_tokens)?(?:_priority)?$/.test(
47692
47696
  field
47693
- ) && typeof value === "number" && Number.isFinite(value))
47697
+ ) || /^output_cost_per_second_\w+$/.test(field)) && typeof value === "number" && Number.isFinite(value))
47694
47698
  out[field] = value;
47695
47699
  }
47696
47700
  for (const field of LITELLM_FLAG_FIELDS) {
@@ -1,54 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import test from 'node:test';
6
- import {
7
- authArtifactNamesForSandbox,
8
- copyAuthArtifacts,
9
- extractSessionId,
10
- readUnifiedConfig,
11
- } from './bench-sandbox.mjs';
12
-
13
- function withDir(run) {
14
- const dir = mkdtempSync(join(tmpdir(), 'bench-sandbox-'));
15
- try {
16
- return run(dir);
17
- } finally {
18
- rmSync(dir, { recursive: true, force: true });
19
- }
20
- }
21
-
22
- test('extractSessionId prefers the labelled id, then any sess_ token', () => {
23
- assert.equal(extractSessionId('sessionId: sess_abc123 other sess_zzz'), 'sess_abc123');
24
- assert.equal(extractSessionId('noise sess_q9 end'), 'sess_q9');
25
- assert.equal(extractSessionId('nothing here'), null);
26
- assert.equal(extractSessionId(undefined), null);
27
- });
28
-
29
- test('readUnifiedConfig returns an object, and {} for a missing or non-object file', () => {
30
- withDir((dir) => {
31
- assert.deepEqual(readUnifiedConfig(dir), {});
32
- writeFileSync(join(dir, 'mixdog-config.json'), '{"a":1}');
33
- assert.deepEqual(readUnifiedConfig(dir), { a: 1 });
34
- writeFileSync(join(dir, 'mixdog-config.json'), 'null');
35
- assert.deepEqual(readUnifiedConfig(dir), {});
36
- });
37
- });
38
-
39
- test('auth artifacts: provider files plus every oauth/credentials json in the real dir', () => {
40
- withDir((real) => {
41
- writeFileSync(join(real, 'grok-oauth.json'), 'g');
42
- writeFileSync(join(real, 'extra-credentials.json'), 'c');
43
- writeFileSync(join(real, 'unrelated.json'), 'u');
44
- const names = authArtifactNamesForSandbox(real, 'grok-oauth').sort();
45
- assert.deepEqual(names, ['extra-credentials.json', 'grok-oauth-models.json', 'grok-oauth.json']);
46
- const sandbox = join(real, 'sandbox');
47
- mkdirSync(sandbox);
48
- const { copied, skipped } = copyAuthArtifacts(real, sandbox, 'grok-oauth');
49
- assert.deepEqual(copied.sort(), ['extra-credentials.json', 'grok-oauth.json']);
50
- assert.deepEqual(skipped, ['grok-oauth-models.json']);
51
- assert.equal(readFileSync(join(sandbox, 'grok-oauth.json'), 'utf8'), 'g');
52
- assert.equal(existsSync(join(sandbox, 'unrelated.json')), false);
53
- });
54
- });
@@ -1,64 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import test from 'node:test';
3
- import { argValue, hasFlag, intArg, numArg, positionalArgs } from './cli-args.mjs';
4
-
5
- test('argValue reads both the separated and the joined option form', () => {
6
- const argv = ['node', 'bench.mjs', '--model', 'opus', '--round=3', '--trailing'];
7
- assert.equal(argValue('--model', null, argv), 'opus');
8
- assert.equal(argValue('--round', null, argv), '3');
9
- assert.equal(argValue('--missing', null, argv), null);
10
- assert.equal(argValue('--missing', 'fallback', argv), 'fallback');
11
- // An explicit `undefined` fallback takes the parameter default, so a
12
- // missing option always reads as null rather than undefined.
13
- assert.equal(argValue('--missing', undefined, argv), null);
14
- });
15
-
16
- test('argValue falls back when the separated form has no value left', () => {
17
- assert.equal(argValue('--save', 'default', ['node', 'bench.mjs', '--save']), 'default');
18
- // An empty joined value is a value, not a missing option.
19
- assert.equal(argValue('--save', 'default', ['node', 'bench.mjs', '--save=']), '');
20
- // The next argument is taken verbatim, even when it looks like a flag.
21
- assert.equal(argValue('--save', null, ['node', 'bench.mjs', '--save', '--json']), '--json');
22
- });
23
-
24
- test('hasFlag reports presence only', () => {
25
- const argv = ['node', 'bench.mjs', '--json', '--limit=5'];
26
- assert.equal(hasFlag('--json', argv), true);
27
- assert.equal(hasFlag('--limit', argv), false);
28
- assert.equal(hasFlag('--quiet', argv), false);
29
- });
30
-
31
- test('intArg accepts positive integers and keeps the fallback otherwise', () => {
32
- assert.equal(intArg('--limit', 30, ['node', 'diag.mjs', '--limit', '5']), 5);
33
- assert.equal(intArg('--limit', 30, ['node', 'diag.mjs', '--limit=12']), 12);
34
- assert.equal(intArg('--limit', 30, ['node', 'diag.mjs']), 30);
35
- assert.equal(intArg('--limit', 30, ['node', 'diag.mjs', '--limit', 'abc']), 30);
36
- assert.equal(intArg('--limit', 30, ['node', 'diag.mjs', '--limit', '0']), 30);
37
- assert.equal(intArg('--limit', 30, ['node', 'diag.mjs', '--limit', '-4']), 30);
38
- // parseInt stops at the first non-digit, as the CLIs have always accepted.
39
- assert.equal(intArg('--limit', 30, ['node', 'diag.mjs', '--limit', '7x']), 7);
40
- });
41
-
42
- test('numArg keeps a deliberate 0 and falls back only on missing or non-numeric values', () => {
43
- assert.equal(numArg('--budget', 16000, ['node', 'bench.mjs', '--budget', '0']), 0);
44
- assert.equal(numArg('--budget', 16000, ['node', 'bench.mjs', '--budget=0']), 0);
45
- assert.equal(numArg('--budget', 16000, ['node', 'bench.mjs', '--budget', '2500']), 2500);
46
- assert.equal(numArg('--budget', 16000, ['node', 'bench.mjs', '--budget', '0.5']), 0.5);
47
- assert.equal(numArg('--budget', 16000, ['node', 'bench.mjs']), 16000);
48
- assert.equal(numArg('--budget', 16000, ['node', 'bench.mjs', '--budget=']), 16000);
49
- assert.equal(numArg('--budget', 16000, ['node', 'bench.mjs', '--budget', 'lots']), 16000);
50
- assert.equal(numArg('--budget', 16000, ['node', 'bench.mjs', '--budget']), 16000);
51
- });
52
-
53
- test('positionalArgs never lets a boolean flag or a joined value swallow a positional', () => {
54
- const booleans = new Set(['--render']);
55
- assert.deepEqual(positionalArgs(['--render', 'deck.js', 'deck.pptx', '--mode', 'auto'], booleans), [
56
- 'deck.js',
57
- 'deck.pptx',
58
- ]);
59
- assert.deepEqual(positionalArgs(['--mode=auto', 'deck.js', 'deck.pptx'], booleans), ['deck.js', 'deck.pptx']);
60
- assert.deepEqual(positionalArgs(['deck.js', '--out', 'result.json', 'deck.pptx', '--render'], booleans), [
61
- 'deck.js',
62
- 'deck.pptx',
63
- ]);
64
- });
@@ -1,47 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import test from 'node:test';
3
- import { selectDeterministicCorpus } from './embedding-model-bench-core.mjs';
4
-
5
- const documents = Object.freeze([6, 3, 2, 5, 4, 1, 3].map((id) => Object.freeze({ id })));
6
-
7
- test('negative sampling preserves SHA-256 order, document order, and duplicate rows', () => {
8
- // SHA-256("4") starts 4b22, then "3" starts 4e07; the other IDs sort after both.
9
- const selection = selectDeterministicCorpus(documents, [{ positiveIdsByTarget: [[6]] }], 3);
10
- assert.deepEqual(selection, {
11
- documents: [{ id: 6 }, { id: 3 }, { id: 4 }, { id: 3 }],
12
- sourceDocuments: 7,
13
- selectedDocuments: 3,
14
- positiveDocuments: 1,
15
- method: 'all-positive-roots + sha256(document-id) negatives',
16
- });
17
- });
18
-
19
- test('positive caps use the same hash ordering without counting duplicate IDs twice', () => {
20
- const selection = selectDeterministicCorpus(documents, [{ positiveIdsByTarget: [[1, 2, 3, 4, 5, 6, 3]] }], 2, 2);
21
- assert.deepEqual(selection, {
22
- documents: [{ id: 3 }, { id: 4 }, { id: 3 }],
23
- sourceDocuments: 7,
24
- selectedDocuments: 2,
25
- positiveDocuments: 2,
26
- method: 'up-to-2-positive-roots-per-target + sha256(document-id) negatives',
27
- });
28
- });
29
-
30
- test('full-corpus selections retain the original array and capped positive count', () => {
31
- for (const limit of [0, 1.5, 7, 9]) {
32
- const selection = selectDeterministicCorpus(
33
- documents,
34
- [{ positiveIdsByTarget: [[1, 2, 3, 4, 5, 6, 3]] }],
35
- limit,
36
- 2
37
- );
38
- assert.equal(selection.documents, documents);
39
- assert.deepEqual(selection, {
40
- documents,
41
- sourceDocuments: 7,
42
- selectedDocuments: 7,
43
- positiveDocuments: 2,
44
- method: 'full-corpus',
45
- });
46
- }
47
- });
@@ -1,22 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import test from 'node:test';
3
- import { parseDuration, summarize } from './smoke-loop-shared.mjs';
4
-
5
- test('parseDuration reads bare milliseconds and unit suffixes', () => {
6
- assert.equal(parseDuration('1500'), 1500);
7
- assert.equal(parseDuration('30s'), 30_000);
8
- assert.equal(parseDuration('5M'), 300_000);
9
- assert.equal(parseDuration('1.5h'), 5_400_000);
10
- assert.equal(parseDuration('0ms'), 1);
11
- });
12
-
13
- test('parseDuration returns the fallback when empty and throws on garbage', () => {
14
- assert.equal(parseDuration('', 7), 7);
15
- assert.equal(parseDuration(undefined), null);
16
- assert.throws(() => parseDuration('soon'), /invalid duration: soon/);
17
- });
18
-
19
- test('summarize rounds min/max/avg to one decimal, zeros when empty', () => {
20
- assert.deepEqual(summarize([]), { min: 0, max: 0, avg: 0 });
21
- assert.deepEqual(summarize([1.04, 2.06, 3.31]), { min: 1, max: 3.3, avg: 2.1 });
22
- });