@causyne/agent 0.0.0-stage → 1.10.0
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/LICENSE +102 -0
- package/README.md +31 -2
- package/SECURITY.md +24 -0
- package/bin/causyne.js +13 -0
- package/docs/ARCHITECTURE.md +63 -0
- package/docs/PROVENANCE.md +29 -0
- package/docs/SECURITY_REVIEW.md +89 -0
- package/docs/THIRD_PARTY_NOTICES.md +49 -0
- package/native-skills/ai-engineering/context-window-management/SKILL.md +43 -0
- package/native-skills/ai-engineering/llm-evaluation/SKILL.md +46 -0
- package/native-skills/ai-engineering/model-cost-and-latency/SKILL.md +46 -0
- package/native-skills/ai-engineering/prompt-construction/SKILL.md +42 -0
- package/native-skills/ai-engineering/retrieval-augmented-generation/SKILL.md +42 -0
- package/native-skills/ai-engineering/structured-output-and-schemas/SKILL.md +45 -0
- package/native-skills/ai-engineering/tool-calling-agents/SKILL.md +43 -0
- package/native-skills/android/app-performance-profiling/SKILL.md +42 -0
- package/native-skills/android/background-work-scheduling/SKILL.md +47 -0
- package/native-skills/android/compose-state-and-recomposition/SKILL.md +43 -0
- package/native-skills/android/gradle-build-configuration/SKILL.md +41 -0
- package/native-skills/android/lifecycle-and-leaks/SKILL.md +46 -0
- package/native-skills/android/play-store-release/SKILL.md +43 -0
- package/native-skills/android/room-and-persistence/SKILL.md +40 -0
- package/native-skills/backend/authentication-and-sessions/SKILL.md +45 -0
- package/native-skills/backend/background-jobs-and-queues/SKILL.md +42 -0
- package/native-skills/backend/caching-strategy/SKILL.md +44 -0
- package/native-skills/backend/graceful-shutdown/SKILL.md +42 -0
- package/native-skills/backend/rate-limiting-and-quotas/SKILL.md +40 -0
- package/native-skills/backend/rest-api-design/SKILL.md +40 -0
- package/native-skills/backend/webhook-delivery-and-receipt/SKILL.md +43 -0
- package/native-skills/cloud/cost-optimisation/SKILL.md +46 -0
- package/native-skills/cloud/iam-least-privilege/SKILL.md +43 -0
- package/native-skills/cloud/infrastructure-as-code/SKILL.md +43 -0
- package/native-skills/cloud/multi-region-and-failover/SKILL.md +43 -0
- package/native-skills/cloud/serverless-patterns/SKILL.md +47 -0
- package/native-skills/data/data-quality-monitoring/SKILL.md +51 -0
- package/native-skills/data/etl-pipeline-reliability/SKILL.md +43 -0
- package/native-skills/data/exploratory-analysis/SKILL.md +43 -0
- package/native-skills/data/metric-definition/SKILL.md +42 -0
- package/native-skills/data/notebook-to-production/SKILL.md +43 -0
- package/native-skills/database/backup-and-recovery/SKILL.md +43 -0
- package/native-skills/database/connection-pooling/SKILL.md +42 -0
- package/native-skills/database/data-integrity-and-constraints/SKILL.md +42 -0
- package/native-skills/database/query-optimisation/SKILL.md +38 -0
- package/native-skills/database/safe-migrations/SKILL.md +47 -0
- package/native-skills/database/schema-design/SKILL.md +38 -0
- package/native-skills/database/transactions-and-isolation/SKILL.md +41 -0
- package/native-skills/design/colour-system-construction/SKILL.md +38 -0
- package/native-skills/design/design-direction-brief/SKILL.md +41 -0
- package/native-skills/design/motion-and-transitions/SKILL.md +38 -0
- package/native-skills/design/responsive-strategy/SKILL.md +39 -0
- package/native-skills/design/spacing-and-rhythm/SKILL.md +38 -0
- package/native-skills/design/type-scale-and-hierarchy/SKILL.md +39 -0
- package/native-skills/design/visual-qa-loop/SKILL.md +42 -0
- package/native-skills/devops/docker-multi-stage-builds/SKILL.md +60 -0
- package/native-skills/devops/github-actions-ci/SKILL.md +60 -0
- package/native-skills/devops/incident-triage/SKILL.md +38 -0
- package/native-skills/devops/kubernetes-deployment-hardening/SKILL.md +64 -0
- package/native-skills/devops/observability-baseline/SKILL.md +38 -0
- package/native-skills/devops/secret-management/SKILL.md +40 -0
- package/native-skills/devops/terraform-state-and-modules/SKILL.md +36 -0
- package/native-skills/devops/zero-downtime-deploys/SKILL.md +40 -0
- package/native-skills/git/bisect-regression-hunt/SKILL.md +43 -0
- package/native-skills/git/branch-strategy/SKILL.md +39 -0
- package/native-skills/git/commit-hygiene/SKILL.md +41 -0
- package/native-skills/git/history-recovery/SKILL.md +44 -0
- package/native-skills/git/large-repository-surgery/SKILL.md +46 -0
- package/native-skills/git/merge-conflict-resolution/SKILL.md +42 -0
- package/native-skills/media/cinematic-3d-shot/SKILL.md +104 -0
- package/native-skills/mobile/app-startup-optimisation/SKILL.md +41 -0
- package/native-skills/mobile/cross-platform-tradeoffs/SKILL.md +42 -0
- package/native-skills/mobile/deep-linking/SKILL.md +39 -0
- package/native-skills/mobile/mobile-storage-security/SKILL.md +45 -0
- package/native-skills/mobile/offline-first-sync/SKILL.md +42 -0
- package/native-skills/mobile/push-notifications/SKILL.md +38 -0
- package/native-skills/performance/algorithmic-complexity/SKILL.md +43 -0
- package/native-skills/performance/concurrency-and-parallelism/SKILL.md +42 -0
- package/native-skills/performance/load-testing/SKILL.md +42 -0
- package/native-skills/performance/memory-and-leak-hunting/SKILL.md +41 -0
- package/native-skills/performance/network-and-payload-efficiency/SKILL.md +42 -0
- package/native-skills/performance/profiling-methodology/SKILL.md +46 -0
- package/native-skills/python/async-correctness/SKILL.md +40 -0
- package/native-skills/python/cli-tool-construction/SKILL.md +44 -0
- package/native-skills/python/data-processing-pipelines/SKILL.md +38 -0
- package/native-skills/python/environment-isolation/SKILL.md +41 -0
- package/native-skills/python/packaging-and-distribution/SKILL.md +56 -0
- package/native-skills/python/pytest-suite-design/SKILL.md +39 -0
- package/native-skills/python/type-hints-and-static-checking/SKILL.md +42 -0
- package/native-skills/security/authorization-model/SKILL.md +42 -0
- package/native-skills/security/crypto-usage/SKILL.md +48 -0
- package/native-skills/security/dependency-supply-chain/SKILL.md +42 -0
- package/native-skills/security/input-validation-and-injection/SKILL.md +44 -0
- package/native-skills/security/secure-file-uploads/SKILL.md +46 -0
- package/native-skills/security/security-headers-and-csp/SKILL.md +44 -0
- package/native-skills/security/threat-modelling/SKILL.md +46 -0
- package/native-skills/testing/end-to-end-test-stability/SKILL.md +42 -0
- package/native-skills/testing/property-based-testing/SKILL.md +46 -0
- package/native-skills/testing/regression-test-from-a-bug/SKILL.md +41 -0
- package/native-skills/testing/snapshot-and-visual-regression/SKILL.md +42 -0
- package/native-skills/testing/test-doubles-and-mocking/SKILL.md +43 -0
- package/native-skills/testing/test-strategy-and-pyramid/SKILL.md +39 -0
- package/native-skills/testing/writing-good-assertions/SKILL.md +42 -0
- package/native-skills/web/accessible-components/SKILL.md +45 -0
- package/native-skills/web/api-client-resilience/SKILL.md +39 -0
- package/native-skills/web/bundle-size-reduction/SKILL.md +38 -0
- package/native-skills/web/core-web-vitals/SKILL.md +40 -0
- package/native-skills/web/css-layout-systems/SKILL.md +41 -0
- package/native-skills/web/form-validation-ux/SKILL.md +39 -0
- package/native-skills/web/react-state-architecture/SKILL.md +41 -0
- package/native-skills/web/ssr-hydration-correctness/SKILL.md +45 -0
- package/package.json +68 -4
- package/src/agentLoop.js +2458 -0
- package/src/appChannel.js +109 -0
- package/src/approvalGrants.js +66 -0
- package/src/auditLog.js +52 -0
- package/src/autoUpdate.js +12 -0
- package/src/capabilities.js +36 -0
- package/src/checkpoints.js +1199 -0
- package/src/cliMode.js +638 -0
- package/src/commands.js +805 -0
- package/src/compiledSkills.js +540 -0
- package/src/config.js +130 -0
- package/src/contextManager.js +498 -0
- package/src/cortex/autoCondenser.js +192 -0
- package/src/cortex/provisioner.js +27 -0
- package/src/cortex/vectorDb.js +447 -0
- package/src/designPalettes.js +99 -0
- package/src/devMode.js +6 -0
- package/src/embodimentSession.js +180 -0
- package/src/entrypoint.js +194 -0
- package/src/execSafe.js +21 -0
- package/src/gitSnapshot.js +378 -0
- package/src/mcp/jsonRpc.js +165 -0
- package/src/mcp/mcpClient.js +136 -0
- package/src/mcp/mcpRegistry.js +207 -0
- package/src/memory.js +62 -0
- package/src/memoryManager.js +124 -0
- package/src/modelCatalogue.js +132 -0
- package/src/modelIds.js +42 -0
- package/src/modelRouter.js +145 -0
- package/src/outputStore.js +246 -0
- package/src/perception.js +124 -0
- package/src/planCache.js +437 -0
- package/src/procSignal.js +32 -0
- package/src/providers/anthropic.js +408 -0
- package/src/providers/base.js +328 -0
- package/src/providers/dashscope-byok.js +86 -0
- package/src/providers/deepseek.js +80 -0
- package/src/providers/factory.js +37 -0
- package/src/providers/gemini.js +346 -0
- package/src/providers/moonshot.js +73 -0
- package/src/providers/muse.js +169 -0
- package/src/providers/ollama.js +104 -0
- package/src/providers/openai-compatible.js +84 -0
- package/src/providers/openai.js +20 -0
- package/src/providers/qwen-byok.js +76 -0
- package/src/providers/xai.js +73 -0
- package/src/provisioning/capabilityConsent.js +24 -0
- package/src/provisioning/capabilityProvisioner.js +271 -0
- package/src/provisioning/capabilityTools.js +29 -0
- package/src/reporting.js +373 -0
- package/src/sandbox.js +529 -0
- package/src/sessionStore.js +279 -0
- package/src/skillsManager.js +288 -0
- package/src/slopDetector.js +307 -0
- package/src/taskBuffer.js +45 -0
- package/src/taskShape.js +241 -0
- package/src/textDiff.js +150 -0
- package/src/tokenTracker.js +560 -0
- package/src/tool-scripts/ax_snapshot.applescript +39 -0
- package/src/tool-scripts/ax_snapshot.ps1 +10 -0
- package/src/tool-scripts/desktop_act.applescript +58 -0
- package/src/tool-scripts/resolve_automator.py +119 -0
- package/src/toolPacks.js +524 -0
- package/src/tools/archive.js +63 -0
- package/src/tools/astryxOracle.js +78 -0
- package/src/tools/axSnapshot.js +165 -0
- package/src/tools/blenderAutomator.js +349 -0
- package/src/tools/browserAutomator.js +252 -0
- package/src/tools/checkSystemSpecs.js +64 -0
- package/src/tools/clipboard.js +36 -0
- package/src/tools/codeSearchAst.js +101 -0
- package/src/tools/codeSearcher.js +230 -0
- package/src/tools/codeTools.js +262 -0
- package/src/tools/codebaseSearch.js +75 -0
- package/src/tools/createSkill.js +129 -0
- package/src/tools/deferredMcpSearch.js +246 -0
- package/src/tools/desktopAct.js +208 -0
- package/src/tools/desktopOpen.js +34 -0
- package/src/tools/dynamicLoader.js +44 -0
- package/src/tools/executeProceduralMedia.js +93 -0
- package/src/tools/fsDelete.js +51 -0
- package/src/tools/fsEdit.js +105 -0
- package/src/tools/fsFind.js +128 -0
- package/src/tools/fsList.js +77 -0
- package/src/tools/fsMove.js +57 -0
- package/src/tools/fsRead.js +87 -0
- package/src/tools/fsSearch.js +198 -0
- package/src/tools/fsWrite.js +41 -0
- package/src/tools/gameAiBehaviorTree.js +208 -0
- package/src/tools/gameAnimationRig.js +194 -0
- package/src/tools/gameAssetFetcher.js +372 -0
- package/src/tools/gameAssetHarvester.js +210 -0
- package/src/tools/gameAudioSynth.js +267 -0
- package/src/tools/gameBuilder.js +886 -0
- package/src/tools/gameCanvasAudit.js +101 -0
- package/src/tools/gameCinematicSequencer.js +151 -0
- package/src/tools/gameDataForge.js +124 -0
- package/src/tools/gameDialogueTreeGen.js +166 -0
- package/src/tools/gameEconomyBalancer.js +161 -0
- package/src/tools/gameEntitySpawner.js +207 -0
- package/src/tools/gameLoreForge.js +153 -0
- package/src/tools/gameQuestArcBuilder.js +152 -0
- package/src/tools/gameSceneBuilder.js +158 -0
- package/src/tools/gameShaderForge.js +181 -0
- package/src/tools/gameTilemapGenerator.js +164 -0
- package/src/tools/gameUiHudBuilder.js +292 -0
- package/src/tools/gameVfxEmitterGen.js +108 -0
- package/src/tools/gameWebRuntimeInspector.js +91 -0
- package/src/tools/generateMediaViaApi.js +358 -0
- package/src/tools/gitExec.js +54 -0
- package/src/tools/godotAutomator.js +437 -0
- package/src/tools/manageLocalAiDependencies.js +220 -0
- package/src/tools/memoryWrite.js +99 -0
- package/src/tools/multiEdit.js +107 -0
- package/src/tools/palmierAutomator.js +104 -0
- package/src/tools/pdfReader.js +112 -0
- package/src/tools/persistentShell.js +148 -0
- package/src/tools/printer.js +316 -0
- package/src/tools/procTerm.js +91 -0
- package/src/tools/processList.js +52 -0
- package/src/tools/readNativeSkill.js +39 -0
- package/src/tools/registry.js +277 -0
- package/src/tools/resolveAutomator.js +102 -0
- package/src/tools/screenshot.js +75 -0
- package/src/tools/subagentOrchestrator.js +305 -0
- package/src/tools/sysRun.js +125 -0
- package/src/tools/todoWrite.js +75 -0
- package/src/tools/toolDispatch.js +127 -0
- package/src/tools/toolForge.js +124 -0
- package/src/tools/updatePlan.js +88 -0
- package/src/tools/viewImage.js +82 -0
- package/src/tools/visionInspector.js +154 -0
- package/src/tools/webFetch.js +74 -0
- package/src/tools/webSearch.js +82 -0
- package/src/tui/ask.js +164 -0
- package/src/tui/banner.js +158 -0
- package/src/tui/block.js +152 -0
- package/src/tui/chatRenderer.js +953 -0
- package/src/tui/completion.js +424 -0
- package/src/tui/dialogs.js +606 -0
- package/src/tui/diff.js +296 -0
- package/src/tui/history.js +135 -0
- package/src/tui/home.js +217 -0
- package/src/tui/invocation.js +105 -0
- package/src/tui/keys.js +443 -0
- package/src/tui/lineEditor.js +256 -0
- package/src/tui/overlay.js +256 -0
- package/src/tui/prompt.js +1258 -0
- package/src/tui/selectList.js +335 -0
- package/src/tui/stage.js +330 -0
- package/src/tui/stageSurface.js +609 -0
- package/src/tui/statusLine.js +254 -0
- package/src/tui/text.js +345 -0
- package/src/tui/theme.js +393 -0
- package/src/tui/toolDisplay.js +176 -0
- package/src/tui/transcript.js +235 -0
- package/src/tui/viewport.js +337 -0
- package/src/uncertainty.js +271 -0
- package/src/userSkills.js +265 -0
- package/src/utils/cli_ui.js +0 -0
- package/src/utils/inquirer.js +12 -0
- package/src/utils/paths.js +37 -0
- package/src/version.js +38 -0
- package/src/vfeTracker.js +220 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// The bridge between user-configured MCP servers and Causyne's tool catalogue.
|
|
2
|
+
//
|
|
3
|
+
// Discovered tools are namespaced `mcp__<server>__<tool>`, which is what keeps
|
|
4
|
+
// them from ever colliding with a native tool name — and makes it obvious in a
|
|
5
|
+
// transcript, an audit log and an approval prompt that the call is leaving
|
|
6
|
+
// Causyne's own code.
|
|
7
|
+
|
|
8
|
+
import { McpClient } from './mcpClient.js';
|
|
9
|
+
import { loadConfig, saveConfig } from '../config.js';
|
|
10
|
+
import { auditLog } from '../auditLog.js';
|
|
11
|
+
|
|
12
|
+
const NAMESPACE = 'mcp__';
|
|
13
|
+
const connected = new Map(); // server name -> McpClient
|
|
14
|
+
|
|
15
|
+
/** Server definitions the USER configured. Never model-supplied. */
|
|
16
|
+
export function configuredServers() {
|
|
17
|
+
const config = loadConfig();
|
|
18
|
+
const servers = Array.isArray(config.mcpServers) ? config.mcpServers : [];
|
|
19
|
+
return servers.filter(s => McpClient.validateDefinition(s) === null);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
export function addServer(definition) {
|
|
24
|
+
const problem = McpClient.validateDefinition(definition);
|
|
25
|
+
if (problem) return { ok: false, error: problem };
|
|
26
|
+
|
|
27
|
+
const config = loadConfig();
|
|
28
|
+
const servers = Array.isArray(config.mcpServers) ? [...config.mcpServers] : [];
|
|
29
|
+
|
|
30
|
+
if (servers.some(s => s && s.name === definition.name)) {
|
|
31
|
+
return { ok: false, error: `a server named "${definition.name}" is already configured` };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Store only the fields the client actually reads. Anything else a caller
|
|
35
|
+
// sends is dropped rather than persisted into the user's config.
|
|
36
|
+
const stored = { name: definition.name };
|
|
37
|
+
if (definition.url) {
|
|
38
|
+
stored.url = definition.url;
|
|
39
|
+
if (definition.headers && typeof definition.headers === 'object') stored.headers = definition.headers;
|
|
40
|
+
} else {
|
|
41
|
+
stored.command = definition.command;
|
|
42
|
+
if (Array.isArray(definition.args)) stored.args = definition.args;
|
|
43
|
+
if (definition.env && typeof definition.env === 'object') stored.env = definition.env;
|
|
44
|
+
if (typeof definition.cwd === 'string') stored.cwd = definition.cwd;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
config.mcpServers = [...servers, stored];
|
|
48
|
+
// Registering is not consenting: leave it ungranted.
|
|
49
|
+
config.mcpConsent = { ...(config.mcpConsent || {}), [definition.name]: false };
|
|
50
|
+
saveConfig(config);
|
|
51
|
+
|
|
52
|
+
auditLog.log({ event: 'mcp_server_added', server: definition.name, transport: definition.url ? 'http' : 'stdio' });
|
|
53
|
+
return { ok: true };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Remove a server, drop its consent, and close any live connection. */
|
|
57
|
+
export function removeServer(name) {
|
|
58
|
+
const config = loadConfig();
|
|
59
|
+
const servers = Array.isArray(config.mcpServers) ? config.mcpServers : [];
|
|
60
|
+
if (!servers.some(s => s && s.name === name)) {
|
|
61
|
+
return { ok: false, error: `no server named "${name}" is configured` };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
config.mcpServers = servers.filter(s => !s || s.name !== name);
|
|
65
|
+
const consent = { ...(config.mcpConsent || {}) };
|
|
66
|
+
delete consent[name];
|
|
67
|
+
config.mcpConsent = consent;
|
|
68
|
+
saveConfig(config);
|
|
69
|
+
|
|
70
|
+
const client = connected.get(name);
|
|
71
|
+
if (client) { try { client.close(); } catch (e) {} connected.delete(name); }
|
|
72
|
+
|
|
73
|
+
auditLog.log({ event: 'mcp_server_removed', server: name });
|
|
74
|
+
return { ok: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function isServerGranted(name) {
|
|
78
|
+
const config = loadConfig();
|
|
79
|
+
return (config.mcpConsent || {})[name] === true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function grantServer(name) {
|
|
83
|
+
const config = loadConfig();
|
|
84
|
+
config.mcpConsent = { ...(config.mcpConsent || {}), [name]: true };
|
|
85
|
+
saveConfig(config);
|
|
86
|
+
// The consent boundary is exactly what a post-hoc review needs to see.
|
|
87
|
+
auditLog.log({ event: 'mcp_server_granted', server: name });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function revokeServer(name) {
|
|
91
|
+
const config = loadConfig();
|
|
92
|
+
config.mcpConsent = { ...(config.mcpConsent || {}), [name]: false };
|
|
93
|
+
saveConfig(config);
|
|
94
|
+
const client = connected.get(name);
|
|
95
|
+
if (client) { client.close(); connected.delete(name); }
|
|
96
|
+
auditLog.log({ event: 'mcp_server_revoked', server: name });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function toolName(serverName, tool) {
|
|
100
|
+
return `${NAMESPACE}${serverName}__${tool}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isMcpToolName(name) {
|
|
104
|
+
return typeof name === 'string' && name.startsWith(NAMESPACE);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Split `mcp__server__tool` back into its parts. Tool names may contain `__`. */
|
|
108
|
+
export function parseToolName(name) {
|
|
109
|
+
if (!isMcpToolName(name)) return null;
|
|
110
|
+
const rest = name.slice(NAMESPACE.length);
|
|
111
|
+
const separator = rest.indexOf('__');
|
|
112
|
+
if (separator === -1) return null;
|
|
113
|
+
return { server: rest.slice(0, separator), tool: rest.slice(separator + 2) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Connect to a granted server, or return the live client.
|
|
118
|
+
* Connecting is never automatic: the caller has already checked consent.
|
|
119
|
+
*/
|
|
120
|
+
export async function connectServer(name) {
|
|
121
|
+
if (connected.has(name)) return connected.get(name);
|
|
122
|
+
const definition = configuredServers().find(s => s.name === name);
|
|
123
|
+
if (!definition) throw new Error(`no MCP server named "${name}" is configured`);
|
|
124
|
+
const client = await new McpClient(definition).connect();
|
|
125
|
+
connected.set(name, client);
|
|
126
|
+
return client;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Tool schemas for every granted, reachable server.
|
|
131
|
+
*
|
|
132
|
+
* A server that fails to start is skipped silently rather than failing the
|
|
133
|
+
* whole catalogue — one broken third-party server must not take the agent down.
|
|
134
|
+
*/
|
|
135
|
+
export async function discoverTools() {
|
|
136
|
+
const definitions = [];
|
|
137
|
+
for (const server of configuredServers()) {
|
|
138
|
+
if (!isServerGranted(server.name)) continue;
|
|
139
|
+
let client;
|
|
140
|
+
try {
|
|
141
|
+
client = await connectServer(server.name);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.warn(`[MCP] ${server.name} unavailable: ${e.message}`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
for (const tool of client.tools) {
|
|
147
|
+
definitions.push({
|
|
148
|
+
name: toolName(server.name, tool.name),
|
|
149
|
+
description: `[MCP: ${server.name}] ${tool.description}`.trim(),
|
|
150
|
+
parameters: tool.inputSchema
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return definitions;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A tool object the registry can execute, for any `mcp__…` name.
|
|
159
|
+
* Returns null for a name that is not MCP, or whose server is not granted.
|
|
160
|
+
*/
|
|
161
|
+
export function resolveMcpTool(name) {
|
|
162
|
+
const parsed = parseToolName(name);
|
|
163
|
+
if (!parsed) return null;
|
|
164
|
+
if (!configuredServers().some(s => s.name === parsed.server)) return null;
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
schema: { name, description: `MCP tool ${parsed.tool} on server ${parsed.server}`, parameters: { type: 'object', properties: {} } },
|
|
168
|
+
async execute(args) {
|
|
169
|
+
if (!isServerGranted(parsed.server)) {
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
requiresConsent: true,
|
|
173
|
+
error: `The MCP server "${parsed.server}" is configured but not enabled. ` +
|
|
174
|
+
`Call manage_local_dependencies with action "grant_mcp_server" and group "${parsed.server}" ` +
|
|
175
|
+
`to request the user's approval.`
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const client = await connectServer(parsed.server);
|
|
180
|
+
return await client.callTool(parsed.tool, args);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
return { success: false, error: `MCP server "${parsed.server}": ${e.message}` };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** State for the user-facing listing. */
|
|
189
|
+
export function serverStatus() {
|
|
190
|
+
return configuredServers().map(server => ({
|
|
191
|
+
name: server.name,
|
|
192
|
+
transport: server.url ? 'http' : 'stdio',
|
|
193
|
+
// The full invocation, not just the binary. "npx" alone tells a user nothing
|
|
194
|
+
// about what they are consenting to run; "npx -y @scope/server-fs /tmp" does.
|
|
195
|
+
target: server.url || [server.command, ...(server.args || [])].join(' '),
|
|
196
|
+
granted: isServerGranted(server.name),
|
|
197
|
+
connected: connected.has(server.name),
|
|
198
|
+
tools: connected.has(server.name) ? connected.get(server.name).tools.map(t => t.name) : []
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function closeAll() {
|
|
203
|
+
for (const [, client] of connected) {
|
|
204
|
+
try { client.close(); } catch (e) {}
|
|
205
|
+
}
|
|
206
|
+
connected.clear();
|
|
207
|
+
}
|
package/src/memory.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { causyneHome } from './utils/paths.js';
|
|
5
|
+
|
|
6
|
+
const MEMORY_FILE_PATH = path.join(causyneHome(), 'memory.json');
|
|
7
|
+
|
|
8
|
+
// Ensure the directory and file exist
|
|
9
|
+
function initMemory() {
|
|
10
|
+
const dir = path.dirname(MEMORY_FILE_PATH);
|
|
11
|
+
if (!fs.existsSync(dir)) {
|
|
12
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
if (!fs.existsSync(MEMORY_FILE_PATH)) {
|
|
15
|
+
fs.writeFileSync(MEMORY_FILE_PATH, JSON.stringify({ facts: [] }, null, 2));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getMemory() {
|
|
20
|
+
try {
|
|
21
|
+
initMemory();
|
|
22
|
+
const data = fs.readFileSync(MEMORY_FILE_PATH, 'utf-8');
|
|
23
|
+
return JSON.parse(data);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.error("Failed to read memory.json:", e.message);
|
|
26
|
+
return { facts: [] };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function saveMemory(data) {
|
|
31
|
+
try {
|
|
32
|
+
initMemory();
|
|
33
|
+
fs.writeFileSync(MEMORY_FILE_PATH, JSON.stringify(data, null, 2));
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.error("Failed to write memory.json:", e.message);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function addFact(factStr) {
|
|
40
|
+
const mem = getMemory();
|
|
41
|
+
// Only add if it doesn't already exist to avoid duplicates
|
|
42
|
+
if (!mem.facts.includes(factStr)) {
|
|
43
|
+
mem.facts.push(factStr);
|
|
44
|
+
saveMemory(mem);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function clearMemory() {
|
|
49
|
+
try {
|
|
50
|
+
initMemory();
|
|
51
|
+
fs.writeFileSync(MEMORY_FILE_PATH, JSON.stringify({ facts: [] }, null, 2));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error("Failed to clear memory:", e.message);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getMemoryContextString() {
|
|
58
|
+
const mem = getMemory();
|
|
59
|
+
if (!mem || !mem.facts || mem.facts.length === 0) return "No prior memory facts available.";
|
|
60
|
+
|
|
61
|
+
return mem.facts.map((fact, index) => `${index + 1}. ${fact}`).join("\n");
|
|
62
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { resolveSession, createSession, switchSession, saveSessionMessages } from './sessionStore.js';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { causyneHome } from './utils/paths.js';
|
|
6
|
+
|
|
7
|
+
const MEMORY_DIR = path.join(causyneHome(), 'memory');
|
|
8
|
+
const CHAT_HISTORY_FILE = path.join(MEMORY_DIR, 'chat_history.json');
|
|
9
|
+
|
|
10
|
+
// Persistence ceilings. Without them chat_history.json grew for the lifetime of
|
|
11
|
+
// the installation: every new task reloaded a file that could be tens of
|
|
12
|
+
// megabytes, and the sliding window filled to its limit before the user had
|
|
13
|
+
// typed a single word — so even "list my files" paid a full context window.
|
|
14
|
+
const MAX_PERSISTED_MESSAGES = 120;
|
|
15
|
+
const MAX_PERSISTED_CHARS = 400000;
|
|
16
|
+
const MAX_SINGLE_MESSAGE_CHARS = 40000;
|
|
17
|
+
|
|
18
|
+
function contentLength(content) {
|
|
19
|
+
if (typeof content === 'string') return content.length;
|
|
20
|
+
if (content == null) return 0;
|
|
21
|
+
return JSON.stringify(content).length;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class MemoryManager {
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} workspace The directory this conversation belongs to.
|
|
27
|
+
*
|
|
28
|
+
* History is now per SESSION, and a session belongs to a workspace. It used
|
|
29
|
+
* to be one global chat_history.json read by every invocation in every
|
|
30
|
+
* directory — so typing continue in a new project resumed a conversation
|
|
31
|
+
* from a different one, months old. See src/sessionStore.js.
|
|
32
|
+
*/
|
|
33
|
+
constructor(workspace = process.cwd()) {
|
|
34
|
+
this.ensureMemoryDir();
|
|
35
|
+
this.workspace = workspace;
|
|
36
|
+
this.session = resolveSession(workspace);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The active session's id, for the status line and /sessions. */
|
|
40
|
+
get sessionId() { return this.session ? this.session.id : null; }
|
|
41
|
+
get sessionTitle() { return this.session ? this.session.title : ''; }
|
|
42
|
+
|
|
43
|
+
/** Abandon this conversation and open a clean one in the same workspace. */
|
|
44
|
+
startNewSession() {
|
|
45
|
+
this.session = createSession(this.workspace);
|
|
46
|
+
return this.session;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Resume a specific past conversation. */
|
|
50
|
+
useSession(id) {
|
|
51
|
+
const next = switchSession(id);
|
|
52
|
+
if (next) this.session = next;
|
|
53
|
+
return next;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
ensureMemoryDir() {
|
|
57
|
+
if (!fs.existsSync(MEMORY_DIR)) {
|
|
58
|
+
fs.mkdirSync(MEMORY_DIR, { recursive: true, mode: 0o700 });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
loadHistory() {
|
|
63
|
+
if (!this.session || !Array.isArray(this.session.messages)) return [];
|
|
64
|
+
// Only return non-system messages to prevent stale system prompts.
|
|
65
|
+
return this.session.messages.filter(m => m.role !== 'system');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
boundHistory(messages) {
|
|
70
|
+
const persistable = messages.filter(m => m.role !== 'system');
|
|
71
|
+
const bounded = [];
|
|
72
|
+
let totalChars = 0;
|
|
73
|
+
|
|
74
|
+
for (let i = persistable.length - 1; i >= 0; i--) {
|
|
75
|
+
if (bounded.length >= MAX_PERSISTED_MESSAGES) break;
|
|
76
|
+
|
|
77
|
+
const msg = persistable[i];
|
|
78
|
+
let content = msg.content;
|
|
79
|
+
|
|
80
|
+
if (Array.isArray(content)) {
|
|
81
|
+
const hasMedia = content.some(
|
|
82
|
+
block => block && (block.type === 'image' || block.type === 'image_url' || block.type === 'input_audio')
|
|
83
|
+
);
|
|
84
|
+
if (hasMedia) {
|
|
85
|
+
const captions = content
|
|
86
|
+
.filter(block => block && block.type === 'text' && block.text)
|
|
87
|
+
.map(block => block.text)
|
|
88
|
+
.join(' ');
|
|
89
|
+
content = `[media attachment not persisted] ${captions}`.trim();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (typeof content === 'string' && content.length > MAX_SINGLE_MESSAGE_CHARS) {
|
|
94
|
+
content = content.slice(0, MAX_SINGLE_MESSAGE_CHARS) + '\n[... truncated on save ...]';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const length = contentLength(content);
|
|
98
|
+
if (totalChars + length > MAX_PERSISTED_CHARS && bounded.length > 0) break;
|
|
99
|
+
totalChars += length;
|
|
100
|
+
|
|
101
|
+
bounded.unshift(content === msg.content ? msg : { ...msg, content });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// A history that opens on tool results is unusable: the assistant message
|
|
105
|
+
// that called them was trimmed away.
|
|
106
|
+
while (bounded.length > 0 && bounded[0].role === 'tool') {
|
|
107
|
+
bounded.shift();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return bounded;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
saveHistory(messages) {
|
|
114
|
+
if (!this.session) return;
|
|
115
|
+
saveSessionMessages(this.session.id, this.boundHistory(messages));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** /clear — empties THIS conversation, and leaves every other one alone. */
|
|
119
|
+
clearHistory() {
|
|
120
|
+
if (!this.session) return;
|
|
121
|
+
this.session.messages = [];
|
|
122
|
+
saveSessionMessages(this.session.id, []);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// THE model catalogue. One list, one truth.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS: on 2026-09-09 an audit found the supported-model list
|
|
4
|
+
// duplicated by hand in FIVE places, drifting independently:
|
|
5
|
+
//
|
|
6
|
+
// 1. alibaba-cloud/ui-config.js /api/agent-models — overrides the app entirely
|
|
7
|
+
// 2. AgentViewModel.kt getModelsForProvider() — the app's offline fallback
|
|
8
|
+
// 3. src/tui/home.js PROVIDERS — the terminal home screen's defaults
|
|
9
|
+
// 4. src/config.js loadConfig() — the first-run default
|
|
10
|
+
// 5. src/providers/anthropic.js — the capability lists that decide behaviour
|
|
11
|
+
//
|
|
12
|
+
// ui-config.js already carried the comment "Three lists, one truth" — it was
|
|
13
|
+
// four, and the count was the smallest of the problems. The drift had shipped:
|
|
14
|
+
|
|
15
|
+
// does not publish (it uses dashes), which matched NONE of anthropic.js's
|
|
16
|
+
// capability lists and so silently disabled adaptive thinking and deferred
|
|
17
|
+
// tool loading and sent `temperature` to models that reject it. The terminal
|
|
18
|
+
// suggested `gpt-4o` and `gemini-2.5-pro` — models two generations old. And
|
|
19
|
+
// `gpt-6-astra`, `claude-opus-5` and `claude-fable-5-1` could not be selected
|
|
20
|
+
|
|
21
|
+
//
|
|
22
|
+
// This module is now the source. 3 and 4 import it. 1 and 2 cannot (they are a
|
|
23
|
+
// separately deployed Express service and a Kotlin file), so
|
|
24
|
+
// tests/modelCatalogue.test.js READS them and fails the build when they drift.
|
|
25
|
+
// That is the part that stops this recurring: fixing the lists fixes today,
|
|
26
|
+
// pinning them by test fixes every tomorrow.
|
|
27
|
+
//
|
|
28
|
+
// WHEN A PROVIDER SHIPS A NEW MODEL: add it here, run `npm test`, and the test
|
|
29
|
+
// will name every other file that must be updated to match.
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Selectable models per provider, newest first.
|
|
33
|
+
*
|
|
34
|
+
* The label keys are a stable configuration format, not display sugar.
|
|
35
|
+
*/
|
|
36
|
+
export const MODEL_CATALOGUE = {
|
|
37
|
+
'DeepSeek': ['deepseek-v4-pro', 'deepseek-v4-flash'],
|
|
38
|
+
'Qwen': ['qwen3.7-max', 'qwen3.7-plus', 'qwen3.6-flash'],
|
|
39
|
+
// Anthropic ids use DASHES and carry no date suffix. A dotted id here is a
|
|
40
|
+
// bug, not a variant — see src/modelIds.js.
|
|
41
|
+
'Anthropic': [
|
|
42
|
+
'claude-fable-5-1',
|
|
43
|
+
'claude-opus-5',
|
|
44
|
+
'claude-opus-4-8',
|
|
45
|
+
'claude-sonnet-5',
|
|
46
|
+
'claude-haiku-4-5'
|
|
47
|
+
],
|
|
48
|
+
// GPT-6 Astra leads: it is the current flagship and the only model in the
|
|
49
|
+
// line-up trained for computer use. It is also the priciest ($10/$50 against
|
|
50
|
+
// Sol's $5/$30), which is why it heads the list but is not the default.
|
|
51
|
+
'OpenAI': ['gpt-6-astra', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna', 'gpt-5.4-mini'],
|
|
52
|
+
'Gemini': ['gemini-3.6-flash', 'gemini-3.5-flash', 'gemini-3.1-pro', 'gemini-3.1-flash-lite'],
|
|
53
|
+
// Standard first. The -contributor variants are cheaper because Meta trains
|
|
54
|
+
// on the user's prompts and completions, so they must be a choice made
|
|
55
|
+
// deliberately, never the one at the top of the list.
|
|
56
|
+
'Meta (Muse)': [
|
|
57
|
+
'muse-spark-1.3',
|
|
58
|
+
'muse-spark-1.2',
|
|
59
|
+
'muse-spark-1.1',
|
|
60
|
+
'muse-spark-1.3-contributor',
|
|
61
|
+
'muse-spark-1.2-contributor'
|
|
62
|
+
],
|
|
63
|
+
'Ollama (Local)': ['llama3', 'mistral', 'gemma2', 'custom'],
|
|
64
|
+
'Alibaba (DashScope)': [
|
|
65
|
+
'qwen3.7-max', 'qwen3.7-plus', 'kimi-k2.7-code', 'kimi-k2.6',
|
|
66
|
+
'kimi-k2.5', 'kimi-k2-thinking', 'Moonshot-Kimi-K2-Instruct',
|
|
67
|
+
'deepseek-v4-flash', 'deepseek-v4-pro'
|
|
68
|
+
],
|
|
69
|
+
'Moonshot (Kimi)': ['kimi-k3', 'kimi-k2.7-code', 'kimi-k2.6'],
|
|
70
|
+
'xAI (Grok)': ['grok-4.5', 'grok-latest']
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Provider labels in picker order. */
|
|
74
|
+
export const PROVIDER_LABELS = Object.keys(MODEL_CATALOGUE);
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
export const DEFAULT_MODEL_BY_PROVIDER = {
|
|
78
|
+
deepseek: 'deepseek-v4-pro',
|
|
79
|
+
qwen: 'qwen3.7-plus',
|
|
80
|
+
'dashscope-byok': 'qwen3.7-plus',
|
|
81
|
+
dashscope: 'qwen3.7-plus',
|
|
82
|
+
muse: 'muse-spark-1.3',
|
|
83
|
+
anthropic: 'claude-opus-5',
|
|
84
|
+
openai: 'gpt-5.6-sol',
|
|
85
|
+
gemini: 'gemini-3.5-flash',
|
|
86
|
+
moonshot: 'kimi-k3',
|
|
87
|
+
xai: 'grok-4.5',
|
|
88
|
+
ollama: 'llama3',
|
|
89
|
+
'openai-compatible': ''
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Every id this catalogue can produce, flattened. Used by the drift test and
|
|
94
|
+
* by anything that needs to ask "is this a model we claim to support?".
|
|
95
|
+
*
|
|
96
|
+
* @returns {string[]} unique ids, order not significant
|
|
97
|
+
*/
|
|
98
|
+
/**
|
|
99
|
+
* The cheapest model of each provider that is still competent at UTILITY work —
|
|
100
|
+
* classification, keyword extraction, log filtering, condensation, triage.
|
|
101
|
+
*
|
|
102
|
+
* SAME PROVIDER, ALWAYS. The research spec proposes routing light work to
|
|
103
|
+
* "DeepSeek V4 Flash or GPT-4o mini" regardless of what the user configured.
|
|
104
|
+
* That cannot work here: Causyne is BYOK and the user holds exactly ONE key.
|
|
105
|
+
* Routing across providers would call an endpoint there is no credential for,
|
|
106
|
+
* so a route may only ever move WITHIN the configured provider.
|
|
107
|
+
*
|
|
108
|
+
* MUSE IS DELIBERATELY ABSENT. Its cheap tier is the `-contributor` variant,
|
|
109
|
+
* which is cheaper precisely because Meta trains on the user's prompts and
|
|
110
|
+
* completions. That is a privacy decision, not a price decision, and it must
|
|
111
|
+
* stay something the user opts into by hand — never something a cost optimiser
|
|
112
|
+
* does to them silently. A muse user simply gets no utility routing.
|
|
113
|
+
*
|
|
114
|
+
* Absent providers (moonshot, xai, ollama, openai-compatible) have no published
|
|
115
|
+
* second tier we can rely on, so they route to nothing and pay full rate. A
|
|
116
|
+
* guess here would show up on someone's invoice.
|
|
117
|
+
*
|
|
118
|
+
* @type {Record<string, string>}
|
|
119
|
+
*/
|
|
120
|
+
export const UTILITY_MODEL_BY_PROVIDER = {
|
|
121
|
+
deepseek: 'deepseek-v4-flash', // $0.14/$0.28 against pro's $0.435/$0.87
|
|
122
|
+
qwen: 'qwen3.6-flash', // $0.19/$1.13 against plus's $0.32/$1.28
|
|
123
|
+
dashscope: 'qwen3.6-flash',
|
|
124
|
+
'dashscope-byok': 'qwen3.6-flash',
|
|
125
|
+
anthropic: 'claude-haiku-4-5', // $1/$5 against opus's $5/$25
|
|
126
|
+
openai: 'gpt-5.4-mini', // $0.75/$4.50 against sol's $5/$30
|
|
127
|
+
gemini: 'gemini-3.1-flash-lite' // $0.25/$1.50 against flash's $1.50/$9
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export function allCatalogueModels() {
|
|
131
|
+
return [...new Set(Object.values(MODEL_CATALOGUE).flat())];
|
|
132
|
+
}
|
package/src/modelIds.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Canonical model identifiers.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS: earlier configuration accepted Anthropic model ids with
|
|
4
|
+
// dots, while the canonical ids use dashes. Nothing normalised them, so the
|
|
5
|
+
// invalid string went straight into the provider request payload.
|
|
6
|
+
//
|
|
7
|
+
// The damage was not cosmetic. `providers/anthropic.js modelMatches()` compares
|
|
8
|
+
// with `startsWith`, so a dotted id matched NONE of the capability lists:
|
|
9
|
+
// - NO_SAMPLING_MODELS missed → `temperature` was sent to models that
|
|
10
|
+
// reject sampling parameters with a 400
|
|
11
|
+
// - ADAPTIVE_THINKING_MODELS missed → thinking silently off
|
|
12
|
+
// - TOOL_SEARCH_MODELS missed → deferred tool loading off, so the full
|
|
13
|
+
// core tool payload shipped on every request
|
|
14
|
+
//
|
|
15
|
+
// THE RULE IS DELIBERATELY NARROW. A blanket dot→dash replacement would
|
|
16
|
+
// destroy every other vendor's ids, because dots are CORRECT for them:
|
|
17
|
+
// `gpt-5.6-sol`, `qwen3.7-max`, `gemini-3.5-flash`, `grok-4.5`,
|
|
18
|
+
// `kimi-k2.7-code`, `muse-spark-1.3`. Only the Anthropic family is rewritten,
|
|
19
|
+
// and no published Anthropic id contains a dot, so the mapping is lossless.
|
|
20
|
+
|
|
21
|
+
/** Vendor prefixes whose canonical ids never contain a dot. */
|
|
22
|
+
const DOTLESS_MODEL_FAMILIES = ['claude-'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a model id to the form its vendor actually publishes.
|
|
26
|
+
*
|
|
27
|
+
* Pure and total: a non-string, an empty string, or an id from any other
|
|
28
|
+
* vendor is returned unchanged. Never throws — this sits on the request path.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} model Model id as supplied by a client or config file.
|
|
31
|
+
* @returns {string} The canonical id.
|
|
32
|
+
*/
|
|
33
|
+
export function canonicalModelId(model) {
|
|
34
|
+
if (typeof model !== 'string' || model === '') return model;
|
|
35
|
+
|
|
36
|
+
const lowered = model.toLowerCase();
|
|
37
|
+
for (const family of DOTLESS_MODEL_FAMILIES) {
|
|
38
|
+
if (lowered.startsWith(family)) return model.replace(/\./g, '-');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return model;
|
|
42
|
+
}
|