@botcord/daemon 0.2.2 → 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
+ }
@@ -76,18 +76,30 @@ export class ClaudeCodeAdapter extends NdjsonStreamAdapter {
76
76
  }
77
77
  buildArgs(opts) {
78
78
  const args = ["-p", opts.text, "--output-format", "stream-json", "--verbose"];
79
+ // Headless `-p` mode does not load project `.claude/` by default, so
80
+ // per-agent skills seeded at `<workspace>/.claude/skills/` are invisible
81
+ // unless we opt in. `extraArgs` wins so operators can still override.
82
+ if (!opts.extraArgs?.some((a) => a.startsWith("--setting-sources"))) {
83
+ args.push("--setting-sources", "project");
84
+ }
79
85
  if (opts.sessionId) {
80
86
  if (!isValidClaudeSessionId(opts.sessionId))
81
87
  throw new Error(invalidClaudeSessionIdError());
82
88
  args.push("--resume", opts.sessionId);
83
89
  }
84
90
  // Permission-mode policy:
85
- // - owner: acceptEdits (owner trusts their own agent).
86
- // - 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).
87
99
  // `extraArgs` still wins — operators who know what they're doing can override either.
88
100
  if (!opts.extraArgs?.some((a) => a.startsWith("--permission-mode"))) {
89
101
  if (opts.trustLevel === "owner") {
90
- args.push("--permission-mode", "acceptEdits");
102
+ args.push("--permission-mode", "bypassPermissions");
91
103
  }
92
104
  else {
93
105
  args.push("--permission-mode", "default");
package/dist/index.js CHANGED
@@ -34,15 +34,18 @@ Commands:
34
34
  Without --agent, the daemon discovers
35
35
  identities from ~/.botcord/credentials
36
36
  at startup (repeat --agent to pin).
37
- start [--foreground] [--relogin] [--hub <url>] [--label <name>]
38
- Start the daemon. Without credentials
39
- and on a TTY, runs the interactive
40
- device-code login first. --hub defaults
41
- to ${DEFAULT_HUB} (or the URL stored in
42
- a previous login). --relogin forces
43
- re-login. --label is sent to the Hub
44
- on connect for the dashboard device
45
- list (defaults to hostname). Non-TTY
37
+ start [--background|-d] [--relogin] [--hub <url>] [--label <name>]
38
+ Start the daemon in the foreground by
39
+ default. Pass --background (alias -d)
40
+ to detach and return to the shell.
41
+ Without credentials and on a TTY, runs
42
+ the interactive device-code login
43
+ first. --hub defaults to ${DEFAULT_HUB}
44
+ (or the URL stored in a previous
45
+ login). --relogin forces re-login.
46
+ --label is sent to the Hub on connect
47
+ for the dashboard device list
48
+ (defaults to hostname). Non-TTY
46
49
  environments must mount a pre-existing
47
50
  user-auth.json (plan §6.4).
48
51
  stop Stop the running daemon (SIGTERM)
@@ -79,6 +82,8 @@ Env:
79
82
  /** Known boolean flags — never consume the following token as a value. */
80
83
  const BOOLEAN_FLAGS = new Set([
81
84
  "foreground",
85
+ "background",
86
+ "d",
82
87
  "f",
83
88
  "follow",
84
89
  "json",
@@ -285,9 +290,12 @@ async function ensureUserAuthForStart(args) {
285
290
  }
286
291
  async function cmdStart(args) {
287
292
  const cfg = loadConfig();
288
- const foreground = args.flags.foreground === true;
293
+ // Foreground is now the default. --background (alias -d) detaches.
294
+ // --foreground is still accepted (no-op) for backwards compatibility and
295
+ // is also what the detached child re-execs itself with.
296
+ const background = args.flags.background === true || args.flags.d === true;
289
297
  log.info("cmd start", {
290
- foreground,
298
+ background,
291
299
  relogin: args.flags.relogin === true,
292
300
  child: process.env.BOTCORD_DAEMON_CHILD === "1",
293
301
  });
@@ -304,7 +312,7 @@ async function cmdStart(args) {
304
312
  if (process.env.BOTCORD_DAEMON_CHILD !== "1") {
305
313
  await ensureUserAuthForStart(args);
306
314
  }
307
- if (!foreground) {
315
+ if (background) {
308
316
  // Detached child re-exec in foreground mode. The child writes the PID
309
317
  // file once it's up; the parent only polls to confirm startup so the
310
318
  // two never race on the same file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/daemon",
3
- "version": "0.2.2",
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
+ }
@@ -96,17 +96,29 @@ export class ClaudeCodeAdapter extends NdjsonStreamAdapter {
96
96
 
97
97
  protected buildArgs(opts: RuntimeRunOptions): string[] {
98
98
  const args = ["-p", opts.text, "--output-format", "stream-json", "--verbose"];
99
+ // Headless `-p` mode does not load project `.claude/` by default, so
100
+ // per-agent skills seeded at `<workspace>/.claude/skills/` are invisible
101
+ // unless we opt in. `extraArgs` wins so operators can still override.
102
+ if (!opts.extraArgs?.some((a) => a.startsWith("--setting-sources"))) {
103
+ args.push("--setting-sources", "project");
104
+ }
99
105
  if (opts.sessionId) {
100
106
  if (!isValidClaudeSessionId(opts.sessionId)) throw new Error(invalidClaudeSessionIdError());
101
107
  args.push("--resume", opts.sessionId);
102
108
  }
103
109
  // Permission-mode policy:
104
- // - owner: acceptEdits (owner trusts their own agent).
105
- // - 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).
106
118
  // `extraArgs` still wins — operators who know what they're doing can override either.
107
119
  if (!opts.extraArgs?.some((a) => a.startsWith("--permission-mode"))) {
108
120
  if (opts.trustLevel === "owner") {
109
- args.push("--permission-mode", "acceptEdits");
121
+ args.push("--permission-mode", "bypassPermissions");
110
122
  } else {
111
123
  args.push("--permission-mode", "default");
112
124
  }
package/src/index.ts CHANGED
@@ -74,15 +74,18 @@ Commands:
74
74
  Without --agent, the daemon discovers
75
75
  identities from ~/.botcord/credentials
76
76
  at startup (repeat --agent to pin).
77
- start [--foreground] [--relogin] [--hub <url>] [--label <name>]
78
- Start the daemon. Without credentials
79
- and on a TTY, runs the interactive
80
- device-code login first. --hub defaults
81
- to ${DEFAULT_HUB} (or the URL stored in
82
- a previous login). --relogin forces
83
- re-login. --label is sent to the Hub
84
- on connect for the dashboard device
85
- list (defaults to hostname). Non-TTY
77
+ start [--background|-d] [--relogin] [--hub <url>] [--label <name>]
78
+ Start the daemon in the foreground by
79
+ default. Pass --background (alias -d)
80
+ to detach and return to the shell.
81
+ Without credentials and on a TTY, runs
82
+ the interactive device-code login
83
+ first. --hub defaults to ${DEFAULT_HUB}
84
+ (or the URL stored in a previous
85
+ login). --relogin forces re-login.
86
+ --label is sent to the Hub on connect
87
+ for the dashboard device list
88
+ (defaults to hostname). Non-TTY
86
89
  environments must mount a pre-existing
87
90
  user-auth.json (plan §6.4).
88
91
  stop Stop the running daemon (SIGTERM)
@@ -128,6 +131,8 @@ interface ParsedArgs {
128
131
  /** Known boolean flags — never consume the following token as a value. */
129
132
  const BOOLEAN_FLAGS = new Set([
130
133
  "foreground",
134
+ "background",
135
+ "d",
131
136
  "f",
132
137
  "follow",
133
138
  "json",
@@ -363,9 +368,13 @@ async function ensureUserAuthForStart(args: ParsedArgs): Promise<UserAuthRecord
363
368
 
364
369
  async function cmdStart(args: ParsedArgs): Promise<void> {
365
370
  const cfg = loadConfig();
366
- const foreground = args.flags.foreground === true;
371
+ // Foreground is now the default. --background (alias -d) detaches.
372
+ // --foreground is still accepted (no-op) for backwards compatibility and
373
+ // is also what the detached child re-execs itself with.
374
+ const background =
375
+ args.flags.background === true || args.flags.d === true;
367
376
  log.info("cmd start", {
368
- foreground,
377
+ background,
369
378
  relogin: args.flags.relogin === true,
370
379
  child: process.env.BOTCORD_DAEMON_CHILD === "1",
371
380
  });
@@ -385,7 +394,7 @@ async function cmdStart(args: ParsedArgs): Promise<void> {
385
394
  await ensureUserAuthForStart(args);
386
395
  }
387
396
 
388
- if (!foreground) {
397
+ if (background) {
389
398
  // Detached child re-exec in foreground mode. The child writes the PID
390
399
  // file once it's up; the parent only polls to confirm startup so the
391
400
  // two never race on the same file.