@botcord/daemon 0.2.3 → 0.2.4

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.
@@ -102,3 +102,26 @@ export interface BatchedInboxRaw extends InboxMessage {
102
102
  */
103
103
  export declare function createBotCordChannel(options: BotCordChannelOptions): ChannelAdapter;
104
104
  export { normalizeInbox as __normalizeInboxForTests };
105
+ export { normalizeBlockForHub as __normalizeBlockForHubForTests };
106
+ /**
107
+ * Reshape a runtime StreamBlock `{ raw, kind, seq }` into the
108
+ * `{ kind, payload, seq }` form the owner-chat frontend renders.
109
+ *
110
+ * Daemon-internal kinds are Claude Code / Codex specific; the dashboard's
111
+ * StreamBlocksView expects a smaller vocabulary (`assistant`, `tool_call`,
112
+ * `tool_result`, `reasoning`) with structured `payload` fields. Without this
113
+ * remap the UI falls back to printing the bare kind string per step, which
114
+ * is what users see as "system / assistant_text / other / other".
115
+ *
116
+ * Extraction is best-effort — unknown shapes pass through as `other` with
117
+ * an empty payload rather than throwing.
118
+ */
119
+ declare function normalizeBlockForHub(block: {
120
+ raw?: unknown;
121
+ kind?: string;
122
+ seq?: number;
123
+ } | undefined, seq: number): {
124
+ kind: string;
125
+ seq: number;
126
+ payload: Record<string, unknown>;
127
+ };
@@ -555,6 +555,7 @@ export function createBotCordChannel(options) {
555
555
  try {
556
556
  const token = await client.ensureToken();
557
557
  const block = ctx.block;
558
+ const seq = typeof block?.seq === "number" ? block.seq : 0;
558
559
  const resp = await fetch(`${hubUrl}/hub/stream-block`, {
559
560
  method: "POST",
560
561
  headers: {
@@ -563,8 +564,8 @@ export function createBotCordChannel(options) {
563
564
  },
564
565
  body: JSON.stringify({
565
566
  trace_id: ctx.traceId,
566
- seq: typeof block?.seq === "number" ? block.seq : 0,
567
- block: ctx.block,
567
+ seq,
568
+ block: normalizeBlockForHub(block, seq),
568
569
  }),
569
570
  signal: AbortSignal.timeout(10_000),
570
571
  });
@@ -586,5 +587,94 @@ export function createBotCordChannel(options) {
586
587
  };
587
588
  return adapter;
588
589
  }
589
- // Re-export the normalizer for tests that want to exercise it directly.
590
+ // Re-export the normalizers for tests that want to exercise them directly.
590
591
  export { normalizeInbox as __normalizeInboxForTests };
592
+ export { normalizeBlockForHub as __normalizeBlockForHubForTests };
593
+ /**
594
+ * Reshape a runtime StreamBlock `{ raw, kind, seq }` into the
595
+ * `{ kind, payload, seq }` form the owner-chat frontend renders.
596
+ *
597
+ * Daemon-internal kinds are Claude Code / Codex specific; the dashboard's
598
+ * StreamBlocksView expects a smaller vocabulary (`assistant`, `tool_call`,
599
+ * `tool_result`, `reasoning`) with structured `payload` fields. Without this
600
+ * remap the UI falls back to printing the bare kind string per step, which
601
+ * is what users see as "system / assistant_text / other / other".
602
+ *
603
+ * Extraction is best-effort — unknown shapes pass through as `other` with
604
+ * an empty payload rather than throwing.
605
+ */
606
+ function normalizeBlockForHub(block, seq) {
607
+ const raw = (block?.raw ?? {});
608
+ const kind = block?.kind ?? "other";
609
+ const payload = {};
610
+ if (kind === "assistant_text") {
611
+ // Claude Code: {type:"assistant", message:{content:[{type:"text",text}]}}
612
+ // Codex: {type:"item.completed", item:{type:"agent_message", text}}
613
+ let text = "";
614
+ const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
615
+ for (const c of contents) {
616
+ if (c?.type === "text" && typeof c.text === "string")
617
+ text += c.text;
618
+ }
619
+ if (!text && typeof raw?.item?.text === "string")
620
+ text = raw.item.text;
621
+ return { kind: "assistant", seq, payload: { text } };
622
+ }
623
+ if (kind === "tool_use") {
624
+ // Claude Code: assistant message w/ content[].type === "tool_use" → {id,name,input}
625
+ // Codex: item.started / item.completed for command_execution, file_change, mcp_tool_call, web_search
626
+ const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
627
+ const tu = contents.find((c) => c?.type === "tool_use");
628
+ if (tu) {
629
+ payload.name = typeof tu.name === "string" ? tu.name : "tool";
630
+ if (tu.input && typeof tu.input === "object")
631
+ payload.params = tu.input;
632
+ if (typeof tu.id === "string")
633
+ payload.id = tu.id;
634
+ }
635
+ else if (raw?.item && typeof raw.item === "object") {
636
+ payload.name = typeof raw.item.type === "string" ? raw.item.type : "tool";
637
+ payload.params = raw.item;
638
+ }
639
+ return { kind: "tool_call", seq, payload };
640
+ }
641
+ if (kind === "tool_result") {
642
+ // Claude Code: {type:"user", message:{content:[{type:"tool_result",tool_use_id,content}]}}
643
+ const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
644
+ const tr = contents.find((c) => c?.type === "tool_result");
645
+ if (tr) {
646
+ let resultStr = "";
647
+ if (typeof tr.content === "string") {
648
+ resultStr = tr.content;
649
+ }
650
+ else if (Array.isArray(tr.content)) {
651
+ resultStr = tr.content
652
+ .map((c) => (typeof c?.text === "string" ? c.text : JSON.stringify(c)))
653
+ .join("\n");
654
+ }
655
+ payload.result = resultStr;
656
+ if (typeof tr.tool_use_id === "string")
657
+ payload.tool_use_id = tr.tool_use_id;
658
+ }
659
+ return { kind: "tool_result", seq, payload };
660
+ }
661
+ if (kind === "system") {
662
+ if (typeof raw?.subtype === "string")
663
+ payload.subtype = raw.subtype;
664
+ if (typeof raw?.session_id === "string")
665
+ payload.session_id = raw.session_id;
666
+ if (typeof raw?.model === "string")
667
+ payload.model = raw.model;
668
+ return { kind: "system", seq, payload };
669
+ }
670
+ // "other" — e.g. Claude Code `type:"result"` end-of-turn summary.
671
+ if (raw?.type === "result") {
672
+ if (typeof raw.result === "string")
673
+ payload.text = raw.result;
674
+ if (typeof raw.subtype === "string")
675
+ payload.subtype = raw.subtype;
676
+ if (typeof raw.total_cost_usd === "number")
677
+ payload.total_cost_usd = raw.total_cost_usd;
678
+ }
679
+ return { kind: "other", seq, payload };
680
+ }
@@ -88,12 +88,18 @@ export class ClaudeCodeAdapter extends NdjsonStreamAdapter {
88
88
  args.push("--resume", opts.sessionId);
89
89
  }
90
90
  // Permission-mode policy:
91
- // - owner: acceptEdits (owner trusts their own agent).
92
- // - non-owner (trusted/public): default (let Claude Code prompt / reject edits per its own rules).
91
+ // - owner: bypassPermissions (owner fully trusts their own agent; daemon
92
+ // has no authorization-relay UI yet, so any other mode causes Bash /
93
+ // WebFetch / MCP tool calls to deadlock waiting for a prompt that
94
+ // never reaches the user — see issue #332 for the planned MCP-bridge
95
+ // relay that will let us tighten this back up).
96
+ // - non-owner (trusted/public): default (let Claude Code prompt / reject
97
+ // per its own rules — we must NOT auto-bypass for agents the operator
98
+ // doesn't own).
93
99
  // `extraArgs` still wins — operators who know what they're doing can override either.
94
100
  if (!opts.extraArgs?.some((a) => a.startsWith("--permission-mode"))) {
95
101
  if (opts.trustLevel === "owner") {
96
- args.push("--permission-mode", "acceptEdits");
102
+ args.push("--permission-mode", "bypassPermissions");
97
103
  }
98
104
  else {
99
105
  args.push("--permission-mode", "default");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/daemon",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "BotCord local daemon — bridges Hub inbox push to local Claude Code / Codex / Gemini CLIs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -484,7 +484,11 @@ describe("createBotCordChannel — streamBlock()", () => {
484
484
  traceId: "m_trace",
485
485
  accountId: "ag_self",
486
486
  conversationId: "rm_oc_1",
487
- block: { kind: "assistant_text", seq: 3, raw: { text: "partial" } },
487
+ block: {
488
+ kind: "assistant_text",
489
+ seq: 3,
490
+ raw: { type: "assistant", message: { content: [{ type: "text", text: "partial" }] } },
491
+ },
488
492
  log: silentLog,
489
493
  });
490
494
  expect(fetchSpy).toHaveBeenCalledTimes(1);
@@ -493,10 +497,13 @@ describe("createBotCordChannel — streamBlock()", () => {
493
497
  expect(init.method).toBe("POST");
494
498
  const body = JSON.parse(init.body as string);
495
499
  expect(body.trace_id).toBe("m_trace");
500
+ expect(body.seq).toBe(3);
501
+ // The channel remaps daemon-internal kinds into the shape the dashboard
502
+ // renders: `{ kind, payload, seq }` with `assistant_text` → `assistant`.
496
503
  expect(body.block).toEqual({
497
- kind: "assistant_text",
504
+ kind: "assistant",
498
505
  seq: 3,
499
- raw: { text: "partial" },
506
+ payload: { text: "partial" },
500
507
  });
501
508
  expect((init.headers as Record<string, string>).Authorization).toBe("Bearer test-token");
502
509
  } finally {
@@ -211,7 +211,7 @@ process.stdout.write(JSON.stringify({type:"result", subtype:"success", session_i
211
211
  `,
212
212
  );
213
213
 
214
- it("owner → --permission-mode acceptEdits", async () => {
214
+ it("owner → --permission-mode bypassPermissions", async () => {
215
215
  const adapter = new ClaudeCodeAdapter({ binary: echoScript() });
216
216
  const ctrl = new AbortController();
217
217
  const res = await adapter.run({
@@ -225,7 +225,7 @@ process.stdout.write(JSON.stringify({type:"result", subtype:"success", session_i
225
225
  const argv = JSON.parse(res.text) as string[];
226
226
  const modeIdx = argv.indexOf("--permission-mode");
227
227
  expect(modeIdx).toBeGreaterThanOrEqual(0);
228
- expect(argv[modeIdx + 1]).toBe("acceptEdits");
228
+ expect(argv[modeIdx + 1]).toBe("bypassPermissions");
229
229
  });
230
230
 
231
231
  it("public → --permission-mode default", async () => {
@@ -664,6 +664,7 @@ export function createBotCordChannel(options: BotCordChannelOptions): ChannelAda
664
664
  try {
665
665
  const token = await client.ensureToken();
666
666
  const block = ctx.block as { raw?: unknown; kind?: string; seq?: number } | undefined;
667
+ const seq = typeof block?.seq === "number" ? block.seq : 0;
667
668
  const resp = await fetch(`${hubUrl}/hub/stream-block`, {
668
669
  method: "POST",
669
670
  headers: {
@@ -672,8 +673,8 @@ export function createBotCordChannel(options: BotCordChannelOptions): ChannelAda
672
673
  },
673
674
  body: JSON.stringify({
674
675
  trace_id: ctx.traceId,
675
- seq: typeof block?.seq === "number" ? block.seq : 0,
676
- block: ctx.block,
676
+ seq,
677
+ block: normalizeBlockForHub(block, seq),
677
678
  }),
678
679
  signal: AbortSignal.timeout(10_000),
679
680
  });
@@ -697,5 +698,90 @@ export function createBotCordChannel(options: BotCordChannelOptions): ChannelAda
697
698
  return adapter;
698
699
  }
699
700
 
700
- // Re-export the normalizer for tests that want to exercise it directly.
701
+ // Re-export the normalizers for tests that want to exercise them directly.
701
702
  export { normalizeInbox as __normalizeInboxForTests };
703
+ export { normalizeBlockForHub as __normalizeBlockForHubForTests };
704
+
705
+ /**
706
+ * Reshape a runtime StreamBlock `{ raw, kind, seq }` into the
707
+ * `{ kind, payload, seq }` form the owner-chat frontend renders.
708
+ *
709
+ * Daemon-internal kinds are Claude Code / Codex specific; the dashboard's
710
+ * StreamBlocksView expects a smaller vocabulary (`assistant`, `tool_call`,
711
+ * `tool_result`, `reasoning`) with structured `payload` fields. Without this
712
+ * remap the UI falls back to printing the bare kind string per step, which
713
+ * is what users see as "system / assistant_text / other / other".
714
+ *
715
+ * Extraction is best-effort — unknown shapes pass through as `other` with
716
+ * an empty payload rather than throwing.
717
+ */
718
+ function normalizeBlockForHub(
719
+ block: { raw?: unknown; kind?: string; seq?: number } | undefined,
720
+ seq: number,
721
+ ): { kind: string; seq: number; payload: Record<string, unknown> } {
722
+ const raw = (block?.raw ?? {}) as any;
723
+ const kind = block?.kind ?? "other";
724
+ const payload: Record<string, unknown> = {};
725
+
726
+ if (kind === "assistant_text") {
727
+ // Claude Code: {type:"assistant", message:{content:[{type:"text",text}]}}
728
+ // Codex: {type:"item.completed", item:{type:"agent_message", text}}
729
+ let text = "";
730
+ const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
731
+ for (const c of contents) {
732
+ if (c?.type === "text" && typeof c.text === "string") text += c.text;
733
+ }
734
+ if (!text && typeof raw?.item?.text === "string") text = raw.item.text;
735
+ return { kind: "assistant", seq, payload: { text } };
736
+ }
737
+
738
+ if (kind === "tool_use") {
739
+ // Claude Code: assistant message w/ content[].type === "tool_use" → {id,name,input}
740
+ // Codex: item.started / item.completed for command_execution, file_change, mcp_tool_call, web_search
741
+ const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
742
+ const tu = contents.find((c: any) => c?.type === "tool_use");
743
+ if (tu) {
744
+ payload.name = typeof tu.name === "string" ? tu.name : "tool";
745
+ if (tu.input && typeof tu.input === "object") payload.params = tu.input;
746
+ if (typeof tu.id === "string") payload.id = tu.id;
747
+ } else if (raw?.item && typeof raw.item === "object") {
748
+ payload.name = typeof raw.item.type === "string" ? raw.item.type : "tool";
749
+ payload.params = raw.item;
750
+ }
751
+ return { kind: "tool_call", seq, payload };
752
+ }
753
+
754
+ if (kind === "tool_result") {
755
+ // Claude Code: {type:"user", message:{content:[{type:"tool_result",tool_use_id,content}]}}
756
+ const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
757
+ const tr = contents.find((c: any) => c?.type === "tool_result");
758
+ if (tr) {
759
+ let resultStr = "";
760
+ if (typeof tr.content === "string") {
761
+ resultStr = tr.content;
762
+ } else if (Array.isArray(tr.content)) {
763
+ resultStr = tr.content
764
+ .map((c: any) => (typeof c?.text === "string" ? c.text : JSON.stringify(c)))
765
+ .join("\n");
766
+ }
767
+ payload.result = resultStr;
768
+ if (typeof tr.tool_use_id === "string") payload.tool_use_id = tr.tool_use_id;
769
+ }
770
+ return { kind: "tool_result", seq, payload };
771
+ }
772
+
773
+ if (kind === "system") {
774
+ if (typeof raw?.subtype === "string") payload.subtype = raw.subtype;
775
+ if (typeof raw?.session_id === "string") payload.session_id = raw.session_id;
776
+ if (typeof raw?.model === "string") payload.model = raw.model;
777
+ return { kind: "system", seq, payload };
778
+ }
779
+
780
+ // "other" — e.g. Claude Code `type:"result"` end-of-turn summary.
781
+ if (raw?.type === "result") {
782
+ if (typeof raw.result === "string") payload.text = raw.result;
783
+ if (typeof raw.subtype === "string") payload.subtype = raw.subtype;
784
+ if (typeof raw.total_cost_usd === "number") payload.total_cost_usd = raw.total_cost_usd;
785
+ }
786
+ return { kind: "other", seq, payload };
787
+ }
@@ -107,12 +107,18 @@ export class ClaudeCodeAdapter extends NdjsonStreamAdapter {
107
107
  args.push("--resume", opts.sessionId);
108
108
  }
109
109
  // Permission-mode policy:
110
- // - owner: acceptEdits (owner trusts their own agent).
111
- // - non-owner (trusted/public): default (let Claude Code prompt / reject edits per its own rules).
110
+ // - owner: bypassPermissions (owner fully trusts their own agent; daemon
111
+ // has no authorization-relay UI yet, so any other mode causes Bash /
112
+ // WebFetch / MCP tool calls to deadlock waiting for a prompt that
113
+ // never reaches the user — see issue #332 for the planned MCP-bridge
114
+ // relay that will let us tighten this back up).
115
+ // - non-owner (trusted/public): default (let Claude Code prompt / reject
116
+ // per its own rules — we must NOT auto-bypass for agents the operator
117
+ // doesn't own).
112
118
  // `extraArgs` still wins — operators who know what they're doing can override either.
113
119
  if (!opts.extraArgs?.some((a) => a.startsWith("--permission-mode"))) {
114
120
  if (opts.trustLevel === "owner") {
115
- args.push("--permission-mode", "acceptEdits");
121
+ args.push("--permission-mode", "bypassPermissions");
116
122
  } else {
117
123
  args.push("--permission-mode", "default");
118
124
  }