@khorsheed/dsh-local-agent-kimi 0.2.0 → 0.2.2
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/CHANGELOG.md +17 -0
- package/README.en.md +1 -1
- package/README.i18n.yaml +2 -2
- package/README.md +1 -1
- package/lib/client.js +161 -76
- package/lib/index.js +176 -107
- package/lib/types/client/SettingsCard.js +11 -3
- package/lib/types/client/index.js +2 -2
- package/lib/types/index.js +4 -2
- package/lib/types/live-driver.d.ts +0 -1
- package/lib/types/live-driver.js +44 -24
- package/lib/types/member-bridge-config.d.ts +2 -0
- package/lib/types/member-bridge-config.js +5 -1
- package/lib/types/session-mirror.js +37 -3
- package/lib/types/session-view.d.ts +11 -0
- package/lib/types/session-view.js +45 -1
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -1960,6 +1960,93 @@ async function ensureKimiPermissions(homeDir) {
|
|
|
1960
1960
|
await writeFile(file, `${content.trimEnd()}\n${KIMI_PERMISSION_BLOCK}`);
|
|
1961
1961
|
}
|
|
1962
1962
|
//#endregion
|
|
1963
|
+
//#region lib/types/member-bridge-config.js
|
|
1964
|
+
/**
|
|
1965
|
+
* Kimi scoped-home MCP config injection for the member bridge. Kimi declares
|
|
1966
|
+
* stdio MCP servers in `$KIMI_CODE_HOME/mcp.json` (`mcpServers` map); the file
|
|
1967
|
+
* is read once per CLI session start, and edits never interrupt an open
|
|
1968
|
+
* session — so the family writes one entry PER RUN (keyed by the run's token
|
|
1969
|
+
* suffix, keeping concurrent delegations of the same scoped home race-free),
|
|
1970
|
+
* prunes it when the run settles, and drops stale family entries (crashed
|
|
1971
|
+
* host) on every write. User-declared servers are preserved verbatim.
|
|
1972
|
+
* @module @khorsheed/dsh-local-agent-kimi/member-bridge-config
|
|
1973
|
+
*/
|
|
1974
|
+
/** Kimi's user-level MCP config filename inside the scoped home. */
|
|
1975
|
+
const MCP_CONFIG_FILENAME = "mcp.json";
|
|
1976
|
+
/** The member bridge's per-call bound: as long as a question card waits for the human (30 min). */
|
|
1977
|
+
const MEMBER_TOOL_TIMEOUT_MS = 18e5;
|
|
1978
|
+
/** Server-key prefix marking the family's per-run bridge entries. */
|
|
1979
|
+
const MEMBER_BRIDGE_SERVER_PREFIX = "dsh-member-";
|
|
1980
|
+
/**
|
|
1981
|
+
* The per-run MCP server key. The run's token suffix keeps concurrent runs
|
|
1982
|
+
* distinct; each `kimi -p` round is a fresh CLI session, so per-run (rather
|
|
1983
|
+
* than per-member) naming costs nothing.
|
|
1984
|
+
* @param token - the run's member-channel token.
|
|
1985
|
+
* @returns the mcp.json server key.
|
|
1986
|
+
*/
|
|
1987
|
+
function memberBridgeServerKey(token) {
|
|
1988
|
+
return `${MEMBER_BRIDGE_SERVER_PREFIX}${token.slice(0, 8)}`;
|
|
1989
|
+
}
|
|
1990
|
+
/** Read the scoped mcp.json; a missing or malformed file reads as empty. */
|
|
1991
|
+
function readMcpConfig(file) {
|
|
1992
|
+
let parsed;
|
|
1993
|
+
try {
|
|
1994
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
1995
|
+
} catch {
|
|
1996
|
+
return {};
|
|
1997
|
+
}
|
|
1998
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
1999
|
+
return parsed;
|
|
2000
|
+
}
|
|
2001
|
+
/** Write the config back, preserving every non-family key. */
|
|
2002
|
+
function writeMcpConfig(file, config) {
|
|
2003
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Declare the member bridge for one run: set the per-run server entry and
|
|
2007
|
+
* prune other family entries (settled or crashed runs — an open session's
|
|
2008
|
+
* already-registered server is unaffected, per kimi's mid-session semantics).
|
|
2009
|
+
* Synchronous read-modify-write: single-process callers race-free by event
|
|
2010
|
+
* loop; the file is small and written once per run.
|
|
2011
|
+
* @param homeDir - the kimi scoped home.
|
|
2012
|
+
* @param key - this run's server key ({@link memberBridgeServerKey}).
|
|
2013
|
+
* @param command - the bridge spawn command (`node` + entry path).
|
|
2014
|
+
* @param env - the bridge environment (socket path + per-run token).
|
|
2015
|
+
*/
|
|
2016
|
+
function injectMemberBridge(homeDir, key, command, env) {
|
|
2017
|
+
const file = join(homeDir, MCP_CONFIG_FILENAME);
|
|
2018
|
+
const config = readMcpConfig(file);
|
|
2019
|
+
const servers = { ...config.mcpServers ?? {} };
|
|
2020
|
+
for (const name of Object.keys(servers)) if (name.startsWith("dsh-member-") && name !== key) delete servers[name];
|
|
2021
|
+
servers[key] = {
|
|
2022
|
+
command: command.command,
|
|
2023
|
+
args: command.args,
|
|
2024
|
+
env,
|
|
2025
|
+
toolTimeoutMs: MEMBER_TOOL_TIMEOUT_MS
|
|
2026
|
+
};
|
|
2027
|
+
writeMcpConfig(file, {
|
|
2028
|
+
...config,
|
|
2029
|
+
mcpServers: servers
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Remove one run's bridge entry (settle path). Absent entries and a missing
|
|
2034
|
+
* config are no-ops.
|
|
2035
|
+
* @param homeDir - the kimi scoped home.
|
|
2036
|
+
* @param key - the run's server key.
|
|
2037
|
+
*/
|
|
2038
|
+
function removeMemberBridge(homeDir, key) {
|
|
2039
|
+
const file = join(homeDir, MCP_CONFIG_FILENAME);
|
|
2040
|
+
const config = readMcpConfig(file);
|
|
2041
|
+
if (config.mcpServers === void 0 || !(key in config.mcpServers)) return;
|
|
2042
|
+
const servers = { ...config.mcpServers };
|
|
2043
|
+
delete servers[key];
|
|
2044
|
+
writeMcpConfig(file, {
|
|
2045
|
+
...config,
|
|
2046
|
+
mcpServers: servers
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
//#endregion
|
|
1963
2050
|
//#region lib/types/session-view.js
|
|
1964
2051
|
/**
|
|
1965
2052
|
* Kimi session viewer: read a scoped-home session's `wire.jsonl` event log
|
|
@@ -2047,6 +2134,8 @@ function editArgsOf(name, record) {
|
|
|
2047
2134
|
const patch = record["patch"] ?? record["diff"];
|
|
2048
2135
|
if (typeof patch === "string" && patch.trim() !== "") return patch;
|
|
2049
2136
|
}
|
|
2137
|
+
/** Cap on a structured tool call's mirrored JSON arguments. */
|
|
2138
|
+
const STRUCTURED_ARGS_MAX = 16e3;
|
|
2050
2139
|
/** Render a tool call's arguments into a compact summary. */
|
|
2051
2140
|
function argsOf(name, args) {
|
|
2052
2141
|
if (args === void 0) return void 0;
|
|
@@ -2055,6 +2144,10 @@ function argsOf(name, args) {
|
|
|
2055
2144
|
const record = args;
|
|
2056
2145
|
const edit = editArgsOf(name, record);
|
|
2057
2146
|
if (edit !== void 0) return edit;
|
|
2147
|
+
if (Object.values(record).some((value) => typeof value === "object" && value !== null)) {
|
|
2148
|
+
const json = JSON.stringify(record);
|
|
2149
|
+
return json.length <= STRUCTURED_ARGS_MAX ? json : `${json.slice(0, STRUCTURED_ARGS_MAX)}…`;
|
|
2150
|
+
}
|
|
2058
2151
|
for (const key of [
|
|
2059
2152
|
"query",
|
|
2060
2153
|
"url",
|
|
@@ -2094,6 +2187,8 @@ async function readKimiTranscript(sessionDir) {
|
|
|
2094
2187
|
};
|
|
2095
2188
|
let turn = 0;
|
|
2096
2189
|
let turnSeen = false;
|
|
2190
|
+
/** The wire marks rounds with turn.prompt (current kimi): a user message it did not prompt is an injection. */
|
|
2191
|
+
let promptsMarkTurns = false;
|
|
2097
2192
|
const callsByUuid = /* @__PURE__ */ new Map();
|
|
2098
2193
|
const callsByToolCallId = /* @__PURE__ */ new Map();
|
|
2099
2194
|
let text;
|
|
@@ -2147,6 +2242,7 @@ async function readKimiTranscript(sessionDir) {
|
|
|
2147
2242
|
continue;
|
|
2148
2243
|
}
|
|
2149
2244
|
if (event.type === "turn.prompt") {
|
|
2245
|
+
promptsMarkTurns = true;
|
|
2150
2246
|
lineModel = void 0;
|
|
2151
2247
|
turnSeen = true;
|
|
2152
2248
|
turn += 1;
|
|
@@ -2165,6 +2261,7 @@ async function readKimiTranscript(sessionDir) {
|
|
|
2165
2261
|
if (role === "user" && textContent.trim() !== "" && !isSystemReminder(textContent)) {
|
|
2166
2262
|
const last = lines[lines.length - 1];
|
|
2167
2263
|
if (turnSeen && last?.kind === "user" && last.text === textContent) continue;
|
|
2264
|
+
if (promptsMarkTurns) continue;
|
|
2168
2265
|
turnSeen = true;
|
|
2169
2266
|
turn += 1;
|
|
2170
2267
|
lineModel = void 0;
|
|
@@ -2221,7 +2318,7 @@ async function readKimiTranscript(sessionDir) {
|
|
|
2221
2318
|
if (typeof call.toolCallId === "string") callsByToolCallId.set(call.toolCallId, lineIndex);
|
|
2222
2319
|
} else if (loop.type === "tool.result") {
|
|
2223
2320
|
const result = loop;
|
|
2224
|
-
const output = result.result?.output;
|
|
2321
|
+
const output = toolOutputText(result.result?.output);
|
|
2225
2322
|
let target = typeof result.parentUuid === "string" ? callsByUuid.get(result.parentUuid) : void 0;
|
|
2226
2323
|
if (target === void 0 && typeof result.toolCallId === "string") target = callsByToolCallId.get(result.toolCallId);
|
|
2227
2324
|
if (target === void 0) {
|
|
@@ -2254,6 +2351,26 @@ async function readKimiTranscript(sessionDir) {
|
|
|
2254
2351
|
};
|
|
2255
2352
|
}
|
|
2256
2353
|
/**
|
|
2354
|
+
* A wire tool result's output as display text. Most tools report a string;
|
|
2355
|
+
* a media read (ReadMediaFile) reports content parts — text parts keep their
|
|
2356
|
+
* text, an image part becomes a short marker (its bytes stay in kimi's blob
|
|
2357
|
+
* store). Anything else reads as no output, never a parse failure: one
|
|
2358
|
+
* unreadable result must not stop the whole transcript (and the round's
|
|
2359
|
+
* usage) from folding.
|
|
2360
|
+
* @param raw - the result's `output` field.
|
|
2361
|
+
* @returns the text, or undefined when there is none.
|
|
2362
|
+
*/
|
|
2363
|
+
function toolOutputText(raw) {
|
|
2364
|
+
if (typeof raw === "string") return raw;
|
|
2365
|
+
if (!Array.isArray(raw)) return void 0;
|
|
2366
|
+
return raw.map((part) => {
|
|
2367
|
+
if (part === null || typeof part !== "object") return "";
|
|
2368
|
+
const entry = part;
|
|
2369
|
+
if (entry.type === "text" && typeof entry.text === "string") return entry.text;
|
|
2370
|
+
return typeof entry.type === "string" && entry.type !== "" ? `[${entry.type}]` : "";
|
|
2371
|
+
}).join("");
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2257
2374
|
* Map a wire `usage.record` payload onto the shared usage contract. kimi
|
|
2258
2375
|
* reports input (uncached), cache read, cache creation, and output; the
|
|
2259
2376
|
* creation counter is its cache-write analog. Non-numeric or missing
|
|
@@ -2419,6 +2536,13 @@ function nextWrittenKimiStep(childSession, turn) {
|
|
|
2419
2536
|
}
|
|
2420
2537
|
return last + 1;
|
|
2421
2538
|
}
|
|
2539
|
+
/** Wire usage records already counted, per child session (see mirrorKimiSessionDelta). */
|
|
2540
|
+
const countedUsage = /* @__PURE__ */ new WeakMap();
|
|
2541
|
+
const usageSeen = (childSession) => {
|
|
2542
|
+
let seen = countedUsage.get(childSession);
|
|
2543
|
+
if (seen === void 0) countedUsage.set(childSession, seen = /* @__PURE__ */ new Set());
|
|
2544
|
+
return seen;
|
|
2545
|
+
};
|
|
2422
2546
|
/** The run-progress delta text for one transcript line (any kind). */
|
|
2423
2547
|
function kimiLineProgressText(line) {
|
|
2424
2548
|
if (line.kind !== "tool") return line.text;
|
|
@@ -2552,6 +2676,8 @@ async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSessionId,
|
|
|
2552
2676
|
}
|
|
2553
2677
|
const foldedUnits = holdAt < 0 ? units : units.slice(0, holdAt);
|
|
2554
2678
|
const consumedTotal = fromLines + foldedUnits.reduce((n, unit) => n + unit.length, 0);
|
|
2679
|
+
const seen = usageSeen(childSession);
|
|
2680
|
+
const usageRecords = transcript.usageRecords.filter((record) => !seen.has(record.line));
|
|
2555
2681
|
const unitUsage = [];
|
|
2556
2682
|
const claimed = [];
|
|
2557
2683
|
let unitStart = fromLines;
|
|
@@ -2564,11 +2690,13 @@ async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSessionId,
|
|
|
2564
2690
|
from: start,
|
|
2565
2691
|
to: unitStart
|
|
2566
2692
|
});
|
|
2567
|
-
unitUsage.push(sumUsageRecords(
|
|
2693
|
+
unitUsage.push(sumUsageRecords(usageRecords.filter((record) => record.line >= start && record.line < unitStart)));
|
|
2568
2694
|
} else unitUsage.push(void 0);
|
|
2569
2695
|
}
|
|
2570
|
-
const unclaimedUsage = sumUsageRecords(
|
|
2571
|
-
const
|
|
2696
|
+
const unclaimedUsage = sumUsageRecords(usageRecords.filter((record) => record.line >= fromLines && record.line <= consumedTotal && !claimed.some((span) => record.line >= span.from && record.line < span.to)));
|
|
2697
|
+
const windowRecords = usageRecords.filter((record) => record.line >= fromLines && record.line <= consumedTotal);
|
|
2698
|
+
const deltaUsage = sumUsageRecords(windowRecords);
|
|
2699
|
+
for (const record of windowRecords) seen.add(record.line);
|
|
2572
2700
|
const takenSteps = /* @__PURE__ */ new Map();
|
|
2573
2701
|
for (const event of childSession.snapshotEvents()) {
|
|
2574
2702
|
const data = event.data;
|
|
@@ -2597,6 +2725,12 @@ async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSessionId,
|
|
|
2597
2725
|
written.add(step);
|
|
2598
2726
|
takenSteps.set(turn, written);
|
|
2599
2727
|
};
|
|
2728
|
+
const closedTurns = /* @__PURE__ */ new Set();
|
|
2729
|
+
for (const event of childSession.snapshotEvents()) {
|
|
2730
|
+
if (event.type !== "turn/end") continue;
|
|
2731
|
+
const closed = event.data.turn;
|
|
2732
|
+
if (typeof closed === "number") closedTurns.add(closed);
|
|
2733
|
+
}
|
|
2600
2734
|
/** The window's accounting that no fold carried (dropped units' sums). */
|
|
2601
2735
|
let carryUsage;
|
|
2602
2736
|
let lastContentUnit = -1;
|
|
@@ -2612,6 +2746,7 @@ async function mirrorKimiSessionDelta(ctx, childSession, homeDir, kimiSessionId,
|
|
|
2612
2746
|
carryUsage = void 0;
|
|
2613
2747
|
if (unitIndex === lastContentUnit) usage = addTokenUsage(usage, unclaimedUsage);
|
|
2614
2748
|
const turn = head.turn;
|
|
2749
|
+
if (closedTurns.has(turn)) continue;
|
|
2615
2750
|
if (head.kind === "user") {
|
|
2616
2751
|
if (userAlreadyAppended(childSession, turn, head.text)) continue;
|
|
2617
2752
|
childSession.append("user/message", userEvent(head.text), { surfaceOp: "append" });
|
|
@@ -2808,7 +2943,6 @@ function userAlreadyAppended(childSession, turn, text) {
|
|
|
2808
2943
|
* mode's auto-approve behavior; the proposal's default policy).
|
|
2809
2944
|
* @module @khorsheed/dsh-local-agent-kimi/live-driver
|
|
2810
2945
|
*/
|
|
2811
|
-
/** Default idle lifetime of an unused resident runtime before reclaim. */
|
|
2812
2946
|
const DEFAULT_LIVE_IDLE_MS = 18e5;
|
|
2813
2947
|
/** Default timeout for a wire request acknowledgement (never for session/prompt). */
|
|
2814
2948
|
const DEFAULT_LIVE_REQUEST_TIMEOUT_MS = 3e4;
|
|
@@ -3286,6 +3420,7 @@ var KimiAcpLiveDriver = class {
|
|
|
3286
3420
|
graceMs: DEFAULT_DISPOSE_GRACE_MS,
|
|
3287
3421
|
env: delegationEnv({
|
|
3288
3422
|
KIMI_CODE_HOME: spec.homeDir,
|
|
3423
|
+
KIMI_MCP_TOOL_TIMEOUT_MS: String(MEMBER_TOOL_TIMEOUT_MS),
|
|
3289
3424
|
...endpointAuthEnv(endpoint)
|
|
3290
3425
|
})
|
|
3291
3426
|
};
|
|
@@ -3564,22 +3699,30 @@ var KimiAcpLiveDriver = class {
|
|
|
3564
3699
|
* think-style lines. Each update carries the WHOLE plan, so the fold
|
|
3565
3700
|
* replaces rather than appends.
|
|
3566
3701
|
*/
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3702
|
+
/**
|
|
3703
|
+
* An ACP plan update is the member's todo list: written as the host's
|
|
3704
|
+
* `todo/write` whole-list snapshot (log-only UI state, Claude's TodoWrite
|
|
3705
|
+
* shape), only when it changed. Never a conversation step — a plan
|
|
3706
|
+
* folded as a step landed after the round's answer (it finalized at
|
|
3707
|
+
* settle) and the chat folded the answer into the process.
|
|
3708
|
+
*/
|
|
3574
3709
|
const foldPlan = (update) => {
|
|
3575
|
-
const
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3710
|
+
const entries = Array.isArray(update["entries"]) ? update["entries"] : [];
|
|
3711
|
+
const todos = [];
|
|
3712
|
+
for (const entry of entries) {
|
|
3713
|
+
const content = typeof entry["content"] === "string" ? entry["content"].trim() : "";
|
|
3714
|
+
if (content === "") continue;
|
|
3715
|
+
const status = entry["status"];
|
|
3716
|
+
todos.push({
|
|
3717
|
+
content,
|
|
3718
|
+
status: status === "in_progress" || status === "completed" ? status : "pending"
|
|
3719
|
+
});
|
|
3582
3720
|
}
|
|
3721
|
+
if (todos.length === 0) return;
|
|
3722
|
+
const last = childSession.snapshotEvents().filter((event) => event.type === "todo/write").at(-1);
|
|
3723
|
+
if (last !== void 0 && JSON.stringify(last.data) === JSON.stringify({ todos })) return;
|
|
3724
|
+
childSession.append("todo/write", { todos });
|
|
3725
|
+
persist();
|
|
3583
3726
|
};
|
|
3584
3727
|
/**
|
|
3585
3728
|
* The live-output channel. Snapshots are browser-only previews (never
|
|
@@ -3919,7 +4062,7 @@ var KimiAcpLiveDriver = class {
|
|
|
3919
4062
|
}
|
|
3920
4063
|
}
|
|
3921
4064
|
} else if (kind === "tool_call") {
|
|
3922
|
-
const id = typeof update["toolCallId"] === "string" ? update["toolCallId"] : void 0;
|
|
4065
|
+
const id = typeof update["toolCallId"] === "string" ? update["toolCallId"].replace(/^\d+:/, "") : void 0;
|
|
3923
4066
|
if (id === void 0 || !seenToolCalls.has(id)) {
|
|
3924
4067
|
if (id !== void 0 && toolLogged(id)) seenToolCalls.add(id);
|
|
3925
4068
|
else if (id !== void 0) {
|
|
@@ -4089,7 +4232,16 @@ var KimiAcpLiveDriver = class {
|
|
|
4089
4232
|
});
|
|
4090
4233
|
result.then(() => {
|
|
4091
4234
|
if (turnOpened) {
|
|
4092
|
-
|
|
4235
|
+
let usage;
|
|
4236
|
+
for (const event of childSession.snapshotEvents()) {
|
|
4237
|
+
if (event.type !== "assistant/message") continue;
|
|
4238
|
+
const data = event.data;
|
|
4239
|
+
if (data.turn === turn && data.usage !== void 0) usage = addTokenUsage(usage, data.usage);
|
|
4240
|
+
}
|
|
4241
|
+
const round = {
|
|
4242
|
+
...roundModel === void 0 ? {} : { observedModel: roundModel },
|
|
4243
|
+
...usage === void 0 ? {} : { usage }
|
|
4244
|
+
};
|
|
4093
4245
|
localAgent?.recordRoundSettled?.(childSession.id, round);
|
|
4094
4246
|
}
|
|
4095
4247
|
this.armIdleTimer(String(childSession.id));
|
|
@@ -4113,90 +4265,6 @@ var KimiAcpLiveDriver = class {
|
|
|
4113
4265
|
/** The member handle each runtime carries (for its session declarations). */
|
|
4114
4266
|
const runtimeMember = /* @__PURE__ */ new WeakMap();
|
|
4115
4267
|
//#endregion
|
|
4116
|
-
//#region lib/types/member-bridge-config.js
|
|
4117
|
-
/**
|
|
4118
|
-
* Kimi scoped-home MCP config injection for the member bridge. Kimi declares
|
|
4119
|
-
* stdio MCP servers in `$KIMI_CODE_HOME/mcp.json` (`mcpServers` map); the file
|
|
4120
|
-
* is read once per CLI session start, and edits never interrupt an open
|
|
4121
|
-
* session — so the family writes one entry PER RUN (keyed by the run's token
|
|
4122
|
-
* suffix, keeping concurrent delegations of the same scoped home race-free),
|
|
4123
|
-
* prunes it when the run settles, and drops stale family entries (crashed
|
|
4124
|
-
* host) on every write. User-declared servers are preserved verbatim.
|
|
4125
|
-
* @module @khorsheed/dsh-local-agent-kimi/member-bridge-config
|
|
4126
|
-
*/
|
|
4127
|
-
/** Kimi's user-level MCP config filename inside the scoped home. */
|
|
4128
|
-
const MCP_CONFIG_FILENAME = "mcp.json";
|
|
4129
|
-
/** Server-key prefix marking the family's per-run bridge entries. */
|
|
4130
|
-
const MEMBER_BRIDGE_SERVER_PREFIX = "dsh-member-";
|
|
4131
|
-
/**
|
|
4132
|
-
* The per-run MCP server key. The run's token suffix keeps concurrent runs
|
|
4133
|
-
* distinct; each `kimi -p` round is a fresh CLI session, so per-run (rather
|
|
4134
|
-
* than per-member) naming costs nothing.
|
|
4135
|
-
* @param token - the run's member-channel token.
|
|
4136
|
-
* @returns the mcp.json server key.
|
|
4137
|
-
*/
|
|
4138
|
-
function memberBridgeServerKey(token) {
|
|
4139
|
-
return `${MEMBER_BRIDGE_SERVER_PREFIX}${token.slice(0, 8)}`;
|
|
4140
|
-
}
|
|
4141
|
-
/** Read the scoped mcp.json; a missing or malformed file reads as empty. */
|
|
4142
|
-
function readMcpConfig(file) {
|
|
4143
|
-
let parsed;
|
|
4144
|
-
try {
|
|
4145
|
-
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
4146
|
-
} catch {
|
|
4147
|
-
return {};
|
|
4148
|
-
}
|
|
4149
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
4150
|
-
return parsed;
|
|
4151
|
-
}
|
|
4152
|
-
/** Write the config back, preserving every non-family key. */
|
|
4153
|
-
function writeMcpConfig(file, config) {
|
|
4154
|
-
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
4155
|
-
}
|
|
4156
|
-
/**
|
|
4157
|
-
* Declare the member bridge for one run: set the per-run server entry and
|
|
4158
|
-
* prune other family entries (settled or crashed runs — an open session's
|
|
4159
|
-
* already-registered server is unaffected, per kimi's mid-session semantics).
|
|
4160
|
-
* Synchronous read-modify-write: single-process callers race-free by event
|
|
4161
|
-
* loop; the file is small and written once per run.
|
|
4162
|
-
* @param homeDir - the kimi scoped home.
|
|
4163
|
-
* @param key - this run's server key ({@link memberBridgeServerKey}).
|
|
4164
|
-
* @param command - the bridge spawn command (`node` + entry path).
|
|
4165
|
-
* @param env - the bridge environment (socket path + per-run token).
|
|
4166
|
-
*/
|
|
4167
|
-
function injectMemberBridge(homeDir, key, command, env) {
|
|
4168
|
-
const file = join(homeDir, MCP_CONFIG_FILENAME);
|
|
4169
|
-
const config = readMcpConfig(file);
|
|
4170
|
-
const servers = { ...config.mcpServers ?? {} };
|
|
4171
|
-
for (const name of Object.keys(servers)) if (name.startsWith("dsh-member-") && name !== key) delete servers[name];
|
|
4172
|
-
servers[key] = {
|
|
4173
|
-
command: command.command,
|
|
4174
|
-
args: command.args,
|
|
4175
|
-
env
|
|
4176
|
-
};
|
|
4177
|
-
writeMcpConfig(file, {
|
|
4178
|
-
...config,
|
|
4179
|
-
mcpServers: servers
|
|
4180
|
-
});
|
|
4181
|
-
}
|
|
4182
|
-
/**
|
|
4183
|
-
* Remove one run's bridge entry (settle path). Absent entries and a missing
|
|
4184
|
-
* config are no-ops.
|
|
4185
|
-
* @param homeDir - the kimi scoped home.
|
|
4186
|
-
* @param key - the run's server key.
|
|
4187
|
-
*/
|
|
4188
|
-
function removeMemberBridge(homeDir, key) {
|
|
4189
|
-
const file = join(homeDir, MCP_CONFIG_FILENAME);
|
|
4190
|
-
const config = readMcpConfig(file);
|
|
4191
|
-
if (config.mcpServers === void 0 || !(key in config.mcpServers)) return;
|
|
4192
|
-
const servers = { ...config.mcpServers };
|
|
4193
|
-
delete servers[key];
|
|
4194
|
-
writeMcpConfig(file, {
|
|
4195
|
-
...config,
|
|
4196
|
-
mcpServers: servers
|
|
4197
|
-
});
|
|
4198
|
-
}
|
|
4199
|
-
//#endregion
|
|
4200
4268
|
//#region lib/types/kimi-cli-provider.js
|
|
4201
4269
|
/**
|
|
4202
4270
|
* One-shot and resumable Kimi CLI subagent lifecycle: spawn `kimi -p "<task>"`
|
|
@@ -5344,7 +5412,7 @@ const inject = [
|
|
|
5344
5412
|
* the user layer exactly as before.
|
|
5345
5413
|
*/
|
|
5346
5414
|
const SETTINGS_FIELDS = {
|
|
5347
|
-
live: z.boolean().default(
|
|
5415
|
+
live: z.boolean().default(true),
|
|
5348
5416
|
liveMirrorGranularity: z.union([z.const("event"), z.const("token")]).default("token"),
|
|
5349
5417
|
model: z.string(),
|
|
5350
5418
|
recentModels: z.array(z.string()).default([])
|
|
@@ -5438,7 +5506,7 @@ function apply(ctx, config) {
|
|
|
5438
5506
|
const model = vol(config.model);
|
|
5439
5507
|
const recentModels = vol(config.recentModels);
|
|
5440
5508
|
return {
|
|
5441
|
-
live: vol(config.live) ??
|
|
5509
|
+
live: vol(config.live) ?? true,
|
|
5442
5510
|
liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? "token",
|
|
5443
5511
|
...model === void 0 ? {} : { model },
|
|
5444
5512
|
...recentModels === void 0 ? {} : { recentModels }
|
|
@@ -5455,6 +5523,7 @@ function apply(ctx, config) {
|
|
|
5455
5523
|
}
|
|
5456
5524
|
const face = settingsFace(ctx, {
|
|
5457
5525
|
namespace: KIMI_SETTINGS_NAMESPACE,
|
|
5526
|
+
harness: "kimi",
|
|
5458
5527
|
legacySchema: KIMI_SETTINGS_SCHEMA,
|
|
5459
5528
|
legacyBase,
|
|
5460
5529
|
read: readSettings
|
|
@@ -7,7 +7,7 @@ import { IconChevronDownOutlineMedium } from './icons.js';
|
|
|
7
7
|
// the block carries no runtime identity to share.
|
|
8
8
|
import { ProviderAuthBlock, } from '@khorsheed/dsh-local-agent/src/client/ProviderAuthBlock.tsx';
|
|
9
9
|
import { AuthStatusDot } from '@khorsheed/dsh-local-agent/src/client/AuthStatusDot.tsx';
|
|
10
|
-
import { publishAuthStatus, useHarnessAuthStatus } from '@khorsheed/dsh-local-agent/src/client/auth-status.ts';
|
|
10
|
+
import { publishAuthStatus, useHarnessAuthStatus, usePublishedAuthStatus } from '@khorsheed/dsh-local-agent/src/client/auth-status.ts';
|
|
11
11
|
import { ModelListSection } from '@khorsheed/dsh-local-agent/src/client/ModelListSection.tsx';
|
|
12
12
|
import css from './SettingsCard.module.css';
|
|
13
13
|
/**
|
|
@@ -43,6 +43,10 @@ function useCardState(active, useSettings, scope, endpointAuth) {
|
|
|
43
43
|
const endpointUnavailable = endpointAuth === undefined || endpointState === 'unavailable';
|
|
44
44
|
const endpointLoading = !endpointUnavailable && endpointState === null;
|
|
45
45
|
const apiKeyConfigured = endpointState !== null && endpointState !== 'unavailable' && endpointState.apiKeySet;
|
|
46
|
+
// A login or logout elsewhere in the card (the auth block) changes what
|
|
47
|
+
// the store holds — logout deletes the saved key: re-read, or the form
|
|
48
|
+
// keeps saying 已配置 for a key that is gone.
|
|
49
|
+
const authPublished = usePublishedAuthStatus('kimi');
|
|
46
50
|
useEffect(() => {
|
|
47
51
|
if (!active || endpointAuth === undefined)
|
|
48
52
|
return;
|
|
@@ -51,7 +55,7 @@ function useCardState(active, useSettings, scope, endpointAuth) {
|
|
|
51
55
|
setEndpointState(state ?? 'unavailable'); }, () => { if (!stale)
|
|
52
56
|
setEndpointState('unavailable'); });
|
|
53
57
|
return () => { stale = true; };
|
|
54
|
-
}, [active, endpointAuth, endpointSaved]);
|
|
58
|
+
}, [active, endpointAuth, endpointSaved, authPublished]);
|
|
55
59
|
// The credential save: one `configureEndpoint` call carrying only the
|
|
56
60
|
// dirty fields (a blank draft clears its field); the answer is the
|
|
57
61
|
// post-write resolved state, so the chips and the auth probe flip without
|
|
@@ -81,7 +85,11 @@ function useCardState(active, useSettings, scope, endpointAuth) {
|
|
|
81
85
|
const write = (field, value) => {
|
|
82
86
|
setSaved(false);
|
|
83
87
|
setError(false);
|
|
84
|
-
|
|
88
|
+
// The host answers a refused write with `false` (ConfigForm), not a rejection.
|
|
89
|
+
void scope.set(field, value).then((accepted) => { if (accepted === false)
|
|
90
|
+
setError(true);
|
|
91
|
+
else
|
|
92
|
+
setSaved(true); }, () => { setError(true); });
|
|
85
93
|
};
|
|
86
94
|
return {
|
|
87
95
|
snapshot, saved, error, ready, live, write,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bindCardSettingsScope } from '@khorsheed/dsh-local-agent/src/client/settings-scope.ts';
|
|
2
2
|
/**
|
|
3
3
|
* Local-agent-kimi plugin, browser half: the kimi settings card — a section
|
|
4
4
|
* of the family core's「本机 Agent 委派」family card on the Plugins page
|
|
@@ -44,7 +44,7 @@ export const inject = ['slots', 'remote', 'remote.commands', 'locale'];
|
|
|
44
44
|
export function apply(ctx) {
|
|
45
45
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'local-agent-kimi: dictionaries');
|
|
46
46
|
const t = ctx.locale.bind(NS);
|
|
47
|
-
const scope =
|
|
47
|
+
const scope = bindCardSettingsScope(ctx, 'kimi', 'local-agent-kimi');
|
|
48
48
|
// The auth block's copy lives in the family core's dictionary; binding is
|
|
49
49
|
// stable per namespace and late dictionary registration still resolves.
|
|
50
50
|
const authT = ctx.locale.bind('local-agent');
|
package/lib/types/index.js
CHANGED
|
@@ -40,7 +40,7 @@ export const inject = ['localAgent', 'subagents', 'subprocess', 'settings'];
|
|
|
40
40
|
* the user layer exactly as before.
|
|
41
41
|
*/
|
|
42
42
|
const SETTINGS_FIELDS = {
|
|
43
|
-
live: z.boolean().default(
|
|
43
|
+
live: z.boolean().default(true),
|
|
44
44
|
liveMirrorGranularity: z.union([z.const('event'), z.const('token')]).default('token'),
|
|
45
45
|
// `model` deliberately carries NO default: an unset key must resolve to
|
|
46
46
|
// undefined, which is what keeps the pre-key behavior byte-identical.
|
|
@@ -161,7 +161,7 @@ export function apply(ctx, config) {
|
|
|
161
161
|
const model = vol(config.model);
|
|
162
162
|
const recentModels = vol(config.recentModels);
|
|
163
163
|
return {
|
|
164
|
-
live: vol(config.live) ??
|
|
164
|
+
live: vol(config.live) ?? true,
|
|
165
165
|
liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? 'token',
|
|
166
166
|
...model === undefined ? {} : { model },
|
|
167
167
|
...recentModels === undefined ? {} : { recentModels },
|
|
@@ -175,6 +175,8 @@ export function apply(ctx, config) {
|
|
|
175
175
|
}
|
|
176
176
|
const face = settingsFace(ctx, {
|
|
177
177
|
namespace: KIMI_SETTINGS_NAMESPACE,
|
|
178
|
+
// Card writes live in the harness's own store (card-settings.ts), never the profile patch.
|
|
179
|
+
harness: 'kimi',
|
|
178
180
|
legacySchema: KIMI_SETTINGS_SCHEMA,
|
|
179
181
|
legacyBase,
|
|
180
182
|
read: readSettings,
|
|
@@ -42,7 +42,6 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
42
42
|
import type { Session } from '@deepseek-ai/dsh-session';
|
|
43
43
|
import { type SubagentRun, type SubagentStartRequest, type SubagentStopReason } from '@deepseek-ai/dsh-subagent';
|
|
44
44
|
import { type KimiEndpointAuth } from './endpoint-auth.ts';
|
|
45
|
-
/** Default idle lifetime of an unused resident runtime before reclaim. */
|
|
46
45
|
export declare const DEFAULT_LIVE_IDLE_MS: number;
|
|
47
46
|
/** Default timeout for a wire request acknowledgement (never for session/prompt). */
|
|
48
47
|
export declare const DEFAULT_LIVE_REQUEST_TIMEOUT_MS = 30000;
|
package/lib/types/live-driver.js
CHANGED
|
@@ -47,11 +47,11 @@ import { delegationEnv, persistChildSession, LiveStreamPublisher, LiveFlush, LIV
|
|
|
47
47
|
import { endpointAuthEnv, hasEndpointCredential } from './endpoint-auth.js';
|
|
48
48
|
import { ensureKimiEndpointProvider, KIMI_MANAGED_BASE_URL } from './provision.js';
|
|
49
49
|
import { MEMBER_BRIDGE_SOCKET_ENV, MEMBER_BRIDGE_TOKEN_ENV } from '@khorsheed/dsh-local-agent/types';
|
|
50
|
+
import { MEMBER_TOOL_TIMEOUT_MS } from './member-bridge-config.js';
|
|
50
51
|
import { DEFAULT_DISPOSE_GRACE_MS, mirrorKimiDelta, textTask, } from './kimi-cli-provider.js';
|
|
51
52
|
import { assistantEvent, KIMI_INTERRUPTED_TOOL_RESULT, nextKimiSessionStep, nextWrittenKimiStep } from './session-mirror.js';
|
|
52
53
|
import { addTokenUsage } from './session-view.js';
|
|
53
54
|
import { writeKimiDefaultModel } from './provision.js';
|
|
54
|
-
/** Default idle lifetime of an unused resident runtime before reclaim. */
|
|
55
55
|
export const DEFAULT_LIVE_IDLE_MS = 30 * 60_000;
|
|
56
56
|
/** Default timeout for a wire request acknowledgement (never for session/prompt). */
|
|
57
57
|
export const DEFAULT_LIVE_REQUEST_TIMEOUT_MS = 30_000;
|
|
@@ -559,7 +559,10 @@ export class KimiAcpLiveDriver {
|
|
|
559
559
|
cwd: spec.cwd,
|
|
560
560
|
stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' },
|
|
561
561
|
graceMs: DEFAULT_DISPOSE_GRACE_MS,
|
|
562
|
-
|
|
562
|
+
// The ACP session declares the member bridge inline, and ACP's MCP
|
|
563
|
+
// server shape carries no timeout: lift kimi's 60s MCP default for the
|
|
564
|
+
// whole process, or a question card dies before the human answers.
|
|
565
|
+
env: delegationEnv({ KIMI_CODE_HOME: spec.homeDir, KIMI_MCP_TOOL_TIMEOUT_MS: String(MEMBER_TOOL_TIMEOUT_MS), ...endpointAuthEnv(endpoint) }),
|
|
563
566
|
};
|
|
564
567
|
let child;
|
|
565
568
|
try {
|
|
@@ -877,30 +880,30 @@ export class KimiAcpLiveDriver {
|
|
|
877
880
|
* think-style lines. Each update carries the WHOLE plan, so the fold
|
|
878
881
|
* replaces rather than appends.
|
|
879
882
|
*/
|
|
880
|
-
|
|
883
|
+
/**
|
|
884
|
+
* An ACP plan update is the member's todo list: written as the host's
|
|
885
|
+
* `todo/write` whole-list snapshot (log-only UI state, Claude's TodoWrite
|
|
886
|
+
* shape), only when it changed. Never a conversation step — a plan
|
|
887
|
+
* folded as a step landed after the round's answer (it finalized at
|
|
888
|
+
* settle) and the chat folded the answer into the process.
|
|
889
|
+
*/
|
|
890
|
+
const foldPlan = (update) => {
|
|
881
891
|
const entries = Array.isArray(update['entries']) ? update['entries'] : [];
|
|
882
|
-
const
|
|
883
|
-
|
|
892
|
+
const todos = [];
|
|
893
|
+
for (const entry of entries) {
|
|
894
|
+
const content = typeof entry['content'] === 'string' ? entry['content'].trim() : '';
|
|
895
|
+
if (content === '')
|
|
896
|
+
continue;
|
|
884
897
|
const status = entry['status'];
|
|
885
|
-
|
|
886
|
-
return `${mark} ${content}`;
|
|
887
|
-
}).filter(line => line.trim() !== '☐' && line !== '');
|
|
888
|
-
return lines.join('\n');
|
|
889
|
-
};
|
|
890
|
-
const foldPlan = (update) => {
|
|
891
|
-
const plan = renderPlan(update);
|
|
892
|
-
if (plan === '')
|
|
893
|
-
return;
|
|
894
|
-
// The plan's own snapshot stream (think-styled): every update replaces
|
|
895
|
-
// the text and publishes at the reserved (turn, step); the stream
|
|
896
|
-
// finalizes at settle (the wire carries no plan line that could
|
|
897
|
-
// complete it) — or early, when a fold needs its step closed.
|
|
898
|
-
reserveStream(planKey, 'think');
|
|
899
|
-
const stream = streams.get(planKey);
|
|
900
|
-
if (stream !== undefined && stream.text !== plan) {
|
|
901
|
-
stream.text = plan;
|
|
902
|
-
publishStreamSnapshot(stream, true);
|
|
898
|
+
todos.push({ content, status: status === 'in_progress' || status === 'completed' ? status : 'pending' });
|
|
903
899
|
}
|
|
900
|
+
if (todos.length === 0)
|
|
901
|
+
return;
|
|
902
|
+
const last = childSession.snapshotEvents().filter(event => event.type === 'todo/write').at(-1);
|
|
903
|
+
if (last !== undefined && JSON.stringify(last.data) === JSON.stringify({ todos }))
|
|
904
|
+
return;
|
|
905
|
+
childSession.append('todo/write', { todos });
|
|
906
|
+
persist();
|
|
904
907
|
};
|
|
905
908
|
/**
|
|
906
909
|
* The live-output channel. Snapshots are browser-only previews (never
|
|
@@ -1265,7 +1268,12 @@ export class KimiAcpLiveDriver {
|
|
|
1265
1268
|
}
|
|
1266
1269
|
}
|
|
1267
1270
|
else if (kind === 'tool_call') {
|
|
1268
|
-
|
|
1271
|
+
// ACP prefixes the call id with kimi's turn index ("1:tool_x") from the
|
|
1272
|
+
// second round on; the wire records the bare id. Unprefixed, the two
|
|
1273
|
+
// name one call — otherwise the fold never releases the reservation
|
|
1274
|
+
// and settle re-writes the call as an interrupted orphan after the
|
|
1275
|
+
// answer (the chat then folds the answer into the process).
|
|
1276
|
+
const id = typeof update['toolCallId'] === 'string' ? update['toolCallId'].replace(/^\d+:/, '') : undefined;
|
|
1269
1277
|
if (id === undefined || !seenToolCalls.has(id)) {
|
|
1270
1278
|
// The wire flush can win the race: a call already folded into the
|
|
1271
1279
|
// log (its notification arriving late) needs no reservation — one
|
|
@@ -1488,8 +1496,20 @@ export class KimiAcpLiveDriver {
|
|
|
1488
1496
|
// a round that never opened owns no span to observe. Degrades
|
|
1489
1497
|
// silently on a core predating recordRoundSettled.
|
|
1490
1498
|
if (turnOpened) {
|
|
1499
|
+
// The round's spend for the room's usage chip: every folded step of
|
|
1500
|
+
// this turn carries its own usage (the uncarried rest rides the last
|
|
1501
|
+
// one), so the turn's sum is the round total.
|
|
1502
|
+
let usage;
|
|
1503
|
+
for (const event of childSession.snapshotEvents()) {
|
|
1504
|
+
if (event.type !== 'assistant/message')
|
|
1505
|
+
continue;
|
|
1506
|
+
const data = event.data;
|
|
1507
|
+
if (data.turn === turn && data.usage !== undefined)
|
|
1508
|
+
usage = addTokenUsage(usage, data.usage);
|
|
1509
|
+
}
|
|
1491
1510
|
const round = {
|
|
1492
1511
|
...roundModel === undefined ? {} : { observedModel: roundModel },
|
|
1512
|
+
...usage === undefined ? {} : { usage },
|
|
1493
1513
|
};
|
|
1494
1514
|
const registry = localAgent;
|
|
1495
1515
|
registry?.recordRoundSettled?.(childSession.id, round);
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* host) on every write. User-declared servers are preserved verbatim.
|
|
9
9
|
* @module @khorsheed/dsh-local-agent-kimi/member-bridge-config
|
|
10
10
|
*/
|
|
11
|
+
/** The member bridge's per-call bound: as long as a question card waits for the human (30 min). */
|
|
12
|
+
export declare const MEMBER_TOOL_TIMEOUT_MS: number;
|
|
11
13
|
/** Server-key prefix marking the family's per-run bridge entries. */
|
|
12
14
|
export declare const MEMBER_BRIDGE_SERVER_PREFIX = "dsh-member-";
|
|
13
15
|
/**
|