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.
@@ -3,7 +3,7 @@
3
3
  "sdkVersion": "0.6.132",
4
4
  "artifactFormatVersion": 1,
5
5
  "pluginId": "svaib-workplace",
6
- "pluginVersion": "0.5.5",
6
+ "pluginVersion": "0.5.6",
7
7
  "builtWith": {
8
8
  "bbVersion": "0.49.0",
9
9
  "pluginSdkVersion": "0.6.132"
@@ -4,12 +4,20 @@ import { browserContract } from './browser-contract';
4
4
  import { nativeHookAutomatic } from './hook-native';
5
5
  import { join } from 'node:path';
6
6
  import { inspectKimi, prepareKimi, replaceKimiKey } from '../../kimi-host';
7
+ import { randomUUID } from 'node:crypto';
8
+ import { kimiDiagnostics, kimiDiagnosticSignals, kimiStep } from '../../kimi-diagnostics';
7
9
 
8
10
  // The authorization URL never passes through a model, terminal or saved thread.
9
- export default experimental_defineHostEntry({ contract: browserContract, handlers: {
11
+ export default experimental_defineHostEntry({ contract: browserContract, experimental_signals: kimiDiagnosticSignals, handlers: {
10
12
  async kimiInspect(_, context) { return inspectKimi(join(context.experimental_paths.dataDir, 'kimi-cc'), context.signal); },
11
- async kimiPrepare({ apiKey }, context) { return prepareKimi(join(context.experimental_paths.dataDir, 'kimi-cc'), apiKey, context.signal); },
12
- async kimiReplaceKey({ apiKey }, context) { await replaceKimiKey(join(context.experimental_paths.dataDir, 'kimi-cc'), apiKey, context.signal); return null; },
13
+ async kimiPrepare({ apiKey, diagnosticId }, context) {
14
+ const log = kimiDiagnostics(diagnosticId ?? randomUUID(), 'host', event => context.experimental_emitSignal('kimiDiagnostic', event));
15
+ return kimiStep(log, 'prepare', () => prepareKimi(join(context.experimental_paths.dataDir, 'kimi-cc'), apiKey, context.signal, log));
16
+ },
17
+ async kimiReplaceKey({ apiKey, diagnosticId }, context) {
18
+ const log = kimiDiagnostics(diagnosticId ?? randomUUID(), 'host', event => context.experimental_emitSignal('kimiDiagnostic', event));
19
+ await kimiStep(log, 'key-replace', () => replaceKimiKey(join(context.experimental_paths.dataDir, 'kimi-cc'), apiKey, context.signal, fetch, log)); return null;
20
+ },
13
21
  async automaticHooks(input, context) { return nativeHookAutomatic(input, context.signal); },
14
22
  async openLogin({ url }) {
15
23
  const configured = process.env.BROWSER;
package/kimi-contract.ts CHANGED
@@ -13,11 +13,12 @@ const key = z.string().max(512).regex(/^sk-kimi-[A-Za-z0-9_-]+$/, 'Нужен к
13
13
  export const kimiHostStatusSchema = z.object({
14
14
  nodeReady: z.boolean(), nodeVersion: z.string().nullable(),
15
15
  profile: z.enum(['absent', 'partial', 'ready', 'blocked']), agent: kimiAgentSchema.nullable(),
16
+ verifyModels: z.array(z.string()).optional(),
16
17
  }).strict();
17
18
  export const kimiHostMethods = {
18
19
  kimiInspect: { input: z.null(), output: kimiHostStatusSchema, experimental_retryable: true },
19
- kimiPrepare: { input: z.object({ apiKey: key.optional() }).strict(), output: kimiAgentSchema },
20
- kimiReplaceKey: { input: z.object({ apiKey: key }).strict(), output: z.null() },
20
+ kimiPrepare: { input: z.object({ apiKey: key.optional(), diagnosticId: z.uuid().optional() }).strict(), output: kimiAgentSchema },
21
+ kimiReplaceKey: { input: z.object({ apiKey: key, diagnosticId: z.uuid().optional() }).strict(), output: z.null() },
21
22
  };
22
23
  export const kimiViewSchema = z.object({
23
24
  revision: z.number().int().nonnegative(),
@@ -31,6 +32,7 @@ export const kimiViewSchema = z.object({
31
32
  checkToken: z.string().nullable().default(null),
32
33
  checkStartedAt: z.number().nullable().default(null),
33
34
  catalogVersion: z.number().int().nonnegative().default(0),
35
+ catalogCheckedAt: z.number().nullable().default(null),
34
36
  automatic: z.boolean().default(false),
35
37
  ownedTestThreadId: z.string().nullable().default(null),
36
38
  }).strict();
@@ -0,0 +1,42 @@
1
+ import { z } from 'zod';
2
+
3
+ // No free text: even a provider error containing credentials cannot enter the log.
4
+ export const kimiDiagnosticSchema = z.object({
5
+ diagnosticId: z.uuid(), source: z.enum(['backend', 'host']),
6
+ step: z.enum(['prepare', 'key-replace', 'key-commit', 'runtime', 'adapter-install', 'profile-inspect',
7
+ 'profile-validate', 'profile-write', 'catalog-fetch', 'catalog-normalize', 'bb-discovery',
8
+ 'registration', 'verification', 'refresh', 'migration', 'state', 'cleanup']),
9
+ outcome: z.enum(['start', 'success', 'failure', 'fallback', 'skipped']),
10
+ reason: z.enum(['setup', 'recheck', 'key-change', 'scheduled', 'startup', 'migration',
11
+ 'cancelled', 'timeout', 'failed', 'http-error', 'network-error', 'invalid-json', 'oversized',
12
+ 'invalid-catalog', 'cached', 'conservative', 'unavailable', 'foreign-profile', 'wrong-response']).optional(),
13
+ elapsedMs: z.number().int().nonnegative().optional(),
14
+ httpStatus: z.number().int().min(100).max(599).optional(),
15
+ byteCount: z.number().int().nonnegative().max(262144).optional(),
16
+ modelCount: z.number().int().nonnegative().max(129).optional(),
17
+ longModelCount: z.number().int().nonnegative().max(129).optional(),
18
+ profile: z.enum(['absent', 'partial', 'ready', 'blocked']).optional(),
19
+ stage: z.enum(['intro', 'node', 'key', 'installing', 'registration', 'verify', 'complete', 'blocked']).optional(),
20
+ phase: z.enum(['components', 'access', 'check']).nullable().optional(),
21
+ }).strict();
22
+ export type KimiDiagnostic = z.infer<typeof kimiDiagnosticSchema>;
23
+ export type KimiDiagnosticLog = (step: string, outcome: string, fields?: Record<string, unknown>) => void;
24
+ export const kimiDiagnosticSignals = { kimiDiagnostic: { payload: kimiDiagnosticSchema } };
25
+
26
+ export function kimiDiagnostics(diagnosticId: string, source: KimiDiagnostic['source'], sink: (event: KimiDiagnostic) => unknown): KimiDiagnosticLog {
27
+ return (step, outcome, fields = {}) => {
28
+ const parsed = kimiDiagnosticSchema.safeParse({ diagnosticId, source, step, outcome, ...fields });
29
+ if (!parsed.success) return;
30
+ try { void Promise.resolve(sink(parsed.data)).catch(() => {}); }
31
+ catch { /* Diagnostics must never fail the user's setup. */ }
32
+ };
33
+ }
34
+
35
+ export async function kimiStep<T>(log: KimiDiagnosticLog, step: string, work: () => Promise<T>): Promise<T> {
36
+ const started = Date.now(); log(step, 'start');
37
+ try { const result = await work(); log(step, 'success', { elapsedMs: Date.now() - started }); return result; }
38
+ catch (error) {
39
+ log(step, 'failure', { elapsedMs: Date.now() - started, reason: error instanceof Error && error.name === 'AbortError' ? 'cancelled' : 'failed' });
40
+ throw error;
41
+ }
42
+ }
package/kimi-host.ts CHANGED
@@ -2,9 +2,10 @@ import { spawn } from 'node:child_process';
2
2
  import { lstat, readFile, writeFile, rename, rm } from 'node:fs/promises';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { join, posix, win32 } from 'node:path';
5
- import { provision, resumeProvision, registration, upgradeProvision, ADAPTER } from './scripts/setup-kimi-cc.mjs';
5
+ import { provision, resumeProvision, registration, upgradeProvision, validateProvision, ADAPTER } from './scripts/setup-kimi-cc.mjs';
6
6
  import { kimiAgentSchema } from './kimi-contract';
7
7
  import { ensureKimiRuntime, existingKimiRuntime } from './kimi-runtime';
8
+ import { kimiStep, type KimiDiagnosticLog } from './kimi-diagnostics';
8
9
 
9
10
  /** Windows environment names are case-insensitive, including inside host workers. */
10
11
  export function kimiCommandEnvironment(executable: string, environment: NodeJS.ProcessEnv = process.env, platform: string = process.platform) {
@@ -37,9 +38,9 @@ export function kimiCommand(executable: string, args: string[], signal: AbortSig
37
38
  async function node(signal: AbortSignal) {
38
39
  return existingKimiRuntime(signal);
39
40
  }
40
- async function regular(path: string, directory = false) {
41
+ async function regular(path: string, directory = false, maxBytes = 32_768) {
41
42
  const info = await lstat(path);
42
- if (info.isSymbolicLink() || (directory ? !info.isDirectory() : !info.isFile() || info.size > 32_768)) throw new Error('Профиль настройки изменён. Ничего не перезаписано.');
43
+ if (info.isSymbolicLink() || (directory ? !info.isDirectory() : !info.isFile() || info.size > maxBytes)) throw new Error('Профиль настройки изменён. Ничего не перезаписано.');
43
44
  }
44
45
  export async function inspectKimi(root: string, signal: AbortSignal) {
45
46
  const runtime = await node(signal);
@@ -49,7 +50,7 @@ export async function inspectKimi(root: string, signal: AbortSignal) {
49
50
  return { ...base, profile: (error as NodeJS.ErrnoException).code === 'ENOENT' ? 'absent' as const : 'blocked' as const, agent: null };
50
51
  }
51
52
  try {
52
- await regular(join(root, 'setup-owner.json'));
53
+ await regular(join(root, 'setup-owner.json'), false, 262_144);
53
54
  const owner = JSON.parse(await readFile(join(root, 'setup-owner.json'), 'utf8'));
54
55
  if (owner.schema !== 1 || owner.adapter !== ADAPTER || typeof owner.nodeExecutable !== 'string') throw new Error();
55
56
  try { await regular(join(root, 'bb-agent.json')); }
@@ -59,32 +60,44 @@ export async function inspectKimi(root: string, signal: AbortSignal) {
59
60
  }
60
61
  const agent = kimiAgentSchema.parse(JSON.parse(await readFile(join(root, 'bb-agent.json'), 'utf8')));
61
62
  if (![false, true, 'catalog' as const].some(legacy => JSON.stringify(agent) === JSON.stringify(registration(root, owner.nodeExecutable, legacy)))) throw new Error();
62
- return { ...base, profile: 'ready' as const, agent };
63
+ let verifyModels: string[] = [];
64
+ try {
65
+ const { catalog } = await validateProvision(root);
66
+ verifyModels = catalog.models.filter(model => !model.retired).map(model => model.id + (model.context >= 1_000_000 ? '[1m]' : ''));
67
+ } catch { /* An unrecognized profile cannot supply a trusted verification model. */ }
68
+ return { ...base, profile: 'ready' as const, agent, verifyModels };
63
69
  } catch { return { ...base, profile: 'blocked' as const, agent: null }; }
64
70
  }
65
- export async function prepareKimi(root: string, apiKey: string | undefined, signal: AbortSignal) {
71
+ export async function prepareKimi(root: string, apiKey: string | undefined, signal: AbortSignal, log: KimiDiagnosticLog = () => {}) {
66
72
  signal.throwIfAborted();
67
73
  const status = await inspectKimi(root, signal);
68
- if (status.profile === 'ready') return upgradeProvision(root);
74
+ log('profile-inspect', 'success', { profile: status.profile });
75
+ if (status.profile === 'ready') return upgradeProvision(root, { signal, log });
69
76
  if (status.profile === 'blocked') throw new Error('Папка настройки изменена. Существующие файлы сохранены; нужна помощь партнёра.');
70
77
  const install = async (executable: string, args: string[]) => { await kimiCommand(executable, args, signal); };
71
- const runtime = await ensureKimiRuntime(root, signal);
72
- if (status.profile === 'partial') return resumeProvision({ root, install, npmCli: runtime.npmCli, nodeExecutable: runtime.executable });
78
+ const runtime = await kimiStep(log, 'runtime', () => ensureKimiRuntime(root, signal));
79
+ if (status.profile === 'partial') return resumeProvision({ root, install, npmCli: runtime.npmCli, nodeExecutable: runtime.executable, signal, log });
73
80
  if (!apiKey) throw new Error('Введи ключ из консоли Kimi Code.');
74
- return provision({ root, apiKey, install, nodeExecutable: runtime.executable, npmCli: runtime.npmCli });
81
+ return provision({ root, apiKey, install, nodeExecutable: runtime.executable, npmCli: runtime.npmCli, signal, log });
75
82
  }
76
83
 
77
- export async function replaceKimiKey(root: string, apiKey: string, signal: AbortSignal) {
84
+ export async function replaceKimiKey(root: string, apiKey: string, signal: AbortSignal, fetchImpl: typeof fetch = fetch, log: KimiDiagnosticLog = () => {}) {
78
85
  signal.throwIfAborted();
79
86
  if (!/^sk-kimi-[A-Za-z0-9_-]+$/.test(apiKey)) throw new Error('Нужен ключ из консоли Kimi Code.');
80
87
  const status = await inspectKimi(root, signal);
81
88
  if (status.profile !== 'ready') throw new Error('Собственный профиль настройщика не найден.');
89
+ await validateProvision(root);
82
90
  await regular(join(root, 'credentials.json'));
91
+ // Publish conservative settings while the old key is still in place. Every
92
+ // interruption after the key rename then leaves a recognized, safe profile.
93
+ await upgradeProvision(root, { signal, restrictContext: true, log });
83
94
  signal.throwIfAborted();
84
95
  const temporary = join(root, '.key-' + randomUUID());
85
96
  try {
86
97
  await writeFile(temporary, JSON.stringify({ apiKey }), { mode: 0o600, flag: 'wx' });
87
98
  signal.throwIfAborted();
88
99
  await rename(temporary, join(root, 'credentials.json'));
100
+ log('key-commit', 'success');
89
101
  } finally { await rm(temporary, { force: true }); }
102
+ await upgradeProvision(root, { signal, fetchImpl, log });
90
103
  }
package/kimi-setup.ts CHANGED
@@ -2,10 +2,13 @@ import { randomUUID } from 'node:crypto';
2
2
  import type { BbPluginApi } from '@get-bb/plugin-sdk';
3
3
  import { browserContract } from './experiments/account-prototype/browser-contract';
4
4
  import { kimiRpcContract, kimiViewSchema, type KimiAgent, type KimiView } from './kimi-contract';
5
+ import { kimiDiagnostics, kimiDiagnosticSignals, kimiStep, type KimiDiagnostic } from './kimi-diagnostics';
5
6
 
6
7
  const PROVIDER = 'acp-claude-kimi';
7
8
  const SETTINGS_PLUGIN = 'provider-acp';
8
9
  const PREFERRED_MODEL = 'k3.1-pro';
10
+ const CATALOG_VERSION = 3;
11
+ const CATALOG_TTL = 24 * 60 * 60 * 1000;
9
12
  async function bounded<T>(work: Promise<T>, ms = 30_000): Promise<T> {
10
13
  let timer: ReturnType<typeof setTimeout> | undefined;
11
14
  try { return await Promise.race([work, new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error('BB timeout')), ms); })]); }
@@ -14,12 +17,22 @@ async function bounded<T>(work: Promise<T>, ms = 30_000): Promise<T> {
14
17
 
15
18
  /** Owns only local Kimi UI state. It never reads/writes svaib onboarding marks. */
16
19
  export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null) {
17
- const client = bb.hosts.experimental_client({ contract: browserContract });
20
+ const client = bb.hosts.experimental_client({ contract: browserContract, experimental_signals: kimiDiagnosticSignals });
21
+ const writeDiagnostic = (event: KimiDiagnostic) => {
22
+ const line = '[kimi] ' + JSON.stringify(event);
23
+ try {
24
+ if (event.outcome === 'failure' || event.outcome === 'fallback') bb.log.warn(line); else bb.log.info(line);
25
+ } catch { /* Host signal delivery must also survive a failed log sink. */ }
26
+ };
27
+ const log = kimiDiagnostics(randomUUID(), 'backend', writeDiagnostic);
28
+ bb.onDispose(client.experimental_onSignal('kimiDiagnostic', event => {
29
+ if (event.hostId === hostId) writeDiagnostic(event.payload);
30
+ }));
18
31
  const storageKey = 'kimi-setup:v1:' + (hostId ?? 'unavailable');
19
32
  const saved = kimiViewSchema.safeParse(await bb.storage.kv.get(storageKey));
20
33
  let view: KimiView = saved.success ? saved.data : {
21
34
  revision: 0, canReplaceKey: false, hidden: false, stage: 'intro', message: '', hostId: hostId ?? '', testThreadId: null, verifyModel: null, manualRegistration: null, keyChangedAt: null,
22
- phase: null, startedAt: null, checkToken: null, checkStartedAt: null, catalogVersion: 0, automatic: false, ownedTestThreadId: null,
35
+ phase: null, startedAt: null, checkToken: null, checkStartedAt: null, catalogVersion: 0, catalogCheckedAt: null, automatic: false, ownedTestThreadId: null,
23
36
  };
24
37
  let job: Promise<void> | null = null;
25
38
  let starting: Promise<KimiView> | null = null;
@@ -33,8 +46,10 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
33
46
  const snapshot = () => ({ ...view });
34
47
  async function save(patch: Partial<KimiView>) {
35
48
  if (disposed) return snapshot();
49
+ const changed = patch.stage !== undefined && patch.stage !== view.stage || patch.phase !== undefined && patch.phase !== view.phase;
36
50
  view = { ...view, ...patch, revision: view.revision + 1 };
37
51
  await bb.storage.kv.set(storageKey, view);
52
+ if (changed) log('state', 'success', { stage: view.stage, phase: view.phase });
38
53
  bb.realtime.publish('kimi-setup-changed', null);
39
54
  return snapshot();
40
55
  }
@@ -42,6 +57,16 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
42
57
  if (!hostId) throw new Error('Компьютер BB недоступен. Подключи его и повтори настройку.');
43
58
  return { hostId, signal: abort.signal, timeoutMs: 600_000 };
44
59
  }
60
+ async function hostMutation<T>(step: 'prepare' | 'key-replace', reason: KimiDiagnostic['reason'], work: (diagnosticId: string) => Promise<T>) {
61
+ const diagnosticId = randomUUID();
62
+ const operationLog = kimiDiagnostics(diagnosticId, 'backend', writeDiagnostic);
63
+ operationLog(step, 'start', { reason });
64
+ const started = Date.now();
65
+ try { const result = await work(diagnosticId); operationLog(step, 'success', { elapsedMs: Date.now() - started }); return result; }
66
+ catch (error) { operationLog(step, 'failure', { reason: abort.signal.aborted ? 'cancelled' : 'failed', elapsedMs: Date.now() - started }); throw error; }
67
+ }
68
+ const prepareProfile = (apiKey: string | undefined, reason: KimiDiagnostic['reason']) => hostMutation('prepare', reason,
69
+ diagnosticId => client.call('kimiPrepare', { ...(apiKey ? { apiKey } : {}), diagnosticId }, target()));
45
70
  async function agents() {
46
71
  const current = await bounded(bb.sdk.plugins.getSettings({ pluginId: SETTINGS_PLUGIN }));
47
72
  if (!current.schema.customAgents || current.schema.customAgents.type !== 'string') throw new Error('ACP settings unavailable');
@@ -64,12 +89,20 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
64
89
  /** Only seed models confirmed by this host's provider catalog. */
65
90
  async function resolveVerifyModel(): Promise<string | null> {
66
91
  if (!hostId) return null;
92
+ const started = Date.now(); log('bb-discovery', 'start');
67
93
  try {
68
- const catalog = await bounded(bb.sdk.providers.models({ hostId, providerId: PROVIDER }));
69
- if (catalog.modelLoadError || !catalog.providers.some(provider => provider.id === PROVIDER && provider.available)) return null;
70
- const models = catalog.models.filter(model => !model.routeProviderId || model.routeProviderId === PROVIDER);
71
- return (models.find(model => model.id === PREFERRED_MODEL) ?? models.find(model => model.id === 'k3.1-pro[1m]') ?? models.find(model => model.isDefault) ?? models[0])?.id ?? null;
72
- } catch { return null; }
94
+ const catalog = await bounded(bb.sdk.providers.models({ hostId, providerId: PROVIDER, preferCached: false }));
95
+ if (catalog.modelLoadError || !catalog.providers.some(provider => provider.id === PROVIDER && provider.available)) {
96
+ log('bb-discovery', 'failure', { reason: 'unavailable', elapsedMs: Date.now() - started }); return null;
97
+ }
98
+ const status = await client.call('kimiInspect', null, target());
99
+ const allowed = status.verifyModels && status.agent && sameRegistration((await agents()).find(item => item.id === 'claude-kimi'), status.agent)
100
+ ? new Set(status.verifyModels) : null;
101
+ const models = catalog.models.filter(model => (!model.routeProviderId || model.routeProviderId === PROVIDER) && (!allowed || allowed.has(model.id)));
102
+ const selected = (models.find(model => model.id === PREFERRED_MODEL) ?? models.find(model => model.id === 'k3.1-pro[1m]') ?? models.find(model => model.isDefault) ?? models[0])?.id ?? null;
103
+ log('bb-discovery', selected ? 'success' : 'failure', { modelCount: models.length, elapsedMs: Date.now() - started });
104
+ return selected;
105
+ } catch { log('bb-discovery', 'failure', { reason: 'failed', elapsedMs: Date.now() - started }); return null; }
73
106
  }
74
107
  async function inspect() {
75
108
  if (await hasProvider()) {
@@ -108,11 +141,14 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
108
141
  return sameRegistration(actual, agent);
109
142
  }
110
143
  async function register(agent: KimiAgent) {
144
+ log('registration', 'start');
111
145
  try {
112
146
  const owned = await writeRegistration(agent);
147
+ log('registration', owned ? 'success' : 'skipped', owned ? {} : { reason: 'foreign-profile' });
113
148
  return save({ stage: 'verify', message: '', manualRegistration: null, verifyModel: await resolveVerifyModel(),
114
- canReplaceKey: owned, ...(owned ? { catalogVersion: 1 } : {}) });
149
+ canReplaceKey: owned, ...(owned ? { catalogVersion: CATALOG_VERSION, catalogCheckedAt: Date.now() } : {}) });
115
150
  } catch {
151
+ log('registration', 'failure', { reason: 'failed' });
116
152
  return save({ stage: 'registration', message: 'Профиль готов. В этой версии BB добавь провайдера через настройки ACP.', manualRegistration: JSON.stringify([agent], null, 2) });
117
153
  }
118
154
  }
@@ -125,7 +161,7 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
125
161
  const run = async () => {
126
162
  try {
127
163
  if (status.agent && sameRegistration(current, status.agent)) {
128
- const upgraded = await client.call('kimiPrepare', {}, target());
164
+ const upgraded = await prepareProfile(undefined, 'recheck');
129
165
  await register(upgraded);
130
166
  } else await save({ stage: 'verify', canReplaceKey: false, verifyModel: await resolveVerifyModel() });
131
167
  if (view.stage === 'verify') await beginAutomaticCheck();
@@ -142,7 +178,7 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
142
178
  // a terminal, a model request, command argv, or the returned view.
143
179
  const run = async () => {
144
180
  try {
145
- const agent = await client.call('kimiPrepare', { ...(apiKey ? { apiKey } : {}) }, target());
181
+ const agent = await prepareProfile(apiKey, 'setup');
146
182
  apiKey = undefined;
147
183
  if (!disposed) {
148
184
  await save({ canReplaceKey: true, phase: 'access' });
@@ -198,10 +234,12 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
198
234
  if ((error as { status?: number }).status === 404 || (error as { code?: string }).code === 'NOT_FOUND') {
199
235
  if (view.ownedTestThreadId === id) await save({ ownedTestThreadId: null, checkToken: null, ...(view.testThreadId === id ? { testThreadId: null } : {}) }); return true;
200
236
  }
237
+ log('cleanup', 'failure', { reason: 'failed' });
201
238
  return false;
202
239
  }
203
240
  }
204
241
  async function failAutomaticCheck(message: string, testId: string) {
242
+ log('verification', 'failure', { reason: 'unavailable', elapsedMs: Date.now() - (view.checkStartedAt ?? Date.now()) });
205
243
  await save({ phase: null, message });
206
244
  if (view.ownedTestThreadId === testId) {
207
245
  try { await checkedThread(testId); await bb.sdk.threads.stop({ threadId: testId }); } catch { /* Retry keeps the owned ID for cleanup. */ }
@@ -242,6 +280,7 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
242
280
  await save({ phase: 'check' }); watchAutomaticCheck(); return;
243
281
  }
244
282
  await save({ phase: 'check', stage: 'verify', checkStartedAt: Date.now(), checkToken: randomUUID(), message: '', testThreadId: null });
283
+ log('verification', 'start');
245
284
  try {
246
285
  const model = view.verifyModel;
247
286
  if (!model || !hostId) throw new Error('model-unavailable');
@@ -259,6 +298,7 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
259
298
  await save({ testThreadId: thread.id, ownedTestThreadId: thread.id });
260
299
  watchAutomaticCheck();
261
300
  } catch {
301
+ log('verification', 'failure', { reason: 'failed' });
262
302
  try { await recoverOwnCheck(); } catch { /* Retry retains the nonce for recovery. */ }
263
303
  if (view.ownedTestThreadId) watchAutomaticCheck();
264
304
  else await save({ phase: null, message: 'Не удалось запустить проверку Kimi. Проверь подключение к BB и повтори.' });
@@ -275,7 +315,11 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
275
315
  if (thread.status !== 'idle') return pending('Ответ ещё не получен. Проверь разговор и повтори проверку.');
276
316
  const { output } = await bounded(bb.sdk.threads.output({ threadId: thread.id }));
277
317
  if (!current()) return snapshot();
278
- if (output?.trim() !== 'KIMI_READY') return pending('Проверочный ответ не подтверждён. Открой разговор и проверь результат.');
318
+ if (output?.trim() !== 'KIMI_READY') {
319
+ if (!quiet) log('verification', 'failure', { reason: 'wrong-response' });
320
+ return pending('Проверочный ответ не подтверждён. Открой разговор и проверь результат.');
321
+ }
322
+ log('verification', 'success', { elapsedMs: Date.now() - (view.checkStartedAt ?? Date.now()) });
279
323
  await save({ stage: 'complete', hidden: true, phase: null, message: 'Готово к работе', manualRegistration: null });
280
324
  await cleanOwnCheck();
281
325
  return snapshot();
@@ -339,10 +383,13 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
339
383
  // old key must not complete setup while the key is being replaced.
340
384
  await recoverOwnCheck();
341
385
  if (!await cleanOwnCheck(false)) throw new Error('cleanup-pending');
342
- await save({ hidden: false, stage: 'installing', phase: 'access', startedAt: Date.now(), message: '' });
343
- await client.call('kimiReplaceKey', { apiKey }, target());
386
+ // An RPC failure cannot prove whether the host committed the new key.
387
+ // Invalidate the old manual binding before crossing that boundary.
388
+ await save({ hidden: false, stage: 'installing', phase: 'access', startedAt: Date.now(),
389
+ keyChangedAt: Date.now(), testThreadId: null, verifyModel: null, message: '' });
390
+ await hostMutation('key-replace', 'key-change', diagnosticId => client.call('kimiReplaceKey', { apiKey, diagnosticId }, target()));
344
391
  const keyChangedAt = Date.now();
345
- await save({ stage: 'verify', phase: null, hidden: false, testThreadId: null, ownedTestThreadId: null, keyChangedAt, verifyModel: view.verifyModel ?? await resolveVerifyModel(), message: view.automatic ? '' : 'Ключ обновлён. Отправь новый проверочный запрос.' });
392
+ await save({ stage: 'verify', phase: null, hidden: false, testThreadId: null, ownedTestThreadId: null, keyChangedAt, catalogCheckedAt: Date.now(), verifyModel: await resolveVerifyModel(), message: view.automatic ? '' : 'Ключ обновлён. Отправь новый проверочный запрос.' });
346
393
  if (view.automatic) { await save({ startedAt: Date.now() }); await beginAutomaticCheck(); }
347
394
  return snapshot();
348
395
  } catch (error) {
@@ -351,21 +398,24 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
351
398
  } finally { replacing = false; }
352
399
  },
353
400
  });
354
- const needsResume = view.stage === 'complete' && view.catalogVersion < 1 && view.canReplaceKey
401
+ const needsResume = view.stage === 'complete' && view.catalogVersion < CATALOG_VERSION && view.canReplaceKey
355
402
  || view.automatic && (view.phase === 'check' || view.stage === 'installing');
356
403
  if (needsResume) bb.background.service('kimi-resume', {
357
404
  async start(signal) {
358
405
  if (signal.aborted || disposed) return;
359
406
  // Host calls are forbidden during factory registration. Services start
360
407
  // only once BB has registered the plugin and its host entry.
361
- if (view.stage === 'complete' && view.catalogVersion < 1 && view.canReplaceKey) {
362
- // Upgrade generated 0.4.2 metadata without another model request or reinstall.
408
+ if (view.stage === 'complete' && view.catalogVersion < CATALOG_VERSION && view.canReplaceKey) {
409
+ // Refresh owned context limits without another model request or reinstall.
363
410
  job = (async () => {
364
411
  try {
365
412
  const status = await client.call('kimiInspect', null, target());
366
413
  if (!status.agent || !sameRegistration((await agents()).find(item => item.id === 'claude-kimi'), status.agent)) return;
367
- const agent = await client.call('kimiPrepare', {}, target());
368
- if (await writeRegistration(agent)) await save({ automatic: true, catalogVersion: 1, message: 'Готово к работе' });
414
+ const agent = await prepareProfile(undefined, 'migration');
415
+ if (await writeRegistration(agent)) {
416
+ if (!await resolveVerifyModel()) throw new Error('Kimi catalog discovery failed');
417
+ await save({ automatic: true, catalogVersion: CATALOG_VERSION, catalogCheckedAt: Date.now(), message: 'Готово к работе' });
418
+ }
369
419
  } catch { bb.log.warn('Не удалось обновить настройки моделей Kimi. Работающее подключение сохранено; повтори проверку в настройках плагина.'); }
370
420
  })().finally(() => { job = null; }); void job.catch(() => {});
371
421
  }
@@ -385,5 +435,29 @@ export async function registerKimiSetup(bb: BbPluginApi, hostId: string | null)
385
435
  });
386
436
  },
387
437
  });
438
+ async function refreshCatalog(reason: 'scheduled' | 'startup' = 'scheduled') {
439
+ if (disposed || occupied() || !hostId || view.stage !== 'complete' || !view.canReplaceKey
440
+ || view.catalogCheckedAt !== null && Date.now() - view.catalogCheckedAt < CATALOG_TTL) return;
441
+ job = kimiStep(log, 'refresh', async () => {
442
+ const status = await client.call('kimiInspect', null, target());
443
+ if (!status.agent || !sameRegistration((await agents()).find(item => item.id === 'claude-kimi'), status.agent)) return;
444
+ const agent = await prepareProfile(undefined, reason);
445
+ if (!await writeRegistration(agent)) return;
446
+ // A failed BB discovery must remain due for retry, even if host settings succeeded.
447
+ if (!await resolveVerifyModel()) throw new Error('Kimi catalog discovery failed');
448
+ await save({ catalogVersion: CATALOG_VERSION, catalogCheckedAt: Date.now() });
449
+ }).finally(() => { job = null; });
450
+ await job;
451
+ }
452
+ // Hourly inexpensive TTL check; at most one catalog request per day. On a
453
+ // restart after downtime, refresh immediately once the host entry is available.
454
+ bb.background.schedule('kimi-catalog-refresh', '0 * * * *', () => refreshCatalog('scheduled'));
455
+ if (!needsResume) bb.background.service('kimi-catalog-refresh', {
456
+ async start(signal) {
457
+ try { if (!signal.aborted) await refreshCatalog('startup'); }
458
+ catch { bb.log.warn('Не удалось обновить каталог Kimi. Повторим проверку по расписанию.'); }
459
+ if (!signal.aborted) await new Promise<void>(resolve => signal.addEventListener('abort', () => resolve(), { once: true }));
460
+ },
461
+ });
388
462
  return { settled: async () => { await starting; await job; } };
389
463
  }
package/npm-release.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "repository": "https://github.com/SolomonikVik/svaib-bb",
3
- "tag": "v0.5.5",
4
- "commit": "4489a73e601171d69aa5911853194e3edf216e3b",
5
- "pluginVersion": "0.5.5",
3
+ "tag": "v0.5.6",
4
+ "commit": "bdf865e0df4f2c1f854f140299b02619ccf5c864",
5
+ "pluginVersion": "0.5.6",
6
6
  "methodVersion": "0.11.0"
7
7
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "bb-plugin-svaib-workplace",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "type": "module",
5
5
  "engines": {
6
- "bb": ">=0.48.1",
7
- "bbPluginSdk": ">=0.6.104 <0.7"
6
+ "bb": ">=0.49.0",
7
+ "bbPluginSdk": ">=0.6.132 <0.7"
8
8
  },
9
9
  "bb": {
10
10
  "name": "svaib [Early access]",
@@ -20,10 +20,10 @@
20
20
  ]
21
21
  },
22
22
  "dependencies": {
23
- "echarts": "6.1.0",
24
- "zod": "^4.6.5",
23
+ "@get-bb/plugin-sdk": "0.6.132",
25
24
  "bb-plugin-svaib-screen-pilot": "file:./components/screen",
26
- "@get-bb/plugin-sdk": "0.6.104"
25
+ "echarts": "6.1.0",
26
+ "zod": "^4.6.5"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@testing-library/react": "^16.3.2",
@@ -1,10 +1,13 @@
1
1
  import type { KimiAgent } from '../kimi-contract';
2
+ import type { KimiDiagnosticLog } from '../kimi-diagnostics';
2
3
  type Install = (executable: string, args: string[], options?: object) => Promise<void>;
4
+ type DiscoveryOptions = { fetchImpl?: typeof fetch; signal?: AbortSignal; log?: KimiDiagnosticLog };
3
5
  export const ADAPTER: string;
4
6
  export function npmCliPath(nodeExecutable?: string): Promise<string>;
5
7
  export function registration(root: string, executable?: string, legacy?: boolean | 'catalog'): KimiAgent;
6
- export function provision(options: { root: string; apiKey: string; install?: Install; npmCli?: string; nodeExecutable?: string }): Promise<KimiAgent>;
7
- export function resumeProvision(options: { root: string; install?: Install; npmCli?: string; nodeExecutable?: string }): Promise<KimiAgent>;
8
+ export function provision(options: { root: string; apiKey: string; install?: Install; npmCli?: string; nodeExecutable?: string } & DiscoveryOptions): Promise<KimiAgent>;
9
+ export function resumeProvision(options: { root: string; install?: Install; npmCli?: string; nodeExecutable?: string } & DiscoveryOptions): Promise<KimiAgent>;
8
10
 
9
11
  export const MODELS: string[][];
10
- export function upgradeProvision(root: string): Promise<KimiAgent>;
12
+ export function upgradeProvision(root: string, options?: DiscoveryOptions & { restrictContext?: boolean }): Promise<KimiAgent>;
13
+ export function validateProvision(root: string): Promise<{ catalog: { models: { id: string; context: number; retired: boolean }[] } }>;