@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,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 */
|