catence 0.2.0-beta.1 → 0.2.0-beta.10
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 +144 -2
- package/config.example.json +32 -20
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/progress.d.ts +23 -0
- package/dist/contracts/progress.js +48 -0
- package/dist/contracts/release.d.ts +1 -1
- package/dist/contracts/release.js +1 -1
- package/dist/core/logging.d.ts +16 -0
- package/dist/core/logging.js +68 -0
- package/dist/core/query/repository.d.ts +2 -0
- package/dist/core/query/repository.js +42 -0
- package/dist/core/runtime/catalog.js +17 -7
- package/dist/core/runtime/self-update.d.ts +44 -0
- package/dist/core/runtime/self-update.js +138 -0
- package/dist/elt/application/management.d.ts +2 -0
- package/dist/elt/application/management.js +194 -17
- package/dist/elt/ingestion/importer.d.ts +3 -2
- package/dist/elt/ingestion/importer.js +4 -3
- package/dist/elt/ingestion/providers/intervals.d.ts +3 -1
- package/dist/elt/ingestion/providers/intervals.js +4 -1
- package/dist/elt/ingestion/providers/strava/service.js +7 -1
- package/dist/elt/storage/database.d.ts +7 -2
- package/dist/elt/storage/database.js +81 -4
- package/dist/elt/storage/migrations.js +25 -1
- package/dist/interfaces/cli/commands.js +91 -1
- package/dist/interfaces/mcp/server.js +7 -0
- package/dist/runtime/index.d.ts +4 -1
- package/dist/runtime/index.js +2 -1
- package/docs/deployment.md +338 -0
- package/docs/docker-deployment.md +232 -0
- package/docs/llm-providers.md +467 -0
- package/package.json +5 -1
- package/python/catence/providers/garmin/cli.py +30 -9
- package/python/catence/providers/garmin/progress.py +85 -0
- package/python/catence/providers/garmin/worker.py +132 -5
- package/scripts/discover-opencode-go.mjs +210 -0
- package/scripts/discover-opencode-zen.mjs +218 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Discover OpenCode Go models and emit Catence Console profiles for them.
|
|
2
|
+
//
|
|
3
|
+
// OpenCode Go serves its catalog through three APIs: most models speak the
|
|
4
|
+
// OpenAI Chat Completions API, Grok and GPT-5.6 Luna speak the OpenAI
|
|
5
|
+
// Responses API, and MiniMax and Qwen speak the Anthropic Messages API. The
|
|
6
|
+
// public `GET /models` endpoint returns only model ids, not which API each
|
|
7
|
+
// model needs, so routing comes from the trusted table below and is intersected
|
|
8
|
+
// with the live list (the same "live discovery + trusted metadata" approach
|
|
9
|
+
// OpenClaw uses).
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// node scripts/discover-opencode-go.mjs [--base-url <url>] [--write <config.json>] [--set-default]
|
|
13
|
+
//
|
|
14
|
+
// Flags:
|
|
15
|
+
// --base-url <url> Root of the OpenCode Go API (default https://opencode.ai/zen/go/v1).
|
|
16
|
+
// --write <path> Merge the two profiles into an existing config.json instead of printing to stdout.
|
|
17
|
+
// --set-default With --write, set console.defaultProfile to opencode-go even when one already exists.
|
|
18
|
+
|
|
19
|
+
import process from 'node:process';
|
|
20
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
21
|
+
|
|
22
|
+
const DEFAULT_BASE_URL = 'https://opencode.ai/zen/go/v1';
|
|
23
|
+
const API_KEY_ENV = 'OPENCODE_GO_API_KEY';
|
|
24
|
+
const API_BASE_ENV = 'OPENCODE_GO_API_BASE';
|
|
25
|
+
const MESSAGES_BASE_ENV = 'OPENCODE_GO_MESSAGES_API_BASE';
|
|
26
|
+
|
|
27
|
+
// LiteLLM appends a fixed suffix to the profile base URL per provider, so the
|
|
28
|
+
// three APIs need different prefixes and two base URLs:
|
|
29
|
+
// openai/<id> -> {base}/chat/completions
|
|
30
|
+
// openai/responses/<id> -> {base}/responses
|
|
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 };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function modelReference(id, route) {
|
|
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
|
+
}
|
|
76
|
+
|
|
77
|
+
function usage() {
|
|
78
|
+
process.stderr.write(
|
|
79
|
+
'Usage: node scripts/discover-opencode-go.mjs [--base-url <url>] [--write <config.json>] [--set-default]\n',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function main() {
|
|
84
|
+
const args = process.argv.slice(2);
|
|
85
|
+
let baseUrl = DEFAULT_BASE_URL;
|
|
86
|
+
let writePath = null;
|
|
87
|
+
let setDefault = false;
|
|
88
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
89
|
+
const arg = args[i];
|
|
90
|
+
if (arg === '--base-url') {
|
|
91
|
+
baseUrl = args[i + 1];
|
|
92
|
+
if (!baseUrl) throw new Error('--base-url requires a URL.');
|
|
93
|
+
i += 1;
|
|
94
|
+
} else if (arg === '--write') {
|
|
95
|
+
writePath = args[i + 1];
|
|
96
|
+
if (!writePath) throw new Error('--write requires a config.json path.');
|
|
97
|
+
i += 1;
|
|
98
|
+
} else if (arg === '--set-default') {
|
|
99
|
+
setDefault = true;
|
|
100
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
101
|
+
usage();
|
|
102
|
+
return;
|
|
103
|
+
} else {
|
|
104
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!baseUrl.endsWith('/v1')) {
|
|
109
|
+
process.stderr.write(`Warning: --base-url ${baseUrl} does not end in /v1; chat and responses URLs may be wrong.\n`);
|
|
110
|
+
}
|
|
111
|
+
const modelsUrl = `${baseUrl.replace(/\/$/, '')}/models`;
|
|
112
|
+
const messagesBase = baseUrl.replace(/\/v1\/?$/, '');
|
|
113
|
+
|
|
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
|
+
if (writePath) {
|
|
178
|
+
let root = {};
|
|
179
|
+
try {
|
|
180
|
+
root = JSON.parse(await readFile(writePath, 'utf8'));
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (error.code !== 'ENOENT') throw new Error(`Could not read ${writePath}: ${error.message}`);
|
|
183
|
+
}
|
|
184
|
+
if (typeof root !== 'object' || root === null || Array.isArray(root)) {
|
|
185
|
+
throw new Error(`${writePath} must contain a JSON object.`);
|
|
186
|
+
}
|
|
187
|
+
const existingConsole = typeof root.console === 'object' && root.console !== null ? root.console : {};
|
|
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`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
process.stderr.write(
|
|
198
|
+
`Export the key and base URLs, then add the JSON below under "console" in config.json:\n` +
|
|
199
|
+
` export ${API_KEY_ENV}='sk-…'\n` +
|
|
200
|
+
` export ${API_BASE_ENV}='${baseUrl}'\n` +
|
|
201
|
+
` export ${MESSAGES_BASE_ENV}='${messagesBase}'\n` +
|
|
202
|
+
`(or rerun with --write ~/.catence/config.json to merge into an existing config.)\n`,
|
|
203
|
+
);
|
|
204
|
+
process.stdout.write(`${JSON.stringify(consoleSection, null, 2)}\n`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await main().catch((error) => {
|
|
208
|
+
process.stderr.write(`Error: ${error.message}\n`);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// Discover OpenCode Zen models and emit Catence Console profiles for them.
|
|
2
|
+
//
|
|
3
|
+
// OpenCode Zen serves its catalog through `GET /v1/models` at
|
|
4
|
+
// `https://opencode.ai/zen/v1`. Models are split between the chat/completions
|
|
5
|
+
// endpoint and the responses endpoint. Routing is determined by model ID prefix:
|
|
6
|
+
// GPT-5.x models use the responses API; everything else uses chat/completions.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// node scripts/discover-opencode-zen.mjs [--base-url <url>] [--write <config.json>] [--set-default] [--free-only]
|
|
10
|
+
//
|
|
11
|
+
// Flags:
|
|
12
|
+
// --base-url <url> Root of the OpenCode Zen API (default https://opencode.ai/zen/v1).
|
|
13
|
+
// --write <path> Merge the two profiles into an existing config.json instead of printing to stdout.
|
|
14
|
+
// --set-default With --write, set console.defaultProfile to opencode-zen even when one already exists.
|
|
15
|
+
// --free-only Only include free models (those with -free suffix or known free model IDs).
|
|
16
|
+
|
|
17
|
+
import process from 'node:process';
|
|
18
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
19
|
+
|
|
20
|
+
const DEFAULT_BASE_URL = 'https://opencode.ai/zen/v1';
|
|
21
|
+
const API_KEY_ENV = 'OPENCODE_API_KEY';
|
|
22
|
+
const API_BASE_ENV = 'OPENCODE_ZEN_API_BASE';
|
|
23
|
+
const RESPONSES_BASE_ENV = 'OPENCODE_ZEN_RESPONSES_API_BASE';
|
|
24
|
+
|
|
25
|
+
// Models that use the OpenAI Responses API (gpt-5.x series)
|
|
26
|
+
const RESPONSES_PREFIXES = ['gpt-5', 'gpt-5.'];
|
|
27
|
+
|
|
28
|
+
// Known free model IDs (in case the API doesn't mark them explicitly)
|
|
29
|
+
const KNOWN_FREE_MODELS = new Set([
|
|
30
|
+
'deepseek-v4-flash-free',
|
|
31
|
+
'mimo-v2.5-free',
|
|
32
|
+
'nemotron-3-ultra-free',
|
|
33
|
+
'nemotron-3.5-lightning-free',
|
|
34
|
+
'hy3-free',
|
|
35
|
+
'laguna-s-2.1-free',
|
|
36
|
+
'big-pickle',
|
|
37
|
+
'north-mini-code-free',
|
|
38
|
+
'longcat-2.0-free',
|
|
39
|
+
'ling-3.0-flash-free',
|
|
40
|
+
'ling-3.0-tiny-free',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const BRAND_NAMES = new Map([
|
|
44
|
+
['deepseek', 'DeepSeek'],
|
|
45
|
+
['gpt', 'GPT'],
|
|
46
|
+
['mimo', 'MiMo'],
|
|
47
|
+
['nemotron', 'Nemotron'],
|
|
48
|
+
['hy3', 'Hy3'],
|
|
49
|
+
['laguna', 'Laguna'],
|
|
50
|
+
['big', 'Big'],
|
|
51
|
+
['pickle', 'Pickle'],
|
|
52
|
+
['north', 'North'],
|
|
53
|
+
['mini', 'Mini'],
|
|
54
|
+
['code', 'Code'],
|
|
55
|
+
['longcat', 'LongCat'],
|
|
56
|
+
['ling', 'Ling'],
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
function isFreeModel(id) {
|
|
60
|
+
return id.endsWith('-free') || KNOWN_FREE_MODELS.has(id);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function classify(id) {
|
|
64
|
+
for (const prefix of RESPONSES_PREFIXES) {
|
|
65
|
+
if (id.startsWith(prefix)) return { route: 'responses', guessed: false };
|
|
66
|
+
}
|
|
67
|
+
return { route: 'chat', guessed: false };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function modelReference(id, route) {
|
|
71
|
+
if (route === 'responses') return `openai/responses/${id}`;
|
|
72
|
+
return `openai/${id}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function labelFor(id) {
|
|
76
|
+
return id
|
|
77
|
+
.split('-')
|
|
78
|
+
.map((segment) => BRAND_NAMES.get(segment) ?? segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
79
|
+
.join(' ');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function usage() {
|
|
83
|
+
process.stderr.write(
|
|
84
|
+
'Usage: node scripts/discover-opencode-zen.mjs [--base-url <url>] [--write <config.json>] [--set-default] [--free-only]\n',
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function main() {
|
|
89
|
+
const args = process.argv.slice(2);
|
|
90
|
+
let baseUrl = DEFAULT_BASE_URL;
|
|
91
|
+
let writePath = null;
|
|
92
|
+
let setDefault = false;
|
|
93
|
+
let freeOnly = false;
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
96
|
+
const arg = args[i];
|
|
97
|
+
if (arg === '--base-url') {
|
|
98
|
+
baseUrl = args[i + 1];
|
|
99
|
+
if (!baseUrl) throw new Error('--base-url requires a URL.');
|
|
100
|
+
i += 1;
|
|
101
|
+
} else if (arg === '--write') {
|
|
102
|
+
writePath = args[i + 1];
|
|
103
|
+
if (!writePath) throw new Error('--write requires a config.json path.');
|
|
104
|
+
i += 1;
|
|
105
|
+
} else if (arg === '--set-default') {
|
|
106
|
+
setDefault = true;
|
|
107
|
+
} else if (arg === '--free-only') {
|
|
108
|
+
freeOnly = true;
|
|
109
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
110
|
+
usage();
|
|
111
|
+
return;
|
|
112
|
+
} else {
|
|
113
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!baseUrl.endsWith('/v1')) {
|
|
118
|
+
process.stderr.write(`Warning: --base-url ${baseUrl} does not end in /v1; chat and responses URLs may be wrong.\n`);
|
|
119
|
+
}
|
|
120
|
+
const modelsUrl = `${baseUrl.replace(/\/$/, '')}/models`;
|
|
121
|
+
|
|
122
|
+
let response;
|
|
123
|
+
try {
|
|
124
|
+
response = await fetch(modelsUrl, { headers: { accept: 'application/json' } });
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw new Error(`Could not reach ${modelsUrl}: ${error.message}`);
|
|
127
|
+
}
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
throw new Error(`GET ${modelsUrl} returned HTTP ${response.status}.`);
|
|
130
|
+
}
|
|
131
|
+
const payload = await response.json();
|
|
132
|
+
const ids = (payload.data ?? [])
|
|
133
|
+
.map((entry) => entry.id)
|
|
134
|
+
.filter((id) => typeof id === 'string' && id);
|
|
135
|
+
|
|
136
|
+
const filteredIds = freeOnly ? ids.filter(isFreeModel) : ids;
|
|
137
|
+
|
|
138
|
+
const groups = { chat: [], responses: [] };
|
|
139
|
+
for (const id of filteredIds) {
|
|
140
|
+
const { route } = classify(id);
|
|
141
|
+
if (!(route in groups)) continue;
|
|
142
|
+
groups[route].push(id);
|
|
143
|
+
}
|
|
144
|
+
for (const route of Object.keys(groups)) groups[route].sort();
|
|
145
|
+
|
|
146
|
+
const chatProfileModels = {};
|
|
147
|
+
for (const id of groups.chat) {
|
|
148
|
+
chatProfileModels[id] = { label: labelFor(id), model: modelReference(id, 'chat') };
|
|
149
|
+
}
|
|
150
|
+
const responsesProfileModels = {};
|
|
151
|
+
for (const id of groups.responses) {
|
|
152
|
+
responsesProfileModels[id] = { label: labelFor(id), model: modelReference(id, 'responses') };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const chatDefault = groups.chat[0];
|
|
156
|
+
const responsesDefault = groups.responses[0];
|
|
157
|
+
|
|
158
|
+
const profiles = {};
|
|
159
|
+
if (groups.chat.length) {
|
|
160
|
+
profiles['opencode-zen'] = {
|
|
161
|
+
label: 'OpenCode Zen',
|
|
162
|
+
...(chatDefault ? { defaultModel: chatDefault } : {}),
|
|
163
|
+
models: chatProfileModels,
|
|
164
|
+
apiKeyEnv: API_KEY_ENV,
|
|
165
|
+
apiBaseEnv: API_BASE_ENV,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (groups.responses.length) {
|
|
169
|
+
profiles['opencode-zen-responses'] = {
|
|
170
|
+
label: 'OpenCode Zen (Responses)',
|
|
171
|
+
...(responsesDefault ? { defaultModel: responsesDefault } : {}),
|
|
172
|
+
models: responsesProfileModels,
|
|
173
|
+
apiKeyEnv: API_KEY_ENV,
|
|
174
|
+
apiBaseEnv: RESPONSES_BASE_ENV,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const consoleSection = { defaultProfile: 'opencode-zen', profiles };
|
|
179
|
+
|
|
180
|
+
process.stderr.write(
|
|
181
|
+
`Discovered ${ids.length} models (${groups.chat.length} chat, ${groups.responses.length} responses).` +
|
|
182
|
+
(freeOnly ? ` Filtered to ${filteredIds.length} free models.` : '') + '\n',
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (writePath) {
|
|
186
|
+
let root = {};
|
|
187
|
+
try {
|
|
188
|
+
root = JSON.parse(await readFile(writePath, 'utf8'));
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error.code !== 'ENOENT') throw new Error(`Could not read ${writePath}: ${error.message}`);
|
|
191
|
+
}
|
|
192
|
+
if (typeof root !== 'object' || root === null || Array.isArray(root)) {
|
|
193
|
+
throw new Error(`${writePath} must contain a JSON object.`);
|
|
194
|
+
}
|
|
195
|
+
const existingConsole = typeof root.console === 'object' && root.console !== null ? root.console : {};
|
|
196
|
+
const mergedProfiles = { ...(existingConsole.profiles ?? {}), ...profiles };
|
|
197
|
+
const defaultProfile =
|
|
198
|
+
existingConsole.defaultProfile && !setDefault ? existingConsole.defaultProfile : 'opencode-zen';
|
|
199
|
+
root.console = { ...existingConsole, defaultProfile, profiles: mergedProfiles };
|
|
200
|
+
await writeFile(writePath, `${JSON.stringify(root, null, 2)}\n`);
|
|
201
|
+
process.stderr.write(`Merged profiles into ${writePath} (defaultProfile: ${defaultProfile}).\n`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
process.stderr.write(
|
|
206
|
+
`Export the key and base URLs, then add the JSON below under "console" in config.json:\n` +
|
|
207
|
+
` export ${API_KEY_ENV}='sk-…'\n` +
|
|
208
|
+
` export ${API_BASE_ENV}='${baseUrl}'\n` +
|
|
209
|
+
` export ${RESPONSES_BASE_ENV}='${baseUrl}'\n` +
|
|
210
|
+
`(or rerun with --write ~/.catence/config.json to merge into an existing config.)\n`,
|
|
211
|
+
);
|
|
212
|
+
process.stdout.write(`${JSON.stringify(consoleSection, null, 2)}\n`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await main().catch((error) => {
|
|
216
|
+
process.stderr.write(`Error: ${error.message}\n`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
});
|