mixdog 0.9.2 → 0.9.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/package.json +2 -1
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/tool-smoke.mjs +7 -4
- package/src/agents/debugger/AGENT.md +2 -2
- package/src/agents/heavy-worker/AGENT.md +20 -11
- package/src/agents/reviewer/AGENT.md +2 -2
- package/src/agents/worker/AGENT.md +17 -11
- package/src/mixdog-session-runtime.mjs +424 -1812
- package/src/repl.mjs +5 -5
- package/src/rules/agent/30-explorer.md +8 -11
- package/src/rules/lead/lead-tool.md +9 -5
- package/src/rules/shared/01-tool.md +11 -5
- package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -68
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +46 -7
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +7 -5
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +33 -23
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +31 -14
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +169 -918
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +65 -1032
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
- package/src/runtime/agent/orchestrator/tools/patch.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/index.mjs +14 -233
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +8 -0
- package/src/runtime/channels/lib/telegram-format.mjs +19 -22
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/memory/index.mjs +314 -359
- package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +56 -3
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +20 -0
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/tool-defs.mjs +4 -4
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/transcript-writer.mjs +29 -2
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +49 -10
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/explore-tool.mjs +10 -3
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +14 -3
- package/src/tui/App.jsx +884 -143
- package/src/tui/components/PromptInput.jsx +272 -15
- package/src/tui/components/ToolExecution.jsx +20 -10
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/dist/index.mjs +1771 -1947
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +311 -851
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +2 -2
- package/src/tui/lib/voice-recorder.mjs +35 -19
- package/src/tui/markdown/format-token.mjs +7 -8
- package/src/tui/markdown/format-token.test.mjs +3 -3
- package/src/tui/paste-attachments.mjs +38 -0
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/themes/base.mjs +2 -2
- package/src/tui/themes/kanagawa.mjs +4 -4
- package/src/tui/themes/teal.mjs +4 -5
- package/src/tui/themes/utils.mjs +1 -1
- package/src/ui/statusline.mjs +49 -0
- package/src/workflows/default/WORKFLOW.md +16 -9
- package/src/workflows/sequential/WORKFLOW.md +16 -11
- package/src/workflows/solo/WORKFLOW.md +5 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { performance } from 'node:perf_hooks';
|
|
6
6
|
import { ensureStandaloneEnvironment } from './standalone/seeds.mjs';
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
renderProviderStatus,
|
|
39
39
|
saveOpenAIUsageSessionKey,
|
|
40
40
|
saveOpenCodeGoUsageAuth,
|
|
41
|
+
loginOpenCodeGoUsage,
|
|
41
42
|
saveProviderApiKey,
|
|
42
43
|
setLocalProvider,
|
|
43
44
|
} from './standalone/provider-admin.mjs';
|
|
@@ -88,50 +89,125 @@ import {
|
|
|
88
89
|
summarizeContextMessages,
|
|
89
90
|
} from './runtime/agent/orchestrator/session/context-utils.mjs';
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
92
|
+
import {
|
|
93
|
+
sessionMessageText,
|
|
94
|
+
messageContextText,
|
|
95
|
+
isSessionPreviewNoise,
|
|
96
|
+
cleanSessionPreview,
|
|
97
|
+
clean,
|
|
98
|
+
hasOwn,
|
|
99
|
+
toolResponseText,
|
|
100
|
+
isEmptyRecallText,
|
|
101
|
+
currentSessionRecallRows,
|
|
102
|
+
sessionHasConversationMessages,
|
|
103
|
+
} from './session-runtime/session-text.mjs';
|
|
104
|
+
import {
|
|
105
|
+
TOOL_MODES,
|
|
106
|
+
ALL_EFFORT_LEVELS,
|
|
107
|
+
EFFORT_LABELS,
|
|
108
|
+
EFFORT_OPTIONS_BY_PROVIDER,
|
|
109
|
+
EFFORT_BY_FAMILY,
|
|
110
|
+
EFFORT_FALLBACKS,
|
|
111
|
+
normalizeToolMode,
|
|
112
|
+
normalizeEffortInput,
|
|
113
|
+
effortOptionsFor,
|
|
114
|
+
coerceEffortFor,
|
|
115
|
+
normalizeSavedEffort,
|
|
116
|
+
effortItemsFor,
|
|
117
|
+
toolSpecForMode,
|
|
118
|
+
deferredSurfaceModeForLead,
|
|
119
|
+
} from './session-runtime/effort.mjs';
|
|
120
|
+
import {
|
|
121
|
+
LAZY_SECRET_PROVIDERS,
|
|
122
|
+
routeFastKey,
|
|
123
|
+
fastCapableFor,
|
|
124
|
+
makeSearchCapableFor,
|
|
125
|
+
fastPreferenceFor,
|
|
126
|
+
saveModelSettings,
|
|
127
|
+
} from './session-runtime/model-capabilities.mjs';
|
|
128
|
+
import {
|
|
129
|
+
DEFAULT_PROVIDER,
|
|
130
|
+
DEFAULT_MODEL,
|
|
131
|
+
makeResolveDefaultProvider,
|
|
132
|
+
findPreset,
|
|
133
|
+
makeResolveRoute,
|
|
134
|
+
isLikelyRawModelId,
|
|
135
|
+
validateRequestedModelSelector,
|
|
136
|
+
ensureProviderEnabled,
|
|
137
|
+
normalizeSystemShellConfig,
|
|
138
|
+
normalizeSystemShellCommand,
|
|
139
|
+
normalizeAutoClearConfig,
|
|
140
|
+
normalizeCompactionConfig,
|
|
141
|
+
moduleEnabled,
|
|
142
|
+
setModuleEnabledInConfig,
|
|
143
|
+
formatDurationMs,
|
|
144
|
+
parseDurationMs,
|
|
145
|
+
modelMetaLooksResolved,
|
|
146
|
+
modelSettingsFor,
|
|
147
|
+
normalizeCompactTypeSetting,
|
|
148
|
+
} from './session-runtime/config-helpers.mjs';
|
|
149
|
+
import {
|
|
150
|
+
routeForStatusline,
|
|
151
|
+
writeStatuslineRoute,
|
|
152
|
+
} from './session-runtime/statusline-route.mjs';
|
|
153
|
+
import {
|
|
154
|
+
normalizeOutputStyleId,
|
|
155
|
+
listOutputStyleCatalog,
|
|
156
|
+
findOutputStyle,
|
|
157
|
+
outputStyleStatus as outputStyleStatusRaw,
|
|
158
|
+
} from './session-runtime/output-styles.mjs';
|
|
159
|
+
import { readJsonSafe, readTextSafe } from './session-runtime/fs-utils.mjs';
|
|
160
|
+
import {
|
|
161
|
+
readProjectMcpServers,
|
|
162
|
+
countSkillFiles,
|
|
163
|
+
mcpScriptForPlugin,
|
|
164
|
+
normalizePluginMcpServerConfig,
|
|
165
|
+
pluginManifest,
|
|
166
|
+
pluginMcpServerName,
|
|
167
|
+
} from './session-runtime/plugin-mcp.mjs';
|
|
168
|
+
import {
|
|
169
|
+
WORKFLOW_ROUTE_SLOTS,
|
|
170
|
+
FIXED_AGENT_SLOTS,
|
|
171
|
+
SEARCH_DEFAULT_PROVIDER,
|
|
172
|
+
SEARCH_DEFAULT_MODEL,
|
|
173
|
+
workflowPresetId,
|
|
174
|
+
agentPresetSlot,
|
|
175
|
+
normalizeAgentId,
|
|
176
|
+
normalizeWorkflowId,
|
|
177
|
+
createWorkflowHelpers,
|
|
178
|
+
normalizeSearchProviderId,
|
|
179
|
+
isDefaultSearchRouteConfig,
|
|
180
|
+
isSearchCapableProvider,
|
|
181
|
+
normalizeSearchRouteConfig,
|
|
182
|
+
normalizeWorkflowRoute,
|
|
183
|
+
upsertWorkflowPreset,
|
|
184
|
+
createWorkflowRouteHelpers,
|
|
185
|
+
} from './session-runtime/workflow.mjs';
|
|
186
|
+
import {
|
|
187
|
+
MEASURED_TOOL_USAGE,
|
|
188
|
+
DEFERRED_DEFAULT_FULL_TOOLS,
|
|
189
|
+
DEFERRED_DEFAULT_READONLY_TOOLS,
|
|
190
|
+
DEFERRED_DEFAULT_LEAD_TOOLS,
|
|
191
|
+
toolKind,
|
|
192
|
+
toolSchemaBucket,
|
|
193
|
+
estimateToolSchemaBreakdown,
|
|
194
|
+
measuredToolUsage,
|
|
195
|
+
parseToolSelection,
|
|
196
|
+
parseToolSearchQuerySelection,
|
|
197
|
+
sortedCatalogByMeasuredUsage,
|
|
198
|
+
filterDisallowedTools,
|
|
199
|
+
sortedNamesByMeasuredUsage,
|
|
200
|
+
defaultDeferredToolNames,
|
|
201
|
+
compactToolSearchDescription,
|
|
202
|
+
toolRow,
|
|
203
|
+
toolSearchMatches,
|
|
204
|
+
applyDeferredToolSurface,
|
|
205
|
+
selectDeferredTools,
|
|
206
|
+
renderToolSearch,
|
|
207
|
+
} from './session-runtime/tool-catalog.mjs';
|
|
208
|
+
// Re-exported for external consumers (scripts/tool-smoke.mjs) that imported
|
|
209
|
+
// these from this module before the tool-catalog extraction.
|
|
210
|
+
export { defaultDeferredToolNames, compactToolSearchDescription } from './session-runtime/tool-catalog.mjs';
|
|
135
211
|
|
|
136
212
|
const RUNTIME = './runtime/agent/orchestrator';
|
|
137
213
|
const SEARCH_RUNTIME = './runtime/search/index.mjs';
|
|
@@ -151,26 +227,9 @@ const STANDALONE_ROOT = STANDALONE_SOURCE_ROOT;
|
|
|
151
227
|
const MIXDOG_HOME = process.env.MIXDOG_HOME || join(homedir(), '.mixdog');
|
|
152
228
|
const STANDALONE_DATA_DIR = process.env.MIXDOG_DATA_DIR || join(MIXDOG_HOME, 'data');
|
|
153
229
|
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
// Resolve the provider to use when a route carries no explicit provider.
|
|
158
|
-
// Priority: config.defaultProvider (when it names a known provider) > DEFAULT_PROVIDER.
|
|
159
|
-
function resolveDefaultProvider(config) {
|
|
160
|
-
const configured = clean(config?.defaultProvider);
|
|
161
|
-
if (configured && isKnownProvider(configured)) return configured;
|
|
162
|
-
return DEFAULT_PROVIDER;
|
|
163
|
-
}
|
|
164
|
-
const TOOL_MODES = new Set(['full', 'readonly', 'lead']);
|
|
165
|
-
const ALL_EFFORT_LEVELS = new Set(['none', 'low', 'medium', 'high', 'xhigh', 'max']);
|
|
166
|
-
const EFFORT_LABELS = {
|
|
167
|
-
none: 'None',
|
|
168
|
-
low: 'Low',
|
|
169
|
-
medium: 'Medium',
|
|
170
|
-
high: 'High',
|
|
171
|
-
xhigh: 'Extra High',
|
|
172
|
-
max: 'Max',
|
|
173
|
-
};
|
|
230
|
+
const resolveDefaultProvider = makeResolveDefaultProvider(isKnownProvider);
|
|
231
|
+
const resolveRoute = makeResolveRoute(resolveDefaultProvider);
|
|
232
|
+
const searchCapableFor = makeSearchCapableFor(normalizeSearchProviderId, isSearchCapableProvider);
|
|
174
233
|
|
|
175
234
|
function envFlag(name) {
|
|
176
235
|
return /^(1|true|yes|on)$/i.test(String(process.env[name] || ''));
|
|
@@ -217,37 +276,6 @@ async function profiledImport(label, spec, { optional = false } = {}) {
|
|
|
217
276
|
throw error;
|
|
218
277
|
}
|
|
219
278
|
}
|
|
220
|
-
const EFFORT_OPTIONS_BY_PROVIDER = {
|
|
221
|
-
openai: ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
222
|
-
'openai-oauth': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
223
|
-
anthropic: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
224
|
-
'anthropic-oauth': ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
225
|
-
xai: ['none', 'low', 'medium', 'high'],
|
|
226
|
-
'grok-oauth': ['none', 'low', 'medium', 'high'],
|
|
227
|
-
'opencode-go': ['high', 'max'],
|
|
228
|
-
};
|
|
229
|
-
const EFFORT_BY_FAMILY = {
|
|
230
|
-
opus: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
231
|
-
sonnet: ['low', 'medium', 'high'],
|
|
232
|
-
haiku: [],
|
|
233
|
-
'gpt-5.5': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
234
|
-
'gpt-5.4': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
235
|
-
'gpt-5.2': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
236
|
-
'gpt-5': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
237
|
-
'gpt-mini': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
238
|
-
'gpt-nano': ['none', 'low', 'medium', 'high'],
|
|
239
|
-
'gpt-codex': ['none', 'low', 'medium', 'high'],
|
|
240
|
-
grok: ['none', 'low', 'medium', 'high'],
|
|
241
|
-
};
|
|
242
|
-
const EFFORT_FALLBACKS = {
|
|
243
|
-
max: ['max', 'xhigh', 'high', 'medium', 'low'],
|
|
244
|
-
xhigh: ['xhigh', 'high', 'medium', 'low'],
|
|
245
|
-
high: ['high', 'medium', 'low'],
|
|
246
|
-
medium: ['medium', 'low'],
|
|
247
|
-
low: ['low'],
|
|
248
|
-
none: ['none'],
|
|
249
|
-
};
|
|
250
|
-
|
|
251
279
|
export const TOOL_SEARCH_TOOL = {
|
|
252
280
|
name: 'tool_search',
|
|
253
281
|
title: 'Tool Search',
|
|
@@ -330,86 +358,7 @@ export const SKILL_TOOL = {
|
|
|
330
358
|
},
|
|
331
359
|
};
|
|
332
360
|
|
|
333
|
-
const MEASURED_TOOL_USAGE = Object.freeze({
|
|
334
|
-
read: 710,
|
|
335
|
-
code_graph: 520,
|
|
336
|
-
grep: 500,
|
|
337
|
-
find: 480,
|
|
338
|
-
glob: 460,
|
|
339
|
-
list: 430,
|
|
340
|
-
apply_patch: 400,
|
|
341
|
-
explore: 360,
|
|
342
|
-
agent: 330,
|
|
343
|
-
shell: 81,
|
|
344
|
-
cwd: 2,
|
|
345
|
-
diagnostics: 2,
|
|
346
|
-
recall: 2,
|
|
347
|
-
search: 2,
|
|
348
|
-
web_fetch: 2,
|
|
349
|
-
provider_status: 2,
|
|
350
|
-
channel_status: 2,
|
|
351
|
-
});
|
|
352
|
-
const MEASURED_TOOL_ORDER = Object.freeze(Object.keys(MEASURED_TOOL_USAGE));
|
|
353
361
|
const LEAD_DISALLOWED_TOOLS = Object.freeze(['diagnostics', 'open_config']);
|
|
354
|
-
const DEFERRED_DEFAULT_FULL_TOOLS = Object.freeze([
|
|
355
|
-
'read',
|
|
356
|
-
'code_graph',
|
|
357
|
-
'grep',
|
|
358
|
-
'find',
|
|
359
|
-
'glob',
|
|
360
|
-
'list',
|
|
361
|
-
'explore',
|
|
362
|
-
'apply_patch',
|
|
363
|
-
'Skill',
|
|
364
|
-
'tool_search',
|
|
365
|
-
]);
|
|
366
|
-
const DEFERRED_DEFAULT_READONLY_TOOLS = Object.freeze([
|
|
367
|
-
'read',
|
|
368
|
-
'code_graph',
|
|
369
|
-
'grep',
|
|
370
|
-
'find',
|
|
371
|
-
'glob',
|
|
372
|
-
'list',
|
|
373
|
-
'explore',
|
|
374
|
-
'Skill',
|
|
375
|
-
'tool_search',
|
|
376
|
-
]);
|
|
377
|
-
const DEFERRED_DEFAULT_LEAD_TOOLS = Object.freeze([
|
|
378
|
-
'read',
|
|
379
|
-
'code_graph',
|
|
380
|
-
'grep',
|
|
381
|
-
'find',
|
|
382
|
-
'glob',
|
|
383
|
-
'list',
|
|
384
|
-
'shell',
|
|
385
|
-
'task',
|
|
386
|
-
'explore',
|
|
387
|
-
'apply_patch',
|
|
388
|
-
'agent',
|
|
389
|
-
'recall',
|
|
390
|
-
'search',
|
|
391
|
-
'web_fetch',
|
|
392
|
-
'cwd',
|
|
393
|
-
'Skill',
|
|
394
|
-
'tool_search',
|
|
395
|
-
]);
|
|
396
|
-
const READONLY_TOOL_NAMES = new Set([
|
|
397
|
-
'read',
|
|
398
|
-
'list',
|
|
399
|
-
'grep',
|
|
400
|
-
'find',
|
|
401
|
-
'glob',
|
|
402
|
-
'code_graph',
|
|
403
|
-
'search',
|
|
404
|
-
'web_fetch',
|
|
405
|
-
'recall',
|
|
406
|
-
'memory',
|
|
407
|
-
'provider_status',
|
|
408
|
-
'channel_status',
|
|
409
|
-
'schedule_status',
|
|
410
|
-
'fetch',
|
|
411
|
-
'Skill',
|
|
412
|
-
]);
|
|
413
362
|
const AGENT_HIDDEN_WRAPPER_TOOLS = new Set([]);
|
|
414
363
|
|
|
415
364
|
export function __applyStandaloneToolDefaultsForTest(tool) {
|
|
@@ -423,761 +372,9 @@ export function __applyStandaloneToolDefaultsForTest(tool) {
|
|
|
423
372
|
};
|
|
424
373
|
}
|
|
425
374
|
const applyStandaloneToolDefaults = __applyStandaloneToolDefaultsForTest;
|
|
426
|
-
const
|
|
427
|
-
filesystem: ['read', 'list', 'grep', 'find', 'glob'],
|
|
428
|
-
search: ['search', 'web_fetch'],
|
|
429
|
-
web: ['web_fetch', 'search'],
|
|
430
|
-
memory: ['memory', 'recall'],
|
|
431
|
-
channels: ['reply', 'fetch', 'react', 'edit_message', 'download_attachment', 'schedule_status', 'trigger_schedule', 'schedule_control', 'reload_config'],
|
|
432
|
-
discord: ['reply', 'fetch', 'react', 'edit_message', 'download_attachment'],
|
|
433
|
-
providers: ['provider_status'],
|
|
434
|
-
provider: ['provider_status'],
|
|
435
|
-
status: ['provider_status', 'channel_status', 'schedule_status'],
|
|
436
|
-
schedule: ['schedule_status', 'trigger_schedule', 'schedule_control'],
|
|
437
|
-
channel: ['channel_status'],
|
|
438
|
-
explore: ['explore'],
|
|
439
|
-
discovery: ['explore'],
|
|
440
|
-
agent: ['agent'],
|
|
441
|
-
graph: ['code_graph'],
|
|
442
|
-
code: ['code_graph'],
|
|
443
|
-
shell: ['shell', 'task'],
|
|
444
|
-
};
|
|
445
|
-
const TOOL_SEARCH_SAFE_AUTO_ALIASES = new Set([
|
|
446
|
-
'shell',
|
|
447
|
-
'web',
|
|
448
|
-
'search',
|
|
449
|
-
'agent',
|
|
450
|
-
'provider',
|
|
451
|
-
'providers',
|
|
452
|
-
'channel',
|
|
453
|
-
'schedule',
|
|
454
|
-
'memory',
|
|
455
|
-
]);
|
|
456
|
-
const TOOL_SEARCH_AMBIGUOUS_AUTO_QUERIES = new Set([
|
|
457
|
-
'status',
|
|
458
|
-
'state',
|
|
459
|
-
'info',
|
|
460
|
-
'list',
|
|
461
|
-
'show',
|
|
462
|
-
'config',
|
|
463
|
-
]);
|
|
464
|
-
const TOOL_SEARCH_STOP_WORDS = new Set([
|
|
465
|
-
'a',
|
|
466
|
-
'an',
|
|
467
|
-
'and',
|
|
468
|
-
'are',
|
|
469
|
-
'as',
|
|
470
|
-
'for',
|
|
471
|
-
'from',
|
|
472
|
-
'how',
|
|
473
|
-
'i',
|
|
474
|
-
'in',
|
|
475
|
-
'is',
|
|
476
|
-
'me',
|
|
477
|
-
'need',
|
|
478
|
-
'of',
|
|
479
|
-
'on',
|
|
480
|
-
'please',
|
|
481
|
-
'the',
|
|
482
|
-
'to',
|
|
483
|
-
'tool',
|
|
484
|
-
'tools',
|
|
485
|
-
'use',
|
|
486
|
-
'using',
|
|
487
|
-
'with',
|
|
488
|
-
]);
|
|
489
|
-
const TOOL_SEARCH_ROW_ALIASES = Object.freeze({
|
|
490
|
-
agent: ['delegate', 'subagent', 'worker', 'parallel agent', 'background agent', 'reviewer', 'explorer'],
|
|
491
|
-
channel_status: ['channel status', 'discord status', 'channel config'],
|
|
492
|
-
cwd: ['cwd', 'working directory', 'current directory', 'project root', 'folder'],
|
|
493
|
-
memory: ['save memory', 'store memory', 'delete memory', 'forget memory', 'memory status'],
|
|
494
|
-
provider_status: ['provider status', 'auth status', 'model status', 'oauth status', 'provider config'],
|
|
495
|
-
recall: ['recall', 'previous work', 'past work', 'prior context', 'history', 'resume context'],
|
|
496
|
-
schedule_status: ['schedule status', 'cron status'],
|
|
497
|
-
search: ['web search', 'internet search', 'current info', 'latest info', 'online search', 'docs search'],
|
|
498
|
-
shell: ['run command', 'execute command', 'terminal', 'powershell', 'bash', 'run tests', 'test command', 'build command', 'npm', 'node', 'git'],
|
|
499
|
-
task: ['background task', 'async task', 'wait task', 'cancel task', 'task status'],
|
|
500
|
-
web_fetch: ['fetch url', 'fetch page', 'open url', 'web page', 'read url', 'docs page'],
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
function normalizeToolMode(mode) {
|
|
504
|
-
const value = String(mode || '').trim().toLowerCase();
|
|
505
|
-
return TOOL_MODES.has(value) ? value : 'full';
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function normalizeEffortInput(value) {
|
|
509
|
-
const v = clean(value).toLowerCase();
|
|
510
|
-
if (!v || v === 'auto') return null;
|
|
511
|
-
if (!ALL_EFFORT_LEVELS.has(v)) {
|
|
512
|
-
throw new Error(`effort must be one of auto, ${[...ALL_EFFORT_LEVELS].join(', ')}`);
|
|
513
|
-
}
|
|
514
|
-
return v;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function effortOptionsFor(provider, model) {
|
|
518
|
-
const providerAllowed = EFFORT_OPTIONS_BY_PROVIDER[provider] || null;
|
|
519
|
-
const filterProvider = (values) => {
|
|
520
|
-
const unique = [...new Set((values || []).map(clean).filter(Boolean))];
|
|
521
|
-
return providerAllowed ? unique.filter((v) => providerAllowed.includes(v)) : unique;
|
|
522
|
-
};
|
|
523
|
-
const declared = Array.isArray(model?.reasoningLevels)
|
|
524
|
-
? model.reasoningLevels.map(clean).filter(Boolean)
|
|
525
|
-
: [];
|
|
526
|
-
const family = clean(model?.family).toLowerCase();
|
|
527
|
-
if (Array.isArray(model?.reasoningLevels)) {
|
|
528
|
-
if (declared.length) return filterProvider(declared);
|
|
529
|
-
if (Object.prototype.hasOwnProperty.call(EFFORT_BY_FAMILY, family)) {
|
|
530
|
-
return filterProvider(EFFORT_BY_FAMILY[family]);
|
|
531
|
-
}
|
|
532
|
-
return [];
|
|
533
|
-
}
|
|
534
|
-
const reasoningOptionEffort = Array.isArray(model?.reasoningOptions)
|
|
535
|
-
? model.reasoningOptions.find((option) => clean(option?.type).toLowerCase() === 'effort')
|
|
536
|
-
: null;
|
|
537
|
-
const reasoningOptionValues = Array.isArray(reasoningOptionEffort?.values)
|
|
538
|
-
? reasoningOptionEffort.values.map(clean).filter(Boolean)
|
|
539
|
-
: [];
|
|
540
|
-
if (reasoningOptionValues.length) return filterProvider(reasoningOptionValues);
|
|
541
|
-
if (Object.prototype.hasOwnProperty.call(EFFORT_BY_FAMILY, family)) {
|
|
542
|
-
return filterProvider(EFFORT_BY_FAMILY[family]);
|
|
543
|
-
}
|
|
544
|
-
return providerAllowed || [];
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function coerceEffortFor(provider, model, effort) {
|
|
548
|
-
if (!effort) return null;
|
|
549
|
-
const allowed = effortOptionsFor(provider, model);
|
|
550
|
-
if (!allowed || allowed.length === 0) return null;
|
|
551
|
-
if (allowed.includes(effort)) return effort;
|
|
552
|
-
for (const candidate of EFFORT_FALLBACKS[effort] || []) {
|
|
553
|
-
if (allowed.includes(candidate)) return candidate;
|
|
554
|
-
}
|
|
555
|
-
return null;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function hasOwn(obj, key) {
|
|
559
|
-
return Object.prototype.hasOwnProperty.call(obj || {}, key);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function modelSettingsFor(config, provider, model) {
|
|
563
|
-
const key = routeFastKey(provider, model);
|
|
564
|
-
const value = key ? config?.modelSettings?.[key] : null;
|
|
565
|
-
return value && typeof value === 'object' ? value : {};
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
function normalizeSavedEffort(value) {
|
|
569
|
-
try {
|
|
570
|
-
return normalizeEffortInput(value);
|
|
571
|
-
} catch {
|
|
572
|
-
return null;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function effortItemsFor(provider, model, activeEffort) {
|
|
577
|
-
const allowed = effortOptionsFor(provider, model);
|
|
578
|
-
const items = [];
|
|
579
|
-
for (const value of allowed || []) {
|
|
580
|
-
items.push({
|
|
581
|
-
value,
|
|
582
|
-
label: EFFORT_LABELS[value] || value,
|
|
583
|
-
description: value === activeEffort ? 'current' : '',
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
return items;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function toolSpecForMode(mode) {
|
|
590
|
-
return mode === 'readonly' ? ['tools:readonly'] : 'full';
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function deferredSurfaceModeForLead(mode) {
|
|
594
|
-
return mode === 'readonly' ? 'readonly' : 'lead';
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function clean(value) {
|
|
598
|
-
return String(value ?? '').trim();
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// A resolved model meta carries catalog-derived fields (contextWindow, pricing,
|
|
602
|
-
// capabilities, …). The lookupModelMeta() fallback for an unknown id is the
|
|
603
|
-
// bare shape `{ id, provider }`, so "more than id/provider" reliably tells a
|
|
604
|
-
// real catalog hit apart from that placeholder.
|
|
605
|
-
function modelMetaLooksResolved(meta) {
|
|
606
|
-
if (!meta || typeof meta !== 'object') return false;
|
|
607
|
-
return Object.keys(meta).some((key) => key !== 'id' && key !== 'provider');
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
const OUTPUT_STYLE_ORDER = ['default', 'simple', 'minimal', 'oneline'];
|
|
611
|
-
const OUTPUT_STYLE_ALIASES = new Map([
|
|
612
|
-
['compact', 'default'],
|
|
613
|
-
['normal', 'default'],
|
|
614
|
-
['extreme', 'minimal'],
|
|
615
|
-
['extremesimple', 'minimal'],
|
|
616
|
-
['extreme-simple', 'minimal'],
|
|
617
|
-
['extreme_simple', 'minimal'],
|
|
618
|
-
['mono', 'oneline'],
|
|
619
|
-
['oneline', 'oneline'],
|
|
620
|
-
['one-line', 'oneline'],
|
|
621
|
-
['one_line', 'oneline'],
|
|
622
|
-
]);
|
|
623
|
-
|
|
624
|
-
function normalizeOutputStyleId(value) {
|
|
625
|
-
const raw = clean(value).toLowerCase();
|
|
626
|
-
if (!raw) return '';
|
|
627
|
-
const slug = raw.replace(/[_\s]+/g, '-').replace(/^-+|-+$/g, '');
|
|
628
|
-
const compact = slug.replace(/[_.-]+/g, '');
|
|
629
|
-
if (OUTPUT_STYLE_ALIASES.has(slug)) return OUTPUT_STYLE_ALIASES.get(slug);
|
|
630
|
-
if (OUTPUT_STYLE_ALIASES.has(compact)) return OUTPUT_STYLE_ALIASES.get(compact);
|
|
631
|
-
return /^[a-z0-9.-]+$/.test(slug) ? slug : '';
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function outputStyleCompactKey(value) {
|
|
635
|
-
return normalizeOutputStyleId(value).replace(/[_.-]+/g, '');
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
function titleCaseOutputStyle(id) {
|
|
639
|
-
return clean(id)
|
|
640
|
-
.split(/[_.-]+/)
|
|
641
|
-
.filter(Boolean)
|
|
642
|
-
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
|
|
643
|
-
.join(' ') || 'Default';
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function parseOutputStyleFrontmatter(markdown) {
|
|
647
|
-
const match = String(markdown || '').match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
648
|
-
const meta = {};
|
|
649
|
-
if (!match) return meta;
|
|
650
|
-
for (const line of match[1].split(/\r?\n/)) {
|
|
651
|
-
const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
|
|
652
|
-
if (!kv) continue;
|
|
653
|
-
meta[kv[1]] = kv[2].replace(/^['"]|['"]$/g, '').trim();
|
|
654
|
-
}
|
|
655
|
-
return meta;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function readOutputStyleMetadata(filePath, source) {
|
|
659
|
-
let raw = '';
|
|
660
|
-
try { raw = readFileSync(filePath, 'utf8'); } catch { return null; }
|
|
661
|
-
const meta = parseOutputStyleFrontmatter(raw);
|
|
662
|
-
const fileId = normalizeOutputStyleId(basename(filePath).replace(/\.md$/i, ''));
|
|
663
|
-
const id = normalizeOutputStyleId(meta.name) || fileId;
|
|
664
|
-
if (!id) return null;
|
|
665
|
-
const aliases = clean(meta.aliases)
|
|
666
|
-
.split(',')
|
|
667
|
-
.map((value) => normalizeOutputStyleId(value))
|
|
668
|
-
.filter(Boolean);
|
|
669
|
-
const label = clean(meta.title || meta.label) || titleCaseOutputStyle(id);
|
|
670
|
-
return {
|
|
671
|
-
id,
|
|
672
|
-
label,
|
|
673
|
-
description: clean(meta.description),
|
|
674
|
-
aliases,
|
|
675
|
-
source,
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function listOutputStyleCatalog(dataDir = STANDALONE_DATA_DIR) {
|
|
680
|
-
const byId = new Map();
|
|
681
|
-
const dirs = [
|
|
682
|
-
{ dir: join(STANDALONE_ROOT, 'output-styles'), source: 'builtin' },
|
|
683
|
-
{ dir: join(dataDir || STANDALONE_DATA_DIR, 'output-styles'), source: 'user' },
|
|
684
|
-
];
|
|
685
|
-
for (const { dir, source } of dirs) {
|
|
686
|
-
let entries = [];
|
|
687
|
-
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
688
|
-
for (const entry of entries) {
|
|
689
|
-
if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.md')) continue;
|
|
690
|
-
const style = readOutputStyleMetadata(join(dir, entry.name), source);
|
|
691
|
-
if (style) byId.set(style.id, style);
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
return [...byId.values()].sort((a, b) => {
|
|
695
|
-
const ai = OUTPUT_STYLE_ORDER.indexOf(a.id);
|
|
696
|
-
const bi = OUTPUT_STYLE_ORDER.indexOf(b.id);
|
|
697
|
-
if (ai !== bi) return (ai < 0 ? 999 : ai) - (bi < 0 ? 999 : bi);
|
|
698
|
-
return a.label.localeCompare(b.label, 'en', { sensitivity: 'base' });
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function findOutputStyle(value, styles) {
|
|
703
|
-
const id = normalizeOutputStyleId(value);
|
|
704
|
-
const compact = outputStyleCompactKey(value);
|
|
705
|
-
if (!id && !compact) return null;
|
|
706
|
-
return (styles || []).find((style) => {
|
|
707
|
-
if (style.id === id || outputStyleCompactKey(style.id) === compact) return true;
|
|
708
|
-
if (outputStyleCompactKey(style.label) === compact) return true;
|
|
709
|
-
return (style.aliases || []).some((alias) => alias === id || outputStyleCompactKey(alias) === compact);
|
|
710
|
-
}) || null;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
function configuredOutputStyleValue(dataDir = STANDALONE_DATA_DIR) {
|
|
714
|
-
const unified = readJsonSafe(join(dataDir || STANDALONE_DATA_DIR, 'mixdog-config.json')) || {};
|
|
715
|
-
return clean(unified.outputStyle || (unified.agent && unified.agent.outputStyle) || 'default') || 'default';
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function outputStyleStatus(dataDir = STANDALONE_DATA_DIR) {
|
|
719
|
-
const styles = listOutputStyleCatalog(dataDir);
|
|
720
|
-
const configured = configuredOutputStyleValue(dataDir);
|
|
721
|
-
const current = findOutputStyle(configured, styles)
|
|
722
|
-
|| findOutputStyle('default', styles)
|
|
723
|
-
|| styles[0]
|
|
724
|
-
|| { id: 'default', label: 'Default', description: '', aliases: [], source: 'builtin' };
|
|
725
|
-
return { configured, current, styles };
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
function sessionHasConversationMessages(activeSession) {
|
|
729
|
-
const messages = Array.isArray(activeSession?.messages) ? activeSession.messages : [];
|
|
730
|
-
return messages.some((message) => {
|
|
731
|
-
const role = message?.role;
|
|
732
|
-
if (role !== 'user' && role !== 'assistant' && role !== 'tool') return false;
|
|
733
|
-
const text = sessionMessageText(message.content).trim();
|
|
734
|
-
if (!text && role !== 'assistant') return false;
|
|
735
|
-
if (role === 'user' && isSessionPreviewNoise(text)) return false;
|
|
736
|
-
return true;
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
function readJsonSafe(path) {
|
|
741
|
-
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// Standard Claude Code project-local MCP ingress: read `.mcp.json` from the
|
|
745
|
-
// project root and return a cleaned { name: cfg } map. Best-effort — never
|
|
746
|
-
// throws. Accepts either the standard `{ mcpServers: {...} }` shape or a bare
|
|
747
|
-
// name->cfg map. Self-ref servers (`mixdog` / `trib-plugin`) are stripped for
|
|
748
|
-
// parity with loadConfig (which strips them from the persisted agent section
|
|
749
|
-
// but never sees `.mcp.json`). Inputs are not mutated.
|
|
750
|
-
function readProjectMcpServers(cwd) {
|
|
751
|
-
const path = join(cwd || '.', '.mcp.json');
|
|
752
|
-
if (!existsSync(path)) return {};
|
|
753
|
-
let raw;
|
|
754
|
-
try {
|
|
755
|
-
raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
756
|
-
} catch (error) {
|
|
757
|
-
process.stderr.write(`[mcp-client] Ignoring unparseable .mcp.json at ${path}: ${error?.message || String(error)}\n`);
|
|
758
|
-
return {};
|
|
759
|
-
}
|
|
760
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
|
|
761
|
-
const map = raw.mcpServers && typeof raw.mcpServers === 'object' && !Array.isArray(raw.mcpServers)
|
|
762
|
-
? raw.mcpServers
|
|
763
|
-
: raw;
|
|
764
|
-
if (!map || typeof map !== 'object' || Array.isArray(map)) return {};
|
|
765
|
-
const out = {};
|
|
766
|
-
for (const [name, cfg] of Object.entries(map)) {
|
|
767
|
-
const key = clean(name);
|
|
768
|
-
if (!key) continue;
|
|
769
|
-
const lower = key.toLowerCase();
|
|
770
|
-
if (lower === 'mixdog' || lower === 'trib-plugin') continue;
|
|
771
|
-
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) continue;
|
|
772
|
-
// stdio entries (command + no url) spawn relative to the process launch
|
|
773
|
-
// dir, but mixdog tracks the project dir in memory (no process.chdir).
|
|
774
|
-
// Anchor their cwd to the .mcp.json directory: default when absent, resolve
|
|
775
|
-
// relative values against it, keep absolute values as-is. resolve(base, p)
|
|
776
|
-
// already returns `p` unchanged when it is absolute. Clone — never mutate.
|
|
777
|
-
const isStdio = typeof cfg.command === 'string' && cfg.command !== '' && !cfg.url;
|
|
778
|
-
if (isStdio) {
|
|
779
|
-
out[key] = { ...cfg, cwd: typeof cfg.cwd === 'string' && cfg.cwd ? resolve(cwd, cfg.cwd) : resolve(cwd) };
|
|
780
|
-
} else {
|
|
781
|
-
out[key] = cfg;
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
return out;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
function countSkillFiles(root) {
|
|
788
|
-
const skillsDir = join(root, 'skills');
|
|
789
|
-
if (!existsSync(skillsDir)) return 0;
|
|
790
|
-
let count = 0;
|
|
791
|
-
const walk = (dir) => {
|
|
792
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
793
|
-
const full = join(dir, entry.name);
|
|
794
|
-
if (entry.isDirectory()) walk(full);
|
|
795
|
-
else if (/^(SKILL|skill)\.md$/i.test(entry.name) || entry.name.toLowerCase().endsWith('.md')) count += 1;
|
|
796
|
-
}
|
|
797
|
-
};
|
|
798
|
-
try { walk(skillsDir); } catch { return count; }
|
|
799
|
-
return count;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
function mcpScriptForPlugin(root) {
|
|
803
|
-
const candidates = [
|
|
804
|
-
'scripts/run-mcp.mjs',
|
|
805
|
-
'mcp/server.mjs',
|
|
806
|
-
'server.mjs',
|
|
807
|
-
];
|
|
808
|
-
return candidates.find((rel) => existsSync(join(root, rel))) || null;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
function pluginManifest(root) {
|
|
812
|
-
return readJsonSafe(join(root, '.codex-plugin', 'plugin.json'))
|
|
813
|
-
|| readJsonSafe(join(root, 'plugin.json'))
|
|
814
|
-
|| {};
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function pluginMcpServerName(plugin = {}) {
|
|
818
|
-
const base = clean(plugin.name || plugin.title || 'plugin')
|
|
819
|
-
.toLowerCase()
|
|
820
|
-
.replace(/[^a-z0-9_.-]+/g, '-')
|
|
821
|
-
.replace(/^-+|-+$/g, '');
|
|
822
|
-
return base ? `plugin-${base}` : 'plugin-mcp';
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
function findPreset(config, key) {
|
|
826
|
-
const wanted = clean(key).toLowerCase();
|
|
827
|
-
if (!wanted) return null;
|
|
828
|
-
const presets = Array.isArray(config?.presets) ? config.presets : [];
|
|
829
|
-
return presets.find((p) => {
|
|
830
|
-
const id = clean(p?.id).toLowerCase();
|
|
831
|
-
const name = clean(p?.name).toLowerCase();
|
|
832
|
-
return id === wanted || name === wanted;
|
|
833
|
-
}) || null;
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
function resolveRoute(config, { provider, model, effort, fast } = {}) {
|
|
837
|
-
const explicitProvider = clean(provider);
|
|
838
|
-
const explicitModel = clean(model);
|
|
839
|
-
const hasExplicitEffort = effort !== undefined;
|
|
840
|
-
const explicitEffort = hasExplicitEffort ? normalizeEffortInput(effort) : undefined;
|
|
841
|
-
const hasExplicitFast = fast !== undefined;
|
|
842
|
-
const explicitFast = fast === true;
|
|
843
|
-
|
|
844
|
-
if (explicitModel && !explicitProvider) {
|
|
845
|
-
const preset = findPreset(config, explicitModel);
|
|
846
|
-
if (preset) {
|
|
847
|
-
const p = clean(preset.provider) || DEFAULT_PROVIDER;
|
|
848
|
-
const m = clean(preset.model) || DEFAULT_MODEL;
|
|
849
|
-
const saved = modelSettingsFor(config, p, m);
|
|
850
|
-
return {
|
|
851
|
-
provider: p,
|
|
852
|
-
model: m,
|
|
853
|
-
preset,
|
|
854
|
-
effort: hasExplicitEffort ? explicitEffort : normalizeSavedEffort(saved.effort ?? preset.effort),
|
|
855
|
-
fast: hasExplicitFast ? explicitFast : (hasOwn(saved, 'fast') ? saved.fast === true : (preset.fast === true || fastPreferenceFor(config, p, m))),
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
if (!explicitProvider && !explicitModel) {
|
|
861
|
-
const defaultKey = config?.default;
|
|
862
|
-
const preset = findPreset(config, defaultKey);
|
|
863
|
-
if (preset) {
|
|
864
|
-
const p = clean(preset.provider) || DEFAULT_PROVIDER;
|
|
865
|
-
const m = clean(preset.model) || DEFAULT_MODEL;
|
|
866
|
-
const saved = modelSettingsFor(config, p, m);
|
|
867
|
-
return {
|
|
868
|
-
provider: p,
|
|
869
|
-
model: m,
|
|
870
|
-
preset,
|
|
871
|
-
effort: hasExplicitEffort ? explicitEffort : normalizeSavedEffort(saved.effort ?? preset.effort),
|
|
872
|
-
fast: hasExplicitFast ? explicitFast : (hasOwn(saved, 'fast') ? saved.fast === true : (preset.fast === true || fastPreferenceFor(config, p, m))),
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
const p = explicitProvider || resolveDefaultProvider(config);
|
|
878
|
-
const m = explicitModel || DEFAULT_MODEL;
|
|
879
|
-
const saved = modelSettingsFor(config, p, m);
|
|
880
|
-
return {
|
|
881
|
-
provider: p,
|
|
882
|
-
model: m,
|
|
883
|
-
preset: null,
|
|
884
|
-
effort: hasExplicitEffort ? explicitEffort : normalizeSavedEffort(saved.effort),
|
|
885
|
-
fast: hasExplicitFast ? explicitFast : (hasOwn(saved, 'fast') ? saved.fast === true : fastPreferenceFor(config, p, m)),
|
|
886
|
-
};
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
function isLikelyRawModelId(value) {
|
|
890
|
-
const model = clean(value);
|
|
891
|
-
if (!model || model.length > 160) return false;
|
|
892
|
-
if (/\s/.test(model)) return false;
|
|
893
|
-
return /^[A-Za-z0-9][A-Za-z0-9._:/@+-]*$/.test(model);
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
function validateRequestedModelSelector(config, requested = {}) {
|
|
897
|
-
const model = clean(requested.model);
|
|
898
|
-
if (!model) return;
|
|
899
|
-
if (findPreset(config, model)) return;
|
|
900
|
-
if (isLikelyRawModelId(model)) return;
|
|
901
|
-
throw new Error(`Invalid model selector "${model}". Use a preset or a model id; free-form text cannot be used as a model.`);
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
function ensureProviderEnabled(config, provider) {
|
|
905
|
-
const providers = { ...(config?.providers || {}) };
|
|
906
|
-
providers[provider] = { ...(providers[provider] || {}), enabled: true };
|
|
907
|
-
return providers;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
const AUTO_CLEAR_DEFAULT_IDLE_MS = 60 * 60 * 1000;
|
|
911
|
-
|
|
912
|
-
function normalizeSystemShellConfig(value = {}) {
|
|
913
|
-
const raw = value && typeof value === 'object' ? value : {};
|
|
914
|
-
const command = clean(raw.command ?? raw.path ?? raw.executable ?? raw.shell);
|
|
915
|
-
const envCommand = clean(process.env.MIXDOG_SHELL);
|
|
916
|
-
return {
|
|
917
|
-
command,
|
|
918
|
-
effective: command || envCommand || '',
|
|
919
|
-
source: command ? 'config' : (envCommand ? 'env' : 'auto'),
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
function normalizeSystemShellCommand(value) {
|
|
924
|
-
const command = clean(value).replace(/^auto$/i, '').replace(/^['"](.+)['"]$/, '$1').trim();
|
|
925
|
-
if (!command) return '';
|
|
926
|
-
if (process.platform === 'win32') {
|
|
927
|
-
const stem = command.split(/[\\/]/).pop().toLowerCase().replace(/\.exe$/, '');
|
|
928
|
-
if (stem !== 'powershell' && stem !== 'pwsh') {
|
|
929
|
-
throw new Error('system shell command must be powershell.exe or pwsh on Windows');
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
return command;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
function normalizeAutoClearConfig(value = {}) {
|
|
936
|
-
const raw = value && typeof value === 'object' ? value : {};
|
|
937
|
-
const idleMs = Number(raw.idleMs ?? raw.thresholdMs ?? raw.idleMillis);
|
|
938
|
-
return {
|
|
939
|
-
enabled: raw.enabled !== false,
|
|
940
|
-
idleMs: Number.isFinite(idleMs) && idleMs > 0 ? Math.max(60_000, Math.round(idleMs)) : AUTO_CLEAR_DEFAULT_IDLE_MS,
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
function normalizeCompactTypeSetting(value, fallback = 'semantic') {
|
|
945
|
-
const raw = clean(value).toLowerCase().replace(/_/g, '-');
|
|
946
|
-
if (!raw) return fallback;
|
|
947
|
-
if (raw === '1' || raw === 'type1' || raw === 'type-1' || raw === 'semantic' || raw === 'summary' || raw === 'default') return 'semantic';
|
|
948
|
-
if (raw === '2' || raw === 'type2' || raw === 'type-2' || raw === 'recall' || raw === 'recall-fast' || raw === 'recall-fasttrack' || raw === 'recall-fast-track' || raw === 'fasttrack' || raw === 'fast-track') return 'recall-fasttrack';
|
|
949
|
-
return fallback;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
function normalizeCompactionConfig(value = {}, { memoryEnabled = true } = {}) {
|
|
953
|
-
const raw = value && typeof value === 'object' ? value : {};
|
|
954
|
-
let compactType = normalizeCompactTypeSetting(raw.compactType ?? raw.compact_type ?? raw.type, 'semantic');
|
|
955
|
-
if (compactType === 'recall-fasttrack' && memoryEnabled === false) compactType = 'semantic';
|
|
956
|
-
return {
|
|
957
|
-
...raw,
|
|
958
|
-
auto: raw.auto !== false && raw.enabled !== false,
|
|
959
|
-
type: compactType,
|
|
960
|
-
compactType,
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
function moduleEnabled(configLike, name, fallback = true) {
|
|
965
|
-
const entry = configLike?.modules?.[name];
|
|
966
|
-
if (entry && typeof entry === 'object' && entry.enabled === false) return false;
|
|
967
|
-
return fallback !== false;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
function setModuleEnabledInConfig(configLike, name, enabled) {
|
|
971
|
-
const next = { ...(configLike || {}) };
|
|
972
|
-
next.modules = { ...(next.modules || {}) };
|
|
973
|
-
next.modules[name] = {
|
|
974
|
-
...(next.modules[name] || {}),
|
|
975
|
-
enabled: enabled !== false,
|
|
976
|
-
};
|
|
977
|
-
return next;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
function formatDurationMs(ms) {
|
|
981
|
-
const value = Math.max(0, Number(ms) || 0);
|
|
982
|
-
if (value % 3_600_000 === 0) return `${value / 3_600_000}h`;
|
|
983
|
-
if (value % 60_000 === 0) return `${value / 60_000}m`;
|
|
984
|
-
return `${Math.round(value / 1000)}s`;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
function parseDurationMs(input) {
|
|
988
|
-
const text = clean(input).toLowerCase();
|
|
989
|
-
if (!text) return null;
|
|
990
|
-
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(text);
|
|
991
|
-
if (!match) return null;
|
|
992
|
-
const n = Number(match[1]);
|
|
993
|
-
if (!Number.isFinite(n) || n <= 0) return null;
|
|
994
|
-
const unit = match[2] || 'm';
|
|
995
|
-
const mult = unit === 'h' ? 3_600_000 : unit === 'm' ? 60_000 : unit === 's' ? 1000 : 1;
|
|
996
|
-
return Math.max(60_000, Math.round(n * mult));
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
const FAST_CAPABLE_PROVIDERS = new Set(['anthropic', 'anthropic-oauth', 'openai', 'openai-oauth']);
|
|
1000
|
-
const LAZY_SECRET_PROVIDERS = new Set(['openai-oauth', 'anthropic-oauth', 'grok-oauth', 'ollama', 'lmstudio']);
|
|
1001
|
-
|
|
1002
|
-
function routeFastKey(provider, model) {
|
|
1003
|
-
const p = clean(provider);
|
|
1004
|
-
const m = clean(model);
|
|
1005
|
-
return p && m ? `${p}/${m}` : '';
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
function openAiModelMetaSupportsFast(model) {
|
|
1009
|
-
const tiers = Array.isArray(model?.serviceTiers) ? model.serviceTiers : [];
|
|
1010
|
-
const speedTiers = Array.isArray(model?.additionalSpeedTiers) ? model.additionalSpeedTiers : [];
|
|
1011
|
-
if (tiers.length || speedTiers.length || model?.defaultServiceTier) {
|
|
1012
|
-
return tiers.some((tier) => tier?.id === 'priority')
|
|
1013
|
-
|| speedTiers.includes('priority')
|
|
1014
|
-
|| model?.defaultServiceTier === 'priority';
|
|
1015
|
-
}
|
|
1016
|
-
const id = clean(model?.id || model).toLowerCase();
|
|
1017
|
-
if (id.includes('mini') || id.includes('nano') || id.includes('codex')) return false;
|
|
1018
|
-
return /^gpt-5(\.|-|$)/.test(id);
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
function openAiDirectModelSupportsFast(model) {
|
|
1022
|
-
const id = clean(model?.id || model);
|
|
1023
|
-
return /^gpt-5\.5(?:-\d{4}|$)/.test(id)
|
|
1024
|
-
|| /^gpt-5\.4(?:-\d{4}|$)/.test(id)
|
|
1025
|
-
|| /^gpt-5\.4-mini(?:-\d{4}|$)/.test(id);
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
function openAiModelSupportsHostedWebSearch(model) {
|
|
1029
|
-
const id = clean(model?.id || model).toLowerCase();
|
|
1030
|
-
if (!id) return false;
|
|
1031
|
-
if (model?.supportsWebSearch === true) return true;
|
|
1032
|
-
const tools = [
|
|
1033
|
-
...(Array.isArray(model?.supportedTools) ? model.supportedTools : []),
|
|
1034
|
-
...(Array.isArray(model?.tools) ? model.tools : []),
|
|
1035
|
-
...(Array.isArray(model?.capabilities?.tools) ? model.capabilities.tools : []),
|
|
1036
|
-
].map((tool) => clean(tool?.type || tool?.name || tool).toLowerCase());
|
|
1037
|
-
if (tools.some((tool) => tool === 'web_search' || tool === 'web_search_preview')) return true;
|
|
1038
|
-
if (/codex|image|audio|tts|stt|embedding|rerank|moderation|search-preview/.test(id)) return false;
|
|
1039
|
-
return /^gpt-(5(?:\.|$|-)|4\.1(?:-|$)|4o(?:-|$)|4\.5(?:-|$))/.test(id)
|
|
1040
|
-
|| /^o[34](?:-|$)/.test(id);
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
function grokModelSupportsHostedWebSearch(model) {
|
|
1044
|
-
const id = clean(model?.id || model).toLowerCase();
|
|
1045
|
-
if (!id || /imagine|image|video|composer/.test(id)) return false;
|
|
1046
|
-
if (id === 'grok-build') return false;
|
|
1047
|
-
return /^grok-/.test(id);
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
function geminiModelSupportsHostedWebSearch(model) {
|
|
1051
|
-
const id = clean(model?.id || model).toLowerCase();
|
|
1052
|
-
if (!id || /embedding|aqa|imagen|veo|tts|image|computer-use|customtools/.test(id)) return false;
|
|
1053
|
-
return /^gemini-(3(?:\.|-|$)|2\.5-|2\.0-flash)/.test(id);
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
function anthropicModelSupportsHostedWebSearch(model) {
|
|
1057
|
-
const id = clean(model?.id || model).toLowerCase();
|
|
1058
|
-
if (!id) return false;
|
|
1059
|
-
const match = id.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:[-.](\d+))?/);
|
|
1060
|
-
if (!match) return false;
|
|
1061
|
-
const major = Number(match[2]) || 0;
|
|
1062
|
-
const minor = Number(match[3]) || 0;
|
|
1063
|
-
return major > 4 || (major === 4 && minor >= 0);
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
function anthropicModelMetaSupportsFast(model) {
|
|
1067
|
-
const id = clean(model?.id || model).toLowerCase();
|
|
1068
|
-
return /^claude-(opus|sonnet)/.test(id);
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
function fastCapableFor(provider, model) {
|
|
1072
|
-
const p = clean(provider);
|
|
1073
|
-
if (!FAST_CAPABLE_PROVIDERS.has(p)) return false;
|
|
1074
|
-
if (p === 'openai') return openAiDirectModelSupportsFast(model);
|
|
1075
|
-
if (p === 'openai-oauth') return openAiModelMetaSupportsFast(model);
|
|
1076
|
-
if (p === 'anthropic' || p === 'anthropic-oauth') return anthropicModelMetaSupportsFast(model);
|
|
1077
|
-
return false;
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
function searchCapableFor(provider, model) {
|
|
1081
|
-
const p = normalizeSearchProviderId(provider);
|
|
1082
|
-
if (!isSearchCapableProvider(p)) return false;
|
|
1083
|
-
if (p === 'openai' || p === 'openai-oauth') return openAiModelSupportsHostedWebSearch(model);
|
|
1084
|
-
if (p === 'grok-oauth' || p === 'xai') return grokModelSupportsHostedWebSearch(model);
|
|
1085
|
-
if (p === 'gemini') return geminiModelSupportsHostedWebSearch(model);
|
|
1086
|
-
if (p === 'anthropic' || p === 'anthropic-oauth') return anthropicModelSupportsHostedWebSearch(model);
|
|
1087
|
-
return model?.supportsWebSearch === true;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
function fastPreferenceFor(config, provider, model) {
|
|
1091
|
-
const key = routeFastKey(provider, model);
|
|
1092
|
-
if (!key) return false;
|
|
1093
|
-
const saved = config?.modelSettings?.[key];
|
|
1094
|
-
if (saved && typeof saved === 'object' && hasOwn(saved, 'fast')) return saved.fast === true;
|
|
1095
|
-
return config?.fastModels?.[key] === true;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
function saveModelSettings(cfgMod, route, { fastCapable = true, baseConfig = null } = {}) {
|
|
1099
|
-
const key = routeFastKey(route?.provider, route?.model);
|
|
1100
|
-
if (!key) return baseConfig || cfgMod.loadConfig();
|
|
1101
|
-
const nextConfig = baseConfig || cfgMod.loadConfig();
|
|
1102
|
-
const modelSettings = { ...(nextConfig.modelSettings || {}) };
|
|
1103
|
-
const nextSetting = { ...(modelSettings[key] || {}) };
|
|
1104
|
-
if (hasOwn(route, 'effort') && route.effort) nextSetting.effort = route.effort;
|
|
1105
|
-
else delete nextSetting.effort;
|
|
1106
|
-
if (fastCapable) nextSetting.fast = route.fast === true;
|
|
1107
|
-
else nextSetting.fast = false;
|
|
1108
|
-
modelSettings[key] = nextSetting;
|
|
1109
|
-
|
|
1110
|
-
// Legacy compatibility: keep fastModels true entries for old readers, but
|
|
1111
|
-
// let modelSettings.fast=false override them in new readers.
|
|
1112
|
-
const fastModels = { ...(nextConfig.fastModels || {}) };
|
|
1113
|
-
if (nextSetting.fast === true) fastModels[key] = true;
|
|
1114
|
-
else delete fastModels[key];
|
|
1115
|
-
|
|
1116
|
-
const savedConfig = { ...nextConfig, modelSettings, fastModels };
|
|
1117
|
-
cfgMod.saveConfig(savedConfig);
|
|
1118
|
-
return savedConfig;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
function routeForStatusline(route) {
|
|
1122
|
-
const out = {
|
|
1123
|
-
mode: 'fixed',
|
|
1124
|
-
defaultProvider: route.provider,
|
|
1125
|
-
defaultModel: route.model,
|
|
1126
|
-
};
|
|
1127
|
-
const preset = route.preset || {};
|
|
1128
|
-
if (preset.id) out.presetId = preset.id;
|
|
1129
|
-
if (preset.name) out.presetName = preset.name;
|
|
1130
|
-
// Prefer the preset's curated label, then the route's resolved model display
|
|
1131
|
-
// (set by refreshRouteEffort from the live/offline catalog). Without the
|
|
1132
|
-
// route fallback, a preset-less direct model (e.g. claude-fable-5) reaches
|
|
1133
|
-
// the statusline with no display and renders as the raw id.
|
|
1134
|
-
const modelDisplay = clean(preset.modelDisplay) || clean(route.modelDisplay);
|
|
1135
|
-
if (modelDisplay) out.modelDisplay = modelDisplay;
|
|
1136
|
-
if (route.fast === true || route.fast === false) out.fast = route.fast;
|
|
1137
|
-
else if (preset.fast === true || preset.fast === false) out.fast = preset.fast;
|
|
1138
|
-
if (route.effectiveEffort) {
|
|
1139
|
-
out.effort = route.effectiveEffort;
|
|
1140
|
-
out.displayEffort = route.effectiveEffort;
|
|
1141
|
-
} else if (hasOwn(route, 'effort')) {
|
|
1142
|
-
delete out.effort;
|
|
1143
|
-
delete out.displayEffort;
|
|
1144
|
-
}
|
|
1145
|
-
return out;
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
function writeStatuslineRoute(statusRoutes, session, route) {
|
|
1149
|
-
if (!session?.id || !route) return;
|
|
1150
|
-
const clientHostPid = session?.clientHostPid || process.pid;
|
|
1151
|
-
statusRoutes?.writeGatewaySessionRoute?.(session.id, routeForStatusline(route), { clientHostPid });
|
|
1152
|
-
}
|
|
375
|
+
const outputStyleStatus = (dataDir = STANDALONE_DATA_DIR) => outputStyleStatusRaw(STANDALONE_ROOT, dataDir || STANDALONE_DATA_DIR);
|
|
1153
376
|
|
|
1154
377
|
const ONBOARDING_VERSION = 1;
|
|
1155
|
-
const WORKFLOW_ROUTE_SLOTS = ['lead', 'agent', 'explorer', 'memory'];
|
|
1156
|
-
const FIXED_AGENT_SLOTS = Object.freeze([
|
|
1157
|
-
{ id: 'explore', label: 'Explore', description: 'Broad repository exploration', workflowSlot: 'explorer' },
|
|
1158
|
-
{ id: 'maintainer', label: 'Maintainer', description: 'Background memory and upkeep', workflowSlot: 'memory' },
|
|
1159
|
-
{ id: 'worker', label: 'Worker', description: 'Scoped implementation' },
|
|
1160
|
-
{ id: 'heavy-worker', label: 'Heavy Worker', description: 'Broad or multi-file implementation' },
|
|
1161
|
-
{ id: 'reviewer', label: 'Reviewer', description: 'Diff review and risk checks' },
|
|
1162
|
-
{ id: 'debugger', label: 'Debugger', description: 'Root-cause analysis and failure tracing' },
|
|
1163
|
-
]);
|
|
1164
|
-
const SEARCH_CAPABLE_PROVIDERS = new Set([
|
|
1165
|
-
'openai-oauth',
|
|
1166
|
-
'openai',
|
|
1167
|
-
'grok-oauth',
|
|
1168
|
-
'xai',
|
|
1169
|
-
'gemini',
|
|
1170
|
-
'anthropic',
|
|
1171
|
-
'anthropic-oauth',
|
|
1172
|
-
]);
|
|
1173
|
-
const SEARCH_DEFAULT_PROVIDER = 'default';
|
|
1174
|
-
const SEARCH_DEFAULT_MODEL = 'default';
|
|
1175
|
-
const SEARCH_PROVIDER_ALIASES = Object.freeze({
|
|
1176
|
-
'openai-api': 'openai',
|
|
1177
|
-
'xai-api': 'xai',
|
|
1178
|
-
'gemini-api': 'gemini',
|
|
1179
|
-
'anthropic-api': 'anthropic',
|
|
1180
|
-
});
|
|
1181
378
|
const QUICK_SEARCH_MODELS = Object.freeze({
|
|
1182
379
|
'openai-oauth': [
|
|
1183
380
|
{ id: 'gpt-5.5', display: 'GPT-5.5', latest: true, contextWindow: 1000000 },
|
|
@@ -1219,855 +416,26 @@ const QUICK_SEARCH_MODELS = Object.freeze({
|
|
|
1219
416
|
{ id: 'claude-haiku-4-5-20251001', display: 'Claude Haiku 4.5', contextWindow: 200000 },
|
|
1220
417
|
],
|
|
1221
418
|
});
|
|
1222
|
-
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
if (id === 'heavy' || id === 'heavyworker') return 'heavy-worker';
|
|
1243
|
-
if (id === 'review') return 'reviewer';
|
|
1244
|
-
if (id === 'debug') return 'debugger';
|
|
1245
|
-
return AGENT_ROLE_IDS.has(id) ? id : '';
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
function normalizeWorkflowId(value, fallback = '') {
|
|
1249
|
-
const id = clean(value).toLowerCase().replace(/[\s_]+/g, '-');
|
|
1250
|
-
return /^[a-z0-9][a-z0-9_.-]*$/.test(id) ? id : fallback;
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
function readTextSafe(path) {
|
|
1254
|
-
try { return readFileSync(path, 'utf8').trim(); } catch { return ''; }
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
function workflowSourceDirs(dataDir) {
|
|
1258
|
-
return [
|
|
1259
|
-
{ root: join(STANDALONE_ROOT, 'workflows'), source: 'built-in' },
|
|
1260
|
-
{ root: join(dataDir || STANDALONE_DATA_DIR, 'workflows'), source: 'user' },
|
|
1261
|
-
];
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
function agentSourceDirs(dataDir, id) {
|
|
1265
|
-
return [
|
|
1266
|
-
join(dataDir || STANDALONE_DATA_DIR, 'agents', id),
|
|
1267
|
-
join(STANDALONE_ROOT, 'agents', id),
|
|
1268
|
-
];
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
function readWorkflowPackFromDir(dir, source = 'built-in', dirName = '') {
|
|
1272
|
-
const entry = 'WORKFLOW.md';
|
|
1273
|
-
const doc = readMarkdownDocument(readTextSafe(join(dir, entry)));
|
|
1274
|
-
const body = doc.body;
|
|
1275
|
-
if (!body) return null;
|
|
1276
|
-
const fm = doc.frontmatter || {};
|
|
1277
|
-
const id = normalizeWorkflowId(clean(fm.id) || dirName || basename(dir));
|
|
1278
|
-
if (!id) return null;
|
|
1279
|
-
const agentsConfigured = Object.prototype.hasOwnProperty.call(fm, 'agents');
|
|
1280
|
-
return {
|
|
1281
|
-
id,
|
|
1282
|
-
name: clean(fm.name) || id,
|
|
1283
|
-
description: clean(fm.description),
|
|
1284
|
-
entry,
|
|
1285
|
-
agentsConfigured,
|
|
1286
|
-
agents: agentsConfigured
|
|
1287
|
-
? String(fm.agents || '')
|
|
1288
|
-
.split(',')
|
|
1289
|
-
.map((agent) => normalizeAgentId(agent) || normalizeWorkflowId(agent))
|
|
1290
|
-
.filter(Boolean)
|
|
1291
|
-
: [],
|
|
1292
|
-
body,
|
|
1293
|
-
source,
|
|
1294
|
-
};
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
function listWorkflowPacks(dataDir) {
|
|
1298
|
-
const byId = new Map();
|
|
1299
|
-
for (const { root, source } of workflowSourceDirs(dataDir)) {
|
|
1300
|
-
if (!existsSync(root)) continue;
|
|
1301
|
-
let entries = [];
|
|
1302
|
-
try { entries = readdirSync(root, { withFileTypes: true }); } catch { entries = []; }
|
|
1303
|
-
for (const entry of entries) {
|
|
1304
|
-
if (!entry.isDirectory()) continue;
|
|
1305
|
-
const dir = join(root, entry.name);
|
|
1306
|
-
if (!existsSync(join(dir, 'WORKFLOW.md'))) continue;
|
|
1307
|
-
const pack = readWorkflowPackFromDir(dir, source, entry.name);
|
|
1308
|
-
if (pack) byId.set(pack.id, pack);
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
return [...byId.values()].sort((a, b) => {
|
|
1312
|
-
if (a.id === DEFAULT_WORKFLOW_ID) return -1;
|
|
1313
|
-
if (b.id === DEFAULT_WORKFLOW_ID) return 1;
|
|
1314
|
-
return a.name.localeCompare(b.name);
|
|
1315
|
-
});
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
function activeWorkflowId(config) {
|
|
1319
|
-
return normalizeWorkflowId(config?.workflow?.active, DEFAULT_WORKFLOW_ID);
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
function loadWorkflowPack(dataDir, id) {
|
|
1323
|
-
const wanted = normalizeWorkflowId(id, DEFAULT_WORKFLOW_ID);
|
|
1324
|
-
for (const { root, source } of workflowSourceDirs(dataDir).reverse()) {
|
|
1325
|
-
const pack = readWorkflowPackFromDir(join(root, wanted), source, wanted);
|
|
1326
|
-
if (pack) return pack;
|
|
1327
|
-
}
|
|
1328
|
-
return readWorkflowPackFromDir(join(STANDALONE_ROOT, 'workflows', DEFAULT_WORKFLOW_ID), 'built-in', DEFAULT_WORKFLOW_ID);
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
function workflowSummary(pack) {
|
|
1332
|
-
const id = normalizeWorkflowId(pack?.id, DEFAULT_WORKFLOW_ID);
|
|
1333
|
-
return {
|
|
1334
|
-
id,
|
|
1335
|
-
name: clean(pack?.name) || (id === DEFAULT_WORKFLOW_ID ? 'Default' : id),
|
|
1336
|
-
description: clean(pack?.description),
|
|
1337
|
-
source: clean(pack?.source),
|
|
1338
|
-
};
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
function activeWorkflowSummary(config, dataDir) {
|
|
1342
|
-
return workflowSummary(loadWorkflowPack(dataDir, activeWorkflowId(config)));
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
function loadAgentDefinition(dataDir, id) {
|
|
1346
|
-
const agentId = normalizeAgentId(id) || normalizeWorkflowId(id);
|
|
1347
|
-
if (!agentId) return null;
|
|
1348
|
-
const cacheKey = `${dataDir || STANDALONE_DATA_DIR}\n${agentId}`;
|
|
1349
|
-
if (agentDefinitionCache.has(cacheKey)) return agentDefinitionCache.get(cacheKey);
|
|
1350
|
-
for (const dir of agentSourceDirs(dataDir, agentId)) {
|
|
1351
|
-
const manifest = readJsonSafe(join(dir, 'agent.json')) || {};
|
|
1352
|
-
const entry = clean(manifest.entry) || 'AGENT.md';
|
|
1353
|
-
const doc = readMarkdownDocument(readTextSafe(join(dir, entry)));
|
|
1354
|
-
const body = doc.body;
|
|
1355
|
-
if (!body) continue;
|
|
1356
|
-
const definition = {
|
|
1357
|
-
id: agentId,
|
|
1358
|
-
name: clean(manifest.name) || FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.label || agentId,
|
|
1359
|
-
description: clean(manifest.description) || FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.description || '',
|
|
1360
|
-
permission: normalizeAgentPermissionOrNone(doc.frontmatter.permission),
|
|
1361
|
-
frontmatter: doc.frontmatter,
|
|
1362
|
-
body,
|
|
1363
|
-
};
|
|
1364
|
-
agentDefinitionCache.set(cacheKey, definition);
|
|
1365
|
-
return definition;
|
|
1366
|
-
}
|
|
1367
|
-
const legacyDoc = readMarkdownDocument(readTextSafe(join(STANDALONE_ROOT, 'agents', `${agentId}.md`)));
|
|
1368
|
-
if (!legacyDoc.body) {
|
|
1369
|
-
agentDefinitionCache.set(cacheKey, null);
|
|
1370
|
-
return null;
|
|
1371
|
-
}
|
|
1372
|
-
const definition = {
|
|
1373
|
-
id: agentId,
|
|
1374
|
-
name: FIXED_AGENT_SLOTS.find((agent) => agent.id === agentId)?.label || agentId,
|
|
1375
|
-
description: '',
|
|
1376
|
-
permission: normalizeAgentPermissionOrNone(legacyDoc.frontmatter.permission),
|
|
1377
|
-
frontmatter: legacyDoc.frontmatter,
|
|
1378
|
-
body: legacyDoc.body,
|
|
1379
|
-
};
|
|
1380
|
-
agentDefinitionCache.set(cacheKey, definition);
|
|
1381
|
-
return definition;
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
function workflowContextBlock(config, dataDir) {
|
|
1385
|
-
const pack = loadWorkflowPack(dataDir, activeWorkflowId(config));
|
|
1386
|
-
if (!pack) return '';
|
|
1387
|
-
const lines = [
|
|
1388
|
-
`# Active Workflow: ${pack.name}`,
|
|
1389
|
-
];
|
|
1390
|
-
if (pack.description) lines.push(pack.description);
|
|
1391
|
-
lines.push(pack.body);
|
|
1392
|
-
|
|
1393
|
-
const agentIds = pack.agentsConfigured ? pack.agents : FIXED_AGENT_SLOTS.map((agent) => agent.id);
|
|
1394
|
-
const agentBlocks = agentIds
|
|
1395
|
-
.map((id) => loadAgentDefinition(dataDir, id))
|
|
1396
|
-
.filter(Boolean);
|
|
1397
|
-
if (agentBlocks.length) {
|
|
1398
|
-
lines.push('# Available Agents');
|
|
1399
|
-
for (const agent of agentBlocks) {
|
|
1400
|
-
lines.push(`## ${agent.name} (${agent.id})`);
|
|
1401
|
-
if (agent.description) lines.push(agent.description);
|
|
1402
|
-
lines.push(agent.body);
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
return lines.join('\n\n');
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
function normalizeWorkflowRoute(routeLike, fallback = {}) {
|
|
1409
|
-
const provider = clean(routeLike?.provider) || clean(fallback.provider);
|
|
1410
|
-
const model = clean(routeLike?.model) || clean(fallback.model);
|
|
1411
|
-
if (!provider || !model) return null;
|
|
1412
|
-
// Defensive: a workflow/agent route must carry a real model id. Reject values
|
|
1413
|
-
// that are obviously free-form text (whitespace, prose) so a bad string can
|
|
1414
|
-
// never be persisted as a preset/workflow route. Normal model ids pass
|
|
1415
|
-
// isLikelyRawModelId unchanged.
|
|
1416
|
-
if (!isLikelyRawModelId(model)) return null;
|
|
1417
|
-
const effort = normalizeEffortInput(routeLike?.effort ?? fallback.effort);
|
|
1418
|
-
const fast = routeLike?.fast ?? fallback.fast;
|
|
1419
|
-
return {
|
|
1420
|
-
provider,
|
|
1421
|
-
model,
|
|
1422
|
-
...(effort ? { effort } : {}),
|
|
1423
|
-
...(fast === true ? { fast: true } : {}),
|
|
1424
|
-
};
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
function normalizeSearchProviderId(provider) {
|
|
1428
|
-
const id = clean(provider);
|
|
1429
|
-
return SEARCH_PROVIDER_ALIASES[id] || id;
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
function isDefaultSearchRouteConfig(routeLike = {}) {
|
|
1433
|
-
return normalizeSearchProviderId(routeLike?.provider) === SEARCH_DEFAULT_PROVIDER
|
|
1434
|
-
&& clean(routeLike?.model).toLowerCase() === SEARCH_DEFAULT_MODEL;
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
function isSearchCapableProvider(provider) {
|
|
1438
|
-
return SEARCH_CAPABLE_PROVIDERS.has(normalizeSearchProviderId(provider));
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
function normalizeSearchRouteConfig(routeLike, fallback = {}) {
|
|
1442
|
-
const provider = normalizeSearchProviderId(routeLike?.provider || fallback.provider);
|
|
1443
|
-
const model = clean(routeLike?.model || fallback.model);
|
|
1444
|
-
if (!provider || !model) return null;
|
|
1445
|
-
let effort = null;
|
|
1446
|
-
try {
|
|
1447
|
-
effort = normalizeEffortInput(routeLike?.effort ?? fallback.effort);
|
|
1448
|
-
} catch {
|
|
1449
|
-
effort = null;
|
|
1450
|
-
}
|
|
1451
|
-
const fast = routeLike?.fast ?? fallback.fast;
|
|
1452
|
-
const toolType = clean(routeLike?.toolType || fallback.toolType);
|
|
1453
|
-
return {
|
|
1454
|
-
provider,
|
|
1455
|
-
model,
|
|
1456
|
-
...(effort ? { effort } : {}),
|
|
1457
|
-
...(fast === true ? { fast: true } : {}),
|
|
1458
|
-
...(toolType ? { toolType } : {}),
|
|
1459
|
-
};
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
function upsertWorkflowPreset(presets, slot, routeLike) {
|
|
1463
|
-
const route = normalizeWorkflowRoute(routeLike);
|
|
1464
|
-
if (!route) return presets;
|
|
1465
|
-
const id = workflowPresetId(slot);
|
|
1466
|
-
const preset = {
|
|
1467
|
-
id,
|
|
1468
|
-
name: workflowPresetName(slot),
|
|
1469
|
-
type: 'agent',
|
|
1470
|
-
provider: route.provider,
|
|
1471
|
-
model: route.model,
|
|
1472
|
-
...(route.effort ? { effort: route.effort } : {}),
|
|
1473
|
-
...(route.fast === true ? { fast: true } : {}),
|
|
1474
|
-
tools: 'full',
|
|
1475
|
-
};
|
|
1476
|
-
const next = (Array.isArray(presets) ? presets : []).filter((p) => clean(p?.id) !== id && clean(p?.name) !== preset.name);
|
|
1477
|
-
next.push(preset);
|
|
1478
|
-
return next;
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
function summarizeWorkflowRoutes(config) {
|
|
1482
|
-
const routes = config?.workflowRoutes && typeof config.workflowRoutes === 'object' ? config.workflowRoutes : {};
|
|
1483
|
-
const fallbackProvider = resolveDefaultProvider(config);
|
|
1484
|
-
const out = {};
|
|
1485
|
-
for (const slot of WORKFLOW_ROUTE_SLOTS) {
|
|
1486
|
-
const route = routes[slot];
|
|
1487
|
-
// Read/interpret path: a route with a model but no provider falls back to
|
|
1488
|
-
// config.defaultProvider (then DEFAULT_PROVIDER).
|
|
1489
|
-
if (route?.model && (route?.provider || fallbackProvider)) {
|
|
1490
|
-
out[slot] = normalizeWorkflowRoute(route, { provider: fallbackProvider });
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
return out;
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
function routeFromPreset(config, slotValue) {
|
|
1497
|
-
// Maintenance slots now store a direct {provider, model} route. Accept that
|
|
1498
|
-
// shape first; fall back to the legacy preset-NAME string lookup so configs
|
|
1499
|
-
// written before the route migration still resolve.
|
|
1500
|
-
if (slotValue && typeof slotValue === 'object' && !Array.isArray(slotValue)) {
|
|
1501
|
-
const direct = normalizeWorkflowRoute(slotValue);
|
|
1502
|
-
if (direct) return direct;
|
|
1503
|
-
}
|
|
1504
|
-
const preset = findPreset(config, slotValue);
|
|
1505
|
-
return preset ? normalizeWorkflowRoute(preset) : null;
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
function agentRouteFromConfig(config, agentId, _dataDir) {
|
|
1509
|
-
const id = normalizeAgentId(agentId);
|
|
1510
|
-
if (!id) return null;
|
|
1511
|
-
// Read/interpret path: inject config.defaultProvider (then DEFAULT_PROVIDER)
|
|
1512
|
-
// when a stored route omits its provider.
|
|
1513
|
-
const fallback = { provider: resolveDefaultProvider(config) };
|
|
1514
|
-
const explicit = normalizeWorkflowRoute(config?.agents?.[id], fallback)
|
|
1515
|
-
|| (id === 'maintainer' ? normalizeWorkflowRoute(config?.agents?.maintenance, fallback) : null);
|
|
1516
|
-
if (explicit) return explicit;
|
|
1517
|
-
|
|
1518
|
-
const agent = FIXED_AGENT_SLOTS.find((item) => item.id === id);
|
|
1519
|
-
if (agent?.workflowSlot) {
|
|
1520
|
-
const workflowRoute = normalizeWorkflowRoute(config?.workflowRoutes?.[agent.workflowSlot], fallback);
|
|
1521
|
-
if (workflowRoute) return workflowRoute;
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
if (id === 'explore') return routeFromPreset(config, config?.maintenance?.explore);
|
|
1525
|
-
if (id === 'maintainer') return routeFromPreset(config, config?.maintenance?.memory);
|
|
1526
|
-
|
|
1527
|
-
return null;
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
function toolResponseText(result) {
|
|
1531
|
-
if (result && typeof result === 'object' && Array.isArray(result.content)) {
|
|
1532
|
-
return result.content
|
|
1533
|
-
.map((part) => (part?.type === 'text' ? part.text || '' : JSON.stringify(part)))
|
|
1534
|
-
.join('\n');
|
|
1535
|
-
}
|
|
1536
|
-
if (typeof result === 'string') return result;
|
|
1537
|
-
return JSON.stringify(result, null, 2);
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
function isEmptyRecallText(value) {
|
|
1541
|
-
const text = String(value || '').trim();
|
|
1542
|
-
return !text || /^\(?no results\)?$/i.test(text) || /^\(?empty memory result\)?$/i.test(text);
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
function currentSessionRecallRows(session, query, { limit = 10 } = {}) {
|
|
1546
|
-
const messages = Array.isArray(session?.messages) ? session.messages : [];
|
|
1547
|
-
if (!messages.length) return '(no results)';
|
|
1548
|
-
const terms = [...new Set(String(query || '').toLowerCase().match(/[\p{L}\p{N}_./:-]{2,}/gu) || [])]
|
|
1549
|
-
.filter(Boolean)
|
|
1550
|
-
.slice(0, 16);
|
|
1551
|
-
const max = Math.max(1, Math.min(100, Number(limit) || 10));
|
|
1552
|
-
const rows = [];
|
|
1553
|
-
for (let i = messages.length - 1; i >= 0 && rows.length < max; i -= 1) {
|
|
1554
|
-
const m = messages[i];
|
|
1555
|
-
if (!m || (m.role !== 'user' && m.role !== 'assistant' && m.role !== 'tool')) continue;
|
|
1556
|
-
const text = messageContextText(m).replace(/\s+/g, ' ').trim();
|
|
1557
|
-
if (!text) continue;
|
|
1558
|
-
if (terms.length && !terms.some((term) => text.toLowerCase().includes(term))) continue;
|
|
1559
|
-
rows.push(`[session:${i + 1}] ${m.role}: ${text.slice(0, 1000)}`);
|
|
1560
|
-
}
|
|
1561
|
-
return rows.length ? rows.join('\n') : '(no results)';
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
function parseToolSelection(value) {
|
|
1565
|
-
if (Array.isArray(value)) return value.map(clean).filter(Boolean);
|
|
1566
|
-
if (value && typeof value !== 'string' && typeof value[Symbol.iterator] === 'function') {
|
|
1567
|
-
return [...value].map(clean).filter(Boolean);
|
|
1568
|
-
}
|
|
1569
|
-
return String(value || '').replace(/^select\s*:/i, '')
|
|
1570
|
-
.split(/[,\s]+/)
|
|
1571
|
-
.map(clean)
|
|
1572
|
-
.filter(Boolean);
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
function parseToolSearchQuerySelection(query) {
|
|
1576
|
-
const match = clean(query).match(/^select\s*:\s*(.+)$/i);
|
|
1577
|
-
return match ? parseToolSelection(match[1]) : [];
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
function toolKind(tool) {
|
|
1581
|
-
const name = clean(tool?.name);
|
|
1582
|
-
if (name.startsWith('mcp__')) return 'mcp';
|
|
1583
|
-
if (name.startsWith('skill:') || tool?.annotations?.mixdogKind === 'skill') return 'skill';
|
|
1584
|
-
if (name === 'Skill' || name.startsWith('skill_') || name === 'skills_list' || name === 'skill_view') return 'skill';
|
|
1585
|
-
if (tool?.annotations?.agentHidden) return 'control';
|
|
1586
|
-
if (['apply_patch', 'shell'].includes(name)) return 'mutation';
|
|
1587
|
-
return 'tool';
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
function toolSchemaBucket(tool) {
|
|
1591
|
-
const name = clean(tool?.name);
|
|
1592
|
-
const kind = toolKind(tool);
|
|
1593
|
-
if (kind === 'mcp') return 'mcp';
|
|
1594
|
-
if (kind === 'skill') return 'skills';
|
|
1595
|
-
if (name === 'memory' || name === 'recall' || name.includes('memory')) return 'memory';
|
|
1596
|
-
if (name === 'search' || name === 'web_fetch') return 'web';
|
|
1597
|
-
if (['read', 'grep', 'find', 'glob', 'list', 'code_graph', 'explore'].includes(name)) return 'code';
|
|
1598
|
-
if (['shell', 'apply_patch'].includes(name)) return 'mutation';
|
|
1599
|
-
if (name === 'agent' || name === 'delegate') return 'agents';
|
|
1600
|
-
if (name.includes('channel') || name.includes('discord') || name.includes('webhook')) return 'channels';
|
|
1601
|
-
if (name.includes('provider') || name === 'tool_search' || name === 'cwd') return 'setup';
|
|
1602
|
-
return 'other';
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
function estimateToolSchemaBreakdown(tools) {
|
|
1606
|
-
const out = {};
|
|
1607
|
-
for (const tool of Array.isArray(tools) ? tools : []) {
|
|
1608
|
-
const bucket = toolSchemaBucket(tool);
|
|
1609
|
-
const row = out[bucket] || { count: 0, tokens: 0 };
|
|
1610
|
-
row.count += 1;
|
|
1611
|
-
row.tokens += estimateToolSchemaTokens([tool]);
|
|
1612
|
-
out[bucket] = row;
|
|
1613
|
-
}
|
|
1614
|
-
return out;
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
function measuredToolUsage(name) {
|
|
1618
|
-
return MEASURED_TOOL_USAGE[clean(name)] || 0;
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
function measuredToolRank(name) {
|
|
1622
|
-
const index = MEASURED_TOOL_ORDER.indexOf(clean(name));
|
|
1623
|
-
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
function sortedCatalogByMeasuredUsage(catalog) {
|
|
1627
|
-
return (catalog || [])
|
|
1628
|
-
.map((tool, index) => ({ tool, index }))
|
|
1629
|
-
.sort((a, b) => {
|
|
1630
|
-
const au = measuredToolUsage(a.tool?.name);
|
|
1631
|
-
const bu = measuredToolUsage(b.tool?.name);
|
|
1632
|
-
if (bu !== au) return bu - au;
|
|
1633
|
-
const ar = measuredToolRank(a.tool?.name);
|
|
1634
|
-
const br = measuredToolRank(b.tool?.name);
|
|
1635
|
-
if (ar !== br) return ar - br;
|
|
1636
|
-
return a.index - b.index;
|
|
1637
|
-
})
|
|
1638
|
-
.map((entry) => entry.tool);
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
function activeToolForSurface(tool) {
|
|
1642
|
-
if (!tool || typeof tool !== 'object') return tool;
|
|
1643
|
-
return JSON.parse(JSON.stringify(tool));
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
function deferredProviderMode(provider) {
|
|
1647
|
-
const p = clean(provider).toLowerCase();
|
|
1648
|
-
if (p === 'gemini') return 'full';
|
|
1649
|
-
if (p === 'anthropic' || p === 'anthropic-oauth'
|
|
1650
|
-
|| p === 'openai' || p === 'openai-oauth'
|
|
1651
|
-
|| p === 'xai' || p === 'grok-oauth') {
|
|
1652
|
-
return 'native';
|
|
1653
|
-
}
|
|
1654
|
-
return 'legacy';
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
function filterDisallowedTools(tools, disallowed = []) {
|
|
1658
|
-
if (!Array.isArray(disallowed) || disallowed.length === 0) return tools;
|
|
1659
|
-
const deny = new Set(disallowed.map((name) => clean(name)).filter(Boolean));
|
|
1660
|
-
if (deny.size === 0) return tools;
|
|
1661
|
-
return (tools || []).filter((tool) => !deny.has(clean(tool?.name)));
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
function sortedNamesByMeasuredUsage(names) {
|
|
1665
|
-
return [...(names || [])].sort((a, b) => {
|
|
1666
|
-
const au = measuredToolUsage(a);
|
|
1667
|
-
const bu = measuredToolUsage(b);
|
|
1668
|
-
if (bu !== au) return bu - au;
|
|
1669
|
-
const ar = measuredToolRank(a);
|
|
1670
|
-
const br = measuredToolRank(b);
|
|
1671
|
-
if (ar !== br) return ar - br;
|
|
1672
|
-
return String(a).localeCompare(String(b));
|
|
1673
|
-
});
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
export function defaultDeferredToolNames(catalog, mode) {
|
|
1677
|
-
const available = new Set((catalog || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
1678
|
-
if (mode === 'lead') {
|
|
1679
|
-
return new Set(DEFERRED_DEFAULT_LEAD_TOOLS.filter((name) => available.has(name)));
|
|
1680
|
-
}
|
|
1681
|
-
if (mode === 'readonly') {
|
|
1682
|
-
return new Set(DEFERRED_DEFAULT_READONLY_TOOLS.filter((name) => available.has(name)));
|
|
1683
|
-
}
|
|
1684
|
-
return new Set(DEFERRED_DEFAULT_FULL_TOOLS.filter((name) => available.has(name)));
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
export function compactToolSearchDescription(value, max = 220) {
|
|
1688
|
-
const text = clean(value).replace(/\s+/g, ' ');
|
|
1689
|
-
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
function toolRow(tool, activeNames = new Set()) {
|
|
1693
|
-
const name = clean(tool?.name);
|
|
1694
|
-
return {
|
|
1695
|
-
name,
|
|
1696
|
-
kind: toolKind(tool),
|
|
1697
|
-
usage: measuredToolUsage(name),
|
|
1698
|
-
active: activeNames.has(name),
|
|
1699
|
-
description: compactToolSearchDescription(tool?.description),
|
|
1700
|
-
};
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
function providerSupportsResponsesCustomTools(provider) {
|
|
1704
|
-
const p = clean(provider).toLowerCase();
|
|
1705
|
-
if (!p) return true;
|
|
1706
|
-
return p === 'openai' || p === 'openai-oauth';
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
function openAILoadableToolSpec(tool, provider = '') {
|
|
1710
|
-
if (providerSupportsResponsesCustomTools(provider) && isResponsesFreeformTool(tool)) return toResponsesCustomTool(tool);
|
|
1711
|
-
return {
|
|
1712
|
-
type: 'function',
|
|
1713
|
-
name: clean(tool?.name),
|
|
1714
|
-
description: clean(tool?.description),
|
|
1715
|
-
defer_loading: true,
|
|
1716
|
-
parameters: tool?.inputSchema && typeof tool.inputSchema === 'object'
|
|
1717
|
-
? tool.inputSchema
|
|
1718
|
-
: { type: 'object', properties: {} },
|
|
1719
|
-
};
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
function toolSearchNativePayload(catalog, names, provider = '') {
|
|
1723
|
-
const selected = new Set((names || []).map(clean).filter(Boolean));
|
|
1724
|
-
if (!selected.size) return null;
|
|
1725
|
-
const tools = [];
|
|
1726
|
-
const refs = [];
|
|
1727
|
-
for (const tool of catalog || []) {
|
|
1728
|
-
const name = clean(tool?.name);
|
|
1729
|
-
if (!name || !selected.has(name)) continue;
|
|
1730
|
-
refs.push(name);
|
|
1731
|
-
tools.push(openAILoadableToolSpec(tool, provider));
|
|
1732
|
-
}
|
|
1733
|
-
if (!refs.length) return null;
|
|
1734
|
-
return {
|
|
1735
|
-
toolReferences: refs,
|
|
1736
|
-
openaiTools: tools,
|
|
1737
|
-
summary: `Loaded deferred tools: ${refs.join(', ')}`,
|
|
1738
|
-
};
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
function toolSearchTokens(value) {
|
|
1742
|
-
return (clean(value).toLowerCase().match(/[a-z0-9_.-]+/g) || [])
|
|
1743
|
-
.map((token) => token.replace(/[-.]+/g, '_'))
|
|
1744
|
-
.filter(Boolean);
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
function toolSearchMeaningfulTokens(value) {
|
|
1748
|
-
return toolSearchTokens(value).filter((token) => !TOOL_SEARCH_STOP_WORDS.has(token));
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
function toolSearchText(row) {
|
|
1752
|
-
const text = `${row.name} ${String(row.name || '').replace(/_/g, ' ')} ${row.kind} ${row.description} ${row.active ? 'active' : 'deferred'}`;
|
|
1753
|
-
return `${text} ${text.replace(/[-.]+/g, '_')}`.toLowerCase();
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
function toolSearchRowAliases(name) {
|
|
1757
|
-
return TOOL_SEARCH_ROW_ALIASES[clean(name)] || [];
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
function toolSearchRank(row, query) {
|
|
1761
|
-
const raw = clean(query).toLowerCase();
|
|
1762
|
-
if (!raw) return { score: 0, reasons: [] };
|
|
1763
|
-
const name = clean(row?.name).toLowerCase();
|
|
1764
|
-
const prettyName = name.replace(/_/g, ' ');
|
|
1765
|
-
const haystack = toolSearchText(row);
|
|
1766
|
-
const aliases = toolSearchRowAliases(name);
|
|
1767
|
-
const aliasText = aliases.join(' ').toLowerCase();
|
|
1768
|
-
const queryTokens = toolSearchMeaningfulTokens(raw);
|
|
1769
|
-
const nameTokens = new Set(toolSearchTokens(`${name} ${prettyName}`));
|
|
1770
|
-
const aliasTokens = new Set(toolSearchTokens(aliasText));
|
|
1771
|
-
let score = 0;
|
|
1772
|
-
const reasons = [];
|
|
1773
|
-
if (raw === name || raw === prettyName) {
|
|
1774
|
-
score += 120;
|
|
1775
|
-
reasons.push('exact-name');
|
|
1776
|
-
}
|
|
1777
|
-
if (aliases.some((alias) => raw === alias || raw === alias.replace(/[_-]+/g, ' '))) {
|
|
1778
|
-
score += 100;
|
|
1779
|
-
reasons.push('exact-alias');
|
|
1780
|
-
}
|
|
1781
|
-
if (haystack.includes(raw)) {
|
|
1782
|
-
score += 34;
|
|
1783
|
-
reasons.push('phrase');
|
|
1784
|
-
}
|
|
1785
|
-
for (const alias of aliases) {
|
|
1786
|
-
const normalizedAlias = alias.toLowerCase();
|
|
1787
|
-
if (normalizedAlias && raw.includes(normalizedAlias)) {
|
|
1788
|
-
score += 58;
|
|
1789
|
-
reasons.push('alias-phrase');
|
|
1790
|
-
break;
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
let matchedTokens = 0;
|
|
1794
|
-
for (const token of queryTokens) {
|
|
1795
|
-
if (nameTokens.has(token) || name.includes(token)) {
|
|
1796
|
-
score += 24;
|
|
1797
|
-
matchedTokens += 1;
|
|
1798
|
-
continue;
|
|
1799
|
-
}
|
|
1800
|
-
if (aliasTokens.has(token) || aliasText.includes(token)) {
|
|
1801
|
-
score += 18;
|
|
1802
|
-
matchedTokens += 1;
|
|
1803
|
-
continue;
|
|
1804
|
-
}
|
|
1805
|
-
if (haystack.includes(token)) {
|
|
1806
|
-
score += 7;
|
|
1807
|
-
matchedTokens += 1;
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
if (queryTokens.length && matchedTokens === queryTokens.length) {
|
|
1811
|
-
score += Math.min(18, queryTokens.length * 6);
|
|
1812
|
-
reasons.push('all-tokens');
|
|
1813
|
-
}
|
|
1814
|
-
if (clean(row?.kind).toLowerCase() === raw) score += 10;
|
|
1815
|
-
if (score > 0) score += Math.min(6, Math.floor(measuredToolUsage(name) / 150));
|
|
1816
|
-
return { score, reasons };
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
function toolSearchMatches(row, query) {
|
|
1820
|
-
const raw = clean(query).toLowerCase();
|
|
1821
|
-
if (!raw) return true;
|
|
1822
|
-
return toolSearchRank(row, raw).score > 0;
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
function rankedToolSearchRows(rows, query) {
|
|
1826
|
-
const raw = clean(query).toLowerCase();
|
|
1827
|
-
if (!raw) return rows;
|
|
1828
|
-
return rows
|
|
1829
|
-
.map((row) => {
|
|
1830
|
-
const ranked = toolSearchRank(row, raw);
|
|
1831
|
-
return { ...row, score: ranked.score, reasons: ranked.reasons };
|
|
1832
|
-
})
|
|
1833
|
-
.filter((row) => row.score > 0)
|
|
1834
|
-
.sort((a, b) => {
|
|
1835
|
-
if (b.score !== a.score) return b.score - a.score;
|
|
1836
|
-
if (a.active !== b.active) return a.active ? 1 : -1;
|
|
1837
|
-
if (b.usage !== a.usage) return b.usage - a.usage;
|
|
1838
|
-
return String(a.name).localeCompare(String(b.name));
|
|
1839
|
-
});
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
function queryHasAnyPhrase(query, phrases) {
|
|
1843
|
-
const text = clean(query).toLowerCase().replace(/[_-]+/g, ' ');
|
|
1844
|
-
return phrases.some((phrase) => text.includes(phrase));
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
function autoToolSelectionNames(query, rows) {
|
|
1848
|
-
const raw = clean(query).toLowerCase();
|
|
1849
|
-
if (!raw || TOOL_SEARCH_AMBIGUOUS_AUTO_QUERIES.has(raw)) return [];
|
|
1850
|
-
if (TOOL_SEARCH_SAFE_AUTO_ALIASES.has(raw) && DEFERRED_SELECT_ALIASES[raw]) {
|
|
1851
|
-
return DEFERRED_SELECT_ALIASES[raw];
|
|
1852
|
-
}
|
|
1853
|
-
if (queryHasAnyPhrase(raw, ['save memory', 'store memory', 'delete memory', 'forget memory', 'memory status', '기억 저장', '메모리 저장'])) {
|
|
1854
|
-
return ['memory'];
|
|
1855
|
-
}
|
|
1856
|
-
if (queryHasAnyPhrase(raw, ['run', 'execute', 'test', 'tests', 'build', 'terminal', 'command', 'powershell', 'bash', 'shell', 'npm', 'node', 'git', '실행', '테스트', '빌드', '터미널', '쉘'])) {
|
|
1857
|
-
return ['shell', 'task'];
|
|
1858
|
-
}
|
|
1859
|
-
if (queryHasAnyPhrase(raw, ['web', 'internet', 'online', 'current info', 'latest', 'news', 'browse', 'docs', 'documentation', '웹', '인터넷', '최신', '뉴스', '문서'])) {
|
|
1860
|
-
return ['search', 'web_fetch'];
|
|
1861
|
-
}
|
|
1862
|
-
if (queryHasAnyPhrase(raw, ['recall', 'remember', 'memory previous', 'previous', 'history', 'past', 'prior', 'earlier', 'resume', '이전', '기억', '히스토리'])) {
|
|
1863
|
-
return ['recall'];
|
|
1864
|
-
}
|
|
1865
|
-
if (queryHasAnyPhrase(raw, ['delegate', 'subagent', 'worker', 'parallel agent', 'background agent', 'reviewer', 'explorer', '에이전트', '워커', '병렬'])) {
|
|
1866
|
-
return ['agent'];
|
|
1867
|
-
}
|
|
1868
|
-
if (queryHasAnyPhrase(raw, ['working directory', 'project root', 'current directory', 'cwd'])) {
|
|
1869
|
-
return ['cwd'];
|
|
1870
|
-
}
|
|
1871
|
-
const ranked = rankedToolSearchRows(rows, raw);
|
|
1872
|
-
const top = ranked[0];
|
|
1873
|
-
if (!top) return [];
|
|
1874
|
-
const reasons = new Set(top.reasons || []);
|
|
1875
|
-
if (reasons.has('exact-name') || reasons.has('exact-alias')) return [top.name];
|
|
1876
|
-
return [];
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
function expandSelectionNames(names) {
|
|
1880
|
-
const out = [];
|
|
1881
|
-
for (const raw of names || []) {
|
|
1882
|
-
const key = clean(raw);
|
|
1883
|
-
if (!key) continue;
|
|
1884
|
-
const alias = DEFERRED_SELECT_ALIASES[key.toLowerCase()];
|
|
1885
|
-
if (alias) out.push(...alias);
|
|
1886
|
-
else out.push(key);
|
|
1887
|
-
}
|
|
1888
|
-
return [...new Set(out)];
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
function storedDeferredToolNames(session) {
|
|
1892
|
-
for (const source of [session?.deferredDiscoveredTools, session?.deferredSelectedTools]) {
|
|
1893
|
-
const names = parseToolSelection(source);
|
|
1894
|
-
if (names.length) return names;
|
|
1895
|
-
}
|
|
1896
|
-
return [];
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
function canonicalDeferredToolNames(catalog, names) {
|
|
1900
|
-
const byName = new Map();
|
|
1901
|
-
for (const tool of catalog || []) {
|
|
1902
|
-
const name = clean(tool?.name);
|
|
1903
|
-
if (!name) continue;
|
|
1904
|
-
byName.set(name, name);
|
|
1905
|
-
byName.set(name.toLowerCase(), name);
|
|
1906
|
-
}
|
|
1907
|
-
const out = [];
|
|
1908
|
-
for (const raw of expandSelectionNames(names)) {
|
|
1909
|
-
const name = clean(raw);
|
|
1910
|
-
const canonical = byName.get(name) || byName.get(name.toLowerCase());
|
|
1911
|
-
if (canonical) out.push(canonical);
|
|
1912
|
-
}
|
|
1913
|
-
return sortedNamesByMeasuredUsage(new Set(out));
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
function setDeferredToolState(session, names) {
|
|
1917
|
-
if (!session) return [];
|
|
1918
|
-
const selected = sortedNamesByMeasuredUsage(new Set(parseToolSelection(names)));
|
|
1919
|
-
session.deferredDiscoveredTools = selected;
|
|
1920
|
-
session.deferredSelectedTools = selected;
|
|
1921
|
-
return selected;
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
function isReadonlySelectable(tool) {
|
|
1925
|
-
const name = clean(tool?.name);
|
|
1926
|
-
if (READONLY_TOOL_NAMES.has(name)) return true;
|
|
1927
|
-
const annotations = tool?.annotations || {};
|
|
1928
|
-
if (annotations.destructiveHint === true) return false;
|
|
1929
|
-
if (annotations.readOnlyHint === true) return true;
|
|
1930
|
-
return false;
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
function applyDeferredToolSurface(session, mode, extraTools = [], options = {}) {
|
|
1934
|
-
if (!session || !Array.isArray(session.tools)) return session;
|
|
1935
|
-
const providerMode = deferredProviderMode(options.provider || session.provider);
|
|
1936
|
-
const byName = new Map();
|
|
1937
|
-
for (const tool of [...session.tools, ...(extraTools || [])]) {
|
|
1938
|
-
const name = clean(tool?.name);
|
|
1939
|
-
if (!name || byName.has(name)) continue;
|
|
1940
|
-
byName.set(name, activeToolForSurface(tool));
|
|
1941
|
-
}
|
|
1942
|
-
const catalog = sortedCatalogByMeasuredUsage([...byName.values()]);
|
|
1943
|
-
const defaultNames = defaultDeferredToolNames(catalog, mode);
|
|
1944
|
-
const storedNames = providerMode === 'native' ? [] : storedDeferredToolNames(session);
|
|
1945
|
-
let selectedNames = providerMode === 'full'
|
|
1946
|
-
? sortedNamesByMeasuredUsage(catalog.map((tool) => clean(tool?.name)).filter(Boolean))
|
|
1947
|
-
: [];
|
|
1948
|
-
if (providerMode !== 'full') {
|
|
1949
|
-
selectedNames = storedNames.length ? canonicalDeferredToolNames(catalog, storedNames) : [];
|
|
1950
|
-
if (!selectedNames.length || providerMode === 'native') selectedNames = sortedNamesByMeasuredUsage(defaultNames);
|
|
1951
|
-
}
|
|
1952
|
-
const selected = new Set(selectedNames);
|
|
1953
|
-
session.deferredToolCatalog = catalog;
|
|
1954
|
-
session.deferredToolUsage = MEASURED_TOOL_USAGE;
|
|
1955
|
-
session.deferredDefaultTools = sortedNamesByMeasuredUsage(defaultNames);
|
|
1956
|
-
session.deferredProviderMode = providerMode;
|
|
1957
|
-
session.deferredNativeTools = providerMode === 'native';
|
|
1958
|
-
session.tools.length = 0;
|
|
1959
|
-
const active = [];
|
|
1960
|
-
for (const tool of catalog) {
|
|
1961
|
-
if (!selected.has(clean(tool?.name))) continue;
|
|
1962
|
-
if (mode === 'readonly' && !isReadonlySelectable(tool)) continue;
|
|
1963
|
-
session.tools.push(tool);
|
|
1964
|
-
active.push(clean(tool?.name));
|
|
1965
|
-
}
|
|
1966
|
-
if (providerMode === 'native') {
|
|
1967
|
-
const discovered = canonicalDeferredToolNames(catalog, session.deferredDiscoveredTools || []);
|
|
1968
|
-
session.deferredSelectedTools = active;
|
|
1969
|
-
session.deferredDiscoveredTools = discovered.filter((name) => !selected.has(name));
|
|
1970
|
-
} else {
|
|
1971
|
-
setDeferredToolState(session, active);
|
|
1972
|
-
}
|
|
1973
|
-
return session;
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
function selectDeferredTools(session, names, mode) {
|
|
1977
|
-
const catalog = Array.isArray(session?.deferredToolCatalog)
|
|
1978
|
-
? session.deferredToolCatalog
|
|
1979
|
-
: (Array.isArray(session?.tools) ? session.tools : []);
|
|
1980
|
-
const active = new Set((session?.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
1981
|
-
const native = session?.deferredProviderMode === 'native' || session?.deferredNativeTools === true;
|
|
1982
|
-
const discovered = new Set(Array.isArray(session?.deferredDiscoveredTools) ? session.deferredDiscoveredTools : []);
|
|
1983
|
-
const byName = new Map();
|
|
1984
|
-
for (const tool of catalog) {
|
|
1985
|
-
const name = clean(tool?.name);
|
|
1986
|
-
if (!name) continue;
|
|
1987
|
-
byName.set(name, tool);
|
|
1988
|
-
byName.set(name.toLowerCase(), tool);
|
|
1989
|
-
}
|
|
1990
|
-
const added = [];
|
|
1991
|
-
const already = [];
|
|
1992
|
-
const blocked = [];
|
|
1993
|
-
const missing = [];
|
|
1994
|
-
for (const rawName of expandSelectionNames(names)) {
|
|
1995
|
-
const requestedName = clean(rawName);
|
|
1996
|
-
const tool = byName.get(requestedName) || byName.get(requestedName.toLowerCase());
|
|
1997
|
-
const name = clean(tool?.name);
|
|
1998
|
-
if (!tool) {
|
|
1999
|
-
missing.push(requestedName);
|
|
2000
|
-
continue;
|
|
2001
|
-
}
|
|
2002
|
-
if (mode === 'readonly' && !isReadonlySelectable(tool)) {
|
|
2003
|
-
blocked.push({ name, reason: 'readonly mode' });
|
|
2004
|
-
continue;
|
|
2005
|
-
}
|
|
2006
|
-
if (active.has(name) || discovered.has(name)) {
|
|
2007
|
-
already.push(name);
|
|
2008
|
-
continue;
|
|
2009
|
-
}
|
|
2010
|
-
if (native) {
|
|
2011
|
-
discovered.add(name);
|
|
2012
|
-
} else {
|
|
2013
|
-
session.tools.push(tool);
|
|
2014
|
-
active.add(name);
|
|
2015
|
-
}
|
|
2016
|
-
added.push(name);
|
|
2017
|
-
}
|
|
2018
|
-
if (native) {
|
|
2019
|
-
session.deferredDiscoveredTools = sortedNamesByMeasuredUsage(discovered);
|
|
2020
|
-
session.deferredSelectedTools = sortedNamesByMeasuredUsage(active);
|
|
2021
|
-
} else {
|
|
2022
|
-
setDeferredToolState(session, active);
|
|
2023
|
-
}
|
|
2024
|
-
return { added, already, blocked, missing, native };
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
function renderToolSearch(args = {}, session, mode = 'full') {
|
|
2028
|
-
const catalog = Array.isArray(session?.deferredToolCatalog)
|
|
2029
|
-
? session.deferredToolCatalog
|
|
2030
|
-
: (Array.isArray(session?.tools) ? session.tools : []);
|
|
2031
|
-
const rawQuery = clean(args.query);
|
|
2032
|
-
const explicitSelectedNames = parseToolSelection(args.select);
|
|
2033
|
-
const querySelectedNames = explicitSelectedNames.length ? [] : parseToolSearchQuerySelection(rawQuery);
|
|
2034
|
-
const forcedSelectedNames = explicitSelectedNames.length ? explicitSelectedNames : querySelectedNames;
|
|
2035
|
-
const query = querySelectedNames.length ? '' : rawQuery.toLowerCase();
|
|
2036
|
-
const limit = Math.max(1, Math.min(50, Number(args.limit) || 20));
|
|
2037
|
-
const initialActiveNames = new Set((session?.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
2038
|
-
const initialRows = catalog.map((tool) => toolRow(tool, initialActiveNames)).filter((row) => row.name);
|
|
2039
|
-
const autoSelectedNames = (!forcedSelectedNames.length && query)
|
|
2040
|
-
? autoToolSelectionNames(query, initialRows)
|
|
2041
|
-
: [];
|
|
2042
|
-
const selectedNames = forcedSelectedNames.length ? forcedSelectedNames : autoSelectedNames;
|
|
2043
|
-
const toolSelection = selectedNames.length ? selectDeferredTools(session, selectedNames, mode) : null;
|
|
2044
|
-
const selectionMode = forcedSelectedNames.length ? 'select' : (autoSelectedNames.length ? 'auto' : null);
|
|
2045
|
-
const nextActiveNames = new Set((session?.tools || []).map((tool) => clean(tool?.name)).filter(Boolean));
|
|
2046
|
-
const rows = [
|
|
2047
|
-
...catalog.map((tool) => toolRow(tool, nextActiveNames)),
|
|
2048
|
-
].filter((row) => row.name);
|
|
2049
|
-
const matches = query
|
|
2050
|
-
? rankedToolSearchRows(rows, query)
|
|
2051
|
-
: rows;
|
|
2052
|
-
const selected = toolSelection
|
|
2053
|
-
? {
|
|
2054
|
-
mode: selectionMode,
|
|
2055
|
-
tools: toolSelection,
|
|
2056
|
-
}
|
|
2057
|
-
: null;
|
|
2058
|
-
const nativeToolSearch = toolSelection?.native
|
|
2059
|
-
? toolSearchNativePayload(catalog, toolSelection.added, session?.provider)
|
|
2060
|
-
: null;
|
|
2061
|
-
return JSON.stringify({
|
|
2062
|
-
selected,
|
|
2063
|
-
...(nativeToolSearch ? { nativeToolSearch } : {}),
|
|
2064
|
-
totalMatches: matches.length,
|
|
2065
|
-
matches: matches.slice(0, limit),
|
|
2066
|
-
activeTools: sortedNamesByMeasuredUsage(nextActiveNames),
|
|
2067
|
-
discoveredTools: sortedNamesByMeasuredUsage(session?.deferredDiscoveredTools || []),
|
|
2068
|
-
note: 'query ranks matches and auto-loads high-confidence deferred tools; select exact tool names, or query select:a,b, to force load.',
|
|
2069
|
-
}, null, 2);
|
|
2070
|
-
}
|
|
419
|
+
// Workflow/agent pack loaders bound to this runtime's root/data layout.
|
|
420
|
+
const {
|
|
421
|
+
listWorkflowPacks,
|
|
422
|
+
activeWorkflowId,
|
|
423
|
+
loadWorkflowPack,
|
|
424
|
+
workflowSummary,
|
|
425
|
+
activeWorkflowSummary,
|
|
426
|
+
loadAgentDefinition,
|
|
427
|
+
workflowContextBlock,
|
|
428
|
+
} = createWorkflowHelpers({
|
|
429
|
+
rootDir: STANDALONE_ROOT,
|
|
430
|
+
dataDir: STANDALONE_DATA_DIR,
|
|
431
|
+
readMarkdownDocument,
|
|
432
|
+
normalizeAgentPermissionOrNone,
|
|
433
|
+
});
|
|
434
|
+
const {
|
|
435
|
+
summarizeWorkflowRoutes,
|
|
436
|
+
routeFromPreset,
|
|
437
|
+
agentRouteFromConfig,
|
|
438
|
+
} = createWorkflowRouteHelpers({ resolveDefaultProvider, findPreset });
|
|
2071
439
|
|
|
2072
440
|
export function __renderToolSearchForTest(args = {}, session = {}, mode = 'full') {
|
|
2073
441
|
return renderToolSearch(args, session, mode);
|
|
@@ -2881,6 +1249,11 @@ export async function createMixdogSessionRuntime({
|
|
|
2881
1249
|
}
|
|
2882
1250
|
} catch {}
|
|
2883
1251
|
}
|
|
1252
|
+
// CwdChanged: bridge an effective cwd switch to the standard hook bus.
|
|
1253
|
+
// No matcher event — payload is minimal { cwd }. Fire-and-forget.
|
|
1254
|
+
if (changed) {
|
|
1255
|
+
try { void hooks.dispatch('CwdChanged', hookCommonPayload({ cwd: currentCwd })); } catch {}
|
|
1256
|
+
}
|
|
2884
1257
|
return currentCwd;
|
|
2885
1258
|
}
|
|
2886
1259
|
|
|
@@ -2964,7 +1337,8 @@ export async function createMixdogSessionRuntime({
|
|
|
2964
1337
|
mcpScript: mcpScriptForPlugin(root),
|
|
2965
1338
|
};
|
|
2966
1339
|
plugin.mcpServerName = pluginMcpServerName(plugin);
|
|
2967
|
-
plugin.mcpEnabled = Object.prototype.hasOwnProperty.call(configuredMcp, plugin.mcpServerName)
|
|
1340
|
+
plugin.mcpEnabled = Object.prototype.hasOwnProperty.call(configuredMcp, plugin.mcpServerName)
|
|
1341
|
+
|| Object.keys(configuredMcp).some((k) => k.startsWith(`${plugin.mcpServerName}--`));
|
|
2968
1342
|
plugins.push(plugin);
|
|
2969
1343
|
};
|
|
2970
1344
|
|
|
@@ -2986,17 +1360,18 @@ export async function createMixdogSessionRuntime({
|
|
|
2986
1360
|
}
|
|
2987
1361
|
|
|
2988
1362
|
// Merge mixdog-config `agent.mcpServers` with project-local `.mcp.json`.
|
|
2989
|
-
// On name collision the
|
|
2990
|
-
//
|
|
1363
|
+
// On name collision the project-local `.mcp.json` entry WINS (Claude Code
|
|
1364
|
+
// precedence: project > user config). `sources[name]` records each server's
|
|
1365
|
+
// origin ('config' | 'project') for status reporting.
|
|
2991
1366
|
function resolveEffectiveMcpServers() {
|
|
2992
1367
|
const configured = config?.mcpServers && typeof config.mcpServers === 'object'
|
|
2993
1368
|
? config.mcpServers
|
|
2994
1369
|
: {};
|
|
2995
1370
|
const project = readProjectMcpServers(currentCwd);
|
|
2996
|
-
const servers = { ...
|
|
1371
|
+
const servers = { ...configured, ...project };
|
|
2997
1372
|
const sources = {};
|
|
2998
|
-
for (const name of Object.keys(project)) sources[name] = 'project';
|
|
2999
1373
|
for (const name of Object.keys(configured)) sources[name] = 'config';
|
|
1374
|
+
for (const name of Object.keys(project)) sources[name] = 'project';
|
|
3000
1375
|
return { servers, sources };
|
|
3001
1376
|
}
|
|
3002
1377
|
|
|
@@ -3100,6 +1475,18 @@ export async function createMixdogSessionRuntime({
|
|
|
3100
1475
|
mgr,
|
|
3101
1476
|
dataDir: cfgMod.getPluginData(),
|
|
3102
1477
|
cwd,
|
|
1478
|
+
// SubagentStart/SubagentStop: bridge internal worker spawn/finish to the
|
|
1479
|
+
// standard hook bus. agent_type is passed top-level via hookCommonPayload
|
|
1480
|
+
// (added to hook-bus buildEventPayload passthrough). Best-effort.
|
|
1481
|
+
onSubagentEvent: (phase, info = {}) => {
|
|
1482
|
+
try {
|
|
1483
|
+
const event = phase === 'stop' ? 'SubagentStop' : 'SubagentStart';
|
|
1484
|
+
void hooks.dispatch(event, hookCommonPayload({
|
|
1485
|
+
session_id: info?.session_id || null,
|
|
1486
|
+
agent_type: info?.agent_type || null,
|
|
1487
|
+
}));
|
|
1488
|
+
} catch { /* best-effort: subagent hook must never affect worker lifecycle */ }
|
|
1489
|
+
},
|
|
3103
1490
|
});
|
|
3104
1491
|
bootProfile('agent:ready', { ms: (performance.now() - agentToolStartedAt).toFixed(1) });
|
|
3105
1492
|
const agentStatusState = () => {
|
|
@@ -3308,6 +1695,58 @@ export async function createMixdogSessionRuntime({
|
|
|
3308
1695
|
let pendingConfigToSave = null;
|
|
3309
1696
|
let configSaveTimer = null;
|
|
3310
1697
|
const CONFIG_SAVE_DEBOUNCE_MS = 150;
|
|
1698
|
+
// Same debounce pattern as saveConfigAndAdopt below, but for the channels-
|
|
1699
|
+
// section backend switch: channel-admin's setBackend() does a synchronous
|
|
1700
|
+
// file-locked read-modify-write (updateChannelsSection), which is a
|
|
1701
|
+
// secondary hitch source on the settings-toggle key handler. Coalesce
|
|
1702
|
+
// rapid toggles and move the write off the key-handler tick.
|
|
1703
|
+
let pendingBackendName = null;
|
|
1704
|
+
let backendSaveTimer = null;
|
|
1705
|
+
function flushBackendSave() {
|
|
1706
|
+
if (backendSaveTimer) {
|
|
1707
|
+
clearTimeout(backendSaveTimer);
|
|
1708
|
+
backendSaveTimer = null;
|
|
1709
|
+
}
|
|
1710
|
+
if (pendingBackendName === null) return;
|
|
1711
|
+
const name = pendingBackendName;
|
|
1712
|
+
pendingBackendName = null;
|
|
1713
|
+
try {
|
|
1714
|
+
setBackend(name);
|
|
1715
|
+
} catch (err) {
|
|
1716
|
+
process.stderr.write(`[channels] debounced setBackend failed: ${err?.message || err}\n`);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
// Debounced persist for the top-level `outputStyle` config key. This CANNOT
|
|
1720
|
+
// ride saveConfigAndAdopt/flushConfigSave: cfgMod.saveConfig() serializes
|
|
1721
|
+
// ONLY the agent-section fields (see orchestrator config.mjs saveConfig),
|
|
1722
|
+
// so a top-level outputStyle adopted in memory would never reach disk and
|
|
1723
|
+
// would silently revert on the next fresh catalog read/restart. Persist it
|
|
1724
|
+
// through sharedCfgMod.updateConfig (whole-root RMW under the same file
|
|
1725
|
+
// lock), debounced off the key-handler tick like the backend switch above.
|
|
1726
|
+
let pendingOutputStyleId = null;
|
|
1727
|
+
let outputStyleSaveTimer = null;
|
|
1728
|
+
function flushOutputStyleSave() {
|
|
1729
|
+
if (outputStyleSaveTimer) {
|
|
1730
|
+
clearTimeout(outputStyleSaveTimer);
|
|
1731
|
+
outputStyleSaveTimer = null;
|
|
1732
|
+
}
|
|
1733
|
+
if (pendingOutputStyleId === null) return;
|
|
1734
|
+
const styleId = pendingOutputStyleId;
|
|
1735
|
+
pendingOutputStyleId = null;
|
|
1736
|
+
try {
|
|
1737
|
+
sharedCfgMod.updateConfig((root) => {
|
|
1738
|
+
const next = { ...(root || {}), outputStyle: styleId };
|
|
1739
|
+
if (next.agent && typeof next.agent === 'object' && !Array.isArray(next.agent)) {
|
|
1740
|
+
const agent = { ...next.agent };
|
|
1741
|
+
delete agent.outputStyle;
|
|
1742
|
+
next.agent = agent;
|
|
1743
|
+
}
|
|
1744
|
+
return next;
|
|
1745
|
+
});
|
|
1746
|
+
} catch (err) {
|
|
1747
|
+
process.stderr.write(`[config] debounced outputStyle save failed: ${err?.message || err}\n`);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
3311
1750
|
function flushConfigSave() {
|
|
3312
1751
|
if (configSaveTimer) {
|
|
3313
1752
|
clearTimeout(configSaveTimer);
|
|
@@ -4022,11 +2461,61 @@ function parsedProviderModelVersion(id) {
|
|
|
4022
2461
|
configurable: true,
|
|
4023
2462
|
writable: true,
|
|
4024
2463
|
});
|
|
2464
|
+
// PostToolUseFailure: dispatched by loop.mjs only when a tool execution
|
|
2465
|
+
// resolved to a failure (thrown-error path or an is_error result). Same
|
|
2466
|
+
// shape as afterToolHook; `result` carries the error text. Best-effort.
|
|
2467
|
+
Object.defineProperty(session, 'afterToolFailureHook', {
|
|
2468
|
+
value: (input) => hooks.dispatch('PostToolUseFailure', hookCommonPayload({
|
|
2469
|
+
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
2470
|
+
cwd: input?.cwd || currentCwd,
|
|
2471
|
+
tool_name: input?.name,
|
|
2472
|
+
tool_input: input?.args,
|
|
2473
|
+
tool_use_id: input?.toolCallId || input?.tool_use_id,
|
|
2474
|
+
tool_response: input?.result,
|
|
2475
|
+
})),
|
|
2476
|
+
enumerable: false,
|
|
2477
|
+
configurable: true,
|
|
2478
|
+
writable: true,
|
|
2479
|
+
});
|
|
2480
|
+
// PostToolBatch: dispatched by loop.mjs after a full parallel batch of
|
|
2481
|
+
// tool calls resolves and before the next model call. No matcher event.
|
|
2482
|
+
Object.defineProperty(session, 'afterToolBatchHook', {
|
|
2483
|
+
value: (input) => hooks.dispatch('PostToolBatch', hookCommonPayload({
|
|
2484
|
+
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
2485
|
+
cwd: input?.cwd || currentCwd,
|
|
2486
|
+
})),
|
|
2487
|
+
enumerable: false,
|
|
2488
|
+
configurable: true,
|
|
2489
|
+
writable: true,
|
|
2490
|
+
});
|
|
2491
|
+
// PreCompact / PostCompact: dispatched by manager.mjs/loop.mjs compaction
|
|
2492
|
+
// flow via these session-property hooks (manager has no hooks bus access).
|
|
2493
|
+
// payload { trigger: 'auto' | 'manual' }. Best-effort.
|
|
2494
|
+
Object.defineProperty(session, 'preCompactHook', {
|
|
2495
|
+
value: (input) => hooks.dispatch('PreCompact', hookCommonPayload({
|
|
2496
|
+
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
2497
|
+
cwd: input?.cwd || currentCwd,
|
|
2498
|
+
trigger: input?.trigger || 'auto',
|
|
2499
|
+
})),
|
|
2500
|
+
enumerable: false,
|
|
2501
|
+
configurable: true,
|
|
2502
|
+
writable: true,
|
|
2503
|
+
});
|
|
2504
|
+
Object.defineProperty(session, 'postCompactHook', {
|
|
2505
|
+
value: (input) => hooks.dispatch('PostCompact', hookCommonPayload({
|
|
2506
|
+
session_id: input?.sessionId || input?.session_id || session?.id,
|
|
2507
|
+
cwd: input?.cwd || currentCwd,
|
|
2508
|
+
trigger: input?.trigger || 'auto',
|
|
2509
|
+
})),
|
|
2510
|
+
enumerable: false,
|
|
2511
|
+
configurable: true,
|
|
2512
|
+
writable: true,
|
|
2513
|
+
});
|
|
4025
2514
|
applyDeferredToolSurface(session, deferredSurfaceModeForLead(mode), standaloneTools, { provider: route.provider });
|
|
4026
2515
|
applyPreSessionToolSelection();
|
|
4027
2516
|
writeStatuslineRoute(statusRoutes, session, route);
|
|
4028
2517
|
hooks.emit('session:create', { sessionId: session.id, provider: route.provider, model: route.model, toolMode: mode, cwd: currentCwd });
|
|
4029
|
-
// SessionStart: bridge to the standard
|
|
2518
|
+
// SessionStart: bridge to the standard project hook bus. Best-effort;
|
|
4030
2519
|
// a hook error must never break session creation. additionalContext is
|
|
4031
2520
|
// injected before the first user turn as a system-reminder context pair.
|
|
4032
2521
|
try {
|
|
@@ -4266,6 +2755,10 @@ function parsedProviderModelVersion(id) {
|
|
|
4266
2755
|
// boots the channel worker and contends for channel ownership.
|
|
4267
2756
|
function startRemote() {
|
|
4268
2757
|
remoteEnabled = true;
|
|
2758
|
+
// A backend switch may still be sitting in its debounce window; flush it
|
|
2759
|
+
// so the channel worker boots against the backend the user just chose,
|
|
2760
|
+
// not the previous on-disk value.
|
|
2761
|
+
try { flushBackendSave(); } catch {}
|
|
4269
2762
|
if (envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
|
|
4270
2763
|
bootProfile('channels:start-skipped');
|
|
4271
2764
|
return true;
|
|
@@ -4897,6 +3390,10 @@ function parsedProviderModelVersion(id) {
|
|
|
4897
3390
|
return this.getOnboardingStatus();
|
|
4898
3391
|
},
|
|
4899
3392
|
getChannelSetup() {
|
|
3393
|
+
// Flush a pending debounced backend switch first so setup readers
|
|
3394
|
+
// (Settings → Channel Setting, remote toggles) never observe the
|
|
3395
|
+
// previous backend during the 150ms debounce window.
|
|
3396
|
+
try { flushBackendSave(); } catch {}
|
|
4900
3397
|
return channelSetup();
|
|
4901
3398
|
},
|
|
4902
3399
|
getChannelWorkerStatus() {
|
|
@@ -4932,8 +3429,21 @@ function parsedProviderModelVersion(id) {
|
|
|
4932
3429
|
return result;
|
|
4933
3430
|
},
|
|
4934
3431
|
setBackend(name) {
|
|
4935
|
-
|
|
4936
|
-
|
|
3432
|
+
// Validate synchronously (same contract as before: bad input throws on
|
|
3433
|
+
// this call so the TUI's try/catch can react immediately). The actual
|
|
3434
|
+
// channels-section file read-modify-write is the hitch source on the
|
|
3435
|
+
// settings-toggle key handler, so defer it through the same debounce
|
|
3436
|
+
// pattern as saveConfigAndAdopt/flushConfigSave instead of writing to
|
|
3437
|
+
// disk synchronously inside the key handler.
|
|
3438
|
+
const value = String(name || '').trim();
|
|
3439
|
+
if (value !== 'discord' && value !== 'telegram') {
|
|
3440
|
+
throw new Error('backend must be discord or telegram');
|
|
3441
|
+
}
|
|
3442
|
+
pendingBackendName = value;
|
|
3443
|
+
if (backendSaveTimer) clearTimeout(backendSaveTimer);
|
|
3444
|
+
backendSaveTimer = setTimeout(flushBackendSave, CONFIG_SAVE_DEBOUNCE_MS);
|
|
3445
|
+
backendSaveTimer.unref?.();
|
|
3446
|
+
return { ok: true, backend: value };
|
|
4937
3447
|
},
|
|
4938
3448
|
saveWebhookAuthtoken(token) {
|
|
4939
3449
|
const result = saveWebhookAuthtoken(token);
|
|
@@ -5047,6 +3557,12 @@ function parsedProviderModelVersion(id) {
|
|
|
5047
3557
|
invalidateProviderCaches();
|
|
5048
3558
|
return result;
|
|
5049
3559
|
},
|
|
3560
|
+
async loginOpenCodeGoUsage() {
|
|
3561
|
+
const result = await loginOpenCodeGoUsage(cfgMod);
|
|
3562
|
+
reloadFullConfig();
|
|
3563
|
+
invalidateProviderCaches();
|
|
3564
|
+
return result;
|
|
3565
|
+
},
|
|
5050
3566
|
setLocalProvider(providerId, opts) {
|
|
5051
3567
|
const result = setLocalProvider(cfgMod, providerId, opts);
|
|
5052
3568
|
reloadFullConfig();
|
|
@@ -5157,26 +3673,51 @@ function parsedProviderModelVersion(id) {
|
|
|
5157
3673
|
const names = before.styles.map((style) => style.label || style.id).join(', ') || 'Default';
|
|
5158
3674
|
throw new Error(`output style must be one of ${names}`);
|
|
5159
3675
|
}
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
}
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
3676
|
+
// Adopt in-memory immediately so same-tick readers see the new style;
|
|
3677
|
+
// persist off the key-handler tick. NOTE: the disk write goes through
|
|
3678
|
+
// the dedicated flushOutputStyleSave debounce (sharedCfgMod whole-root
|
|
3679
|
+
// RMW) — cfgMod.saveConfig only serializes agent-section fields and
|
|
3680
|
+
// would drop the top-level outputStyle key.
|
|
3681
|
+
const nextConfig = { ...config, outputStyle: selected.id };
|
|
3682
|
+
if (nextConfig.agent && typeof nextConfig.agent === 'object' && !Array.isArray(nextConfig.agent)) {
|
|
3683
|
+
const agent = { ...nextConfig.agent };
|
|
3684
|
+
delete agent.outputStyle;
|
|
3685
|
+
nextConfig.agent = agent;
|
|
3686
|
+
}
|
|
3687
|
+
adoptConfig(nextConfig);
|
|
3688
|
+
pendingOutputStyleId = selected.id;
|
|
3689
|
+
if (outputStyleSaveTimer) clearTimeout(outputStyleSaveTimer);
|
|
3690
|
+
outputStyleSaveTimer = setTimeout(flushOutputStyleSave, CONFIG_SAVE_DEBOUNCE_MS);
|
|
3691
|
+
outputStyleSaveTimer.unref?.();
|
|
3692
|
+
// Reuse the catalog already scanned above for `before` instead of a
|
|
3693
|
+
// second forced-fresh filesystem scan; refresh the status cache
|
|
3694
|
+
// in-memory (short TTL, same as getOutputStyleStatusCached) so reads
|
|
3695
|
+
// during the debounce window see the just-selected style.
|
|
3696
|
+
const freshStatus = { configured: selected.id, current: selected, styles: before.styles };
|
|
3697
|
+
outputStyleStatusCache = freshStatus;
|
|
3698
|
+
outputStyleStatusCacheAt = performance.now();
|
|
3699
|
+
outputStyleStatusCacheDir = resolve(cfgMod.getPluginData?.() || STANDALONE_DATA_DIR);
|
|
5171
3700
|
const hasConversation = sessionHasConversationMessages(session);
|
|
5172
3701
|
let appliedToCurrentSession = !hasConversation;
|
|
5173
3702
|
if (session?.id && !hasConversation) {
|
|
5174
|
-
|
|
3703
|
+
const closedSessionId = session.id;
|
|
3704
|
+
mgr.closeSession(closedSessionId, 'cli-output-style-switch');
|
|
5175
3705
|
session = null;
|
|
5176
|
-
|
|
3706
|
+
// Defer the recreate so the output-style picker can repaint first;
|
|
3707
|
+
// any failure still surfaces via the existing notice path.
|
|
3708
|
+
setTimeout(() => {
|
|
3709
|
+
recreateCurrentSessionIfReady().catch((err) => {
|
|
3710
|
+
try {
|
|
3711
|
+
notifyFnForSession(closedSessionId)(
|
|
3712
|
+
`Failed to start a new session after output style change: ${err?.message || err}`,
|
|
3713
|
+
{ level: 'error' },
|
|
3714
|
+
);
|
|
3715
|
+
} catch {}
|
|
3716
|
+
});
|
|
3717
|
+
}, 0);
|
|
5177
3718
|
}
|
|
5178
3719
|
invalidateContextStatusCache();
|
|
5179
|
-
return { ...
|
|
3720
|
+
return { ...freshStatus, appliedToCurrentSession };
|
|
5180
3721
|
},
|
|
5181
3722
|
async setWorkflow(workflowId) {
|
|
5182
3723
|
const id = normalizeWorkflowId(workflowId, DEFAULT_WORKFLOW_ID);
|
|
@@ -5389,6 +3930,18 @@ function parsedProviderModelVersion(id) {
|
|
|
5389
3930
|
return { result, session };
|
|
5390
3931
|
} catch (error) {
|
|
5391
3932
|
hooks.emit('turn:error', { sessionId: session?.id || null, elapsedMs: Date.now() - startedAt, error: error?.message || String(error) });
|
|
3933
|
+
// StopFailure: bridge a turn error to the standard hook bus. Spec:
|
|
3934
|
+
// output + exit code ignored, so pure fire-and-forget. error_type is a
|
|
3935
|
+
// simple regex mapping from the error message (default 'unknown').
|
|
3936
|
+
try {
|
|
3937
|
+
const msg = String(error?.message || error || '').toLowerCase();
|
|
3938
|
+
const errorType = /rate.?limit|429|too many requests/.test(msg) ? 'rate_limit'
|
|
3939
|
+
: /overloaded|529/.test(msg) ? 'overloaded'
|
|
3940
|
+
: /authenticat|unauthorized|401|invalid.*api.?key/.test(msg) ? 'authentication_failed'
|
|
3941
|
+
: /server.?error|5\d\d|internal error/.test(msg) ? 'server_error'
|
|
3942
|
+
: 'unknown';
|
|
3943
|
+
void hooks.dispatch('StopFailure', hookCommonPayload({ session_id: session?.id || null, error_type: errorType }));
|
|
3944
|
+
} catch { /* best-effort: StopFailure hook must never break teardown */ }
|
|
5392
3945
|
throw error;
|
|
5393
3946
|
} finally {
|
|
5394
3947
|
activeTurnCount = Math.max(0, activeTurnCount - 1);
|
|
@@ -5415,7 +3968,14 @@ function parsedProviderModelVersion(id) {
|
|
|
5415
3968
|
if (activeTurnCount > 0) {
|
|
5416
3969
|
return { changed: false, reason: 'compact skipped: turn in progress' };
|
|
5417
3970
|
}
|
|
3971
|
+
// Manual compact bypasses loop.mjs, so its PreCompact/PostCompact never
|
|
3972
|
+
// fire here — dispatch them explicitly via the session-property hooks
|
|
3973
|
+
// (wired at session create). Best-effort; a hook must not block compaction.
|
|
3974
|
+
try { await session.preCompactHook?.({ trigger: 'manual' }); }
|
|
3975
|
+
catch { /* best-effort: PreCompact hook must never break manual compact */ }
|
|
5418
3976
|
const result = await mgr.compactSessionMessages(session.id);
|
|
3977
|
+
try { await session.postCompactHook?.({ trigger: 'manual' }); }
|
|
3978
|
+
catch { /* best-effort: PostCompact hook must never break manual compact */ }
|
|
5419
3979
|
session = mgr.getSession(session.id) || session;
|
|
5420
3980
|
if (options.recoverAgent === true) {
|
|
5421
3981
|
try { agentTool.recoverWorkers?.({ clientHostPid: session?.clientHostPid || process.pid }); } catch {}
|
|
@@ -5587,9 +4147,15 @@ function parsedProviderModelVersion(id) {
|
|
|
5587
4147
|
const removed = registryRemovePlugin(key, { dataDir });
|
|
5588
4148
|
const nextConfig = { ...config };
|
|
5589
4149
|
const serverName = pluginMcpServerName(plugin);
|
|
5590
|
-
|
|
4150
|
+
const prefix = `${serverName}--`;
|
|
4151
|
+
const hasMatch = nextConfig.mcpServers && Object.keys(nextConfig.mcpServers).some(
|
|
4152
|
+
(k) => k === serverName || k.startsWith(prefix)
|
|
4153
|
+
);
|
|
4154
|
+
if (hasMatch) {
|
|
5591
4155
|
const current = { ...nextConfig.mcpServers };
|
|
5592
|
-
|
|
4156
|
+
for (const k of Object.keys(current)) {
|
|
4157
|
+
if (k === serverName || k.startsWith(prefix)) delete current[k];
|
|
4158
|
+
}
|
|
5593
4159
|
saveConfigAndAdopt({ ...nextConfig, mcpServers: current });
|
|
5594
4160
|
await connectConfiguredMcp({ reset: true });
|
|
5595
4161
|
invalidatePreSessionToolSurface();
|
|
@@ -5602,22 +4168,50 @@ function parsedProviderModelVersion(id) {
|
|
|
5602
4168
|
const root = clean(plugin.root);
|
|
5603
4169
|
const script = clean(plugin.mcpScript);
|
|
5604
4170
|
if (!root || !script) throw new Error('plugin has no MCP script');
|
|
5605
|
-
const scriptPath = join(root, script);
|
|
5606
|
-
if (!existsSync(scriptPath)) throw new Error(`plugin MCP script not found: ${scriptPath}`);
|
|
5607
4171
|
const serverName = pluginMcpServerName(plugin);
|
|
5608
4172
|
const nextConfig = { ...config };
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
4173
|
+
if (script === '.mcp.json') {
|
|
4174
|
+
const mcpJsonPath = join(root, script);
|
|
4175
|
+
if (!existsSync(mcpJsonPath)) throw new Error(`plugin MCP manifest not found: ${mcpJsonPath}`);
|
|
4176
|
+
const manifest = readJsonSafe(mcpJsonPath) || {};
|
|
4177
|
+
const rawServers = manifest.mcpServers;
|
|
4178
|
+
const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
4179
|
+
if (!isPlainObject(rawServers)) throw new Error(`plugin .mcp.json missing mcpServers object: ${mcpJsonPath}`);
|
|
4180
|
+
const keys = Object.keys(rawServers).filter((k) => isPlainObject(rawServers[k]));
|
|
4181
|
+
if (!keys.length) throw new Error(`plugin .mcp.json has no mcpServers: ${mcpJsonPath}`);
|
|
4182
|
+
const ownedPrefix = `${serverName}--`;
|
|
4183
|
+
const nextServers = {};
|
|
4184
|
+
for (const [k, v] of Object.entries(nextConfig.mcpServers || {})) {
|
|
4185
|
+
if (k === serverName || k.startsWith(ownedPrefix)) continue;
|
|
4186
|
+
nextServers[k] = v;
|
|
4187
|
+
}
|
|
4188
|
+
for (const serverKey of keys) {
|
|
4189
|
+
const cfg = normalizePluginMcpServerConfig(rawServers[serverKey], root);
|
|
4190
|
+
cfg.env = {
|
|
4191
|
+
...(cfg.env || {}),
|
|
5616
4192
|
MIXDOG_PLUGIN_ROOT: root,
|
|
5617
4193
|
MIXDOG_PLUGIN_DATA: join(cfgMod.getPluginData?.() || STANDALONE_DATA_DIR, 'plugins', 'data', clean(plugin.id || plugin.name || serverName)),
|
|
4194
|
+
};
|
|
4195
|
+
const key = keys.length === 1 ? serverName : `${serverName}--${serverKey}`;
|
|
4196
|
+
nextServers[key] = cfg;
|
|
4197
|
+
}
|
|
4198
|
+
nextConfig.mcpServers = nextServers;
|
|
4199
|
+
} else {
|
|
4200
|
+
const scriptPath = join(root, script);
|
|
4201
|
+
if (!existsSync(scriptPath)) throw new Error(`plugin MCP script not found: ${scriptPath}`);
|
|
4202
|
+
nextConfig.mcpServers = {
|
|
4203
|
+
...(nextConfig.mcpServers || {}),
|
|
4204
|
+
[serverName]: {
|
|
4205
|
+
command: 'node',
|
|
4206
|
+
args: [scriptPath],
|
|
4207
|
+
cwd: root,
|
|
4208
|
+
env: {
|
|
4209
|
+
MIXDOG_PLUGIN_ROOT: root,
|
|
4210
|
+
MIXDOG_PLUGIN_DATA: join(cfgMod.getPluginData?.() || STANDALONE_DATA_DIR, 'plugins', 'data', clean(plugin.id || plugin.name || serverName)),
|
|
4211
|
+
},
|
|
5618
4212
|
},
|
|
5619
|
-
}
|
|
5620
|
-
}
|
|
4213
|
+
};
|
|
4214
|
+
}
|
|
5621
4215
|
saveConfigAndAdopt(nextConfig);
|
|
5622
4216
|
const status = await connectConfiguredMcp({ reset: true });
|
|
5623
4217
|
invalidatePreSessionToolSurface();
|
|
@@ -5801,10 +4395,28 @@ function parsedProviderModelVersion(id) {
|
|
|
5801
4395
|
async close(reason = 'cli-exit', options = {}) {
|
|
5802
4396
|
const detach = options?.detach === true || options?.wait === false || options?.waitForExit === false;
|
|
5803
4397
|
closeRequested = true;
|
|
4398
|
+
// SessionEnd: bridge teardown to the standard hook bus. reason mapped to
|
|
4399
|
+
// standard values ('clear'/'exit' where applicable, else 'other'). Short
|
|
4400
|
+
// await guard so a slow hook cannot wedge teardown; best-effort.
|
|
4401
|
+
try {
|
|
4402
|
+
const rl = String(reason || '').toLowerCase();
|
|
4403
|
+
const endReason = /clear/.test(rl) ? 'clear'
|
|
4404
|
+
: /exit|quit|cli-exit|shutdown|sigint|sigterm/.test(rl) ? 'exit'
|
|
4405
|
+
: 'other';
|
|
4406
|
+
if (session?.id) {
|
|
4407
|
+
await withTeardownDeadline(
|
|
4408
|
+
Promise.resolve(hooks.dispatch('SessionEnd', hookCommonPayload({ session_id: session.id, reason: endReason }))).catch(() => {}),
|
|
4409
|
+
300,
|
|
4410
|
+
undefined,
|
|
4411
|
+
);
|
|
4412
|
+
}
|
|
4413
|
+
} catch { /* best-effort: SessionEnd hook must never wedge teardown */ }
|
|
5804
4414
|
// Persist any change that is still sitting in the debounce window so a
|
|
5805
4415
|
// toggle made right before exit is not lost. Synchronous + best-effort:
|
|
5806
4416
|
// teardown must continue even if the final write fails.
|
|
5807
4417
|
try { flushConfigSave(); } catch {}
|
|
4418
|
+
try { flushBackendSave(); } catch {}
|
|
4419
|
+
try { flushOutputStyleSave(); } catch {}
|
|
5808
4420
|
if (channelStartTimer) {
|
|
5809
4421
|
clearTimeout(channelStartTimer);
|
|
5810
4422
|
channelStartTimer = null;
|