@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.
- package/dist/gateway/channels/botcord.d.ts +23 -0
- package/dist/gateway/channels/botcord.js +93 -3
- package/dist/gateway/runtimes/claude-code.js +15 -3
- package/dist/index.js +20 -12
- package/package.json +1 -1
- package/src/gateway/__tests__/botcord-channel.test.ts +10 -3
- package/src/gateway/__tests__/claude-code-adapter.test.ts +2 -2
- package/src/gateway/channels/botcord.ts +89 -3
- package/src/gateway/runtimes/claude-code.ts +15 -3
- package/src/index.ts +21 -12
|
@@ -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
|
|
567
|
-
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
|
|
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:
|
|
86
|
-
//
|
|
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", "
|
|
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 [--
|
|
38
|
-
Start the daemon
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
@@ -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: {
|
|
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: "
|
|
504
|
+
kind: "assistant",
|
|
498
505
|
seq: 3,
|
|
499
|
-
|
|
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
|
|
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("
|
|
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
|
|
676
|
-
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
|
|
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:
|
|
105
|
-
//
|
|
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", "
|
|
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 [--
|
|
78
|
-
Start the daemon
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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.
|