@botcord/daemon 0.2.50 → 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.
@@ -768,6 +768,8 @@ function normalizeBlockForHub(block, seq) {
768
768
  if (kind === "assistant_text") {
769
769
  // Claude Code: {type:"assistant", message:{content:[{type:"text",text}]}}
770
770
  // Codex: {type:"item.completed", item:{type:"agent_message", text}}
771
+ // DeepSeek: {event:"message.delta", payload:{content}} or
772
+ // {event:"item.delta", payload:{payload:{kind:"agent_message", delta}}}
771
773
  let text = "";
772
774
  const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
773
775
  for (const c of contents) {
@@ -776,6 +778,15 @@ function normalizeBlockForHub(block, seq) {
776
778
  }
777
779
  if (!text && typeof raw?.item?.text === "string")
778
780
  text = raw.item.text;
781
+ if (!text && raw?.event === "message.delta" && typeof raw?.payload?.content === "string") {
782
+ text = raw.payload.content;
783
+ }
784
+ if (!text &&
785
+ raw?.event === "item.delta" &&
786
+ raw?.payload?.payload?.kind === "agent_message" &&
787
+ typeof raw?.payload?.payload?.delta === "string") {
788
+ text = raw.payload.payload.delta;
789
+ }
779
790
  return { kind: "assistant", seq, payload: { text } };
780
791
  }
781
792
  if (kind === "tool_use") {
@@ -8,7 +8,7 @@ export declare function probeKimi(deps?: ProbeDeps): RuntimeProbeResult;
8
8
  /**
9
9
  * Kimi CLI adapter — spawns:
10
10
  *
11
- * kimi --work-dir <cwd> --print --output-format stream-json --session <sid> --prompt <text>
11
+ * kimi --work-dir <cwd> --print --output-format stream-json --session <sid> --afk --prompt <text>
12
12
  *
13
13
  * `--session <sid>` resumes an existing session or creates a new session with
14
14
  * that id, so the adapter generates a UUID on first turn and persists it for
@@ -17,6 +17,108 @@ function isValidKimiSessionId(sessionId) {
17
17
  function invalidKimiSessionIdError() {
18
18
  return "kimi-cli: invalid sessionId (expected non-control text not starting with '-')";
19
19
  }
20
+ const KIMI_EXTRA_FLAGS_WITH_VALUE = new Set([
21
+ "--add-dir",
22
+ "--agent",
23
+ "--agent-file",
24
+ "--config",
25
+ "--config-file",
26
+ "--max-ralph-iterations",
27
+ "--max-retries-per-step",
28
+ "--max-steps-per-turn",
29
+ "--mcp-config",
30
+ "--mcp-config-file",
31
+ "--model",
32
+ "--skills-dir",
33
+ "-m",
34
+ ]);
35
+ const KIMI_EXTRA_BOOLEAN_FLAGS = new Set([
36
+ "--afk",
37
+ "--auto-approve",
38
+ "--debug",
39
+ "--no-thinking",
40
+ "--plan",
41
+ "--thinking",
42
+ "--verbose",
43
+ "--yes",
44
+ "--yolo",
45
+ "-y",
46
+ ]);
47
+ // Flags owned by the adapter because BotCord depends on Kimi's non-interactive
48
+ // stream-json contract, cwd isolation, prompt placement, and session routing.
49
+ const KIMI_ADAPTER_OWNED_FLAGS = new Set([
50
+ "--acp",
51
+ "--command",
52
+ "--continue",
53
+ "--final-message-only",
54
+ "--help",
55
+ "--input-format",
56
+ "--output-format",
57
+ "--print",
58
+ "--prompt",
59
+ "--quiet",
60
+ "--resume",
61
+ "--session",
62
+ "--version",
63
+ "--wire",
64
+ "--work-dir",
65
+ "-C",
66
+ "-S",
67
+ "-V",
68
+ "-c",
69
+ "-h",
70
+ "-p",
71
+ "-r",
72
+ "-w",
73
+ ]);
74
+ function flagName(arg) {
75
+ if (!arg.startsWith("-"))
76
+ return arg;
77
+ const eq = arg.indexOf("=");
78
+ return eq === -1 ? arg : arg.slice(0, eq);
79
+ }
80
+ function nextValue(args, index) {
81
+ const next = args[index + 1];
82
+ if (typeof next !== "string")
83
+ return undefined;
84
+ if (!next.startsWith("-"))
85
+ return next;
86
+ return /^-\d/.test(next) ? next : undefined;
87
+ }
88
+ function sanitizeKimiExtraArgs(extraArgs) {
89
+ if (!extraArgs?.length)
90
+ return [];
91
+ const out = [];
92
+ for (let i = 0; i < extraArgs.length; i += 1) {
93
+ const arg = extraArgs[i];
94
+ const name = flagName(arg);
95
+ if (KIMI_ADAPTER_OWNED_FLAGS.has(name)) {
96
+ if (!arg.includes("=") && nextValue(extraArgs, i) !== undefined)
97
+ i += 1;
98
+ continue;
99
+ }
100
+ if (KIMI_EXTRA_FLAGS_WITH_VALUE.has(name)) {
101
+ if (arg.includes("=")) {
102
+ out.push(arg);
103
+ continue;
104
+ }
105
+ const value = nextValue(extraArgs, i);
106
+ if (value !== undefined) {
107
+ out.push(arg, value);
108
+ i += 1;
109
+ }
110
+ continue;
111
+ }
112
+ if (KIMI_EXTRA_BOOLEAN_FLAGS.has(name)) {
113
+ out.push(arg);
114
+ continue;
115
+ }
116
+ if (arg.startsWith("-") && !arg.includes("=") && nextValue(extraArgs, i) !== undefined) {
117
+ i += 1;
118
+ }
119
+ }
120
+ return out;
121
+ }
20
122
  /** Resolve the Kimi CLI executable on PATH. */
21
123
  export function resolveKimiCommand(deps = {}) {
22
124
  return resolveCommandOnPath("kimi", deps);
@@ -35,7 +137,7 @@ export function probeKimi(deps = {}) {
35
137
  /**
36
138
  * Kimi CLI adapter — spawns:
37
139
  *
38
- * kimi --work-dir <cwd> --print --output-format stream-json --session <sid> --prompt <text>
140
+ * kimi --work-dir <cwd> --print --output-format stream-json --session <sid> --afk --prompt <text>
39
141
  *
40
142
  * `--session <sid>` resumes an existing session or creates a new session with
41
143
  * that id, so the adapter generates a UUID on first turn and persists it for
@@ -83,8 +185,7 @@ export class KimiAdapter extends NdjsonStreamAdapter {
83
185
  sessionId,
84
186
  "--afk",
85
187
  ];
86
- if (opts.extraArgs?.length)
87
- args.push(...opts.extraArgs);
188
+ args.push(...sanitizeKimiExtraArgs(opts.extraArgs));
88
189
  args.push("--prompt", promptWithSystemContext(opts.text, opts.systemContext));
89
190
  return args;
90
191
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/daemon",
3
- "version": "0.2.50",
3
+ "version": "0.2.51",
4
4
  "description": "BotCord local daemon — bridges Hub inbox push to local Claude Code / Codex / Gemini CLIs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -618,6 +618,92 @@ describe("createBotCordChannel — streamBlock()", () => {
618
618
  }
619
619
  });
620
620
 
621
+ it("normalizes DeepSeek message.delta assistant text", async () => {
622
+ const fetchSpy = vi.fn().mockResolvedValue(new Response(null, { status: 204 }));
623
+ const realFetch = globalThis.fetch;
624
+ globalThis.fetch = fetchSpy as unknown as typeof fetch;
625
+ try {
626
+ const client = makeClient({
627
+ getHubUrl: vi.fn().mockReturnValue("https://hub.example.com"),
628
+ });
629
+ const channel = createBotCordChannel({
630
+ id: "botcord-main",
631
+ accountId: "ag_self",
632
+ agentId: "ag_self",
633
+ client,
634
+ hubBaseUrl: "https://hub.example.com",
635
+ });
636
+ await channel.streamBlock!({
637
+ traceId: "m_trace",
638
+ accountId: "ag_self",
639
+ conversationId: "rm_oc_1",
640
+ block: {
641
+ kind: "assistant_text",
642
+ seq: 4,
643
+ raw: {
644
+ event: "message.delta",
645
+ payload: { thread_id: "thr_1", turn_id: "turn_1", content: "hello " },
646
+ },
647
+ },
648
+ log: silentLog,
649
+ });
650
+ const [, init] = fetchSpy.mock.calls[0];
651
+ const body = JSON.parse(init.body as string);
652
+ expect(body.block).toEqual({
653
+ kind: "assistant",
654
+ seq: 4,
655
+ payload: { text: "hello " },
656
+ });
657
+ } finally {
658
+ globalThis.fetch = realFetch;
659
+ }
660
+ });
661
+
662
+ it("normalizes DeepSeek item.delta assistant text", async () => {
663
+ const fetchSpy = vi.fn().mockResolvedValue(new Response(null, { status: 204 }));
664
+ const realFetch = globalThis.fetch;
665
+ globalThis.fetch = fetchSpy as unknown as typeof fetch;
666
+ try {
667
+ const client = makeClient({
668
+ getHubUrl: vi.fn().mockReturnValue("https://hub.example.com"),
669
+ });
670
+ const channel = createBotCordChannel({
671
+ id: "botcord-main",
672
+ accountId: "ag_self",
673
+ agentId: "ag_self",
674
+ client,
675
+ hubBaseUrl: "https://hub.example.com",
676
+ });
677
+ await channel.streamBlock!({
678
+ traceId: "m_trace",
679
+ accountId: "ag_self",
680
+ conversationId: "rm_oc_1",
681
+ block: {
682
+ kind: "assistant_text",
683
+ seq: 5,
684
+ raw: {
685
+ event: "item.delta",
686
+ payload: {
687
+ thread_id: "thr_1",
688
+ turn_id: "turn_1",
689
+ payload: { kind: "agent_message", delta: "deepseek" },
690
+ },
691
+ },
692
+ },
693
+ log: silentLog,
694
+ });
695
+ const [, init] = fetchSpy.mock.calls[0];
696
+ const body = JSON.parse(init.body as string);
697
+ expect(body.block).toEqual({
698
+ kind: "assistant",
699
+ seq: 5,
700
+ payload: { text: "deepseek" },
701
+ });
702
+ } finally {
703
+ globalThis.fetch = realFetch;
704
+ }
705
+ });
706
+
621
707
  it("normalizes a thinking block with phase/label/source payload", async () => {
622
708
  const fetchSpy = vi.fn().mockResolvedValue(new Response(null, { status: 204 }));
623
709
  const realFetch = globalThis.fetch;
@@ -18,7 +18,7 @@ afterAll(() => {
18
18
  rmSync(tmpRoot, { recursive: true, force: true });
19
19
  });
20
20
 
21
- function runAdapter(script: string, sessionId: string | null = null) {
21
+ function runAdapter(script: string, sessionId: string | null = null, extraArgs?: string[]) {
22
22
  const adapter = new KimiAdapter({ binary: script });
23
23
  const ctrl = new AbortController();
24
24
  return adapter.run({
@@ -28,6 +28,7 @@ function runAdapter(script: string, sessionId: string | null = null) {
28
28
  cwd: tmpRoot,
29
29
  signal: ctrl.signal,
30
30
  trustLevel: "owner",
31
+ extraArgs,
31
32
  });
32
33
  }
33
34
 
@@ -69,6 +70,89 @@ process.stdout.write(JSON.stringify({role:"assistant", content:JSON.stringify(ar
69
70
  expect(argv).toContain("--afk");
70
71
  });
71
72
 
73
+ it("drops non-Kimi inherited extraArgs and their values", async () => {
74
+ const script = makeScript(
75
+ "filter-foreign-argv.js",
76
+ `
77
+ const argv = process.argv.slice(2);
78
+ process.stdout.write(JSON.stringify({role:"assistant", content:JSON.stringify(argv)}) + "\\n");
79
+ `,
80
+ );
81
+ const res = await runAdapter(script, "sid-123", [
82
+ "--permission-mode",
83
+ "bypassPermissions",
84
+ "--model",
85
+ "kimi-k2",
86
+ ]);
87
+ const argv = JSON.parse(res.text) as string[];
88
+ expect(argv).not.toContain("--permission-mode");
89
+ expect(argv).not.toContain("bypassPermissions");
90
+ expect(argv).toContain("--model");
91
+ expect(argv[argv.indexOf("--model") + 1]).toBe("kimi-k2");
92
+ });
93
+
94
+ it("preserves Kimi value flags with negative numeric values", async () => {
95
+ const script = makeScript(
96
+ "negative-value-argv.js",
97
+ `
98
+ const argv = process.argv.slice(2);
99
+ process.stdout.write(JSON.stringify({role:"assistant", content:JSON.stringify(argv)}) + "\\n");
100
+ `,
101
+ );
102
+ const res = await runAdapter(script, "sid-123", [
103
+ "--max-ralph-iterations",
104
+ "-1",
105
+ "--max-steps-per-turn=3",
106
+ ]);
107
+ const argv = JSON.parse(res.text) as string[];
108
+ expect(argv).toContain("--max-ralph-iterations");
109
+ expect(argv[argv.indexOf("--max-ralph-iterations") + 1]).toBe("-1");
110
+ expect(argv).toContain("--max-steps-per-turn=3");
111
+ });
112
+
113
+ it("drops incomplete Kimi value flags instead of passing invalid argv", async () => {
114
+ const script = makeScript(
115
+ "incomplete-value-argv.js",
116
+ `
117
+ const argv = process.argv.slice(2);
118
+ process.stdout.write(JSON.stringify({role:"assistant", content:JSON.stringify(argv)}) + "\\n");
119
+ `,
120
+ );
121
+ const res = await runAdapter(script, "sid-123", ["--model", "--plan"]);
122
+ const argv = JSON.parse(res.text) as string[];
123
+ expect(argv).not.toContain("--model");
124
+ expect(argv).toContain("--plan");
125
+ });
126
+
127
+ it("does not let extraArgs override adapter-owned stream/session/prompt flags", async () => {
128
+ const script = makeScript(
129
+ "filter-owned-argv.js",
130
+ `
131
+ const argv = process.argv.slice(2);
132
+ process.stdout.write(JSON.stringify({role:"assistant", content:JSON.stringify(argv)}) + "\\n");
133
+ `,
134
+ );
135
+ const res = await runAdapter(script, "real-session", [
136
+ "--output-format",
137
+ "text",
138
+ "--session",
139
+ "evil-session",
140
+ "--prompt",
141
+ "evil prompt",
142
+ "--plan",
143
+ ]);
144
+ const argv = JSON.parse(res.text) as string[];
145
+ expect(argv.filter((a) => a === "--output-format")).toHaveLength(1);
146
+ expect(argv[argv.indexOf("--output-format") + 1]).toBe("stream-json");
147
+ expect(argv.filter((a) => a === "--session")).toHaveLength(1);
148
+ expect(argv[argv.indexOf("--session") + 1]).toBe("real-session");
149
+ expect(argv.filter((a) => a === "--prompt")).toHaveLength(1);
150
+ expect(argv[argv.indexOf("--prompt") + 1]).toBe("hi");
151
+ expect(argv).toContain("--plan");
152
+ expect(argv).not.toContain("evil-session");
153
+ expect(argv).not.toContain("evil prompt");
154
+ });
155
+
72
156
  it("rejects session ids that could be parsed as flags", async () => {
73
157
  const script = makeScript(
74
158
  "should-not-spawn.js",
@@ -907,12 +907,25 @@ function normalizeBlockForHub(
907
907
  if (kind === "assistant_text") {
908
908
  // Claude Code: {type:"assistant", message:{content:[{type:"text",text}]}}
909
909
  // Codex: {type:"item.completed", item:{type:"agent_message", text}}
910
+ // DeepSeek: {event:"message.delta", payload:{content}} or
911
+ // {event:"item.delta", payload:{payload:{kind:"agent_message", delta}}}
910
912
  let text = "";
911
913
  const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
912
914
  for (const c of contents) {
913
915
  if (c?.type === "text" && typeof c.text === "string") text += c.text;
914
916
  }
915
917
  if (!text && typeof raw?.item?.text === "string") text = raw.item.text;
918
+ if (!text && raw?.event === "message.delta" && typeof raw?.payload?.content === "string") {
919
+ text = raw.payload.content;
920
+ }
921
+ if (
922
+ !text &&
923
+ raw?.event === "item.delta" &&
924
+ raw?.payload?.payload?.kind === "agent_message" &&
925
+ typeof raw?.payload?.payload?.delta === "string"
926
+ ) {
927
+ text = raw.payload.payload.delta;
928
+ }
916
929
  return { kind: "assistant", seq, payload: { text } };
917
930
  }
918
931
 
@@ -22,6 +22,113 @@ function invalidKimiSessionIdError(): string {
22
22
  return "kimi-cli: invalid sessionId (expected non-control text not starting with '-')";
23
23
  }
24
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
+
25
132
  /** Resolve the Kimi CLI executable on PATH. */
26
133
  export function resolveKimiCommand(deps: ProbeDeps = {}): string | null {
27
134
  return resolveCommandOnPath("kimi", deps);
@@ -41,7 +148,7 @@ export function probeKimi(deps: ProbeDeps = {}): RuntimeProbeResult {
41
148
  /**
42
149
  * Kimi CLI adapter — spawns:
43
150
  *
44
- * kimi --work-dir <cwd> --print --output-format stream-json --session <sid> --prompt <text>
151
+ * kimi --work-dir <cwd> --print --output-format stream-json --session <sid> --afk --prompt <text>
45
152
  *
46
153
  * `--session <sid>` resumes an existing session or creates a new session with
47
154
  * that id, so the adapter generates a UUID on first turn and persists it for
@@ -93,7 +200,7 @@ export class KimiAdapter extends NdjsonStreamAdapter {
93
200
  sessionId,
94
201
  "--afk",
95
202
  ];
96
- if (opts.extraArgs?.length) args.push(...opts.extraArgs);
203
+ args.push(...sanitizeKimiExtraArgs(opts.extraArgs));
97
204
  args.push("--prompt", promptWithSystemContext(opts.text, opts.systemContext));
98
205
  return args;
99
206
  }