@botcord/daemon 0.2.49 → 0.2.51

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.
@@ -0,0 +1,352 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { buildCliEnv } from "../cli-resolver.js";
3
+ import { NdjsonStreamAdapter, type NdjsonEventCtx } from "./ndjson-stream.js";
4
+ import {
5
+ readCommandVersion,
6
+ resolveCommandOnPath,
7
+ type ProbeDeps,
8
+ } from "./probe.js";
9
+ import type { RuntimeProbeResult, RuntimeRunOptions, StreamBlock } from "../types.js";
10
+
11
+ function isValidKimiSessionId(sessionId: string): boolean {
12
+ if (sessionId.length === 0 || sessionId.length > 512) return false;
13
+ if (sessionId.startsWith("-")) return false;
14
+ for (const ch of sessionId) {
15
+ const code = ch.codePointAt(0);
16
+ if (code === undefined || code < 0x20 || code === 0x7f) return false;
17
+ }
18
+ return true;
19
+ }
20
+
21
+ function invalidKimiSessionIdError(): string {
22
+ return "kimi-cli: invalid sessionId (expected non-control text not starting with '-')";
23
+ }
24
+
25
+ const KIMI_EXTRA_FLAGS_WITH_VALUE = new Set([
26
+ "--add-dir",
27
+ "--agent",
28
+ "--agent-file",
29
+ "--config",
30
+ "--config-file",
31
+ "--max-ralph-iterations",
32
+ "--max-retries-per-step",
33
+ "--max-steps-per-turn",
34
+ "--mcp-config",
35
+ "--mcp-config-file",
36
+ "--model",
37
+ "--skills-dir",
38
+ "-m",
39
+ ]);
40
+
41
+ const KIMI_EXTRA_BOOLEAN_FLAGS = new Set([
42
+ "--afk",
43
+ "--auto-approve",
44
+ "--debug",
45
+ "--no-thinking",
46
+ "--plan",
47
+ "--thinking",
48
+ "--verbose",
49
+ "--yes",
50
+ "--yolo",
51
+ "-y",
52
+ ]);
53
+
54
+ // Flags owned by the adapter because BotCord depends on Kimi's non-interactive
55
+ // stream-json contract, cwd isolation, prompt placement, and session routing.
56
+ const KIMI_ADAPTER_OWNED_FLAGS = new Set([
57
+ "--acp",
58
+ "--command",
59
+ "--continue",
60
+ "--final-message-only",
61
+ "--help",
62
+ "--input-format",
63
+ "--output-format",
64
+ "--print",
65
+ "--prompt",
66
+ "--quiet",
67
+ "--resume",
68
+ "--session",
69
+ "--version",
70
+ "--wire",
71
+ "--work-dir",
72
+ "-C",
73
+ "-S",
74
+ "-V",
75
+ "-c",
76
+ "-h",
77
+ "-p",
78
+ "-r",
79
+ "-w",
80
+ ]);
81
+
82
+ function flagName(arg: string): string {
83
+ if (!arg.startsWith("-")) return arg;
84
+ const eq = arg.indexOf("=");
85
+ return eq === -1 ? arg : arg.slice(0, eq);
86
+ }
87
+
88
+ function nextValue(args: string[], index: number): string | undefined {
89
+ const next = args[index + 1];
90
+ if (typeof next !== "string") return undefined;
91
+ if (!next.startsWith("-")) return next;
92
+ return /^-\d/.test(next) ? next : undefined;
93
+ }
94
+
95
+ function sanitizeKimiExtraArgs(extraArgs: string[] | undefined): string[] {
96
+ if (!extraArgs?.length) return [];
97
+ const out: string[] = [];
98
+ for (let i = 0; i < extraArgs.length; i += 1) {
99
+ const arg = extraArgs[i];
100
+ const name = flagName(arg);
101
+
102
+ if (KIMI_ADAPTER_OWNED_FLAGS.has(name)) {
103
+ if (!arg.includes("=") && nextValue(extraArgs, i) !== undefined) i += 1;
104
+ continue;
105
+ }
106
+
107
+ if (KIMI_EXTRA_FLAGS_WITH_VALUE.has(name)) {
108
+ if (arg.includes("=")) {
109
+ out.push(arg);
110
+ continue;
111
+ }
112
+ const value = nextValue(extraArgs, i);
113
+ if (value !== undefined) {
114
+ out.push(arg, value);
115
+ i += 1;
116
+ }
117
+ continue;
118
+ }
119
+
120
+ if (KIMI_EXTRA_BOOLEAN_FLAGS.has(name)) {
121
+ out.push(arg);
122
+ continue;
123
+ }
124
+
125
+ if (arg.startsWith("-") && !arg.includes("=") && nextValue(extraArgs, i) !== undefined) {
126
+ i += 1;
127
+ }
128
+ }
129
+ return out;
130
+ }
131
+
132
+ /** Resolve the Kimi CLI executable on PATH. */
133
+ export function resolveKimiCommand(deps: ProbeDeps = {}): string | null {
134
+ return resolveCommandOnPath("kimi", deps);
135
+ }
136
+
137
+ /** Probe whether the Kimi CLI is installed and report its version. */
138
+ export function probeKimi(deps: ProbeDeps = {}): RuntimeProbeResult {
139
+ const command = resolveKimiCommand(deps);
140
+ if (!command) return { available: false };
141
+ return {
142
+ available: true,
143
+ path: command,
144
+ version: readCommandVersion(command, [], deps) ?? undefined,
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Kimi CLI adapter — spawns:
150
+ *
151
+ * kimi --work-dir <cwd> --print --output-format stream-json --session <sid> --afk --prompt <text>
152
+ *
153
+ * `--session <sid>` resumes an existing session or creates a new session with
154
+ * that id, so the adapter generates a UUID on first turn and persists it for
155
+ * later turns. Kimi does not expose a Codex-style per-invocation AGENTS.md
156
+ * carrier, so dynamic `systemContext` is sent as a system-reminder prefix on
157
+ * the user prompt.
158
+ */
159
+ export class KimiAdapter extends NdjsonStreamAdapter {
160
+ readonly id = "kimi-cli" as const;
161
+
162
+ private readonly explicitBinary: string | undefined;
163
+ private resolvedBinary: string | null = null;
164
+
165
+ constructor(opts?: { binary?: string }) {
166
+ super();
167
+ this.explicitBinary = opts?.binary ?? process.env.BOTCORD_KIMI_CLI_BIN;
168
+ }
169
+
170
+ probe(): RuntimeProbeResult {
171
+ return probeKimi();
172
+ }
173
+
174
+ override async run(opts: RuntimeRunOptions) {
175
+ if (opts.sessionId && !isValidKimiSessionId(opts.sessionId)) {
176
+ return { text: "", newSessionId: "", error: invalidKimiSessionIdError() };
177
+ }
178
+ const sessionId = opts.sessionId || randomUUID();
179
+ return super.run({ ...opts, sessionId });
180
+ }
181
+
182
+ protected resolveBinary(): string {
183
+ if (this.explicitBinary) return this.explicitBinary;
184
+ if (this.resolvedBinary) return this.resolvedBinary;
185
+ this.resolvedBinary = resolveKimiCommand() ?? "kimi";
186
+ return this.resolvedBinary;
187
+ }
188
+
189
+ protected buildArgs(opts: RuntimeRunOptions): string[] {
190
+ const sessionId = opts.sessionId || randomUUID();
191
+ if (!isValidKimiSessionId(sessionId)) throw new Error(invalidKimiSessionIdError());
192
+
193
+ const args = [
194
+ "--work-dir",
195
+ opts.cwd,
196
+ "--print",
197
+ "--output-format",
198
+ "stream-json",
199
+ "--session",
200
+ sessionId,
201
+ "--afk",
202
+ ];
203
+ args.push(...sanitizeKimiExtraArgs(opts.extraArgs));
204
+ args.push("--prompt", promptWithSystemContext(opts.text, opts.systemContext));
205
+ return args;
206
+ }
207
+
208
+ protected spawnEnv(opts: RuntimeRunOptions): NodeJS.ProcessEnv {
209
+ const cliEnv = buildCliEnv({
210
+ hubUrl: opts.hubUrl,
211
+ accountId: opts.accountId,
212
+ basePath: process.env.PATH,
213
+ });
214
+ return {
215
+ ...process.env,
216
+ ...cliEnv,
217
+ FORCE_COLOR: "0",
218
+ NO_COLOR: "1",
219
+ };
220
+ }
221
+
222
+ protected handleEvent(raw: unknown, ctx: NdjsonEventCtx): void {
223
+ const obj = raw as KimiStreamJsonEvent;
224
+
225
+ const status = kimiStatusEvent(obj);
226
+ if (status) ctx.emitStatus(status);
227
+
228
+ ctx.emitBlock(normalizeBlock(obj, ctx.seq));
229
+
230
+ const sessionId = kimiSessionId(obj);
231
+ if (sessionId) ctx.state.newSessionId = sessionId;
232
+
233
+ if (obj.role === "assistant") {
234
+ const text = extractText(obj.content);
235
+ if (text) {
236
+ ctx.appendAssistantText(text);
237
+ ctx.state.finalText = text;
238
+ }
239
+ return;
240
+ }
241
+
242
+ const err = kimiErrorText(obj);
243
+ if (err) ctx.state.errorText = err;
244
+ }
245
+ }
246
+
247
+ type KimiContentPart = {
248
+ type?: string;
249
+ text?: string;
250
+ think?: string;
251
+ };
252
+
253
+ type KimiToolCall = {
254
+ id?: string;
255
+ function?: { name?: string; arguments?: string | null };
256
+ };
257
+
258
+ type KimiStreamJsonEvent = {
259
+ role?: string;
260
+ content?: string | KimiContentPart[] | null;
261
+ tool_calls?: KimiToolCall[] | null;
262
+ tool_call_id?: string | null;
263
+ content_type?: string;
264
+ file_path?: string;
265
+ session_id?: string;
266
+ id?: string;
267
+ category?: string;
268
+ type?: string;
269
+ title?: string;
270
+ body?: string;
271
+ severity?: string;
272
+ error?: string | { message?: string };
273
+ message?: string;
274
+ };
275
+
276
+ function promptWithSystemContext(text: string, systemContext: string | undefined): string {
277
+ if (!systemContext) return text;
278
+ return `<system-reminder>\n${systemContext}\n</system-reminder>\n\n${text}`;
279
+ }
280
+
281
+ function extractText(content: KimiStreamJsonEvent["content"]): string {
282
+ if (typeof content === "string") return content;
283
+ if (!Array.isArray(content)) return "";
284
+ return content
285
+ .filter((part) => part?.type === "text" && typeof part.text === "string")
286
+ .map((part) => part.text)
287
+ .join("");
288
+ }
289
+
290
+ function hasThinking(content: KimiStreamJsonEvent["content"]): boolean {
291
+ return Array.isArray(content)
292
+ ? content.some((part) => part?.type === "think" && typeof part.think === "string" && part.think)
293
+ : false;
294
+ }
295
+
296
+ function firstToolName(toolCalls: KimiToolCall[] | null | undefined): string {
297
+ const name = toolCalls?.find((t) => typeof t.function?.name === "string")?.function?.name;
298
+ return name || "tool";
299
+ }
300
+
301
+ function kimiSessionId(obj: KimiStreamJsonEvent): string | undefined {
302
+ return typeof obj.session_id === "string" && obj.session_id ? obj.session_id : undefined;
303
+ }
304
+
305
+ function kimiErrorText(obj: KimiStreamJsonEvent): string | undefined {
306
+ if (typeof obj.error === "string" && obj.error) return obj.error;
307
+ if (obj.error && typeof obj.error === "object") {
308
+ const message = obj.error.message;
309
+ if (typeof message === "string" && message) return message;
310
+ }
311
+ if (obj.type === "error" && typeof obj.message === "string" && obj.message) {
312
+ return obj.message;
313
+ }
314
+ if (obj.severity === "error") {
315
+ return [obj.title, obj.body].filter(Boolean).join(": ") || "kimi-cli error";
316
+ }
317
+ return undefined;
318
+ }
319
+
320
+ function kimiStatusEvent(
321
+ obj: KimiStreamJsonEvent,
322
+ ): import("../types.js").RuntimeStatusEvent | undefined {
323
+ if (obj.role === "assistant" && hasThinking(obj.content)) {
324
+ return { kind: "thinking", phase: "started", label: "Thinking" };
325
+ }
326
+ if (obj.role === "assistant" && obj.tool_calls?.length) {
327
+ return { kind: "thinking", phase: "updated", label: firstToolName(obj.tool_calls) };
328
+ }
329
+ if (obj.role === "assistant" && extractText(obj.content)) {
330
+ return { kind: "thinking", phase: "stopped" };
331
+ }
332
+ if (obj.role === "tool") {
333
+ return { kind: "thinking", phase: "updated", label: "Tool result" };
334
+ }
335
+ return undefined;
336
+ }
337
+
338
+ function normalizeBlock(obj: KimiStreamJsonEvent, seq: number): StreamBlock {
339
+ let kind: StreamBlock["kind"] = "other";
340
+ if (obj.role === "assistant") {
341
+ if (obj.tool_calls?.length) kind = "tool_use";
342
+ else if (extractText(obj.content)) kind = "assistant_text";
343
+ else if (hasThinking(obj.content)) kind = "other";
344
+ } else if (obj.role === "tool") {
345
+ kind = "tool_result";
346
+ } else if (obj.file_path && typeof obj.content === "string") {
347
+ kind = "other";
348
+ } else if (obj.category || obj.severity) {
349
+ kind = "system";
350
+ }
351
+ return { raw: obj, kind, seq };
352
+ }
@@ -1,7 +1,9 @@
1
1
  import { ClaudeCodeAdapter, probeClaude } from "./claude-code.js";
2
2
  import { CodexAdapter, probeCodex } from "./codex.js";
3
+ import { DeepseekTuiAdapter, probeDeepseekTui } from "./deepseek-tui.js";
3
4
  import { GeminiAdapter, probeGemini } from "./gemini.js";
4
5
  import { HermesAgentAdapter, probeHermesAgent } from "./hermes-agent.js";
6
+ import { KimiAdapter, probeKimi } from "./kimi.js";
5
7
  import { OpenclawAcpAdapter, probeOpenclaw } from "./openclaw-acp.js";
6
8
  import type { RuntimeAdapter, RuntimeProbeResult } from "../types.js";
7
9
 
@@ -55,6 +57,28 @@ export const codexModule: RuntimeModule = {
55
57
  create: () => new CodexAdapter(),
56
58
  };
57
59
 
60
+ /** Built-in runtime module entry for DeepSeek TUI. */
61
+ export const deepseekTuiModule: RuntimeModule = {
62
+ id: "deepseek-tui",
63
+ displayName: "DeepSeek TUI",
64
+ binary: "deepseek",
65
+ envVar: "BOTCORD_DEEPSEEK_TUI_BIN",
66
+ probe: () => probeDeepseekTui(),
67
+ create: () => new DeepseekTuiAdapter(),
68
+ installHint:
69
+ "Install DeepSeek TUI and ensure the `deepseek` dispatcher is on PATH, or set BOTCORD_DEEPSEEK_TUI_BIN.",
70
+ };
71
+
72
+ /** Built-in runtime module entry for Kimi CLI. */
73
+ export const kimiModule: RuntimeModule = {
74
+ id: "kimi-cli",
75
+ displayName: "Kimi CLI",
76
+ binary: "kimi",
77
+ envVar: "BOTCORD_KIMI_CLI_BIN",
78
+ probe: () => probeKimi(),
79
+ create: () => new KimiAdapter(),
80
+ };
81
+
58
82
  /** Built-in runtime module entry for Hermes Agent (ACP stdio). */
59
83
  export const hermesAgentModule: RuntimeModule = {
60
84
  id: "hermes-agent",
@@ -96,6 +120,8 @@ export const openclawAcpModule: RuntimeModule = {
96
120
  export const RUNTIME_MODULES: readonly RuntimeModule[] = [
97
121
  claudeCodeModule,
98
122
  codexModule,
123
+ deepseekTuiModule,
124
+ kimiModule,
99
125
  hermesAgentModule,
100
126
  geminiModule,
101
127
  openclawAcpModule,