@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/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
6
|
import {
|
|
@@ -11,11 +11,18 @@ import {
|
|
|
11
11
|
PID_PATH,
|
|
12
12
|
SNAPSHOT_PATH,
|
|
13
13
|
CONFIG_FILE_PATH,
|
|
14
|
+
CONFIG_MISSING,
|
|
14
15
|
type DaemonConfig,
|
|
15
16
|
type RouteRule,
|
|
16
17
|
type RouteRuleMatch,
|
|
17
18
|
} from "./config.js";
|
|
18
19
|
import { resolveBootAgents } from "./agent-discovery.js";
|
|
20
|
+
import {
|
|
21
|
+
defaultTranscriptRoot,
|
|
22
|
+
resolveTranscriptEnabled,
|
|
23
|
+
transcriptAgentRoot,
|
|
24
|
+
transcriptFilePath,
|
|
25
|
+
} from "./gateway/index.js";
|
|
19
26
|
import { startDaemon } from "./daemon.js";
|
|
20
27
|
import { log, LOG_FILE_PATH } from "./log.js";
|
|
21
28
|
import { detectRuntimes, getAdapterModule, listAdapterIds } from "./adapters/runtimes.js";
|
|
@@ -34,6 +41,7 @@ import {
|
|
|
34
41
|
type UserAuthRecord,
|
|
35
42
|
} from "./user-auth.js";
|
|
36
43
|
import { renderStatus, type StatusRenderInput } from "./status-render.js";
|
|
44
|
+
import { appendNextParam } from "./url-utils.js";
|
|
37
45
|
import {
|
|
38
46
|
channelsFromDaemonConfig,
|
|
39
47
|
defaultHttpFetcher,
|
|
@@ -69,12 +77,8 @@ const HELP = `botcord-daemon — BotCord local daemon
|
|
|
69
77
|
Usage: botcord-daemon <command> [options]
|
|
70
78
|
|
|
71
79
|
Commands:
|
|
72
|
-
init [--agent <ag_xxx> ...] [--cwd <path>]
|
|
73
|
-
Create ~/.botcord/daemon/config.json.
|
|
74
|
-
Without --agent, the daemon discovers
|
|
75
|
-
identities from ~/.botcord/credentials
|
|
76
|
-
at startup (repeat --agent to pin).
|
|
77
80
|
start [--background|-d] [--relogin] [--hub <url>] [--label <name>]
|
|
81
|
+
[--agent <ag_xxx> ...] [--cwd <path>]
|
|
78
82
|
Start the daemon in the foreground by
|
|
79
83
|
default. Pass --background (alias -d)
|
|
80
84
|
to detach and return to the shell.
|
|
@@ -88,9 +92,24 @@ Commands:
|
|
|
88
92
|
(defaults to hostname). Non-TTY
|
|
89
93
|
environments must mount a pre-existing
|
|
90
94
|
user-auth.json (plan §6.4).
|
|
95
|
+
On first run, auto-creates
|
|
96
|
+
~/.botcord/daemon/config.json with a
|
|
97
|
+
default route (claude-code, $HOME) and
|
|
98
|
+
credential auto-discovery. Pass
|
|
99
|
+
--agent/--cwd to seed the file
|
|
100
|
+
(ignored once config exists).
|
|
91
101
|
stop Stop the running daemon (SIGTERM)
|
|
92
102
|
status Print daemon status (pid, agent)
|
|
93
103
|
logs [-f] Print log tail (use -f to follow)
|
|
104
|
+
transcript enable|disable|status Toggle persistent transcript logging
|
|
105
|
+
transcript list --agent <ag_xxx> List rooms with transcripts for an agent
|
|
106
|
+
transcript tail --agent <ag_xxx> --room <rm_xxx> [--topic <tp>] [-n 50] [-f]
|
|
107
|
+
Tail recent transcript records (NDJSON)
|
|
108
|
+
transcript dump --agent <ag_xxx> --room <rm_xxx> [--topic <tp>]
|
|
109
|
+
Print full transcript file to stdout
|
|
110
|
+
transcript prune --agent <ag_xxx> [--older-than 30d] [--all]
|
|
111
|
+
Remove rotated transcript files (or all
|
|
112
|
+
for the agent with --all --yes)
|
|
94
113
|
route add [match flags] --adapter <${ADAPTER_LIST}> --cwd <path>
|
|
95
114
|
match flags (first match wins; at least one conversation/sender selector required):
|
|
96
115
|
--conversation-id <rm_xxx> (alias: --room <rm_xxx>)
|
|
@@ -203,22 +222,33 @@ function pidAlive(pid: number): boolean {
|
|
|
203
222
|
}
|
|
204
223
|
}
|
|
205
224
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Load the daemon config, auto-creating `~/.botcord/daemon/config.json`
|
|
227
|
+
* with sensible defaults on first run. `--agent` (repeated) pins explicit
|
|
228
|
+
* agent ids; `--cwd` overrides the defaultRoute working directory. Both
|
|
229
|
+
* are seed-only — they are ignored once a config already exists, since
|
|
230
|
+
* `route` and direct edits to `config.json` are the canonical way to
|
|
231
|
+
* change a configured daemon.
|
|
232
|
+
*/
|
|
233
|
+
function loadOrInitConfig(args: ParsedArgs): DaemonConfig {
|
|
234
|
+
try {
|
|
235
|
+
return loadConfig();
|
|
236
|
+
} catch (err) {
|
|
237
|
+
const missing = err instanceof Error && (err as { code?: string }).code === CONFIG_MISSING;
|
|
238
|
+
if (!missing) throw err;
|
|
239
|
+
const agents = args.lists.agent ?? [];
|
|
240
|
+
const cwd =
|
|
241
|
+
typeof args.flags.cwd === "string" ? path.resolve(args.flags.cwd) : homedir();
|
|
242
|
+
const cfg = initDefaultConfig(agents, cwd);
|
|
243
|
+
saveConfig(cfg);
|
|
244
|
+
log.info("auto-initialized daemon config", { agents, cwd, path: CONFIG_FILE_PATH });
|
|
245
|
+
console.log(`wrote default config to ${CONFIG_FILE_PATH}`);
|
|
246
|
+
if (agents.length === 0) {
|
|
247
|
+
console.log(
|
|
248
|
+
"no --agent provided; daemon will auto-discover identities from ~/.botcord/credentials",
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return cfg;
|
|
222
252
|
}
|
|
223
253
|
}
|
|
224
254
|
|
|
@@ -260,10 +290,15 @@ async function runDeviceCodeFlow(opts: {
|
|
|
260
290
|
opts.hubUrl,
|
|
261
291
|
opts.label ? { label: opts.label } : undefined,
|
|
262
292
|
);
|
|
263
|
-
const
|
|
293
|
+
const base = dc.verificationUriComplete ?? dc.verificationUri;
|
|
294
|
+
const display = appendNextParam(base, "/settings/daemons");
|
|
264
295
|
console.log("");
|
|
265
|
-
console.log(
|
|
266
|
-
console.log(
|
|
296
|
+
console.log("Open this URL in a browser where you're signed in to BotCord");
|
|
297
|
+
console.log("(typically your laptop, NOT this machine):");
|
|
298
|
+
console.log("");
|
|
299
|
+
console.log(` ${display}`);
|
|
300
|
+
console.log("");
|
|
301
|
+
console.log(`Or enter this code at ${dc.verificationUri}: ${dc.userCode}`);
|
|
267
302
|
console.log("Waiting for authorization (Ctrl-C to abort)...");
|
|
268
303
|
|
|
269
304
|
const expiresAt = Date.now() + dc.expiresIn * 1000;
|
|
@@ -367,7 +402,7 @@ async function ensureUserAuthForStart(args: ParsedArgs): Promise<UserAuthRecord
|
|
|
367
402
|
}
|
|
368
403
|
|
|
369
404
|
async function cmdStart(args: ParsedArgs): Promise<void> {
|
|
370
|
-
const cfg =
|
|
405
|
+
const cfg = loadOrInitConfig(args);
|
|
371
406
|
// Foreground is now the default. --background (alias -d) detaches.
|
|
372
407
|
// --foreground is still accepted (no-op) for backwards compatibility and
|
|
373
408
|
// is also what the detached child re-execs itself with.
|
|
@@ -583,6 +618,225 @@ async function cmdLogs(args: ParsedArgs): Promise<void> {
|
|
|
583
618
|
console.log(lines.slice(-100).join("\n"));
|
|
584
619
|
}
|
|
585
620
|
|
|
621
|
+
// ---------------------------------------------------------------------------
|
|
622
|
+
// transcript subcommands (design §5)
|
|
623
|
+
// ---------------------------------------------------------------------------
|
|
624
|
+
|
|
625
|
+
function transcriptStringFlag(args: ParsedArgs, name: string): string | null {
|
|
626
|
+
const v = args.flags[name];
|
|
627
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function parseDurationToMs(s: string): number | null {
|
|
631
|
+
const m = /^(\d+)\s*([smhd])?$/.exec(s.trim());
|
|
632
|
+
if (!m) return null;
|
|
633
|
+
const n = Number(m[1]);
|
|
634
|
+
const unit = m[2] ?? "d";
|
|
635
|
+
const mult: Record<string, number> = {
|
|
636
|
+
s: 1000,
|
|
637
|
+
m: 60_000,
|
|
638
|
+
h: 3_600_000,
|
|
639
|
+
d: 86_400_000,
|
|
640
|
+
};
|
|
641
|
+
return n * mult[unit];
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function cmdTranscript(args: ParsedArgs): Promise<void> {
|
|
645
|
+
switch (args.sub) {
|
|
646
|
+
case "enable":
|
|
647
|
+
return cmdTranscriptToggle(true);
|
|
648
|
+
case "disable":
|
|
649
|
+
return cmdTranscriptToggle(false);
|
|
650
|
+
case "status":
|
|
651
|
+
return cmdTranscriptStatus();
|
|
652
|
+
case "list":
|
|
653
|
+
return cmdTranscriptList(args);
|
|
654
|
+
case "tail":
|
|
655
|
+
return cmdTranscriptTail(args);
|
|
656
|
+
case "dump":
|
|
657
|
+
return cmdTranscriptDump(args);
|
|
658
|
+
case "prune":
|
|
659
|
+
return cmdTranscriptPrune(args);
|
|
660
|
+
default:
|
|
661
|
+
console.error("usage: botcord-daemon transcript <enable|disable|status|list|tail|dump|prune>");
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function cmdTranscriptToggle(enable: boolean): void {
|
|
667
|
+
let cfg: DaemonConfig;
|
|
668
|
+
try {
|
|
669
|
+
cfg = loadConfig();
|
|
670
|
+
} catch (err) {
|
|
671
|
+
const e = err as Error & { code?: string };
|
|
672
|
+
if (e.code === CONFIG_MISSING) {
|
|
673
|
+
console.error(
|
|
674
|
+
`daemon config not found — run \`botcord-daemon start\` once to initialize, then retry`,
|
|
675
|
+
);
|
|
676
|
+
process.exit(1);
|
|
677
|
+
}
|
|
678
|
+
throw err;
|
|
679
|
+
}
|
|
680
|
+
cfg.transcript = { ...(cfg.transcript ?? {}), enabled: enable };
|
|
681
|
+
saveConfig(cfg);
|
|
682
|
+
console.log(
|
|
683
|
+
`transcript persistence ${enable ? "enabled" : "disabled"} (next daemon start)`,
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function cmdTranscriptStatus(): void {
|
|
688
|
+
let cfg: DaemonConfig | null = null;
|
|
689
|
+
try {
|
|
690
|
+
cfg = loadConfig();
|
|
691
|
+
} catch (err) {
|
|
692
|
+
const e = err as Error & { code?: string };
|
|
693
|
+
if (e.code !== CONFIG_MISSING) throw err;
|
|
694
|
+
}
|
|
695
|
+
const configEnabled = cfg?.transcript?.enabled === true;
|
|
696
|
+
const env = process.env.BOTCORD_TRANSCRIPT;
|
|
697
|
+
const effective = resolveTranscriptEnabled(env, configEnabled);
|
|
698
|
+
let source: string;
|
|
699
|
+
if (env === "1" || env === "0") source = `env BOTCORD_TRANSCRIPT=${env}`;
|
|
700
|
+
else if (configEnabled) source = "config (transcript.enabled=true)";
|
|
701
|
+
else source = "default-off";
|
|
702
|
+
console.log(`enabled: ${effective}`);
|
|
703
|
+
console.log(`source: ${source}`);
|
|
704
|
+
console.log(`root: ${defaultTranscriptRoot()}`);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function cmdTranscriptList(args: ParsedArgs): void {
|
|
708
|
+
const agent = transcriptStringFlag(args, "agent");
|
|
709
|
+
if (!agent) {
|
|
710
|
+
console.error("transcript list requires --agent <ag_xxx>");
|
|
711
|
+
process.exit(1);
|
|
712
|
+
}
|
|
713
|
+
const root = transcriptAgentRoot(defaultTranscriptRoot(), agent);
|
|
714
|
+
if (!existsSync(root)) {
|
|
715
|
+
return; // no rooms → empty output
|
|
716
|
+
}
|
|
717
|
+
for (const entry of readdirSync(root)) {
|
|
718
|
+
const dir = path.join(root, entry);
|
|
719
|
+
let st;
|
|
720
|
+
try {
|
|
721
|
+
st = statSync(dir);
|
|
722
|
+
} catch {
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
if (!st.isDirectory()) continue;
|
|
726
|
+
console.log(entry);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function cmdTranscriptTail(args: ParsedArgs): Promise<void> | void {
|
|
731
|
+
const agent = transcriptStringFlag(args, "agent");
|
|
732
|
+
const room = transcriptStringFlag(args, "room");
|
|
733
|
+
if (!agent || !room) {
|
|
734
|
+
console.error("transcript tail requires --agent <ag_xxx> --room <rm_xxx>");
|
|
735
|
+
process.exit(1);
|
|
736
|
+
}
|
|
737
|
+
const topic = transcriptStringFlag(args, "topic");
|
|
738
|
+
const file = transcriptFilePath(defaultTranscriptRoot(), agent, room, topic);
|
|
739
|
+
if (!existsSync(file)) {
|
|
740
|
+
console.error(`no transcript at ${file}`);
|
|
741
|
+
process.exit(1);
|
|
742
|
+
}
|
|
743
|
+
const follow = args.flags.f === true || args.flags.follow === true;
|
|
744
|
+
const nFlag = transcriptStringFlag(args, "n");
|
|
745
|
+
const n = nFlag && /^\d+$/.test(nFlag) ? Number(nFlag) : 50;
|
|
746
|
+
if (follow) {
|
|
747
|
+
const child = spawn("tail", ["-n", String(n), "-f", file], { stdio: "inherit" });
|
|
748
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
749
|
+
return new Promise<void>((resolve) => {
|
|
750
|
+
child.on("close", () => resolve());
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
const data = readFileSync(file, "utf8");
|
|
754
|
+
const lines = data.split("\n").filter((l) => l.length > 0);
|
|
755
|
+
console.log(lines.slice(-n).join("\n"));
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function cmdTranscriptDump(args: ParsedArgs): void {
|
|
759
|
+
const agent = transcriptStringFlag(args, "agent");
|
|
760
|
+
const room = transcriptStringFlag(args, "room");
|
|
761
|
+
if (!agent || !room) {
|
|
762
|
+
console.error("transcript dump requires --agent <ag_xxx> --room <rm_xxx>");
|
|
763
|
+
process.exit(1);
|
|
764
|
+
}
|
|
765
|
+
const topic = transcriptStringFlag(args, "topic");
|
|
766
|
+
const file = transcriptFilePath(defaultTranscriptRoot(), agent, room, topic);
|
|
767
|
+
if (!existsSync(file)) {
|
|
768
|
+
console.error(`no transcript at ${file}`);
|
|
769
|
+
process.exit(1);
|
|
770
|
+
}
|
|
771
|
+
process.stdout.write(readFileSync(file, "utf8"));
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function cmdTranscriptPrune(args: ParsedArgs): void {
|
|
775
|
+
const agent = transcriptStringFlag(args, "agent");
|
|
776
|
+
if (!agent) {
|
|
777
|
+
console.error("transcript prune requires --agent <ag_xxx>");
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
const all = args.flags.all === true;
|
|
781
|
+
const olderThanFlag = transcriptStringFlag(args, "older-than");
|
|
782
|
+
const yes = args.flags.yes === true;
|
|
783
|
+
const root = transcriptAgentRoot(defaultTranscriptRoot(), agent);
|
|
784
|
+
if (!existsSync(root)) return;
|
|
785
|
+
|
|
786
|
+
if (all) {
|
|
787
|
+
if (!yes) {
|
|
788
|
+
console.error(
|
|
789
|
+
`transcript prune --all will delete every transcript under ${root}; rerun with --yes to confirm`,
|
|
790
|
+
);
|
|
791
|
+
process.exit(1);
|
|
792
|
+
}
|
|
793
|
+
rmSync(root, { recursive: true, force: true });
|
|
794
|
+
console.log(`removed ${root}`);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Default and --older-than: prune rotated files only (the "{topic}.STAMP.jsonl" form).
|
|
799
|
+
// Active files (`{topic}.jsonl` / `_default.jsonl`) are never touched.
|
|
800
|
+
const cutoffMs = olderThanFlag ? parseDurationToMs(olderThanFlag) : null;
|
|
801
|
+
if (olderThanFlag && cutoffMs === null) {
|
|
802
|
+
console.error(`transcript prune --older-than: invalid duration "${olderThanFlag}" (use 30d / 12h / 30m / 60s)`);
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
805
|
+
const cutoff = cutoffMs !== null ? Date.now() - cutoffMs : null;
|
|
806
|
+
|
|
807
|
+
let removed = 0;
|
|
808
|
+
for (const roomEntry of readdirSync(root)) {
|
|
809
|
+
const dir = path.join(root, roomEntry);
|
|
810
|
+
let st;
|
|
811
|
+
try {
|
|
812
|
+
st = statSync(dir);
|
|
813
|
+
} catch {
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (!st.isDirectory()) continue;
|
|
817
|
+
for (const f of readdirSync(dir)) {
|
|
818
|
+
// rotated files: <topic>.<YYYYMMDD-HHMMSS>.jsonl — must contain a stamp segment
|
|
819
|
+
if (!/^.+\.\d{8}-\d{6}\.jsonl$/.test(f)) continue;
|
|
820
|
+
const full = path.join(dir, f);
|
|
821
|
+
if (cutoff !== null) {
|
|
822
|
+
try {
|
|
823
|
+
const fst = statSync(full);
|
|
824
|
+
if (fst.mtimeMs >= cutoff) continue;
|
|
825
|
+
} catch {
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
unlinkSync(full);
|
|
831
|
+
removed += 1;
|
|
832
|
+
} catch (err) {
|
|
833
|
+
console.error(`failed to remove ${full}: ${err instanceof Error ? err.message : String(err)}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
console.log(`removed ${removed} rotated transcript file(s)`);
|
|
838
|
+
}
|
|
839
|
+
|
|
586
840
|
function formatRouteMatch(m: RouteRuleMatch): string {
|
|
587
841
|
const parts: string[] = [];
|
|
588
842
|
if (m.channel) parts.push(`channel=${m.channel}`);
|
|
@@ -893,16 +1147,27 @@ const fsFileReader: DoctorFileReader = {
|
|
|
893
1147
|
};
|
|
894
1148
|
|
|
895
1149
|
async function cmdDoctor(args: ParsedArgs): Promise<void> {
|
|
896
|
-
const entries = detectRuntimes();
|
|
1150
|
+
const entries: import("./doctor.js").DoctorRuntimeEntry[] = detectRuntimes();
|
|
897
1151
|
// Doctor should not hard-fail when no config exists yet; channel probes
|
|
898
1152
|
// simply produce an empty list in that case.
|
|
899
1153
|
let channels: ReturnType<typeof channelsFromDaemonConfig> = [];
|
|
1154
|
+
let cfgForEndpoints: import("./config.js").DaemonConfig | null = null;
|
|
900
1155
|
try {
|
|
901
1156
|
const cfg = loadConfig();
|
|
1157
|
+
cfgForEndpoints = cfg;
|
|
902
1158
|
channels = channelsFromDaemonConfig(cfg);
|
|
903
1159
|
} catch {
|
|
904
1160
|
channels = [];
|
|
905
1161
|
}
|
|
1162
|
+
if (cfgForEndpoints?.openclawGateways && cfgForEndpoints.openclawGateways.length > 0) {
|
|
1163
|
+
const { collectRuntimeSnapshotAsync } = await import("./provision.js");
|
|
1164
|
+
const snap = await collectRuntimeSnapshotAsync({ cfg: cfgForEndpoints });
|
|
1165
|
+
const byId = new Map(snap.runtimes.map((r) => [r.id, r]));
|
|
1166
|
+
for (const e of entries) {
|
|
1167
|
+
const r = byId.get(e.id);
|
|
1168
|
+
if (r?.endpoints) e.endpoints = r.endpoints;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
906
1171
|
|
|
907
1172
|
const credentialsPath = (accountId: string) =>
|
|
908
1173
|
path.join(homedir(), ".botcord", "credentials", `${accountId}.json`);
|
|
@@ -929,9 +1194,6 @@ async function main(): Promise<void> {
|
|
|
929
1194
|
}
|
|
930
1195
|
try {
|
|
931
1196
|
switch (args.cmd) {
|
|
932
|
-
case "init":
|
|
933
|
-
await cmdInit(args);
|
|
934
|
-
break;
|
|
935
1197
|
case "start":
|
|
936
1198
|
await cmdStart(args);
|
|
937
1199
|
break;
|
|
@@ -944,6 +1206,9 @@ async function main(): Promise<void> {
|
|
|
944
1206
|
case "logs":
|
|
945
1207
|
await cmdLogs(args);
|
|
946
1208
|
break;
|
|
1209
|
+
case "transcript":
|
|
1210
|
+
await cmdTranscript(args);
|
|
1211
|
+
break;
|
|
947
1212
|
case "route":
|
|
948
1213
|
await cmdRoute(args);
|
|
949
1214
|
break;
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
export interface MentionTargets {
|
|
12
|
+
/** Daemon-known agent id (e.g. `ag_xxx`). Always included when present. */
|
|
13
|
+
agentId?: string;
|
|
14
|
+
/** Display name from the agent's credentials. */
|
|
15
|
+
displayName?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Return `true` when `text` contains an `@`-prefixed mention of `agentId`
|
|
20
|
+
* or `displayName`. Matches a literal `@` followed by the target — both the
|
|
21
|
+
* `@` and the target are required because plain occurrences of the
|
|
22
|
+
* displayName in conversation should NOT count as a mention.
|
|
23
|
+
*/
|
|
24
|
+
export function scanMention(text: string | undefined, targets: MentionTargets): boolean {
|
|
25
|
+
if (!text) return false;
|
|
26
|
+
const lower = text.toLowerCase();
|
|
27
|
+
const candidates: string[] = [];
|
|
28
|
+
if (targets.agentId) candidates.push(targets.agentId.toLowerCase());
|
|
29
|
+
if (targets.displayName) {
|
|
30
|
+
const trimmed = targets.displayName.trim();
|
|
31
|
+
if (trimmed) candidates.push(trimmed.toLowerCase());
|
|
32
|
+
}
|
|
33
|
+
for (const c of candidates) {
|
|
34
|
+
if (!c) continue;
|
|
35
|
+
if (lower.includes("@" + c)) return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|