@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,107 @@
1
+ /**
2
+ * Kimi credential sentinel. The kimi CLI occasionally wipes
3
+ * `credentials/kimi-code.json` into an empty shell (both tokens zero-length)
4
+ * on its own failure paths — observed twice in production, each time killing
5
+ * every later delegation with "Authentication required" until someone
6
+ * restored the file by hand. The sentinel:
7
+ *
8
+ * - backs the file up whenever a VALID credential is observed (the backup
9
+ * lives inside `credentials/` so the family's logout — a directory delete —
10
+ * clears it too);
11
+ * - restores the backup when the file is an exact empty shell (both tokens
12
+ * empty strings) — the only corruption signature we trust;
13
+ * - logs every state transition with a process snapshot, so the next wipe
14
+ * can be attributed (which process, at which moment).
15
+ *
16
+ * Restore is deliberately narrow: a revoked-elsewhere credential never takes
17
+ * the empty-shell shape, and a stale backup at worst produces one more loud
18
+ * 401, never silent misbehavior.
19
+ * @module @khorsheed/dsh-local-agent-kimi/credential-guard
20
+ */
21
+ import { copyFile, readFile, stat, writeFile } from 'node:fs/promises';
22
+ import { execFile } from 'node:child_process';
23
+ import { join } from 'node:path';
24
+ /** The credential file inside the scoped home, and its sentinel backup. */
25
+ const CREDENTIAL_REL = join('credentials', 'kimi-code.json');
26
+ const BACKUP_REL = `${CREDENTIAL_REL}.bak`;
27
+ /** Last observed state signature per home (`${mtimeMs}:${kind}`) — transition logging. */
28
+ const lastSeen = new Map();
29
+ /** A one-line process snapshot for wipe attribution (kimi/dsh processes only). */
30
+ function processSnapshot() {
31
+ return new Promise((resolve) => {
32
+ execFile('ps', ['-eo', 'pid,ppid,etime,command'], { timeout: 5_000 }, (error, stdout) => {
33
+ if (error !== null) {
34
+ resolve(`(ps failed: ${error.message})`);
35
+ return;
36
+ }
37
+ const lines = stdout.split('\n').filter((line) => /\bkimi\b|dsh/.test(line) && !line.includes('grep')).slice(0, 12);
38
+ resolve(lines.join(' ; '));
39
+ });
40
+ });
41
+ }
42
+ /** Classify the credential file's current content. */
43
+ async function observe(file) {
44
+ let raw;
45
+ try {
46
+ raw = await readFile(file, 'utf8');
47
+ }
48
+ catch {
49
+ return { state: 'absent', mtimeMs: 0 };
50
+ }
51
+ let mtimeMs = 0;
52
+ try {
53
+ mtimeMs = (await stat(file)).mtimeMs;
54
+ }
55
+ catch { /* keep 0 */ }
56
+ try {
57
+ const parsed = JSON.parse(raw);
58
+ const empty = (v) => typeof v === 'string' && v.length === 0;
59
+ if (empty(parsed.access_token) && empty(parsed.refresh_token))
60
+ return { state: 'empty-shell', mtimeMs };
61
+ return { state: 'valid', mtimeMs };
62
+ }
63
+ catch {
64
+ // Unparseable is neither valid nor the known shell: treat as absent and
65
+ // never restore over it (unknown corruption shape — leave it for a human).
66
+ return { state: 'absent', mtimeMs };
67
+ }
68
+ }
69
+ /**
70
+ * Guard the scoped home's kimi credential: back up a valid file, restore the
71
+ * backup over an empty shell, log transitions.
72
+ * @param homeDir - the `kimi` harness's scoped home.
73
+ * @param log - warn channel (plugin logger when available).
74
+ * @returns true when a usable credential exists after the guard ran.
75
+ */
76
+ export async function guardKimiCredential(homeDir, log) {
77
+ const file = join(homeDir, CREDENTIAL_REL);
78
+ const backup = join(homeDir, BACKUP_REL);
79
+ const { state, mtimeMs } = await observe(file);
80
+ const signature = `${mtimeMs}:${state}`;
81
+ const previous = lastSeen.get(homeDir);
82
+ lastSeen.set(homeDir, signature);
83
+ if (state === 'valid') {
84
+ if (previous !== undefined && previous !== signature) {
85
+ log?.(`local-agent-kimi: credential refreshed (mtime ${new Date(mtimeMs).toISOString()}), snapshotting the backup`);
86
+ }
87
+ await writeFile(backup, await readFile(file), { mode: 0o600 });
88
+ return true;
89
+ }
90
+ if (state === 'empty-shell') {
91
+ const snapshot = await processSnapshot();
92
+ log?.(`local-agent-kimi: CREDENTIAL EMPTY SHELL detected (mtime ${new Date(mtimeMs).toISOString()}); previous state ${previous ?? 'unknown'}; processes: ${snapshot}`);
93
+ let backupState = 'absent';
94
+ try {
95
+ backupState = (await observe(backup)).state;
96
+ }
97
+ catch { /* absent */ }
98
+ if (backupState === 'valid') {
99
+ await copyFile(backup, file);
100
+ log?.('local-agent-kimi: credential restored from the sentinel backup — the next delegation should succeed');
101
+ return true;
102
+ }
103
+ log?.('local-agent-kimi: no usable backup to restore; the harness needs a fresh login');
104
+ return false;
105
+ }
106
+ return false;
107
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Kimi Code harness instantiation of the local-agent family. Registers the
3
+ * `kimi` harness into the core registry: scoped home under the shared homes
4
+ * root (`KIMI_CODE_HOME`), device-code login through `kimi login`, and the
5
+ * `session_index.jsonl` records adapter. Delegation mounts through the
6
+ * `kimi-cli` provider (one-shot `kimi -p` under the scoped home); the
7
+ * bundle patch's `tool-subagent-kimi` row puts the `subagent_kimi` tool on
8
+ * the profile root, so every agent preset can delegate without variants.
9
+ *
10
+ * @module @khorsheed/dsh-local-agent-kimi
11
+ */
12
+ import type { Context } from '@deepseek-ai/cordis';
13
+ import z from '@deepseek-ai/schemastery';
14
+ /** Stable Cordis plugin name; the bundle patch row id. */
15
+ export declare const name = "local-agent-kimi";
16
+ /** Services required before the harness can register. */
17
+ export declare const inject: string[];
18
+ /** Plugin config: the model every round runs, plus the live driver. */
19
+ export interface Config {
20
+ /**
21
+ * The model every delegation round starts the CLI with (`kimi -m <model>`;
22
+ * the resident `kimi acp`, which takes no model flag, gets the scoped
23
+ * config's `default_model` rewritten before each spawn instead). The value
24
+ * must name a model the scoped config.toml defines (a `[models."…"]` key).
25
+ *
26
+ * Absent — the default — passes no model at all: `default_model` decides,
27
+ * exactly as it did before this key rode every round. The settings card
28
+ * writes the same key, so a change applies to the next round without a
29
+ * reload.
30
+ *
31
+ * This key ALSO still seeds a FRESH scoped home's `default_model` when
32
+ * there is no user config to mirror, which is all it used to do.
33
+ */
34
+ model?: string;
35
+ /**
36
+ * Reasoning effort a FRESH scoped home's config.toml pins (`[thinking]
37
+ * effort` and the model's `default_effort`). Provision-time only: an
38
+ * existing config is respected untouched, exactly like the mirrored model.
39
+ * Default 'high' — the value the pre-config-item provisioning hardcoded,
40
+ * so the default changes nothing.
41
+ */
42
+ thinkingEffort?: 'low' | 'high' | 'max';
43
+ /**
44
+ * Live driver: keep one resident `kimi acp` process per member and drive
45
+ * turns over ACP (runtime-level graceful cancel, push-triggered mirroring)
46
+ * instead of one `kimi -p` process per round. Default off; the exec
47
+ * one-shot stays the fallback whenever the channel cannot come up.
48
+ */
49
+ live?: boolean;
50
+ /** Idle lifetime of an unused resident runtime before reclaim. */
51
+ liveIdleMs?: number;
52
+ /** @deprecated Accepted for old profiles; live output is always incremental. */
53
+ liveMirrorGranularity?: 'event' | 'token';
54
+ /** Model identifiers the settings card saved before (settings field). */
55
+ recentModels?: readonly string[];
56
+ }
57
+ export declare const Config: z;
58
+ /**
59
+ * Settings namespace for the settings-page card. The Cordis config feeds the
60
+ * composition `base` layer, so a field absent from the user layer inherits the
61
+ * YAML value — the card only ever stores deliberate overrides.
62
+ */
63
+ export declare const KIMI_SETTINGS_NAMESPACE = "local-agent-kimi";
64
+ /**
65
+ * Register the Kimi harness into the local-agent registry.
66
+ * @param ctx - plugin context carrying the registry.
67
+ */
68
+ export declare function apply(ctx: Context, config: Config): void;
69
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Kimi Code harness instantiation of the local-agent family. Registers the
3
+ * `kimi` harness into the core registry: scoped home under the shared homes
4
+ * root (`KIMI_CODE_HOME`), device-code login through `kimi login`, and the
5
+ * `session_index.jsonl` records adapter. Delegation mounts through the
6
+ * `kimi-cli` provider (one-shot `kimi -p` under the scoped home); the
7
+ * bundle patch's `tool-subagent-kimi` row puts the `subagent_kimi` tool on
8
+ * the profile root, so every agent preset can delegate without variants.
9
+ *
10
+ * @module @khorsheed/dsh-local-agent-kimi
11
+ */
12
+ import z from '@deepseek-ai/schemastery';
13
+ import { settingsFace, vol } from '@khorsheed/dsh-local-agent';
14
+ import { endpointHost } from '@khorsheed/dsh-local-agent/types';
15
+ import { KimiCliProvider, kimiCliVersion } from './kimi-cli-provider.js';
16
+ import { DEFAULT_LIVE_IDLE_MS } from './live-driver.js';
17
+ import { LiveDriverSwitch } from './live-switch.js';
18
+ import { KimiModelCatalog } from './model-catalog.js';
19
+ import { KimiModelBroker } from './model-broker.js';
20
+ import { kimiAuthenticated, kimiCredentialStamp, listKimiSessions } from './records.js';
21
+ import { removeLegacyVariants } from './preset-tools.js';
22
+ import { DEFAULT_THINKING_EFFORT, ensureKimiPermissions, kimiLogout, KIMI_MANAGED_BASE_URL, provisionKimiConfig, readKimiAutoApprove, readKimiBaseUrl, readKimiDefaultModel, readKimiReasoningEffort, } from './provision.js';
23
+ import { findKimiSessionDir, readKimiTranscript, renderTranscript } from './session-view.js';
24
+ /** Stable Cordis plugin name; the bundle patch row id. */
25
+ export const name = 'local-agent-kimi';
26
+ /** Services required before the harness can register. */
27
+ export const inject = ['localAgent', 'subagents', 'subprocess', 'settings'];
28
+ /**
29
+ * The settings fields (`live`, `liveMirrorGranularity`, `model`,
30
+ * `recentModels`) ride the plugin Config, marked `.volatile()` where the
31
+ * host's schemastery has it (3.18.4+, the rc.1 line — probed, never sniffed):
32
+ * there the SettingsForms writes hot-track the running fiber's Volatile
33
+ * references without a remount, and the official one-shot import moves an old
34
+ * settings.yaml section into this row's config. On 0.1.5 the call is absent,
35
+ * the fields stay plain config keys (a semantic superset — the profile row
36
+ * can now preset them too), and the legacy settings namespace below carries
37
+ * the user layer exactly as before.
38
+ */
39
+ const SETTINGS_FIELDS = {
40
+ live: z.boolean().default(false),
41
+ liveMirrorGranularity: z.union([z.const('event'), z.const('token')]).default('token'),
42
+ // `model` deliberately carries NO default: an unset key must resolve to
43
+ // undefined, which is what keeps the pre-key behavior byte-identical.
44
+ model: z.string(),
45
+ recentModels: z.array(z.string()).default([]),
46
+ };
47
+ /** Mark one schema field volatile when the host's schemastery has the method (rc.1), pass it through plain when not (0.1.5). */
48
+ function volatilize(field) {
49
+ return typeof field.volatile === 'function' ? field.volatile() : field;
50
+ }
51
+ // Bare `z` annotation: `z<Config>` fails schemastery 3.18.4's variance under
52
+ // exactOptionalPropertyTypes (TS2375) and dropping the annotation trips
53
+ // TS2742; the interface stays the apply signature's contract.
54
+ export const Config = z.object({
55
+ model: volatilize(SETTINGS_FIELDS.model),
56
+ thinkingEffort: z.union([z.const('low'), z.const('high'), z.const('max')]),
57
+ live: volatilize(SETTINGS_FIELDS.live),
58
+ liveIdleMs: z.number().default(DEFAULT_LIVE_IDLE_MS),
59
+ liveMirrorGranularity: volatilize(SETTINGS_FIELDS.liveMirrorGranularity),
60
+ recentModels: volatilize(SETTINGS_FIELDS.recentModels),
61
+ });
62
+ /**
63
+ * Settings namespace for the settings-page card. The Cordis config feeds the
64
+ * composition `base` layer, so a field absent from the user layer inherits the
65
+ * YAML value — the card only ever stores deliberate overrides.
66
+ */
67
+ export const KIMI_SETTINGS_NAMESPACE = 'local-agent-kimi';
68
+ /**
69
+ * The card's schema; field defaults are the innermost layer below `base`.
70
+ * `model` deliberately carries NO default: an unset key must resolve to
71
+ * undefined, which is what keeps the pre-key behavior byte-identical.
72
+ */
73
+ const KIMI_SETTINGS_SCHEMA = z.object(SETTINGS_FIELDS);
74
+ /**
75
+ * Handle the harness's extra subcommands: `session <id>` renders a kimi
76
+ * session transcript. Returns synchronously with undefined for any input
77
+ * that is not this harness's own.
78
+ * @param input - the trimmed subcommand input.
79
+ * @param invocation - the full command invocation.
80
+ * @returns the command result promise, or undefined when not this harness's.
81
+ */
82
+ function handleSubcommand(input, invocation) {
83
+ const [verb] = input.split(/\s+/);
84
+ if (verb === 'session')
85
+ return handleSessionSubcommand(input, invocation);
86
+ return undefined;
87
+ }
88
+ /** Run one `session` subcommand: render the transcript of a kimi session. */
89
+ async function handleSessionSubcommand(input, invocation) {
90
+ const id = input.split(/\s+/)[1];
91
+ if (id === undefined) {
92
+ return { kind: 'error', text: 'Usage: /kimi session <session-id> (see /kimi sessions for ids)' };
93
+ }
94
+ const homeDir = invocation.agent.ctx.localAgent.homeDir('kimi');
95
+ const sessionDir = await findKimiSessionDir(homeDir, id);
96
+ if (sessionDir === undefined) {
97
+ return { kind: 'error', text: `no kimi session "${id}" in the scoped home` };
98
+ }
99
+ try {
100
+ const transcript = await readKimiTranscript(sessionDir);
101
+ return { kind: 'success', text: renderTranscript(transcript) };
102
+ }
103
+ catch (error) {
104
+ return { kind: 'error', text: error instanceof Error ? error.message : String(error) };
105
+ }
106
+ }
107
+ /**
108
+ * Register the Kimi harness into the local-agent registry.
109
+ * @param ctx - plugin context carrying the registry.
110
+ */
111
+ export function apply(ctx, config) {
112
+ ctx.effect(() => {
113
+ const homeDir = ctx.localAgent.homeDir('kimi');
114
+ // The ACP server refuses to authenticate without provider/model config;
115
+ // provision a redacted config once so the scoped home stands alone. The
116
+ // reasoning effort rides the same provisioning (fresh homes only).
117
+ const thinkingEffort = config.thinkingEffort ?? DEFAULT_THINKING_EFFORT;
118
+ // Earlier versions bootstrapped <base>-kimi preset variants; the tool row
119
+ // now mounts at the profile root, so remove the leftovers once.
120
+ void removeLegacyVariants(ctx).catch((error) => {
121
+ ctx.logger.warn(`local-agent-kimi: legacy preset cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
122
+ });
123
+ // The one-shot CLI provider delegates through `kimi -p` in the scoped
124
+ // home; ensure the scoped config allows the subagent's tool use. Chained
125
+ // AFTER provisioning: both used to fire concurrently and the permission
126
+ // bootstrap raced the config write — on a fresh home it could read
127
+ // ENOENT and return early, leaving the Bash(*) allow rule unwritten
128
+ // until the next boot (first-boot delegations answered without tools).
129
+ void provisionKimiConfig(homeDir, vol(config.model) ?? 'kimi-code/k3', thinkingEffort)
130
+ .then(() => ensureKimiPermissions(homeDir))
131
+ .catch((error) => {
132
+ ctx.logger.warn(`local-agent-kimi: config provisioning failed: ${error instanceof Error ? error.message : String(error)}`);
133
+ });
134
+ // The live driver is settings-driven: the settings card's toggle (user
135
+ // layer over the YAML composition base) swaps driver generations without
136
+ // a reload. Toggling OFF drains the retiring generation — new rounds fall
137
+ // back to exec, in-flight rounds finish on their runtime, idle runtimes
138
+ // are reclaimed at once. Legacy granularity settings are accepted but ignored.
139
+ // The settings face hides the two host lines: 0.1.5 serves the legacy
140
+ // namespace scope (register + base); rc.1 reads the row config's volatile
141
+ // fields and re-reads on `settings/document-updated`. Both hot-apply the
142
+ // card's writes without a reload.
143
+ const readSettings = () => {
144
+ const model = vol(config.model);
145
+ const recentModels = vol(config.recentModels);
146
+ return {
147
+ live: vol(config.live) ?? false,
148
+ liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? 'token',
149
+ ...model === undefined ? {} : { model },
150
+ ...recentModels === undefined ? {} : { recentModels },
151
+ };
152
+ };
153
+ const legacyBase = {};
154
+ for (const field of ['live', 'liveMirrorGranularity', 'model']) {
155
+ const value = vol(config[field]);
156
+ if (value !== undefined)
157
+ legacyBase[field] = value;
158
+ }
159
+ const face = settingsFace(ctx, {
160
+ namespace: KIMI_SETTINGS_NAMESPACE,
161
+ legacySchema: KIMI_SETTINGS_SCHEMA,
162
+ legacyBase,
163
+ read: readSettings,
164
+ });
165
+ // The model is read PER ROUND, not captured at apply: the settings card
166
+ // writes the same namespace field, so a change has to reach the next
167
+ // delegation without a plugin reload — exactly like the live toggle.
168
+ // Blank is not a model: a whitespace-only value means "unset", which is
169
+ // the pre-key argv.
170
+ const resolveModel = () => {
171
+ const model = face.get().model?.trim();
172
+ return model === undefined || model === '' ? undefined : model;
173
+ };
174
+ // The model broker owns the per-member state (session-level overrides,
175
+ // start models) and the gateway-facing surface. The live driver's spawn
176
+ // resolver defers to it through a closure because the broker itself needs
177
+ // the switch (a switch retires runtimes through it) — the circularity is
178
+ // construction-order only, and no round can run before both exist.
179
+ let broker;
180
+ const liveSwitch = new LiveDriverSwitch(ctx, face, config.liveIdleMs, child => broker.spawnModel(child));
181
+ const modelCatalog = new KimiModelCatalog(childSessionId => {
182
+ const record = childSessionId === undefined ? undefined : ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId);
183
+ const native = childSessionId === undefined ? undefined : liveSwitch.memberRuntimeConfiguration(childSessionId);
184
+ return {
185
+ homeDir: ctx.localAgent.homeDir('kimi', record?.scope),
186
+ ...record?.cwd === undefined ? {} : { cwd: record.cwd },
187
+ ...native === undefined ? {} : { native },
188
+ };
189
+ });
190
+ broker = new KimiModelBroker(ctx, {
191
+ homeDir: childSessionId => ctx.localAgent.homeDir('kimi', childSessionId === undefined ? undefined : (ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId))?.scope),
192
+ catalog: modelCatalog,
193
+ settingsModel: resolveModel,
194
+ recentModels: () => face.get().recentModels ?? [],
195
+ isLive: () => face.get().live,
196
+ liveSwitch,
197
+ });
198
+ const disposeProvider = ctx.subagents.registerProvider(new KimiCliProvider(ctx, liveSwitch.resolve, resolveModel, broker));
199
+ const disposeHarness = ctx.localAgent.register({
200
+ prepareMember: async ({ binding, childSession, configuration, signal }) => {
201
+ const driver = liveSwitch.resolve(binding.childSessionId);
202
+ if (driver === undefined || driver.disabled)
203
+ throw new Error('Enable the native live harness before preparing a coordinator');
204
+ await driver.prepare({ cwd: binding.cwd, homeDir: ctx.localAgent.homeDir('kimi', binding.scope), childSession,
205
+ parentSessionId: binding.parentSessionId, configuration: configuration.resolved, }, signal);
206
+ },
207
+ name: 'kimi',
208
+ displayName: 'Kimi Code',
209
+ homeEnvVar: 'KIMI_CODE_HOME',
210
+ delegationProvider: 'kimi-cli',
211
+ login: { command: 'kimi', args: ['login'] },
212
+ records: { listSessions: homeDir => listKimiSessions(homeDir) },
213
+ // A NAMED scope's directory is provisioned through this hook when the
214
+ // registry materializes it — the same provider/model config and
215
+ // permission bootstrap the default scope gets from the apply above, in
216
+ // the same order (the permission write reads the config file).
217
+ provision: async (scopedHome) => {
218
+ await provisionKimiConfig(scopedHome, vol(config.model) ?? 'kimi-code/k3', thinkingEffort);
219
+ await ensureKimiPermissions(scopedHome);
220
+ },
221
+ isAuthenticated: kimiAuthenticated,
222
+ credentialStamp: kimiCredentialStamp,
223
+ logout: kimiLogout,
224
+ subcommand: handleSubcommand,
225
+ // The model surface: the settings card reads the memberless info, the
226
+ // member composer reads/switches per member. Registered unconditionally —
227
+ // the gateway answers null only for harnesses WITHOUT one.
228
+ modelBroker: broker,
229
+ // The eval snapshot reads the scoped config as-is — it is authoritative
230
+ // once provisioned, so a person-edited (or user-mirrored) effort and
231
+ // endpoint report what actually applies, not what the config item
232
+ // would have written. The MODEL is the one exception, and the family's
233
+ // fixed order says why: a set `model` key rides every round's launch,
234
+ // so it beats the file it is about to overrule; with no key, the
235
+ // scoped `default_model` is what runs.
236
+ // The directory is a PARAMETER, not the apply-time capture: a status
237
+ // read (or an evaluation snapshot) of a named scope must report the
238
+ // config that scope's rounds would run with.
239
+ effectiveSettings: async (scopedHome) => {
240
+ const [reasoningEffort, baseUrl, autoApprove, defaultModel, cliVersion] = await Promise.all([
241
+ readKimiReasoningEffort(scopedHome).catch(() => undefined),
242
+ readKimiBaseUrl(scopedHome).catch(() => undefined),
243
+ readKimiAutoApprove(scopedHome).catch(() => false),
244
+ readKimiDefaultModel(scopedHome).catch(() => undefined),
245
+ kimiCliVersion(ctx, scopedHome).catch(() => undefined),
246
+ ]);
247
+ // The managed endpoint IS kimi's own service — routing through it is
248
+ // the default, not a pinned custom route.
249
+ const custom = baseUrl !== undefined && baseUrl !== KIMI_MANAGED_BASE_URL;
250
+ const baseUrlHost = custom ? endpointHost(baseUrl) : undefined;
251
+ const model = resolveModel() ?? defaultModel;
252
+ return {
253
+ drive: face.get().live ? 'live' : 'exec',
254
+ autoApprove,
255
+ ...reasoningEffort !== undefined ? { reasoningEffort } : {},
256
+ baseUrlSet: custom,
257
+ ...baseUrlHost !== undefined ? { baseUrlHost } : {},
258
+ ...model !== undefined ? { model } : {},
259
+ ...cliVersion !== undefined ? { cliVersion } : {},
260
+ };
261
+ },
262
+ });
263
+ return () => {
264
+ modelCatalog.dispose();
265
+ disposeProvider();
266
+ disposeHarness();
267
+ liveSwitch.dispose();
268
+ };
269
+ }, 'local-agent-kimi: harness');
270
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@khorsheed/dsh-local-agent-kimi`.
3
+ * @module @khorsheed/dsh-local-agent-kimi/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "local-agent-kimi-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - plugin context carrying the invariant registry.
13
+ * @returns the installed registration's disposer.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Package-owned invariant companion for `@khorsheed/dsh-local-agent-kimi`.
3
+ * @module @khorsheed/dsh-local-agent-kimi/invariant
4
+ */
5
+ const PACKAGE_NAME = '@khorsheed/dsh-local-agent-kimi';
6
+ /** Cordis companion plugin name. */
7
+ export const name = 'local-agent-kimi-invariant';
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export const inject = ['invariants'];
10
+ /**
11
+ * The kimi harness definition is this bundle's contract with the core
12
+ * registry: whenever a `kimi` harness registers, its scoped-home variable must
13
+ * be the one this bundle pins.
14
+ */
15
+ const install = (ctx, fail) => {
16
+ ctx.on('internal/dispatch', (_mode, eventName, args) => {
17
+ if (eventName !== 'localAgent/harness-added')
18
+ return;
19
+ const harness = args[0];
20
+ if (harness.name !== 'kimi')
21
+ return;
22
+ if (harness.homeEnvVar !== 'KIMI_CODE_HOME') {
23
+ fail('kimi harness registered with a home env other than the local-agent-kimi contract');
24
+ }
25
+ }, { global: true });
26
+ };
27
+ /**
28
+ * Register this package's invariant companion.
29
+ * @param ctx - plugin context carrying the invariant registry.
30
+ * @returns the installed registration's disposer.
31
+ */
32
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
33
+ /* jscpd:ignore-end */