bb-plugin-svaib-workplace 0.5.5 → 0.5.6
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 +16 -1
- package/dist/app.meta.json +1 -1
- package/dist/host.js +28 -23
- package/dist/host.js.map +3 -3
- package/dist/host.meta.json +2 -2
- package/dist/server.js +37 -37
- package/dist/server.js.map +3 -3
- package/dist/server.meta.json +1 -1
- package/experiments/account-prototype/browser-host.ts +11 -3
- package/kimi-contract.ts +4 -2
- package/kimi-diagnostics.ts +42 -0
- package/kimi-host.ts +24 -11
- package/kimi-setup.ts +93 -19
- package/npm-release.json +3 -3
- package/package.json +6 -6
- package/scripts/setup-kimi-cc.d.mts +6 -3
- package/scripts/setup-kimi-cc.mjs +232 -25
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Internal provisioning module for the BB Kimi card. Never put keys in BB settings.
|
|
2
|
-
import { access, chmod, lstat, mkdir, readFile, writeFile, rename } from 'node:fs/promises';
|
|
2
|
+
import { access, chmod, lstat, mkdir, readFile, writeFile, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import { homedir, userInfo } from 'node:os';
|
|
4
5
|
import { dirname, join, relative, resolve, sep, win32 } from 'node:path';
|
|
5
6
|
import { spawn } from 'node:child_process';
|
|
@@ -11,7 +12,8 @@ export const MODELS = [
|
|
|
11
12
|
['k3', 'Kimi K3'], ['k3-256k', 'Kimi K3 256K'],
|
|
12
13
|
['kimi-for-coding', 'Kimi K2.8 Preview'], ['kimi-for-coding-highspeed', 'Kimi K2.7 Code Highspeed (больше квоты)'],
|
|
13
14
|
];
|
|
14
|
-
|
|
15
|
+
// Keep the previous generated form recognizable for owned-profile migration.
|
|
16
|
+
function catalogSettings(legacy = false) {
|
|
15
17
|
if (legacy) return { env: legacyEnv() };
|
|
16
18
|
return { model: MODEL, effortLevel: 'high', availableModels: MODELS.map(([id]) => id),
|
|
17
19
|
modelPicker: { replaceBuiltInOptions: true, options: MODELS.map(([model, label]) => ({ model, label })) },
|
|
@@ -21,6 +23,153 @@ function settings(legacy = false) {
|
|
|
21
23
|
CLAUDE_CODE_SUBAGENT_MODEL: MODEL,
|
|
22
24
|
CLAUDE_CODE_MODEL_CAPABILITIES: 'k3*=effort,max_effort,-xhigh_effort;kimi-for-coding=effort,max_effort,-xhigh_effort;kimi-for-coding-highspeed=-effort,-max_effort,-xhigh_effort' } };
|
|
23
25
|
}
|
|
26
|
+
|
|
27
|
+
const SHORT_WINDOW = 262144;
|
|
28
|
+
// Claude Code's [1m] switch means 1,000,000, below Kimi's 1,048,576 limit.
|
|
29
|
+
const LONG_WINDOW = 1000000;
|
|
30
|
+
const safePolicy = () => ({ shortWindow: SHORT_WINDOW, longModels: [] });
|
|
31
|
+
|
|
32
|
+
/** No model request, redirects, response/error logging, or unbounded waiting. */
|
|
33
|
+
async function fetchCatalog(apiKey, { fetchImpl = fetch, signal, log = () => {} } = {}) {
|
|
34
|
+
const started = Date.now();
|
|
35
|
+
const timeout = AbortSignal.timeout(8000);
|
|
36
|
+
log('catalog-fetch', 'start');
|
|
37
|
+
try {
|
|
38
|
+
signal?.throwIfAborted();
|
|
39
|
+
const response = await fetchImpl('https://api.kimi.ai/coding/v1/models', {
|
|
40
|
+
headers: { Authorization: `Bearer ${apiKey}`, 'User-Agent': 'svaib-workplace/kimi-setup' },
|
|
41
|
+
redirect: 'error', signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
log('catalog-fetch', 'fallback', { reason: 'http-error', httpStatus: response.status, elapsedMs: Date.now() - started });
|
|
45
|
+
await response.body?.cancel(); return null;
|
|
46
|
+
}
|
|
47
|
+
// The catalog is small. Bound the body as well as the request duration.
|
|
48
|
+
let text = '';
|
|
49
|
+
let bytes = 0;
|
|
50
|
+
const decoder = new TextDecoder();
|
|
51
|
+
for await (const chunk of response.body) {
|
|
52
|
+
bytes += chunk.byteLength;
|
|
53
|
+
if (bytes > 262144) { log('catalog-fetch', 'fallback', { reason: 'oversized', elapsedMs: Date.now() - started }); return null; }
|
|
54
|
+
text += decoder.decode(chunk, { stream: true });
|
|
55
|
+
}
|
|
56
|
+
text += decoder.decode();
|
|
57
|
+
const body = JSON.parse(text);
|
|
58
|
+
log('catalog-fetch', 'success', { byteCount: bytes, elapsedMs: Date.now() - started });
|
|
59
|
+
return body;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
log('catalog-fetch', signal?.aborted ? 'failure' : 'fallback', {
|
|
62
|
+
reason: signal?.aborted ? 'cancelled' : timeout.aborted ? 'timeout' : error instanceof SyntaxError ? 'invalid-json' : 'network-error', elapsedMs: Date.now() - started,
|
|
63
|
+
});
|
|
64
|
+
signal?.throwIfAborted();
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const modelIdPattern = /^[a-z0-9][a-z0-9._/-]{0,127}$/;
|
|
70
|
+
const reservedModels = new Set(['default', 'opus', 'sonnet', 'haiku', 'best', 'opusplan', '__proto__', 'constructor', 'prototype']);
|
|
71
|
+
function validId(id) { return typeof id === 'string' && modelIdPattern.test(id) && !reservedModels.has(id); }
|
|
72
|
+
function seedCatalog() {
|
|
73
|
+
return { defaultModel: MODEL, models: MODELS.map(([id, label]) => ({ id, label, context: SHORT_WINDOW,
|
|
74
|
+
effort: id !== 'kimi-for-coding-highspeed', maxEffort: id !== 'kimi-for-coding-highspeed', retired: false })) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** API additions need no local model registry; unknown capabilities stay disabled. */
|
|
78
|
+
export function parseCatalog(body) {
|
|
79
|
+
if (!Array.isArray(body?.data) || !body.data.length || body.data.length > 128) return null;
|
|
80
|
+
const models = [];
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
for (const row of body.data) {
|
|
83
|
+
if (!validId(row?.id) || seen.has(row.id)) return null;
|
|
84
|
+
seen.add(row.id);
|
|
85
|
+
const label = typeof row.display_name === 'string' ? row.display_name.replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, 120) : row.id;
|
|
86
|
+
const fixed256 = row.id === 'k3-256k' || row.id === 'kimi-for-coding-highspeed';
|
|
87
|
+
const limits = [row.context_length, row.limit?.context].filter(value => Number.isSafeInteger(value) && value > 0);
|
|
88
|
+
const context = limits.length ? Math.min(...limits, fixed256 ? SHORT_WINDOW : LONG_WINDOW) : SHORT_WINDOW;
|
|
89
|
+
const levels = row.think_efforts?.support === false ? [] : row.think_efforts?.valid_efforts ?? row.reasoning?.effort?.valid;
|
|
90
|
+
// CLI represents low/medium(high)/high and optionally max, not arbitrary enums.
|
|
91
|
+
const effort = row.supports_reasoning !== false && Array.isArray(levels) && levels.includes('low') && levels.includes('high');
|
|
92
|
+
models.push({ id: row.id, label: label || row.id, context, effort, maxEffort: effort && levels.includes('max'), retired: false });
|
|
93
|
+
}
|
|
94
|
+
return models;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function validCatalog(value) {
|
|
98
|
+
return value && Array.isArray(value.models) && value.models.length > 0 && value.models.length <= 129
|
|
99
|
+
&& value.models.some(model => model.id === value.defaultModel)
|
|
100
|
+
&& new Set(value.models.map(model => model.id)).size === value.models.length
|
|
101
|
+
&& value.models.every(model => validId(model.id) && typeof model.label === 'string' && model.label.length <= 120
|
|
102
|
+
&& Number.isSafeInteger(model.context) && model.context > 0 && model.context <= LONG_WINDOW
|
|
103
|
+
&& typeof model.effort === 'boolean' && typeof model.maxEffort === 'boolean' && typeof model.retired === 'boolean');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function discoverCatalog(apiKey, previous = seedCatalog(), options = {}) {
|
|
107
|
+
const body = await fetchCatalog(apiKey, options);
|
|
108
|
+
const fresh = parseCatalog(body);
|
|
109
|
+
const fallback = model => ({ ...model, context: Math.min(SHORT_WINDOW, model.context) });
|
|
110
|
+
if (!fresh) {
|
|
111
|
+
options.log?.('catalog-normalize', 'fallback', { reason: body === null ? 'cached' : 'invalid-catalog', modelCount: previous.models.length, longModelCount: 0 });
|
|
112
|
+
return { ...previous, models: previous.models.map(fallback) };
|
|
113
|
+
}
|
|
114
|
+
options.log?.('catalog-normalize', 'success', { modelCount: fresh.length, longModelCount: fresh.filter(model => model.context >= LONG_WINDOW).length });
|
|
115
|
+
if (options.initial) return { defaultModel: fresh.some(model => model.id === previous.defaultModel) ? previous.defaultModel : fresh[0].id, models: fresh };
|
|
116
|
+
const selected = previous.models.find(model => model.id === previous.defaultModel);
|
|
117
|
+
// A removed default stays visible as unavailable; never silently select a new model.
|
|
118
|
+
if (selected && !fresh.some(model => model.id === selected.id)) fresh.push({ ...fallback(selected), retired: true });
|
|
119
|
+
return { defaultModel: previous.defaultModel, models: fresh };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function dynamicSettings(catalog) {
|
|
123
|
+
const base = catalogSettings();
|
|
124
|
+
const shortWindow = Math.min(SHORT_WINDOW, ...catalog.models.filter(model => model.context < LONG_WINDOW).map(model => model.context));
|
|
125
|
+
const modelId = id => catalog.models.find(model => model.id === id)?.context >= LONG_WINDOW ? `${id}[1m]` : id;
|
|
126
|
+
const window = model => model.context >= LONG_WINDOW ? LONG_WINDOW : shortWindow;
|
|
127
|
+
const defaultModel = modelId(catalog.defaultModel);
|
|
128
|
+
return { ...base, model: defaultModel, availableModels: catalog.models.map(model => modelId(model.id)), availableModelsMatch: 'exact',
|
|
129
|
+
modelPicker: { replaceBuiltInOptions: true, options: catalog.models.map(model => ({ model: modelId(model.id),
|
|
130
|
+
label: `${model.label} (${window(model) === LONG_WINDOW ? '1M' : Math.floor(window(model) / 1024) + 'K'})`,
|
|
131
|
+
...(model.retired ? { description: 'Модель больше не доступна в каталоге Kimi. Выбери другую модель для нового разговора.' } : {}),
|
|
132
|
+
})) },
|
|
133
|
+
modelSettings: Object.fromEntries(catalog.models.map(model => [model.id, { autoCompactWindow: Math.max(100000, window(model)) }])),
|
|
134
|
+
env: { ...base.env, ANTHROPIC_MODEL: defaultModel, ANTHROPIC_DEFAULT_FABLE_MODEL: defaultModel,
|
|
135
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: defaultModel, ANTHROPIC_DEFAULT_SONNET_MODEL: defaultModel,
|
|
136
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: defaultModel, CLAUDE_CODE_SUBAGENT_MODEL: defaultModel,
|
|
137
|
+
CLAUDE_CODE_MAX_CONTEXT_TOKENS: String(shortWindow),
|
|
138
|
+
CLAUDE_CODE_MODEL_CAPABILITIES: catalog.models.map(model => `${model.id}=${model.effort ? '' : '-'}effort,${model.maxEffort ? '' : '-'}max_effort,-xhigh_effort`).join(';'),
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function contextSettingsV2(policy = safePolicy()) {
|
|
144
|
+
const base = catalogSettings();
|
|
145
|
+
const modelId = id => policy.longModels.includes(id) ? `${id}[1m]` : id;
|
|
146
|
+
const window = id => policy.longModels.includes(id) ? LONG_WINDOW : policy.shortWindow;
|
|
147
|
+
return { ...base, model: modelId(MODEL), availableModels: MODELS.map(([id]) => modelId(id)),
|
|
148
|
+
availableModelsMatch: 'exact',
|
|
149
|
+
modelPicker: { replaceBuiltInOptions: true, options: MODELS.map(([id, label]) => ({
|
|
150
|
+
model: modelId(id), label: `${label} (${window(id) === LONG_WINDOW ? '1M' : Math.floor(window(id) / 1024) + 'K'})`,
|
|
151
|
+
})) },
|
|
152
|
+
modelSettings: Object.fromEntries(MODELS.map(([id]) => [id, { autoCompactWindow: Math.max(100000, window(id)) }])),
|
|
153
|
+
env: { ...base.env,
|
|
154
|
+
ANTHROPIC_MODEL: modelId(MODEL), ANTHROPIC_DEFAULT_FABLE_MODEL: modelId(MODEL),
|
|
155
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: modelId(MODEL), ANTHROPIC_DEFAULT_SONNET_MODEL: modelId(MODEL),
|
|
156
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: modelId(MODEL), CLAUDE_CODE_SUBAGENT_MODEL: modelId(MODEL),
|
|
157
|
+
CLAUDE_CODE_MAX_CONTEXT_TOKENS: String(policy.shortWindow),
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Reconstruct only a byte-for-byte generated profile. Any user edit is preserved.
|
|
163
|
+
function generatedSettings(text) {
|
|
164
|
+
if ([catalogSettings(), catalogSettings(true)].some(value => text === JSON.stringify(value, null, 2))) return true;
|
|
165
|
+
try {
|
|
166
|
+
const value = JSON.parse(text);
|
|
167
|
+
const shortWindow = Number(value.env?.CLAUDE_CODE_MAX_CONTEXT_TOKENS);
|
|
168
|
+
if (!Number.isSafeInteger(shortWindow) || shortWindow <= 0 || shortWindow > SHORT_WINDOW) return false;
|
|
169
|
+
const longModels = MODELS.filter(([id]) => value.availableModels?.includes(`${id}[1m]`)).map(([id]) => id);
|
|
170
|
+
return text === JSON.stringify(contextSettingsV2({ shortWindow, longModels }), null, 2);
|
|
171
|
+
} catch { return false; }
|
|
172
|
+
}
|
|
24
173
|
function legacyEnv() {
|
|
25
174
|
const MODEL = 'k3.1-pro[1m]';
|
|
26
175
|
return { ANTHROPIC_BASE_URL: 'https://api.kimi.ai/coding/', ANTHROPIC_MODEL: MODEL,
|
|
@@ -68,7 +217,7 @@ export async function npmCliPath(nodeExecutable = process.execPath) {
|
|
|
68
217
|
throw new Error('npm не найден рядом с Node.js. Установи Node.js 22+ официальным установщиком с npm.');
|
|
69
218
|
}
|
|
70
219
|
|
|
71
|
-
export async function provision({ root, apiKey, install = command, npmCli = null, nodeExecutable = process.execPath }) {
|
|
220
|
+
export async function provision({ root, apiKey, install = command, npmCli = null, nodeExecutable = process.execPath, fetchImpl = fetch, signal, log = () => {} }) {
|
|
72
221
|
if (!/^sk-kimi-[A-Za-z0-9_-]+$/.test(apiKey)) throw new Error('Ожидается ключ Kimi Code вида sk-kimi-…; значение не сохранено.');
|
|
73
222
|
root = resolve(root);
|
|
74
223
|
// A separate directory and exclusive writes preserve every existing profile.
|
|
@@ -78,14 +227,16 @@ export async function provision({ root, apiKey, install = command, npmCli = null
|
|
|
78
227
|
const configRoot = join(root, 'claude');
|
|
79
228
|
await privateDirectory(configRoot);
|
|
80
229
|
await writeFile(join(root, 'credentials.json'), JSON.stringify({ apiKey }), { mode: 0o600, flag: 'wx' });
|
|
81
|
-
|
|
230
|
+
const initialCatalog = seedCatalog();
|
|
231
|
+
await writeFile(join(configRoot, 'settings.json'), JSON.stringify(dynamicSettings(initialCatalog), null, 2), { mode: 0o600, flag: 'wx' });
|
|
82
232
|
await writeFile(join(configRoot, '.claude.json'), JSON.stringify({ hasCompletedOnboarding: true, penguinModeOrgEnabled: true }), { mode: 0o600, flag: 'wx' });
|
|
83
|
-
await writeFile(join(root, 'setup-owner.json'), JSON.stringify({ schema: 1, adapter: ADAPTER, nodeExecutable }), { mode: 0o600, flag: 'wx' });
|
|
84
|
-
|
|
233
|
+
await writeFile(join(root, 'setup-owner.json'), JSON.stringify({ schema: 1, adapter: ADAPTER, nodeExecutable, catalogs: [initialCatalog] }), { mode: 0o600, flag: 'wx' });
|
|
234
|
+
await finishProvision({ root, install, npmCli, nodeExecutable, log });
|
|
235
|
+
return upgradeProvision(root, { fetchImpl, signal, initial: true, log });
|
|
85
236
|
}
|
|
86
237
|
|
|
87
238
|
// Only a profile created by this helper can resume a failed dependency download.
|
|
88
|
-
export async function resumeProvision({ root, install = command, npmCli = null, nodeExecutable = null }) {
|
|
239
|
+
export async function resumeProvision({ root, install = command, npmCli = null, nodeExecutable = null, fetchImpl = fetch, signal, log = () => {} }) {
|
|
89
240
|
for (const name of ['', 'setup-owner.json', 'credentials.json', 'claude', 'claude/settings.json']) {
|
|
90
241
|
if ((await lstat(join(root, name))).isSymbolicLink()) throw new Error('Профиль настройки изменён. Ничего не перезаписано.');
|
|
91
242
|
}
|
|
@@ -97,14 +248,18 @@ export async function resumeProvision({ root, install = command, npmCli = null,
|
|
|
97
248
|
await writeFile(temporary, JSON.stringify(owner), { flag: 'wx', mode: 0o600 });
|
|
98
249
|
await rename(temporary, join(root, 'setup-owner.json'));
|
|
99
250
|
}
|
|
100
|
-
|
|
101
|
-
return upgradeProvision(root
|
|
251
|
+
await finishProvision({ root, install, npmCli, nodeExecutable: owner.nodeExecutable, log });
|
|
252
|
+
return upgradeProvision(root, { fetchImpl, signal, initial: true, log });
|
|
102
253
|
}
|
|
103
254
|
|
|
104
|
-
async function finishProvision({ root, install, npmCli, nodeExecutable }) {
|
|
255
|
+
async function finishProvision({ root, install, npmCli, nodeExecutable, log }) {
|
|
105
256
|
const runtime = join(root, 'runtime');
|
|
106
|
-
|
|
107
|
-
|
|
257
|
+
const started = Date.now(); log('adapter-install', 'start');
|
|
258
|
+
try {
|
|
259
|
+
await install(nodeExecutable,
|
|
260
|
+
[npmCli ?? await npmCliPath(nodeExecutable), 'install', '--prefix', runtime, '--no-audit', '--no-fund', ADAPTER]);
|
|
261
|
+
log('adapter-install', 'success', { elapsedMs: Date.now() - started });
|
|
262
|
+
} catch (error) { log('adapter-install', 'failure', { reason: 'failed', elapsedMs: Date.now() - started }); throw error; }
|
|
108
263
|
const launcher = launcherSource(root);
|
|
109
264
|
await sameOrNew(join(root, 'launch.mjs'), launcher);
|
|
110
265
|
const agent = registration(root, nodeExecutable);
|
|
@@ -119,7 +274,7 @@ async function sameOrNew(path, content) {
|
|
|
119
274
|
}
|
|
120
275
|
}
|
|
121
276
|
|
|
122
|
-
function launcherSource(root, legacy = false) {
|
|
277
|
+
function launcherSource(root, legacy = false, previous = false) {
|
|
123
278
|
const adapterDir = join(root, 'runtime', 'node_modules', '@agentclientprotocol', 'claude-agent-acp', 'dist');
|
|
124
279
|
return `import { readFileSync } from 'node:fs';
|
|
125
280
|
import { dirname, join } from 'node:path';
|
|
@@ -138,13 +293,17 @@ function run(command, args, env) {
|
|
|
138
293
|
});
|
|
139
294
|
}
|
|
140
295
|
if (process.argv.includes('--list-models')) {
|
|
141
|
-
${legacy ? "console.log('k3.1-pro[1m] - Kimi K3.1 Pro (1M)');" : MODELS.map(([id, label]) => `console.log(${JSON.stringify(id + ' - ' + label)});`).join('\n ')}
|
|
296
|
+
${legacy ? "console.log('k3.1-pro[1m] - Kimi K3.1 Pro (1M)');" : previous ? MODELS.map(([id, label]) => `console.log(${JSON.stringify(id + ' - ' + label)});`).join('\n ') : "const settings = JSON.parse(readFileSync(join(root, 'claude', 'settings.json'), 'utf8'));\n for (const model of settings.modelPicker.options) console.log(model.model + ' - ' + model.label);"}
|
|
142
297
|
} else {
|
|
143
298
|
const apiKey = JSON.parse(readFileSync(join(root, 'credentials.json'), 'utf8')).apiKey;
|
|
144
299
|
const settings = JSON.parse(readFileSync(join(root, 'claude', 'settings.json'), 'utf8'));
|
|
145
300
|
const env = { ...process.env, ...settings.env, ANTHROPIC_API_KEY: apiKey, CLAUDE_CONFIG_DIR: join(root, 'claude') };
|
|
146
301
|
${legacy ? '' : "for (const name of ['ANTHROPIC_CUSTOM_MODEL_OPTION', 'ANTHROPIC_CUSTOM_MODEL_OPTION_NAME', 'ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION', 'ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES', 'CLAUDE_CODE_EFFORT_LEVEL', 'CLAUDE_MODEL_CONFIG', 'MAX_THINKING_TOKENS', 'CLAUDE_CODE_AUTO_COMPACT_WINDOW', 'CLAUDE_CODE_MAX_CONTEXT_TOKENS']) delete env[name];\n "}delete env.ANTHROPIC_AUTH_TOKEN;
|
|
147
|
-
|
|
302
|
+
${legacy || previous === true ? '' : `// The 256K ceiling applies to plain IDs; [1m] is selected per model from the catalog.
|
|
303
|
+
env.CLAUDE_CODE_MAX_CONTEXT_TOKENS = settings.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS;
|
|
304
|
+
delete env.DISABLE_COMPACT;
|
|
305
|
+
delete env.CLAUDE_CODE_DISABLE_1M_CONTEXT;
|
|
306
|
+
`}delete env.CLAUDE_CODE_EXECUTABLE;
|
|
148
307
|
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
149
308
|
delete env.CLAUDE_CODE_USE_BEDROCK;
|
|
150
309
|
delete env.CLAUDE_CODE_USE_VERTEX;
|
|
@@ -164,28 +323,76 @@ if (process.argv.includes('--list-models')) {
|
|
|
164
323
|
}
|
|
165
324
|
|
|
166
325
|
/** Upgrade only byte-for-byte generated files. Credentials and user edits are retained. */
|
|
167
|
-
export async function
|
|
168
|
-
for (const name of ['', 'setup-owner.json', 'claude', 'claude/settings.json', 'launch.mjs', 'bb-agent.json']) {
|
|
326
|
+
export async function validateProvision(root) {
|
|
327
|
+
for (const name of ['', 'setup-owner.json', 'credentials.json', 'claude', 'claude/settings.json', 'launch.mjs', 'bb-agent.json']) {
|
|
169
328
|
if ((await lstat(join(root, name))).isSymbolicLink()) throw new Error('Профиль настройки изменён. Ничего не перезаписано.');
|
|
170
329
|
}
|
|
171
|
-
const
|
|
330
|
+
const ownerText = await readFile(join(root, 'setup-owner.json'), 'utf8');
|
|
331
|
+
const owner = JSON.parse(ownerText);
|
|
172
332
|
if (owner.schema !== 1 || owner.adapter !== ADAPTER) throw new Error('Неизвестный профиль настройки.');
|
|
333
|
+
if (owner.catalogs !== undefined && (!Array.isArray(owner.catalogs) || owner.catalogs.length > 2 || !owner.catalogs.every(validCatalog))) throw new Error('Повреждён каталог настройки.');
|
|
173
334
|
const files = [
|
|
174
|
-
['claude/settings.json',
|
|
335
|
+
['claude/settings.json', null, null],
|
|
175
336
|
['launch.mjs', launcherSource(root, true), launcherSource(root)],
|
|
176
337
|
['bb-agent.json', JSON.stringify(registration(root, owner.nodeExecutable, true), null, 2), JSON.stringify(registration(root, owner.nodeExecutable), null, 2)],
|
|
177
338
|
];
|
|
178
339
|
const pending = [];
|
|
340
|
+
const snapshots = [['setup-owner.json', ownerText]];
|
|
341
|
+
let catalog;
|
|
179
342
|
for (const [name, before, after] of files) {
|
|
180
343
|
const actual = await readFile(join(root, name), 'utf8');
|
|
344
|
+
snapshots.push([name, actual]);
|
|
181
345
|
const interim = name === 'bb-agent.json' ? JSON.stringify(registration(root, owner.nodeExecutable, 'catalog'), null, 2) : null;
|
|
182
|
-
if (
|
|
346
|
+
if (name === 'claude/settings.json') catalog = owner.catalogs?.find(value => actual === JSON.stringify(dynamicSettings(value), null, 2));
|
|
347
|
+
const known = name === 'claude/settings.json' ? !!catalog || generatedSettings(actual)
|
|
348
|
+
: actual === before || actual === after || actual === interim || name === 'launch.mjs'
|
|
349
|
+
&& [true, 'context'].some(previous => actual === launcherSource(root, false, previous));
|
|
350
|
+
if (!known) throw new Error('Профиль настройки изменён. Существующие файлы сохранены.');
|
|
351
|
+
if (name === 'claude/settings.json') continue;
|
|
183
352
|
if (actual !== after) pending.push([name, after]);
|
|
184
353
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
354
|
+
return { pending, owner, snapshots, catalog: catalog ?? seedCatalog() };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function atomicWrite(path, content) {
|
|
358
|
+
const temporary = path + '.upgrade-' + randomUUID();
|
|
359
|
+
try {
|
|
360
|
+
await writeFile(temporary, content, { mode: 0o600, flag: 'wx' });
|
|
361
|
+
await rename(temporary, path);
|
|
362
|
+
} finally { await rm(temporary, { force: true }); }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export async function upgradeProvision(root, { fetchImpl = fetch, signal, initial = false, restrictContext = false, log = () => {} } = {}) {
|
|
366
|
+
log('profile-validate', 'start');
|
|
367
|
+
let validated;
|
|
368
|
+
try { validated = await validateProvision(root); }
|
|
369
|
+
catch (error) { log('profile-validate', 'failure', { reason: 'failed' }); throw error; }
|
|
370
|
+
const { pending, owner, snapshots, catalog: previous } = validated;
|
|
371
|
+
log('profile-validate', 'success');
|
|
372
|
+
const credentials = await readFile(join(root, 'credentials.json'), 'utf8');
|
|
373
|
+
const { apiKey } = JSON.parse(credentials);
|
|
374
|
+
const catalog = restrictContext
|
|
375
|
+
? { ...previous, models: previous.models.map(model => ({ ...model, context: Math.min(SHORT_WINDOW, model.context) })) }
|
|
376
|
+
: await discoverCatalog(apiKey, previous, { fetchImpl, signal, initial, log });
|
|
377
|
+
if (restrictContext) log('catalog-normalize', 'fallback', { reason: 'conservative', modelCount: catalog.models.length, longModelCount: 0 });
|
|
378
|
+
signal?.throwIfAborted();
|
|
379
|
+
const started = Date.now(); log('profile-write', 'start');
|
|
380
|
+
try {
|
|
381
|
+
// Network I/O must not open a window for overwriting edits or a replaced key.
|
|
382
|
+
await validateProvision(root);
|
|
383
|
+
for (const [name, before] of [...snapshots, ['credentials.json', credentials]]) {
|
|
384
|
+
if (await readFile(join(root, name), 'utf8') !== before) throw new Error('Профиль изменился во время проверки контекста. Повтори настройку.');
|
|
385
|
+
}
|
|
386
|
+
// Journal both known generations first. A crash between file renames remains
|
|
387
|
+
// recognizable, while an unrelated edit still fails the byte-for-byte check.
|
|
388
|
+
const ownerPath = join(root, 'setup-owner.json');
|
|
389
|
+
await atomicWrite(ownerPath, JSON.stringify({ ...owner, catalogs: [catalog, previous] }));
|
|
390
|
+
pending.push(['claude/settings.json', JSON.stringify(dynamicSettings(catalog), null, 2)]);
|
|
391
|
+
for (const [name, after] of pending) {
|
|
392
|
+
await atomicWrite(join(root, name), after);
|
|
393
|
+
}
|
|
394
|
+
await atomicWrite(ownerPath, JSON.stringify({ ...owner, catalogs: [catalog] }));
|
|
395
|
+
log('profile-write', 'success', { elapsedMs: Date.now() - started });
|
|
396
|
+
} catch (error) { log('profile-write', 'failure', { reason: 'failed', elapsedMs: Date.now() - started }); throw error; }
|
|
190
397
|
return registration(root, owner.nodeExecutable);
|
|
191
398
|
}
|