catence 0.2.0-beta.16 → 0.2.0-beta.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/contracts/release.d.ts +1 -1
- package/dist/contracts/release.js +1 -1
- package/dist/core/runtime/configuration.d.ts +3 -1
- package/dist/core/runtime/configuration.js +5 -1
- package/dist/core/runtime/opencode-go.d.ts +52 -0
- package/dist/core/runtime/opencode-go.js +189 -0
- package/dist/core/runtime/secrets.js +51 -13
- package/dist/elt/application/detached-sync.d.ts +40 -0
- package/dist/elt/application/detached-sync.js +78 -0
- package/dist/elt/application/management.js +102 -1
- package/dist/elt/storage/database.d.ts +6 -0
- package/dist/elt/storage/database.js +18 -0
- package/dist/interfaces/http/server.d.ts +7 -1
- package/dist/interfaces/http/server.js +104 -1
- package/dist/interfaces/mcp/server.d.ts +2 -1
- package/dist/interfaces/mcp/server.js +43 -13
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +2 -0
- package/docs/configuration.md +22 -0
- package/docs/deployment/local-console.md +24 -0
- package/docs/deployment/local-mcp.md +16 -0
- package/docs/development.md +325 -0
- package/docs/llm-providers.md +6 -0
- package/package.json +1 -1
- package/python/catence/providers/garmin/cli.py +7 -0
- package/python/catence/providers/garmin/swim_backfill.py +22 -5
- package/python/catence/providers/garmin/worker.py +138 -4
- package/scripts/discover-opencode-go.mjs +36 -144
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
// Discover OpenCode Go models and emit Catence Console profiles for them.
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// Responses API, and MiniMax and Qwen speak the Anthropic Messages API. The
|
|
4
|
+
// Thin CLI wrapper around the runtime's src/core/runtime/opencode-go.ts
|
|
5
|
+
// module, which owns the "live discovery + trusted metadata" logic: the
|
|
6
6
|
// public `GET /models` endpoint returns only model ids, not which API each
|
|
7
|
-
// model needs, so routing comes from
|
|
8
|
-
//
|
|
9
|
-
// OpenClaw uses).
|
|
7
|
+
// model needs, so routing comes from a trusted table intersected with the
|
|
8
|
+
// live list (the same approach OpenClaw uses).
|
|
10
9
|
//
|
|
11
10
|
// Usage:
|
|
12
11
|
// node scripts/discover-opencode-go.mjs [--base-url <url>] [--write <config.json>] [--set-default]
|
|
@@ -17,62 +16,19 @@
|
|
|
17
16
|
// --set-default With --write, set console.defaultProfile to opencode-go even when one already exists.
|
|
18
17
|
|
|
19
18
|
import process from 'node:process';
|
|
20
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// anthropic/<id> -> {base minus /v1}/v1/messages
|
|
32
|
-
const RESPONSES_MODELS = new Set(['grok-4.5', 'gpt-5.6-luna']);
|
|
33
|
-
const MESSAGES_MODELS = new Set([
|
|
34
|
-
'minimax-m2.5',
|
|
35
|
-
'minimax-m2.7',
|
|
36
|
-
'minimax-m3',
|
|
37
|
-
'qwen3.5-plus',
|
|
38
|
-
'qwen3.6-plus',
|
|
39
|
-
'qwen3.7-plus',
|
|
40
|
-
'qwen3.7-max',
|
|
41
|
-
'qwen3.8-max',
|
|
42
|
-
]);
|
|
43
|
-
|
|
44
|
-
const BRAND_NAMES = new Map([
|
|
45
|
-
['deepseek', 'DeepSeek'],
|
|
46
|
-
['glm', 'GLM'],
|
|
47
|
-
['gpt', 'GPT'],
|
|
48
|
-
['grok', 'Grok'],
|
|
49
|
-
['hy3', 'Hy3'],
|
|
50
|
-
['kimi', 'Kimi'],
|
|
51
|
-
['mimo', 'MiMo'],
|
|
52
|
-
['minimax', 'MiniMax'],
|
|
53
|
-
['qwen', 'Qwen'],
|
|
54
|
-
]);
|
|
55
|
-
|
|
56
|
-
function classify(id) {
|
|
57
|
-
if (RESPONSES_MODELS.has(id)) return { route: 'responses', guessed: false };
|
|
58
|
-
if (MESSAGES_MODELS.has(id)) return { route: 'messages', guessed: false };
|
|
59
|
-
if (/^(grok-|gpt-)/.test(id)) return { route: 'responses', guessed: true };
|
|
60
|
-
if (/^(minimax-|qwen)/.test(id)) return { route: 'messages', guessed: true };
|
|
61
|
-
return { route: 'chat', guessed: false };
|
|
20
|
+
let runtime;
|
|
21
|
+
try {
|
|
22
|
+
runtime = await import(new URL('../dist/core/runtime/opencode-go.js', import.meta.url).href);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error?.code !== 'ERR_MODULE_NOT_FOUND') throw error;
|
|
25
|
+
process.stderr.write(
|
|
26
|
+
'The discovery module is not built yet. Run `npm run build` in the catence checkout first, or use an installed `catence` package.\n',
|
|
27
|
+
);
|
|
28
|
+
process.exit(1);
|
|
62
29
|
}
|
|
63
30
|
|
|
64
|
-
|
|
65
|
-
if (route === 'responses') return `openai/responses/${id}`;
|
|
66
|
-
if (route === 'messages') return `anthropic/${id}`;
|
|
67
|
-
return `openai/${id}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function labelFor(id) {
|
|
71
|
-
return id
|
|
72
|
-
.split('-')
|
|
73
|
-
.map((segment) => BRAND_NAMES.get(segment) ?? segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
74
|
-
.join(' ');
|
|
75
|
-
}
|
|
31
|
+
const { OPENCODE_GO_API_BASE_ENV, OPENCODE_GO_API_KEY_ENV, OPENCODE_GO_MESSAGES_API_BASE_ENV } = runtime;
|
|
76
32
|
|
|
77
33
|
function usage() {
|
|
78
34
|
process.stderr.write(
|
|
@@ -82,7 +38,7 @@ function usage() {
|
|
|
82
38
|
|
|
83
39
|
async function main() {
|
|
84
40
|
const args = process.argv.slice(2);
|
|
85
|
-
let baseUrl =
|
|
41
|
+
let baseUrl = runtime.OPENCODE_GO_DEFAULT_BASE_URL;
|
|
86
42
|
let writePath = null;
|
|
87
43
|
let setDefault = false;
|
|
88
44
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -108,100 +64,36 @@ async function main() {
|
|
|
108
64
|
if (!baseUrl.endsWith('/v1')) {
|
|
109
65
|
process.stderr.write(`Warning: --base-url ${baseUrl} does not end in /v1; chat and responses URLs may be wrong.\n`);
|
|
110
66
|
}
|
|
111
|
-
const modelsUrl = `${baseUrl.replace(/\/$/, '')}/models`;
|
|
112
67
|
const messagesBase = baseUrl.replace(/\/v1\/?$/, '');
|
|
113
68
|
|
|
114
|
-
let response;
|
|
115
|
-
try {
|
|
116
|
-
response = await fetch(modelsUrl, { headers: { accept: 'application/json' } });
|
|
117
|
-
} catch (error) {
|
|
118
|
-
throw new Error(`Could not reach ${modelsUrl}: ${error.message}`);
|
|
119
|
-
}
|
|
120
|
-
if (!response.ok) {
|
|
121
|
-
throw new Error(`GET ${modelsUrl} returned HTTP ${response.status}.`);
|
|
122
|
-
}
|
|
123
|
-
const payload = await response.json();
|
|
124
|
-
const ids = (payload.data ?? []).map((entry) => entry.id).filter((id) => typeof id === 'string' && id);
|
|
125
|
-
|
|
126
|
-
const groups = { chat: [], responses: [], messages: [] };
|
|
127
|
-
const guessed = [];
|
|
128
|
-
for (const id of ids) {
|
|
129
|
-
const { route, guessed: wasGuessed } = classify(id);
|
|
130
|
-
if (!(route in groups)) continue;
|
|
131
|
-
groups[route].push(id);
|
|
132
|
-
if (wasGuessed) guessed.push(`${id} -> ${route}`);
|
|
133
|
-
}
|
|
134
|
-
for (const route of Object.keys(groups)) groups[route].sort();
|
|
135
|
-
|
|
136
|
-
const chatProfileModels = {};
|
|
137
|
-
for (const id of groups.chat) {
|
|
138
|
-
chatProfileModels[id] = { label: labelFor(id), model: modelReference(id, 'chat') };
|
|
139
|
-
}
|
|
140
|
-
for (const id of groups.responses) {
|
|
141
|
-
chatProfileModels[id] = { label: labelFor(id), model: modelReference(id, 'responses') };
|
|
142
|
-
}
|
|
143
|
-
const messagesProfileModels = {};
|
|
144
|
-
for (const id of groups.messages) {
|
|
145
|
-
messagesProfileModels[id] = { label: labelFor(id), model: modelReference(id, 'messages') };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const chatDefault = groups.chat[0] ?? groups.responses[0];
|
|
149
|
-
const profiles = {};
|
|
150
|
-
profiles['opencode-go'] = {
|
|
151
|
-
label: 'OpenCode Go',
|
|
152
|
-
...(chatDefault ? { defaultModel: chatDefault } : {}),
|
|
153
|
-
models: chatProfileModels,
|
|
154
|
-
apiKeyEnv: API_KEY_ENV,
|
|
155
|
-
apiBaseEnv: API_BASE_ENV,
|
|
156
|
-
};
|
|
157
|
-
if (messagesProfileModels && groups.messages.length) {
|
|
158
|
-
profiles['opencode-go-messages'] = {
|
|
159
|
-
label: 'OpenCode Go (Anthropic)',
|
|
160
|
-
defaultModel: groups.messages[0],
|
|
161
|
-
models: messagesProfileModels,
|
|
162
|
-
apiKeyEnv: API_KEY_ENV,
|
|
163
|
-
apiBaseEnv: MESSAGES_BASE_ENV,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const consoleSection = { defaultProfile: 'opencode-go', profiles };
|
|
168
|
-
|
|
169
|
-
if (guessed.length) {
|
|
170
|
-
process.stderr.write('Routed by prefix heuristic (verify before use):\n');
|
|
171
|
-
for (const line of guessed) process.stderr.write(` ${line}\n`);
|
|
172
|
-
}
|
|
173
|
-
process.stderr.write(
|
|
174
|
-
`Discovered ${ids.length} models (${groups.chat.length} chat, ${groups.responses.length} responses, ${groups.messages.length} messages).\n`,
|
|
175
|
-
);
|
|
176
|
-
|
|
177
69
|
if (writePath) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (error.code !== 'ENOENT') throw new Error(`Could not read ${writePath}: ${error.message}`);
|
|
70
|
+
const result = await runtime.mergeOpenCodeGoConsoleProfiles({ configPath: writePath, baseUrl, setDefault });
|
|
71
|
+
if (result.guessedRoutes.length) {
|
|
72
|
+
process.stderr.write('Routed by prefix heuristic (verify before use):\n');
|
|
73
|
+
for (const line of result.guessedRoutes) process.stderr.write(` ${line}\n`);
|
|
183
74
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const mergedProfiles = { ...(existingConsole.profiles ?? {}), ...profiles };
|
|
189
|
-
const defaultProfile =
|
|
190
|
-
existingConsole.defaultProfile && !setDefault ? existingConsole.defaultProfile : 'opencode-go';
|
|
191
|
-
root.console = { ...existingConsole, defaultProfile, profiles: mergedProfiles };
|
|
192
|
-
await writeFile(writePath, `${JSON.stringify(root, null, 2)}\n`);
|
|
193
|
-
process.stderr.write(`Merged profiles into ${writePath} (defaultProfile: ${defaultProfile}).\n`);
|
|
75
|
+
process.stderr.write(
|
|
76
|
+
`Merged ${result.counts.chat} chat, ${result.counts.responses} responses, and ${result.counts.messages} messages models into ${result.configPath}` +
|
|
77
|
+
` as ${result.mergedProfileIds.join(' + ')} (defaultProfile: ${result.defaultProfile}).\n`,
|
|
78
|
+
);
|
|
194
79
|
return;
|
|
195
80
|
}
|
|
196
81
|
|
|
82
|
+
const ids = await runtime.fetchOpenCodeGoModelIds(baseUrl);
|
|
83
|
+
const built = runtime.buildOpenCodeGoConsoleProfiles(ids, baseUrl);
|
|
84
|
+
if (built.guessedRoutes.length) {
|
|
85
|
+
process.stderr.write('Routed by prefix heuristic (verify before use):\n');
|
|
86
|
+
for (const line of built.guessedRoutes) process.stderr.write(` ${line}\n`);
|
|
87
|
+
}
|
|
197
88
|
process.stderr.write(
|
|
198
|
-
`
|
|
199
|
-
`
|
|
200
|
-
` export ${
|
|
201
|
-
` export ${
|
|
89
|
+
`Discovered ${ids.length} models (${built.counts.chat} chat, ${built.counts.responses} responses, ${built.counts.messages} messages).\n` +
|
|
90
|
+
`Export the key and base URLs, then add the JSON below under "console" in config.json:\n` +
|
|
91
|
+
` export ${OPENCODE_GO_API_KEY_ENV}='sk-…'\n` +
|
|
92
|
+
` export ${OPENCODE_GO_API_BASE_ENV}='${baseUrl}'\n` +
|
|
93
|
+
` export ${OPENCODE_GO_MESSAGES_API_BASE_ENV}='${messagesBase}'\n` +
|
|
202
94
|
`(or rerun with --write ~/.catence/config.json to merge into an existing config.)\n`,
|
|
203
95
|
);
|
|
204
|
-
process.stdout.write(`${JSON.stringify(
|
|
96
|
+
process.stdout.write(`${JSON.stringify({ defaultProfile: built.defaultProfile, profiles: built.profiles }, null, 2)}\n`);
|
|
205
97
|
}
|
|
206
98
|
|
|
207
99
|
await main().catch((error) => {
|