@khorsheed/dsh-local-agent-kimi 0.1.0-rc.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.
Files changed (50) hide show
  1. package/README.en.md +132 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +132 -0
  4. package/cordis.patch.yml +34 -0
  5. package/lib/client.js +1066 -0
  6. package/lib/index.js +4716 -0
  7. package/lib/invariant.js +31 -0
  8. package/lib/types/client/SettingsCard.d.ts +77 -0
  9. package/lib/types/client/SettingsCard.js +197 -0
  10. package/lib/types/client/icons.d.ts +18 -0
  11. package/lib/types/client/icons.js +2 -0
  12. package/lib/types/client/index.d.ts +42 -0
  13. package/lib/types/client/index.js +89 -0
  14. package/lib/types/client/locales.d.ts +64 -0
  15. package/lib/types/client/locales.js +55 -0
  16. package/lib/types/credential-guard.d.ts +29 -0
  17. package/lib/types/credential-guard.js +107 -0
  18. package/lib/types/index.d.ts +69 -0
  19. package/lib/types/index.js +270 -0
  20. package/lib/types/invariant.d.ts +16 -0
  21. package/lib/types/invariant.js +33 -0
  22. package/lib/types/kimi-cli-provider.d.ts +215 -0
  23. package/lib/types/kimi-cli-provider.js +855 -0
  24. package/lib/types/live-driver.d.ts +245 -0
  25. package/lib/types/live-driver.js +1312 -0
  26. package/lib/types/live-switch.d.ts +78 -0
  27. package/lib/types/live-switch.js +112 -0
  28. package/lib/types/member-bridge-config.d.ts +43 -0
  29. package/lib/types/member-bridge-config.js +80 -0
  30. package/lib/types/model-broker.d.ts +97 -0
  31. package/lib/types/model-broker.js +205 -0
  32. package/lib/types/model-catalog.d.ts +26 -0
  33. package/lib/types/model-catalog.js +86 -0
  34. package/lib/types/model-configuration.d.ts +11 -0
  35. package/lib/types/model-configuration.js +55 -0
  36. package/lib/types/preset-tools.d.ts +20 -0
  37. package/lib/types/preset-tools.js +36 -0
  38. package/lib/types/provision.d.ts +131 -0
  39. package/lib/types/provision.js +363 -0
  40. package/lib/types/records.d.ts +35 -0
  41. package/lib/types/records.js +85 -0
  42. package/lib/types/session-configuration.d.ts +5 -0
  43. package/lib/types/session-configuration.js +26 -0
  44. package/lib/types/session-mirror.d.ts +180 -0
  45. package/lib/types/session-mirror.js +446 -0
  46. package/lib/types/session-view.d.ts +121 -0
  47. package/lib/types/session-view.js +423 -0
  48. package/locale/en.json +6 -0
  49. package/locale/zh.json +6 -0
  50. package/package.json +107 -0
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Kimi Code scoped-home provisioning: a working `config.toml` (provider and
3
+ * model definitions) is required for the ACP server to authenticate. The
4
+ * scoped home is provisioned once — from the user's own config, redacted —
5
+ * so the scoped setup mirrors the user's models while carrying no secrets.
6
+ * Sign-out removes the credentials and OAuth cache so a later login can
7
+ * authorize a different account. The agent preset the delegation tool row
8
+ * needs is bootstrapped into the user preset root on first boot, so
9
+ * installing the bundle needs no manual copy step.
10
+ * @module @khorsheed/dsh-local-agent-kimi/provision
11
+ */
12
+ import { readFile, rm, writeFile } from 'node:fs/promises';
13
+ import { homedir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ const REAL_CONFIG = join(homedir(), '.kimi-code', 'config.toml');
16
+ /** Credentials directory inside the scoped home; its presence marks auth. */
17
+ const CREDENTIALS_DIR = 'credentials';
18
+ /** OAuth token cache directory inside the scoped home. */
19
+ const OAUTH_DIR = 'oauth';
20
+ /**
21
+ * The managed provider's endpoint, written into a fresh scoped config and the
22
+ * baseline the eval snapshot compares against: a scoped config still pointing
23
+ * here routes through kimi's own service, i.e. no custom endpoint is in force.
24
+ */
25
+ export const KIMI_MANAGED_BASE_URL = 'https://api.kimi.com/coding/v1';
26
+ /**
27
+ * The reasoning effort a fresh scoped config gets when the plugin config does
28
+ * not override it — the value the pre-config-item provisioning hardcoded, so
29
+ * the default is behavior-preserving.
30
+ */
31
+ export const DEFAULT_THINKING_EFFORT = 'high';
32
+ /** Minimal managed config for a fresh home with no user config to mirror. */
33
+ function minimalConfig(model, thinkingEffort) {
34
+ return `default_model = "${model}"
35
+
36
+ [thinking]
37
+ enabled = true
38
+ effort = "${thinkingEffort}"
39
+
40
+ [providers."managed:kimi-code"]
41
+ base_url = "${KIMI_MANAGED_BASE_URL}"
42
+ type = "kimi"
43
+ api_key = ""
44
+
45
+ [providers."managed:kimi-code".oauth]
46
+ storage = "file"
47
+ key = "oauth/kimi-code"
48
+
49
+ [models."${model}"]
50
+ provider = "managed:kimi-code"
51
+ model = "${model.split('/').pop()}"
52
+ max_context_size = 1048576
53
+ capabilities = [ "thinking", "always_thinking", "image_in", "video_in", "tool_use" ]
54
+ display_name = "${model}"
55
+ support_efforts = [ "low", "high", "max" ]
56
+ default_effort = "${thinkingEffort}"
57
+ `;
58
+ }
59
+ /**
60
+ * Redact every api_key value so the scoped copy carries no secrets. The
61
+ * kimi-managed provider keeps an empty api_key (its auth routes through
62
+ * oauth), which is exactly what an empty value means here.
63
+ * @param config - the user's config text.
64
+ * @returns the same config with all api_key values blanked.
65
+ */
66
+ export function redactApiKeys(config) {
67
+ return config.replace(/api_key\s*=\s*"[^"]*"/gu, 'api_key = ""');
68
+ }
69
+ /**
70
+ * Provision `<homeDir>/config.toml` unless one already exists. A present
71
+ * config is respected untouched (a person may have edited it); otherwise the
72
+ * user's own real config is copied redacted so the scoped home mirrors the
73
+ * user's models; with no real config, a minimal kimi-managed config is
74
+ * written. `thinkingEffort` feeds the minimal config's `[thinking] effort`
75
+ * (and the model's `default_effort`) — it applies at provision time only,
76
+ * never retroactively to an existing config.
77
+ * @param homeDir - the `kimi` harness's scoped home.
78
+ * @param model - the kimi-managed model id for a fresh minimal config.
79
+ * @param thinkingEffort - the reasoning effort a fresh minimal config pins.
80
+ * @returns true when a config was written, false when one already existed.
81
+ */
82
+ export async function provisionKimiConfig(homeDir, model, thinkingEffort) {
83
+ try {
84
+ await readFile(join(homeDir, 'config.toml'));
85
+ return false;
86
+ }
87
+ catch (error) {
88
+ // No config yet: fall through to provisioning.
89
+ if (error.code !== 'ENOENT')
90
+ throw error;
91
+ }
92
+ let config = minimalConfig(model, thinkingEffort);
93
+ try {
94
+ config = redactApiKeys(await readFile(REAL_CONFIG, 'utf8'));
95
+ }
96
+ catch (error) {
97
+ // No user config to mirror; the minimal managed config stands.
98
+ if (error.code !== 'ENOENT')
99
+ throw error;
100
+ }
101
+ // `wx` refuses to overwrite a config that appeared since the read check.
102
+ await writeFile(join(homeDir, 'config.toml'), config, { flag: 'wx' });
103
+ return true;
104
+ }
105
+ /** The managed-service provider section provisioned into the scoped config. */
106
+ const MANAGED_PROVIDER_SECTION = 'providers."managed:kimi-code"';
107
+ /**
108
+ * Read the scoped config's effective LLM endpoint: the
109
+ * `[providers."managed:kimi-code"].base_url` key, if present. A missing or
110
+ * malformed config yields undefined (the CLI falls back to its default
111
+ * endpoint). Used only for diagnostics — the config itself is authoritative,
112
+ * and a user-edited value is respected untouched.
113
+ * @param homeDir - the `kimi` harness's scoped home.
114
+ * @returns the configured base URL, or undefined when absent/unreadable.
115
+ */
116
+ export async function readKimiBaseUrl(homeDir) {
117
+ let text;
118
+ try {
119
+ text = await readFile(join(homeDir, 'config.toml'), 'utf8');
120
+ }
121
+ catch {
122
+ return undefined;
123
+ }
124
+ // Section-aware line scan: key order inside the provider table is the
125
+ // writer's choice, so base_url must be found wherever it sits in the
126
+ // section — a diagnostic that silently reports the default would mislead.
127
+ let section = '';
128
+ for (const rawLine of text.split('\n')) {
129
+ const line = rawLine.trim();
130
+ const header = /^\[+([^\]]+)\]+$/.exec(line);
131
+ if (header !== null) {
132
+ section = header[1];
133
+ continue;
134
+ }
135
+ if (section === MANAGED_PROVIDER_SECTION) {
136
+ const match = /^base_url\s*=\s*"([^"]*)"$/.exec(line);
137
+ if (match !== null)
138
+ return match[1];
139
+ }
140
+ }
141
+ return undefined;
142
+ }
143
+ /**
144
+ * Read the scoped config's configured default model: the top-level
145
+ * `default_model` key — the selection a plain `kimi -p` round runs with. A
146
+ * missing or malformed config, or no key, yields undefined — the honest
147
+ * "unknown", never a guessed default. Read-only: the config is authoritative
148
+ * and a user-edited value is reported as-is.
149
+ * @param homeDir - the `kimi` harness's scoped home.
150
+ * @returns the configured model identifier, or undefined when none is set.
151
+ */
152
+ export async function readKimiDefaultModel(homeDir) {
153
+ let text;
154
+ try {
155
+ text = await readFile(join(homeDir, 'config.toml'), 'utf8');
156
+ }
157
+ catch {
158
+ return undefined;
159
+ }
160
+ // default_model is a top-level key, so it only counts before the first
161
+ // table header — the same rule the other readers apply.
162
+ let section = '';
163
+ for (const rawLine of text.split('\n')) {
164
+ const line = rawLine.trim();
165
+ const header = /^\[+([^\]]+)\]+$/.exec(line);
166
+ if (header !== null) {
167
+ section = header[1];
168
+ continue;
169
+ }
170
+ if (section !== '')
171
+ continue;
172
+ const key = /^(\w+)\s*=\s*"([^"]*)"$/.exec(line);
173
+ if (key !== null && key[1] === 'default_model')
174
+ return key[2];
175
+ }
176
+ return undefined;
177
+ }
178
+ /**
179
+ * List the model identifiers the scoped config DEFINES: every
180
+ * `[models."…"]` table key (bare `[models.x]` keys included). This is the
181
+ * pickable vocabulary a model surface can offer without hardcoding a catalog —
182
+ * the same table `-m` resolves against. A missing or malformed config yields
183
+ * an empty list. Read-only: the config is authoritative.
184
+ * @param homeDir - the `kimi` harness's scoped home.
185
+ * @returns the defined model identifiers, in file order, deduplicated.
186
+ */
187
+ export async function listKimiConfigModels(homeDir) {
188
+ let text;
189
+ try {
190
+ text = await readFile(join(homeDir, 'config.toml'), 'utf8');
191
+ }
192
+ catch {
193
+ return [];
194
+ }
195
+ const models = [];
196
+ for (const rawLine of text.split('\n')) {
197
+ const header = /^\[+([^\]]+)\]+$/.exec(rawLine.trim());
198
+ if (header === null)
199
+ continue;
200
+ const table = /^models\.(?:"([^"]+)"|([\w-]+))$/.exec(header[1]);
201
+ const name = table?.[1] ?? table?.[2];
202
+ if (name !== undefined && name !== '' && !models.includes(name))
203
+ models.push(name);
204
+ }
205
+ return models;
206
+ }
207
+ /**
208
+ * Rewrite the scoped config's top-level `default_model` to `model`, so the
209
+ * NEXT process started under this home runs that model. This is the resident
210
+ * path's only lever: `kimi acp` takes no model flag (verified against kimi
211
+ * 0.39.1), while `kimi -p` takes `-m` — so the one-shot rounds pass the flag
212
+ * and the live driver writes the file before spawning its runtime.
213
+ *
214
+ * The write is surgical: the first top-level `default_model` assignment is
215
+ * replaced in place and everything else — model tables, providers, thinking
216
+ * effort, comments, key order — is left byte-identical. A config with no
217
+ * top-level `default_model` gains one as its first line; an absent or
218
+ * unreadable config is left alone (provisioning owns creation, and inventing
219
+ * a config here would hide a broken home).
220
+ * @param homeDir - the `kimi` harness's scoped home.
221
+ * @param model - the model identifier to pin as the default.
222
+ * @returns true when the file now names `model`, false when there was no
223
+ * config to write into.
224
+ */
225
+ export async function writeKimiDefaultModel(homeDir, model) {
226
+ const path = join(homeDir, 'config.toml');
227
+ let text;
228
+ try {
229
+ text = await readFile(path, 'utf8');
230
+ }
231
+ catch {
232
+ return false;
233
+ }
234
+ // TOML basic strings take backslash escapes; a model id with a quote or a
235
+ // backslash would otherwise produce a config kimi cannot parse.
236
+ const assignment = `default_model = "${model.replace(/\\/gu, '\\\\').replace(/"/gu, '\\"')}"`;
237
+ const lines = text.split('\n');
238
+ let section = '';
239
+ for (const [index, rawLine] of lines.entries()) {
240
+ const line = rawLine.trim();
241
+ const header = /^\[+([^\]]+)\]+$/.exec(line);
242
+ if (header !== null) {
243
+ section = header[1];
244
+ continue;
245
+ }
246
+ // Top-level only, exactly as readKimiDefaultModel scans for it.
247
+ if (section !== '')
248
+ continue;
249
+ const key = /^(\w+)\s*=/.exec(line);
250
+ if (key === null || key[1] !== 'default_model')
251
+ continue;
252
+ if (line === assignment)
253
+ return true;
254
+ lines[index] = assignment;
255
+ await writeFile(path, lines.join('\n'), 'utf8');
256
+ return true;
257
+ }
258
+ await writeFile(path, `${assignment}\n${text}`, 'utf8');
259
+ return true;
260
+ }
261
+ /**
262
+ * Read the scoped config's effective reasoning effort: the `[thinking] effort`
263
+ * key, falling back to a `[models."…"]` section's `default_effort` when the
264
+ * thinking table carries none (the CLI resolves effort the same way). A
265
+ * missing or malformed config, or no effort key anywhere, yields undefined —
266
+ * the honest "unknown", never a guessed default. Read-only: the config is
267
+ * authoritative, a user-edited value is reported as-is.
268
+ * @param homeDir - the `kimi` harness's scoped home.
269
+ * @returns the effective effort, or undefined when it cannot be determined.
270
+ */
271
+ export async function readKimiReasoningEffort(homeDir) {
272
+ let text;
273
+ try {
274
+ text = await readFile(join(homeDir, 'config.toml'), 'utf8');
275
+ }
276
+ catch {
277
+ return undefined;
278
+ }
279
+ // Section-aware line scan (same shape as readKimiBaseUrl): effort must be
280
+ // found wherever it sits in its section, and only the thinking table's own
281
+ // effort counts before any model default does.
282
+ let section = '';
283
+ let thinkingEffort;
284
+ let modelDefaultEffort;
285
+ for (const rawLine of text.split('\n')) {
286
+ const line = rawLine.trim();
287
+ const header = /^\[+([^\]]+)\]+$/.exec(line);
288
+ if (header !== null) {
289
+ section = header[1];
290
+ continue;
291
+ }
292
+ const key = /^(\w+)\s*=\s*"([^"]*)"$/.exec(line);
293
+ if (key === null)
294
+ continue;
295
+ if (section === 'thinking' && key[1] === 'effort' && thinkingEffort === undefined)
296
+ thinkingEffort = key[2];
297
+ if (section.startsWith('models.') && key[1] === 'default_effort' && modelDefaultEffort === undefined) {
298
+ modelDefaultEffort = key[2];
299
+ }
300
+ }
301
+ return thinkingEffort ?? modelDefaultEffort;
302
+ }
303
+ /**
304
+ * Whether the scoped config's permission rules auto-approve the subagent's
305
+ * tool use: the `Bash(*)` allow rule the provisioning gate
306
+ * ({@link ensureKimiPermissions}) keys on, detected with the exact same
307
+ * substring so the snapshot and the provisioning can never disagree about
308
+ * the same file.
309
+ * @param homeDir - the `kimi` harness's scoped home.
310
+ * @returns whether the auto-approve rule is present.
311
+ */
312
+ export async function readKimiAutoApprove(homeDir) {
313
+ let text;
314
+ try {
315
+ text = await readFile(join(homeDir, 'config.toml'), 'utf8');
316
+ }
317
+ catch {
318
+ return false;
319
+ }
320
+ return text.includes('pattern = "Bash(*)');
321
+ }
322
+ /** Permission rule block letting the kimi subagent run shell commands. */
323
+ const KIMI_PERMISSION_BLOCK = `
324
+ [[permission.rules]]
325
+ decision = "allow"
326
+ pattern = "Bash(*)"
327
+ reason = "let the kimi subagent run shell commands"
328
+ `;
329
+ /**
330
+ * Ensure the scoped config allows the subagent's tool use. The one-shot
331
+ * `kimi -p` child uses the config's permission rules, so without a rule it
332
+ * answers without executing tools. Idempotent; an existing rule is respected.
333
+ * @param homeDir - the `kimi` harness's scoped home.
334
+ */
335
+ export async function ensureKimiPermissions(homeDir) {
336
+ const file = join(homeDir, 'config.toml');
337
+ let content;
338
+ try {
339
+ content = await readFile(file, 'utf8');
340
+ }
341
+ catch (error) {
342
+ // No config yet; provisioning will write one with the rules when it runs.
343
+ if (error.code === 'ENOENT')
344
+ return;
345
+ throw error;
346
+ }
347
+ if (content.includes('pattern = "Bash(*)'))
348
+ return;
349
+ await writeFile(file, `${content.trimEnd()}\n${KIMI_PERMISSION_BLOCK}`);
350
+ }
351
+ /**
352
+ * Sign out of the scoped account: remove the credentials and OAuth token
353
+ * cache, so `/<name> status` reports not authenticated and the next login
354
+ * authorizes a fresh account. The kimi CLI has no `logout` command, so the
355
+ * scoped files are the logout path.
356
+ * @param homeDir - the `kimi` harness's scoped home.
357
+ */
358
+ export async function kimiLogout(homeDir) {
359
+ await Promise.all([
360
+ rm(join(homeDir, CREDENTIALS_DIR), { recursive: true, force: true }),
361
+ rm(join(homeDir, OAUTH_DIR), { recursive: true, force: true }),
362
+ ]);
363
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Kimi Code session records adapter: reads the scoped home's
3
+ * `session_index.jsonl`, kimi's own append-only index.
4
+ * @module @khorsheed/dsh-local-agent-kimi/records
5
+ */
6
+ import type { LocalAgentSessionRecord } from '@khorsheed/dsh-local-agent';
7
+ /**
8
+ * Whether the scoped home holds usable login credentials, guarded by the
9
+ * credential sentinel: a VALID credential file (both tokens non-empty) reads
10
+ * true and refreshes the backup; an empty shell (the kimi CLI's failure-path
11
+ * wipe) restores the backup when one exists and reads false otherwise. The
12
+ * pre-sentinel "directory non-empty" check could not tell the empty shell
13
+ * from a real credential at all.
14
+ * @param homeDir - the `kimi` harness's scoped home.
15
+ * @returns true when usable credentials are present (after any restore).
16
+ */
17
+ export declare function kimiAuthenticated(homeDir: string): Promise<boolean>;
18
+ /**
19
+ * The newest credential file's modification stamp (epoch ms), undefined when
20
+ * no credential exists. A completed device-code login rewrites the
21
+ * credentials directory, so the stamp distinguishes a fresh login from a
22
+ * leftover (possibly expired) credential.
23
+ * @param homeDir - the `kimi` harness's scoped home.
24
+ * @returns the newest credential mtime, or undefined when none exists.
25
+ */
26
+ export declare function kimiCredentialStamp(homeDir: string): Promise<number | undefined>;
27
+ /**
28
+ * Read the scoped home's session index. The index is kimi's own
29
+ * newline-delimited JSONL; a torn tail line (the file is append-only) is
30
+ * skipped rather than failing the whole list.
31
+ * @param homeDir - the `kimi` harness's scoped home.
32
+ * @returns the parsed records in file order, empty when no index exists yet.
33
+ */
34
+ export declare function listKimiSessions(homeDir: string): Promise<readonly LocalAgentSessionRecord[]>;
35
+ //# sourceMappingURL=records.d.ts.map
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Kimi Code session records adapter: reads the scoped home's
3
+ * `session_index.jsonl`, kimi's own append-only index.
4
+ * @module @khorsheed/dsh-local-agent-kimi/records
5
+ */
6
+ import { readdir, readFile, stat } from 'node:fs/promises';
7
+ import { join } from 'node:path';
8
+ import { guardKimiCredential } from './credential-guard.js';
9
+ /** Credentials live under this directory inside the scoped home. */
10
+ const CREDENTIALS_DIR = 'credentials';
11
+ /**
12
+ * Whether the scoped home holds usable login credentials, guarded by the
13
+ * credential sentinel: a VALID credential file (both tokens non-empty) reads
14
+ * true and refreshes the backup; an empty shell (the kimi CLI's failure-path
15
+ * wipe) restores the backup when one exists and reads false otherwise. The
16
+ * pre-sentinel "directory non-empty" check could not tell the empty shell
17
+ * from a real credential at all.
18
+ * @param homeDir - the `kimi` harness's scoped home.
19
+ * @returns true when usable credentials are present (after any restore).
20
+ */
21
+ export async function kimiAuthenticated(homeDir) {
22
+ return guardKimiCredential(homeDir);
23
+ }
24
+ /**
25
+ * The newest credential file's modification stamp (epoch ms), undefined when
26
+ * no credential exists. A completed device-code login rewrites the
27
+ * credentials directory, so the stamp distinguishes a fresh login from a
28
+ * leftover (possibly expired) credential.
29
+ * @param homeDir - the `kimi` harness's scoped home.
30
+ * @returns the newest credential mtime, or undefined when none exists.
31
+ */
32
+ export async function kimiCredentialStamp(homeDir) {
33
+ try {
34
+ const entries = await readdir(join(homeDir, CREDENTIALS_DIR));
35
+ let newest;
36
+ for (const entry of entries) {
37
+ const { mtimeMs } = await stat(join(homeDir, CREDENTIALS_DIR, entry));
38
+ if (newest === undefined || mtimeMs > newest)
39
+ newest = mtimeMs;
40
+ }
41
+ return newest;
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ /**
48
+ * Read the scoped home's session index. The index is kimi's own
49
+ * newline-delimited JSONL; a torn tail line (the file is append-only) is
50
+ * skipped rather than failing the whole list.
51
+ * @param homeDir - the `kimi` harness's scoped home.
52
+ * @returns the parsed records in file order, empty when no index exists yet.
53
+ */
54
+ export async function listKimiSessions(homeDir) {
55
+ const indexPath = join(homeDir, 'session_index.jsonl');
56
+ let text;
57
+ try {
58
+ text = await readFile(indexPath, 'utf8');
59
+ }
60
+ catch (error) {
61
+ // No index yet: the scoped home has never run a session. Nothing to list.
62
+ if (error.code === 'ENOENT')
63
+ return [];
64
+ throw error;
65
+ }
66
+ const records = [];
67
+ for (const line of text.split('\n')) {
68
+ const trimmed = line.trim();
69
+ if (trimmed === '')
70
+ continue;
71
+ try {
72
+ const parsed = JSON.parse(trimmed);
73
+ records.push({
74
+ id: parsed.sessionId,
75
+ workDir: parsed.workDir,
76
+ ...parsed.title !== undefined && typeof parsed.title === 'string' && { title: parsed.title },
77
+ ...parsed.createdAt !== undefined && typeof parsed.createdAt === 'string' && { startedAt: Date.parse(parsed.createdAt) },
78
+ });
79
+ }
80
+ catch {
81
+ // A torn tail line from an in-flight append; skip it and keep the rest.
82
+ }
83
+ }
84
+ return records;
85
+ }
@@ -0,0 +1,5 @@
1
+ import type { LocalAgentResolvedConfiguration } from '@khorsheed/dsh-local-agent/types';
2
+ import { type KimiNativeConfiguration } from './model-catalog.ts';
3
+ /** Confirm model first: its returned options determine which efforts are valid. */
4
+ export declare function configureKimiSession(sessionId: string, initial: KimiNativeConfiguration | undefined, desired: LocalAgentResolvedConfiguration, request: (method: string, params: Record<string, unknown>) => Promise<unknown>): Promise<KimiNativeConfiguration | undefined>;
5
+ //# sourceMappingURL=session-configuration.d.ts.map
@@ -0,0 +1,26 @@
1
+ import { kimiNativeConfiguration } from './model-catalog.js';
2
+ /** Confirm model first: its returned options determine which efforts are valid. */
3
+ export async function configureKimiSession(sessionId, initial, desired, request) {
4
+ let current = initial;
5
+ if (desired.model !== undefined && current?.currentModel !== desired.model) {
6
+ if (current?.modelConfigId === undefined)
7
+ throw new Error('This Kimi ACP session exposes no verified model selection control');
8
+ if (!current.directory.entries.some(entry => entry.value === desired.model))
9
+ throw new Error('The running Kimi session does not advertise this model');
10
+ current = kimiNativeConfiguration(await request('session/set_config_option', { sessionId, configId: current.modelConfigId, value: desired.model }));
11
+ if (current?.currentModel !== desired.model)
12
+ throw new Error('Kimi did not confirm the requested model');
13
+ }
14
+ if (desired.effort !== undefined && current?.currentEffort !== desired.effort) {
15
+ const currentModel = current?.currentModel;
16
+ const entry = current?.directory.entries.find(entry => entry.value === currentModel);
17
+ if (current?.effortConfigId === undefined || !entry?.reasoning?.options.some(option => option.value === desired.effort)) {
18
+ throw new Error('The running Kimi session does not advertise this model/effort combination');
19
+ }
20
+ current = kimiNativeConfiguration(await request('session/set_config_option', { sessionId, configId: current.effortConfigId, value: desired.effort }));
21
+ if (current?.currentEffort !== desired.effort || (desired.model !== undefined && current.currentModel !== desired.model)) {
22
+ throw new Error('Kimi did not confirm the requested model/effort configuration');
23
+ }
24
+ }
25
+ return current;
26
+ }