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.
- package/README.md +8 -0
- package/package.json +2 -2
- package/src/defaults/skills/browser-use/SKILL.md +14 -6
- package/src/defaults/skills/setup/references/actions.md +6 -3
- package/src/defaults/skills/setup/references/surfaces.md +8 -4
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +40 -99
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +6 -1
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +7 -3
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +18 -38
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-path-fanout.mjs +59 -32
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +15 -10
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-request.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shared-native-scan.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +17 -9
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/patch/wave.mjs +4 -0
- package/src/runtime/browser-bridge/input-fields.mjs +1 -1
- package/src/runtime/browser-bridge/tool-defs.mjs +1 -1
- package/src/runtime/computer-bridge/actions.mjs +0 -13
- package/src/runtime/media/adapters/antigravity-image.mjs +7 -3
- package/src/runtime/media/adapters/codex-image.mjs +12 -1
- package/src/runtime/media/adapters/gemini-image.mjs +17 -6
- package/src/runtime/media/adapters/gemini-video.mjs +19 -9
- package/src/runtime/media/adapters/xai-media.mjs +20 -7
- package/src/runtime/media/jobs.mjs +35 -3
- package/src/runtime/media/media-usage.mjs +126 -0
- package/src/runtime/media/tool.mjs +16 -4
- package/src/runtime/shared/llm/cost.mjs +50 -3
- package/src/runtime/shared/llm/model-catalog-projection.mjs +7 -2
- package/src/runtime/shared/llm/model-catalog.mjs +2 -1
- package/src/runtime/shared/llm/model-pricing-rates.mjs +21 -0
- package/src/runtime/shared/llm/usage-accounting.mjs +25 -5
- package/src/runtime/shared/llm/usage-context.mjs +6 -0
- package/src/runtime/shared/llm/usage-ledger-import.mjs +1 -0
- package/src/session-runtime/cwd-plugins/plugin-status.mjs +24 -4
- package/src/session-runtime/internal-tool-executor/feature-tools.mjs +9 -1
- package/src/session-runtime/services/agent-tool/index-write-queue.mjs +38 -6
- package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +9 -2
- package/src/session-runtime/services/agent-tool/worker-index.mjs +6 -3
- package/src/session-runtime/services/hook-bus/event-runner.mjs +10 -2
- package/src/session-runtime/services/hook-bus/tool-gate.mjs +5 -2
- package/src/session-runtime/setup-tool/tool-defs.mjs +1 -1
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +8 -5
- package/src/tui/app/doctor.mjs +78 -2
- package/src/tui/dist/index.mjs +7 -3
- package/scripts/lib/bench-sandbox.test.mjs +0 -54
- package/scripts/lib/cli-args.test.mjs +0 -64
- package/scripts/lib/embedding-model-bench-core.test.mjs +0 -47
- package/scripts/lib/smoke-loop-shared.test.mjs +0 -22
- package/scripts/lib/trace-row.test.mjs +0 -47
- 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
|
|
69
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
30
|
+
const { rows, tombstones: savedTombstones } = store.readSnapshot();
|
|
31
31
|
if (rows.length === 0) return rows;
|
|
32
|
-
const tombstones = new Map(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
|
91
|
-
const nextConfig = withAgentDisabled(getConfig(), id,
|
|
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 };
|
package/src/tui/app/doctor.mjs
CHANGED
|
@@ -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
|
-
|
|
255
|
+
const issues = [...broken, ...(label === 'plugins' ? missing : [])];
|
|
256
|
+
if (!issues.length) {
|
|
252
257
|
row('ok', detail);
|
|
253
258
|
return;
|
|
254
259
|
}
|
|
255
|
-
detail += ` · issues: ${
|
|
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',
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -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
|
-
});
|