@botcord/daemon 0.2.4 → 0.2.6
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/agent-discovery.d.ts +7 -3
- package/dist/agent-discovery.js +9 -1
- package/dist/agent-workspace.d.ts +62 -0
- package/dist/agent-workspace.js +140 -10
- package/dist/config.d.ts +49 -1
- package/dist/config.js +57 -1
- package/dist/control-channel.d.ts +1 -4
- package/dist/control-channel.js +1 -4
- package/dist/daemon-config-map.d.ts +29 -12
- package/dist/daemon-config-map.js +105 -8
- package/dist/daemon.d.ts +2 -0
- package/dist/daemon.js +52 -5
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.js +22 -1
- package/dist/gateway/cli-resolver.d.ts +34 -0
- package/dist/gateway/cli-resolver.js +74 -0
- package/dist/gateway/dispatcher.d.ts +66 -1
- package/dist/gateway/dispatcher.js +583 -56
- package/dist/gateway/gateway.d.ts +29 -1
- package/dist/gateway/gateway.js +10 -0
- package/dist/gateway/index.d.ts +2 -0
- package/dist/gateway/index.js +2 -0
- package/dist/gateway/policy-resolver.d.ts +57 -0
- package/dist/gateway/policy-resolver.js +123 -0
- package/dist/gateway/runtimes/acp-stream.d.ts +99 -0
- package/dist/gateway/runtimes/acp-stream.js +394 -0
- package/dist/gateway/runtimes/codex.js +7 -0
- package/dist/gateway/runtimes/hermes-agent.d.ts +83 -0
- package/dist/gateway/runtimes/hermes-agent.js +180 -0
- package/dist/gateway/runtimes/ndjson-stream.d.ts +7 -2
- package/dist/gateway/runtimes/ndjson-stream.js +16 -3
- package/dist/gateway/runtimes/openclaw-acp.d.ts +44 -0
- package/dist/gateway/runtimes/openclaw-acp.js +500 -0
- package/dist/gateway/runtimes/registry.d.ts +4 -0
- package/dist/gateway/runtimes/registry.js +22 -0
- package/dist/gateway/transcript-paths.d.ts +30 -0
- package/dist/gateway/transcript-paths.js +114 -0
- package/dist/gateway/transcript.d.ts +123 -0
- package/dist/gateway/transcript.js +147 -0
- package/dist/gateway/types.d.ts +31 -0
- package/dist/index.js +286 -27
- package/dist/mention-scan.d.ts +22 -0
- package/dist/mention-scan.js +35 -0
- package/dist/provision.d.ts +73 -3
- package/dist/provision.js +373 -12
- package/dist/system-context.d.ts +5 -4
- package/dist/system-context.js +35 -5
- package/dist/turn-text.js +20 -1
- package/dist/url-utils.d.ts +9 -0
- package/dist/url-utils.js +18 -0
- package/dist/user-auth.js +0 -2
- package/dist/working-memory.js +1 -1
- package/package.json +2 -1
- package/src/__tests__/agent-workspace.test.ts +93 -0
- package/src/__tests__/daemon-config-map.test.ts +79 -0
- package/src/__tests__/openclaw-acp.test.ts +234 -0
- package/src/__tests__/policy-resolver.test.ts +124 -0
- package/src/__tests__/policy-updated-handler.test.ts +144 -0
- package/src/__tests__/provision.test.ts +160 -0
- package/src/__tests__/system-context.test.ts +52 -0
- package/src/__tests__/url-utils.test.ts +37 -0
- package/src/agent-discovery.ts +12 -4
- package/src/agent-workspace.ts +173 -9
- package/src/config.ts +132 -4
- package/src/control-channel.ts +1 -4
- package/src/daemon-config-map.ts +156 -12
- package/src/daemon.ts +66 -5
- package/src/doctor.ts +49 -2
- package/src/gateway/__tests__/dispatcher.test.ts +440 -2
- package/src/gateway/__tests__/hermes-agent-adapter.test.ts +302 -0
- package/src/gateway/__tests__/transcript.test.ts +496 -0
- package/src/gateway/cli-resolver.ts +92 -0
- package/src/gateway/dispatcher.ts +681 -58
- package/src/gateway/gateway.ts +46 -0
- package/src/gateway/index.ts +25 -0
- package/src/gateway/policy-resolver.ts +171 -0
- package/src/gateway/runtimes/acp-stream.ts +535 -0
- package/src/gateway/runtimes/codex.ts +7 -0
- package/src/gateway/runtimes/hermes-agent.ts +206 -0
- package/src/gateway/runtimes/ndjson-stream.ts +16 -3
- package/src/gateway/runtimes/openclaw-acp.ts +606 -0
- package/src/gateway/runtimes/registry.ts +24 -0
- package/src/gateway/transcript-paths.ts +145 -0
- package/src/gateway/transcript.ts +300 -0
- package/src/gateway/types.ts +32 -0
- package/src/index.ts +295 -30
- package/src/mention-scan.ts +38 -0
- package/src/provision.ts +446 -20
- package/src/system-context.ts +41 -9
- package/src/turn-text.ts +22 -1
- package/src/url-utils.ts +17 -0
- package/src/user-auth.ts +0 -2
- package/src/working-memory.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, readdirSync, statSync, rmSync } from "node:fs";
|
|
4
4
|
import { homedir, hostname } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { loadConfig, saveConfig, initDefaultConfig, resolveConfiguredAgentIds, PID_PATH, SNAPSHOT_PATH, CONFIG_FILE_PATH, } from "./config.js";
|
|
6
|
+
import { loadConfig, saveConfig, initDefaultConfig, resolveConfiguredAgentIds, PID_PATH, SNAPSHOT_PATH, CONFIG_FILE_PATH, CONFIG_MISSING, } from "./config.js";
|
|
7
7
|
import { resolveBootAgents } from "./agent-discovery.js";
|
|
8
|
+
import { defaultTranscriptRoot, resolveTranscriptEnabled, transcriptAgentRoot, transcriptFilePath, } from "./gateway/index.js";
|
|
8
9
|
import { startDaemon } from "./daemon.js";
|
|
9
10
|
import { log, LOG_FILE_PATH } from "./log.js";
|
|
10
11
|
import { detectRuntimes, getAdapterModule, listAdapterIds } from "./adapters/runtimes.js";
|
|
11
12
|
import { pollDeviceToken, requestDeviceCode, } from "@botcord/protocol-core";
|
|
12
13
|
import { AUTH_EXPIRED_FLAG_PATH, clearAuthExpiredFlag, loadUserAuth, saveUserAuth, userAuthFromTokenResponse, } from "./user-auth.js";
|
|
13
14
|
import { renderStatus } from "./status-render.js";
|
|
15
|
+
import { appendNextParam } from "./url-utils.js";
|
|
14
16
|
import { channelsFromDaemonConfig, defaultHttpFetcher, renderDoctor, runDoctor, } from "./doctor.js";
|
|
15
17
|
import { clearWorkingMemory, readWorkingMemory, resolveMemoryDir, updateWorkingMemory, DEFAULT_SECTION, } from "./working-memory.js";
|
|
16
18
|
const ADAPTER_LIST = listAdapterIds().join("|");
|
|
@@ -29,12 +31,8 @@ const HELP = `botcord-daemon — BotCord local daemon
|
|
|
29
31
|
Usage: botcord-daemon <command> [options]
|
|
30
32
|
|
|
31
33
|
Commands:
|
|
32
|
-
init [--agent <ag_xxx> ...] [--cwd <path>]
|
|
33
|
-
Create ~/.botcord/daemon/config.json.
|
|
34
|
-
Without --agent, the daemon discovers
|
|
35
|
-
identities from ~/.botcord/credentials
|
|
36
|
-
at startup (repeat --agent to pin).
|
|
37
34
|
start [--background|-d] [--relogin] [--hub <url>] [--label <name>]
|
|
35
|
+
[--agent <ag_xxx> ...] [--cwd <path>]
|
|
38
36
|
Start the daemon in the foreground by
|
|
39
37
|
default. Pass --background (alias -d)
|
|
40
38
|
to detach and return to the shell.
|
|
@@ -48,9 +46,24 @@ Commands:
|
|
|
48
46
|
(defaults to hostname). Non-TTY
|
|
49
47
|
environments must mount a pre-existing
|
|
50
48
|
user-auth.json (plan §6.4).
|
|
49
|
+
On first run, auto-creates
|
|
50
|
+
~/.botcord/daemon/config.json with a
|
|
51
|
+
default route (claude-code, $HOME) and
|
|
52
|
+
credential auto-discovery. Pass
|
|
53
|
+
--agent/--cwd to seed the file
|
|
54
|
+
(ignored once config exists).
|
|
51
55
|
stop Stop the running daemon (SIGTERM)
|
|
52
56
|
status Print daemon status (pid, agent)
|
|
53
57
|
logs [-f] Print log tail (use -f to follow)
|
|
58
|
+
transcript enable|disable|status Toggle persistent transcript logging
|
|
59
|
+
transcript list --agent <ag_xxx> List rooms with transcripts for an agent
|
|
60
|
+
transcript tail --agent <ag_xxx> --room <rm_xxx> [--topic <tp>] [-n 50] [-f]
|
|
61
|
+
Tail recent transcript records (NDJSON)
|
|
62
|
+
transcript dump --agent <ag_xxx> --room <rm_xxx> [--topic <tp>]
|
|
63
|
+
Print full transcript file to stdout
|
|
64
|
+
transcript prune --agent <ag_xxx> [--older-than 30d] [--all]
|
|
65
|
+
Remove rotated transcript files (or all
|
|
66
|
+
for the agent with --all --yes)
|
|
54
67
|
route add [match flags] --adapter <${ADAPTER_LIST}> --cwd <path>
|
|
55
68
|
match flags (first match wins; at least one conversation/sender selector required):
|
|
56
69
|
--conversation-id <rm_xxx> (alias: --room <rm_xxx>)
|
|
@@ -154,19 +167,32 @@ function pidAlive(pid) {
|
|
|
154
167
|
return false;
|
|
155
168
|
}
|
|
156
169
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Load the daemon config, auto-creating `~/.botcord/daemon/config.json`
|
|
172
|
+
* with sensible defaults on first run. `--agent` (repeated) pins explicit
|
|
173
|
+
* agent ids; `--cwd` overrides the defaultRoute working directory. Both
|
|
174
|
+
* are seed-only — they are ignored once a config already exists, since
|
|
175
|
+
* `route` and direct edits to `config.json` are the canonical way to
|
|
176
|
+
* change a configured daemon.
|
|
177
|
+
*/
|
|
178
|
+
function loadOrInitConfig(args) {
|
|
179
|
+
try {
|
|
180
|
+
return loadConfig();
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
const missing = err instanceof Error && err.code === CONFIG_MISSING;
|
|
184
|
+
if (!missing)
|
|
185
|
+
throw err;
|
|
186
|
+
const agents = args.lists.agent ?? [];
|
|
187
|
+
const cwd = typeof args.flags.cwd === "string" ? path.resolve(args.flags.cwd) : homedir();
|
|
188
|
+
const cfg = initDefaultConfig(agents, cwd);
|
|
189
|
+
saveConfig(cfg);
|
|
190
|
+
log.info("auto-initialized daemon config", { agents, cwd, path: CONFIG_FILE_PATH });
|
|
191
|
+
console.log(`wrote default config to ${CONFIG_FILE_PATH}`);
|
|
192
|
+
if (agents.length === 0) {
|
|
193
|
+
console.log("no --agent provided; daemon will auto-discover identities from ~/.botcord/credentials");
|
|
194
|
+
}
|
|
195
|
+
return cfg;
|
|
170
196
|
}
|
|
171
197
|
}
|
|
172
198
|
/**
|
|
@@ -200,10 +226,15 @@ async function runDeviceCodeFlow(opts) {
|
|
|
200
226
|
label: opts.label ?? null,
|
|
201
227
|
});
|
|
202
228
|
const dc = await requestDeviceCode(opts.hubUrl, opts.label ? { label: opts.label } : undefined);
|
|
203
|
-
const
|
|
229
|
+
const base = dc.verificationUriComplete ?? dc.verificationUri;
|
|
230
|
+
const display = appendNextParam(base, "/settings/daemons");
|
|
231
|
+
console.log("");
|
|
232
|
+
console.log("Open this URL in a browser where you're signed in to BotCord");
|
|
233
|
+
console.log("(typically your laptop, NOT this machine):");
|
|
234
|
+
console.log("");
|
|
235
|
+
console.log(` ${display}`);
|
|
204
236
|
console.log("");
|
|
205
|
-
console.log(`
|
|
206
|
-
console.log(`Code: ${dc.userCode}`);
|
|
237
|
+
console.log(`Or enter this code at ${dc.verificationUri}: ${dc.userCode}`);
|
|
207
238
|
console.log("Waiting for authorization (Ctrl-C to abort)...");
|
|
208
239
|
const expiresAt = Date.now() + dc.expiresIn * 1000;
|
|
209
240
|
let intervalSec = dc.interval;
|
|
@@ -289,7 +320,7 @@ async function ensureUserAuthForStart(args) {
|
|
|
289
320
|
return runDeviceCodeFlow({ hubUrl, label });
|
|
290
321
|
}
|
|
291
322
|
async function cmdStart(args) {
|
|
292
|
-
const cfg =
|
|
323
|
+
const cfg = loadOrInitConfig(args);
|
|
293
324
|
// Foreground is now the default. --background (alias -d) detaches.
|
|
294
325
|
// --foreground is still accepted (no-op) for backwards compatibility and
|
|
295
326
|
// is also what the detached child re-execs itself with.
|
|
@@ -492,6 +523,222 @@ async function cmdLogs(args) {
|
|
|
492
523
|
const lines = data.split("\n");
|
|
493
524
|
console.log(lines.slice(-100).join("\n"));
|
|
494
525
|
}
|
|
526
|
+
// ---------------------------------------------------------------------------
|
|
527
|
+
// transcript subcommands (design §5)
|
|
528
|
+
// ---------------------------------------------------------------------------
|
|
529
|
+
function transcriptStringFlag(args, name) {
|
|
530
|
+
const v = args.flags[name];
|
|
531
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
532
|
+
}
|
|
533
|
+
function parseDurationToMs(s) {
|
|
534
|
+
const m = /^(\d+)\s*([smhd])?$/.exec(s.trim());
|
|
535
|
+
if (!m)
|
|
536
|
+
return null;
|
|
537
|
+
const n = Number(m[1]);
|
|
538
|
+
const unit = m[2] ?? "d";
|
|
539
|
+
const mult = {
|
|
540
|
+
s: 1000,
|
|
541
|
+
m: 60_000,
|
|
542
|
+
h: 3_600_000,
|
|
543
|
+
d: 86_400_000,
|
|
544
|
+
};
|
|
545
|
+
return n * mult[unit];
|
|
546
|
+
}
|
|
547
|
+
async function cmdTranscript(args) {
|
|
548
|
+
switch (args.sub) {
|
|
549
|
+
case "enable":
|
|
550
|
+
return cmdTranscriptToggle(true);
|
|
551
|
+
case "disable":
|
|
552
|
+
return cmdTranscriptToggle(false);
|
|
553
|
+
case "status":
|
|
554
|
+
return cmdTranscriptStatus();
|
|
555
|
+
case "list":
|
|
556
|
+
return cmdTranscriptList(args);
|
|
557
|
+
case "tail":
|
|
558
|
+
return cmdTranscriptTail(args);
|
|
559
|
+
case "dump":
|
|
560
|
+
return cmdTranscriptDump(args);
|
|
561
|
+
case "prune":
|
|
562
|
+
return cmdTranscriptPrune(args);
|
|
563
|
+
default:
|
|
564
|
+
console.error("usage: botcord-daemon transcript <enable|disable|status|list|tail|dump|prune>");
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function cmdTranscriptToggle(enable) {
|
|
569
|
+
let cfg;
|
|
570
|
+
try {
|
|
571
|
+
cfg = loadConfig();
|
|
572
|
+
}
|
|
573
|
+
catch (err) {
|
|
574
|
+
const e = err;
|
|
575
|
+
if (e.code === CONFIG_MISSING) {
|
|
576
|
+
console.error(`daemon config not found — run \`botcord-daemon start\` once to initialize, then retry`);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
throw err;
|
|
580
|
+
}
|
|
581
|
+
cfg.transcript = { ...(cfg.transcript ?? {}), enabled: enable };
|
|
582
|
+
saveConfig(cfg);
|
|
583
|
+
console.log(`transcript persistence ${enable ? "enabled" : "disabled"} (next daemon start)`);
|
|
584
|
+
}
|
|
585
|
+
function cmdTranscriptStatus() {
|
|
586
|
+
let cfg = null;
|
|
587
|
+
try {
|
|
588
|
+
cfg = loadConfig();
|
|
589
|
+
}
|
|
590
|
+
catch (err) {
|
|
591
|
+
const e = err;
|
|
592
|
+
if (e.code !== CONFIG_MISSING)
|
|
593
|
+
throw err;
|
|
594
|
+
}
|
|
595
|
+
const configEnabled = cfg?.transcript?.enabled === true;
|
|
596
|
+
const env = process.env.BOTCORD_TRANSCRIPT;
|
|
597
|
+
const effective = resolveTranscriptEnabled(env, configEnabled);
|
|
598
|
+
let source;
|
|
599
|
+
if (env === "1" || env === "0")
|
|
600
|
+
source = `env BOTCORD_TRANSCRIPT=${env}`;
|
|
601
|
+
else if (configEnabled)
|
|
602
|
+
source = "config (transcript.enabled=true)";
|
|
603
|
+
else
|
|
604
|
+
source = "default-off";
|
|
605
|
+
console.log(`enabled: ${effective}`);
|
|
606
|
+
console.log(`source: ${source}`);
|
|
607
|
+
console.log(`root: ${defaultTranscriptRoot()}`);
|
|
608
|
+
}
|
|
609
|
+
function cmdTranscriptList(args) {
|
|
610
|
+
const agent = transcriptStringFlag(args, "agent");
|
|
611
|
+
if (!agent) {
|
|
612
|
+
console.error("transcript list requires --agent <ag_xxx>");
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
const root = transcriptAgentRoot(defaultTranscriptRoot(), agent);
|
|
616
|
+
if (!existsSync(root)) {
|
|
617
|
+
return; // no rooms → empty output
|
|
618
|
+
}
|
|
619
|
+
for (const entry of readdirSync(root)) {
|
|
620
|
+
const dir = path.join(root, entry);
|
|
621
|
+
let st;
|
|
622
|
+
try {
|
|
623
|
+
st = statSync(dir);
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (!st.isDirectory())
|
|
629
|
+
continue;
|
|
630
|
+
console.log(entry);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function cmdTranscriptTail(args) {
|
|
634
|
+
const agent = transcriptStringFlag(args, "agent");
|
|
635
|
+
const room = transcriptStringFlag(args, "room");
|
|
636
|
+
if (!agent || !room) {
|
|
637
|
+
console.error("transcript tail requires --agent <ag_xxx> --room <rm_xxx>");
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
const topic = transcriptStringFlag(args, "topic");
|
|
641
|
+
const file = transcriptFilePath(defaultTranscriptRoot(), agent, room, topic);
|
|
642
|
+
if (!existsSync(file)) {
|
|
643
|
+
console.error(`no transcript at ${file}`);
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
|
646
|
+
const follow = args.flags.f === true || args.flags.follow === true;
|
|
647
|
+
const nFlag = transcriptStringFlag(args, "n");
|
|
648
|
+
const n = nFlag && /^\d+$/.test(nFlag) ? Number(nFlag) : 50;
|
|
649
|
+
if (follow) {
|
|
650
|
+
const child = spawn("tail", ["-n", String(n), "-f", file], { stdio: "inherit" });
|
|
651
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
652
|
+
return new Promise((resolve) => {
|
|
653
|
+
child.on("close", () => resolve());
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
const data = readFileSync(file, "utf8");
|
|
657
|
+
const lines = data.split("\n").filter((l) => l.length > 0);
|
|
658
|
+
console.log(lines.slice(-n).join("\n"));
|
|
659
|
+
}
|
|
660
|
+
function cmdTranscriptDump(args) {
|
|
661
|
+
const agent = transcriptStringFlag(args, "agent");
|
|
662
|
+
const room = transcriptStringFlag(args, "room");
|
|
663
|
+
if (!agent || !room) {
|
|
664
|
+
console.error("transcript dump requires --agent <ag_xxx> --room <rm_xxx>");
|
|
665
|
+
process.exit(1);
|
|
666
|
+
}
|
|
667
|
+
const topic = transcriptStringFlag(args, "topic");
|
|
668
|
+
const file = transcriptFilePath(defaultTranscriptRoot(), agent, room, topic);
|
|
669
|
+
if (!existsSync(file)) {
|
|
670
|
+
console.error(`no transcript at ${file}`);
|
|
671
|
+
process.exit(1);
|
|
672
|
+
}
|
|
673
|
+
process.stdout.write(readFileSync(file, "utf8"));
|
|
674
|
+
}
|
|
675
|
+
function cmdTranscriptPrune(args) {
|
|
676
|
+
const agent = transcriptStringFlag(args, "agent");
|
|
677
|
+
if (!agent) {
|
|
678
|
+
console.error("transcript prune requires --agent <ag_xxx>");
|
|
679
|
+
process.exit(1);
|
|
680
|
+
}
|
|
681
|
+
const all = args.flags.all === true;
|
|
682
|
+
const olderThanFlag = transcriptStringFlag(args, "older-than");
|
|
683
|
+
const yes = args.flags.yes === true;
|
|
684
|
+
const root = transcriptAgentRoot(defaultTranscriptRoot(), agent);
|
|
685
|
+
if (!existsSync(root))
|
|
686
|
+
return;
|
|
687
|
+
if (all) {
|
|
688
|
+
if (!yes) {
|
|
689
|
+
console.error(`transcript prune --all will delete every transcript under ${root}; rerun with --yes to confirm`);
|
|
690
|
+
process.exit(1);
|
|
691
|
+
}
|
|
692
|
+
rmSync(root, { recursive: true, force: true });
|
|
693
|
+
console.log(`removed ${root}`);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
// Default and --older-than: prune rotated files only (the "{topic}.STAMP.jsonl" form).
|
|
697
|
+
// Active files (`{topic}.jsonl` / `_default.jsonl`) are never touched.
|
|
698
|
+
const cutoffMs = olderThanFlag ? parseDurationToMs(olderThanFlag) : null;
|
|
699
|
+
if (olderThanFlag && cutoffMs === null) {
|
|
700
|
+
console.error(`transcript prune --older-than: invalid duration "${olderThanFlag}" (use 30d / 12h / 30m / 60s)`);
|
|
701
|
+
process.exit(1);
|
|
702
|
+
}
|
|
703
|
+
const cutoff = cutoffMs !== null ? Date.now() - cutoffMs : null;
|
|
704
|
+
let removed = 0;
|
|
705
|
+
for (const roomEntry of readdirSync(root)) {
|
|
706
|
+
const dir = path.join(root, roomEntry);
|
|
707
|
+
let st;
|
|
708
|
+
try {
|
|
709
|
+
st = statSync(dir);
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
if (!st.isDirectory())
|
|
715
|
+
continue;
|
|
716
|
+
for (const f of readdirSync(dir)) {
|
|
717
|
+
// rotated files: <topic>.<YYYYMMDD-HHMMSS>.jsonl — must contain a stamp segment
|
|
718
|
+
if (!/^.+\.\d{8}-\d{6}\.jsonl$/.test(f))
|
|
719
|
+
continue;
|
|
720
|
+
const full = path.join(dir, f);
|
|
721
|
+
if (cutoff !== null) {
|
|
722
|
+
try {
|
|
723
|
+
const fst = statSync(full);
|
|
724
|
+
if (fst.mtimeMs >= cutoff)
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
try {
|
|
732
|
+
unlinkSync(full);
|
|
733
|
+
removed += 1;
|
|
734
|
+
}
|
|
735
|
+
catch (err) {
|
|
736
|
+
console.error(`failed to remove ${full}: ${err instanceof Error ? err.message : String(err)}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
console.log(`removed ${removed} rotated transcript file(s)`);
|
|
741
|
+
}
|
|
495
742
|
function formatRouteMatch(m) {
|
|
496
743
|
const parts = [];
|
|
497
744
|
if (m.channel)
|
|
@@ -793,13 +1040,25 @@ async function cmdDoctor(args) {
|
|
|
793
1040
|
// Doctor should not hard-fail when no config exists yet; channel probes
|
|
794
1041
|
// simply produce an empty list in that case.
|
|
795
1042
|
let channels = [];
|
|
1043
|
+
let cfgForEndpoints = null;
|
|
796
1044
|
try {
|
|
797
1045
|
const cfg = loadConfig();
|
|
1046
|
+
cfgForEndpoints = cfg;
|
|
798
1047
|
channels = channelsFromDaemonConfig(cfg);
|
|
799
1048
|
}
|
|
800
1049
|
catch {
|
|
801
1050
|
channels = [];
|
|
802
1051
|
}
|
|
1052
|
+
if (cfgForEndpoints?.openclawGateways && cfgForEndpoints.openclawGateways.length > 0) {
|
|
1053
|
+
const { collectRuntimeSnapshotAsync } = await import("./provision.js");
|
|
1054
|
+
const snap = await collectRuntimeSnapshotAsync({ cfg: cfgForEndpoints });
|
|
1055
|
+
const byId = new Map(snap.runtimes.map((r) => [r.id, r]));
|
|
1056
|
+
for (const e of entries) {
|
|
1057
|
+
const r = byId.get(e.id);
|
|
1058
|
+
if (r?.endpoints)
|
|
1059
|
+
e.endpoints = r.endpoints;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
803
1062
|
const credentialsPath = (accountId) => path.join(homedir(), ".botcord", "credentials", `${accountId}.json`);
|
|
804
1063
|
const input = await runDoctor(entries, channels, {
|
|
805
1064
|
credentialsPath,
|
|
@@ -821,9 +1080,6 @@ async function main() {
|
|
|
821
1080
|
}
|
|
822
1081
|
try {
|
|
823
1082
|
switch (args.cmd) {
|
|
824
|
-
case "init":
|
|
825
|
-
await cmdInit(args);
|
|
826
|
-
break;
|
|
827
1083
|
case "start":
|
|
828
1084
|
await cmdStart(args);
|
|
829
1085
|
break;
|
|
@@ -836,6 +1092,9 @@ async function main() {
|
|
|
836
1092
|
case "logs":
|
|
837
1093
|
await cmdLogs(args);
|
|
838
1094
|
break;
|
|
1095
|
+
case "transcript":
|
|
1096
|
+
await cmdTranscript(args);
|
|
1097
|
+
break;
|
|
839
1098
|
case "route":
|
|
840
1099
|
await cmdRoute(args);
|
|
841
1100
|
break;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mention text-fallback (design §4.2). The Hub's `messages.mentioned` flag is
|
|
3
|
+
* sender-supplied and therefore not trustworthy on its own; we OR it with a
|
|
4
|
+
* local scan for `@<display_name>` or `@<agent_id>` so an agent that the
|
|
5
|
+
* sender forgot (or refused) to mark mentioned still wakes when addressed.
|
|
6
|
+
*
|
|
7
|
+
* Kept tiny and synchronous — runs on every inbound message. Both inputs are
|
|
8
|
+
* normalized to lowercase to keep the match case-insensitive.
|
|
9
|
+
*/
|
|
10
|
+
export interface MentionTargets {
|
|
11
|
+
/** Daemon-known agent id (e.g. `ag_xxx`). Always included when present. */
|
|
12
|
+
agentId?: string;
|
|
13
|
+
/** Display name from the agent's credentials. */
|
|
14
|
+
displayName?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Return `true` when `text` contains an `@`-prefixed mention of `agentId`
|
|
18
|
+
* or `displayName`. Matches a literal `@` followed by the target — both the
|
|
19
|
+
* `@` and the target are required because plain occurrences of the
|
|
20
|
+
* displayName in conversation should NOT count as a mention.
|
|
21
|
+
*/
|
|
22
|
+
export declare function scanMention(text: string | undefined, targets: MentionTargets): boolean;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mention text-fallback (design §4.2). The Hub's `messages.mentioned` flag is
|
|
3
|
+
* sender-supplied and therefore not trustworthy on its own; we OR it with a
|
|
4
|
+
* local scan for `@<display_name>` or `@<agent_id>` so an agent that the
|
|
5
|
+
* sender forgot (or refused) to mark mentioned still wakes when addressed.
|
|
6
|
+
*
|
|
7
|
+
* Kept tiny and synchronous — runs on every inbound message. Both inputs are
|
|
8
|
+
* normalized to lowercase to keep the match case-insensitive.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Return `true` when `text` contains an `@`-prefixed mention of `agentId`
|
|
12
|
+
* or `displayName`. Matches a literal `@` followed by the target — both the
|
|
13
|
+
* `@` and the target are required because plain occurrences of the
|
|
14
|
+
* displayName in conversation should NOT count as a mention.
|
|
15
|
+
*/
|
|
16
|
+
export function scanMention(text, targets) {
|
|
17
|
+
if (!text)
|
|
18
|
+
return false;
|
|
19
|
+
const lower = text.toLowerCase();
|
|
20
|
+
const candidates = [];
|
|
21
|
+
if (targets.agentId)
|
|
22
|
+
candidates.push(targets.agentId.toLowerCase());
|
|
23
|
+
if (targets.displayName) {
|
|
24
|
+
const trimmed = targets.displayName.trim();
|
|
25
|
+
if (trimmed)
|
|
26
|
+
candidates.push(trimmed.toLowerCase());
|
|
27
|
+
}
|
|
28
|
+
for (const c of candidates) {
|
|
29
|
+
if (!c)
|
|
30
|
+
continue;
|
|
31
|
+
if (lower.includes("@" + c))
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
package/dist/provision.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { BotCordClient, type ControlAck, type ControlFrame, type ListRuntimesResult } from "@botcord/protocol-core";
|
|
1
|
+
import { BotCordClient, type AgentIdentitySnapshot, type ControlAck, type ControlFrame, type ListRuntimesResult } from "@botcord/protocol-core";
|
|
2
2
|
import type { Gateway } from "./gateway/index.js";
|
|
3
|
+
import type { PolicyResolverLike } from "./gateway/policy-resolver.js";
|
|
3
4
|
import { type DaemonConfig } from "./config.js";
|
|
4
5
|
/** Options accepted by {@link createProvisioner}. */
|
|
5
6
|
export interface ProvisionerOptions {
|
|
@@ -10,6 +11,14 @@ export interface ProvisionerOptions {
|
|
|
10
11
|
* run without a real Hub.
|
|
11
12
|
*/
|
|
12
13
|
register?: typeof BotCordClient.register;
|
|
14
|
+
/**
|
|
15
|
+
* Optional policy-resolver handle (PR3). When present, the
|
|
16
|
+
* `policy_updated` control frame routes through it: cache is invalidated
|
|
17
|
+
* for the (agent, room?) pair, and any embedded `policy` payload is
|
|
18
|
+
* applied directly so the next inbound sees the fresh policy without an
|
|
19
|
+
* extra round-trip.
|
|
20
|
+
*/
|
|
21
|
+
policyResolver?: PolicyResolverLike;
|
|
13
22
|
}
|
|
14
23
|
/** The value a frame handler returns (minus the `id` which the channel fills in). */
|
|
15
24
|
type AckBody = Omit<ControlAck, "id">;
|
|
@@ -36,6 +45,68 @@ export declare function removeAgentFromConfig(cfg: DaemonConfig, agentId: string
|
|
|
36
45
|
* gateway already isolates from throwing) and reading the wall clock.
|
|
37
46
|
*/
|
|
38
47
|
export declare function collectRuntimeSnapshot(): ListRuntimesResult;
|
|
48
|
+
/** Maximum number of `endpoints[]` entries persisted per runtime (RFC §3.8.2). */
|
|
49
|
+
export declare const RUNTIME_ENDPOINTS_CAP = 32;
|
|
50
|
+
/** Injection seam for L2 + L3 endpoint probes — kept testable + side-effect-free. */
|
|
51
|
+
export type WsEndpointProbeFn = (args: {
|
|
52
|
+
url: string;
|
|
53
|
+
token?: string;
|
|
54
|
+
timeoutMs: number;
|
|
55
|
+
}) => Promise<{
|
|
56
|
+
ok: boolean;
|
|
57
|
+
version?: string;
|
|
58
|
+
/**
|
|
59
|
+
* L3 — populated when `agents.list` succeeds. `id` is the stable key
|
|
60
|
+
* consumed by route lookups / `openclawAgent`; `name` is display-only.
|
|
61
|
+
*/
|
|
62
|
+
agents?: Array<{
|
|
63
|
+
id: string;
|
|
64
|
+
name?: string;
|
|
65
|
+
workspace?: string;
|
|
66
|
+
model?: {
|
|
67
|
+
name?: string;
|
|
68
|
+
provider?: string;
|
|
69
|
+
};
|
|
70
|
+
}>;
|
|
71
|
+
error?: string;
|
|
72
|
+
}>;
|
|
73
|
+
/**
|
|
74
|
+
* Async variant that includes L2 (gateway reachability) and L3 (agent listing)
|
|
75
|
+
* probes for runtimes that talk to external services. Used by the production
|
|
76
|
+
* `list_runtimes` and first-connect snapshot paths.
|
|
77
|
+
*
|
|
78
|
+
* `cfg` is optional so existing callers without a loaded config (e.g. tests)
|
|
79
|
+
* can keep using the sync `collectRuntimeSnapshot()` — when absent, the result
|
|
80
|
+
* is identical to that function.
|
|
81
|
+
*/
|
|
82
|
+
export declare function collectRuntimeSnapshotAsync(opts?: {
|
|
83
|
+
cfg?: {
|
|
84
|
+
openclawGateways?: Array<{
|
|
85
|
+
name: string;
|
|
86
|
+
url: string;
|
|
87
|
+
token?: string;
|
|
88
|
+
tokenFile?: string;
|
|
89
|
+
}>;
|
|
90
|
+
};
|
|
91
|
+
wsProbe?: WsEndpointProbeFn;
|
|
92
|
+
timeoutMs?: number;
|
|
93
|
+
}): Promise<ListRuntimesResult>;
|
|
94
|
+
interface HelloIdentityResult {
|
|
95
|
+
updated: number;
|
|
96
|
+
skipped: number;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Reconcile every agent identity carried by the `hello.agents` snapshot
|
|
100
|
+
* against the on-disk `identity.md`. Best-effort: a malformed entry or a
|
|
101
|
+
* file-system error for one agent never aborts the rest.
|
|
102
|
+
*
|
|
103
|
+
* Identity-snapshot semantics intentionally only touch the metadata
|
|
104
|
+
* line + Bio body — Role/Boundaries paragraphs the user authored locally
|
|
105
|
+
* are preserved (see `applyAgentIdentity`). Missing identity.md files
|
|
106
|
+
* (agent provisioned on a different daemon, or workspace cleared) are
|
|
107
|
+
* silently skipped.
|
|
108
|
+
*/
|
|
109
|
+
export declare function applyHelloIdentitySnapshot(snapshot: AgentIdentitySnapshot[] | undefined): HelloIdentityResult;
|
|
39
110
|
interface ReloadResult {
|
|
40
111
|
reloaded: true;
|
|
41
112
|
added: string[];
|
|
@@ -52,8 +123,7 @@ export declare function reloadConfig(ctx: {
|
|
|
52
123
|
gateway: Gateway;
|
|
53
124
|
}): Promise<ReloadResult>;
|
|
54
125
|
/**
|
|
55
|
-
* Per-agent entry returned by `list_agents`.
|
|
56
|
-
* `docs/daemon-control-plane-api-contract.md` §3.2 — `{id, name, online}`.
|
|
126
|
+
* Per-agent entry returned by `list_agents`. Wire shape: `{id, name, online}`.
|
|
57
127
|
* `status` and `lastMessageAt` are extra daemon-only fields the dashboard
|
|
58
128
|
* may ignore; kept so future contract revisions can promote them without
|
|
59
129
|
* breaking the wire.
|