@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.
- package/README.en.md +132 -0
- package/README.i18n.yaml +6 -0
- package/README.md +132 -0
- package/cordis.patch.yml +34 -0
- package/lib/client.js +1066 -0
- package/lib/index.js +4716 -0
- package/lib/invariant.js +31 -0
- package/lib/types/client/SettingsCard.d.ts +77 -0
- package/lib/types/client/SettingsCard.js +197 -0
- package/lib/types/client/icons.d.ts +18 -0
- package/lib/types/client/icons.js +2 -0
- package/lib/types/client/index.d.ts +42 -0
- package/lib/types/client/index.js +89 -0
- package/lib/types/client/locales.d.ts +64 -0
- package/lib/types/client/locales.js +55 -0
- package/lib/types/credential-guard.d.ts +29 -0
- package/lib/types/credential-guard.js +107 -0
- package/lib/types/index.d.ts +69 -0
- package/lib/types/index.js +270 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +33 -0
- package/lib/types/kimi-cli-provider.d.ts +215 -0
- package/lib/types/kimi-cli-provider.js +855 -0
- package/lib/types/live-driver.d.ts +245 -0
- package/lib/types/live-driver.js +1312 -0
- package/lib/types/live-switch.d.ts +78 -0
- package/lib/types/live-switch.js +112 -0
- package/lib/types/member-bridge-config.d.ts +43 -0
- package/lib/types/member-bridge-config.js +80 -0
- package/lib/types/model-broker.d.ts +97 -0
- package/lib/types/model-broker.js +205 -0
- package/lib/types/model-catalog.d.ts +26 -0
- package/lib/types/model-catalog.js +86 -0
- package/lib/types/model-configuration.d.ts +11 -0
- package/lib/types/model-configuration.js +55 -0
- package/lib/types/preset-tools.d.ts +20 -0
- package/lib/types/preset-tools.js +36 -0
- package/lib/types/provision.d.ts +131 -0
- package/lib/types/provision.js +363 -0
- package/lib/types/records.d.ts +35 -0
- package/lib/types/records.js +85 -0
- package/lib/types/session-configuration.d.ts +5 -0
- package/lib/types/session-configuration.js +26 -0
- package/lib/types/session-mirror.d.ts +180 -0
- package/lib/types/session-mirror.js +446 -0
- package/lib/types/session-view.d.ts +121 -0
- package/lib/types/session-view.js +423 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- 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
|
+
}
|