@botcord/daemon 0.2.75 → 0.2.77
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/cloud-auth.d.ts +47 -0
- package/dist/cloud-auth.js +51 -0
- package/dist/cloud-daemon.d.ts +43 -0
- package/dist/cloud-daemon.js +252 -0
- package/dist/cloud-mode.d.ts +45 -0
- package/dist/cloud-mode.js +55 -0
- package/dist/cloud-settle.d.ts +81 -0
- package/dist/cloud-settle.js +100 -0
- package/dist/daemon-singleton.d.ts +26 -0
- package/dist/daemon-singleton.js +91 -0
- package/dist/daemon.d.ts +1 -1
- package/dist/daemon.js +15 -6
- package/dist/doctor.d.ts +4 -1
- package/dist/doctor.js +15 -4
- package/dist/gateway/channels/botcord.d.ts +1 -1
- package/dist/gateway/channels/botcord.js +280 -52
- package/dist/gateway/dispatcher.d.ts +34 -1
- package/dist/gateway/dispatcher.js +277 -20
- package/dist/gateway/gateway.d.ts +9 -1
- package/dist/gateway/gateway.js +4 -1
- package/dist/gateway/runtime-errors.d.ts +6 -0
- package/dist/gateway/runtime-errors.js +14 -0
- package/dist/gateway/runtimes/claude-code.d.ts +8 -0
- package/dist/gateway/runtimes/claude-code.js +92 -4
- package/dist/gateway/runtimes/deepseek-tui.js +19 -5
- package/dist/gateway/transcript.d.ts +1 -1
- package/dist/gateway/types.d.ts +33 -0
- package/dist/index.js +71 -80
- package/dist/provision.d.ts +2 -0
- package/dist/provision.js +39 -1
- package/dist/status-render.js +17 -0
- package/package.json +2 -2
- package/src/__tests__/cloud-auth.test.ts +42 -0
- package/src/__tests__/cloud-daemon.test.ts +237 -0
- package/src/__tests__/cloud-mode.test.ts +65 -0
- package/src/__tests__/cloud-settle.test.ts +287 -0
- package/src/__tests__/daemon-singleton.test.ts +89 -0
- package/src/__tests__/doctor.test.ts +34 -0
- package/src/__tests__/runtime-discovery.test.ts +90 -0
- package/src/__tests__/status-render.test.ts +34 -0
- package/src/cloud-auth.ts +78 -0
- package/src/cloud-daemon.ts +338 -0
- package/src/cloud-mode.ts +70 -0
- package/src/cloud-settle.ts +182 -0
- package/src/daemon-singleton.ts +122 -0
- package/src/daemon.ts +18 -5
- package/src/doctor.ts +18 -5
- package/src/gateway/__tests__/botcord-channel.test.ts +98 -0
- package/src/gateway/__tests__/claude-code-adapter.test.ts +101 -1
- package/src/gateway/__tests__/deepseek-tui-adapter.test.ts +19 -0
- package/src/gateway/__tests__/dispatcher.test.ts +120 -0
- package/src/gateway/channels/botcord.ts +299 -43
- package/src/gateway/dispatcher.ts +354 -21
- package/src/gateway/gateway.ts +16 -1
- package/src/gateway/runtime-errors.ts +15 -0
- package/src/gateway/runtimes/claude-code.ts +98 -2
- package/src/gateway/runtimes/deepseek-tui.ts +23 -5
- package/src/gateway/transcript.ts +1 -1
- package/src/gateway/types.ts +34 -0
- package/src/index.ts +83 -74
- package/src/provision.ts +45 -1
- package/src/status-render.ts +24 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface SingletonLogger {
|
|
2
|
+
info(message: string, meta?: Record<string, unknown>): void;
|
|
3
|
+
warn(message: string, meta?: Record<string, unknown>): void;
|
|
4
|
+
}
|
|
5
|
+
export declare function readPid(pidPath?: string): number | null;
|
|
6
|
+
export declare function pidAlive(pid: number): boolean;
|
|
7
|
+
export declare function waitForPidExit(pid: number, timeoutMs: number): Promise<boolean>;
|
|
8
|
+
export declare function stopExistingDaemonForRestart(pid: number, opts?: {
|
|
9
|
+
pidPath?: string;
|
|
10
|
+
currentPid?: number;
|
|
11
|
+
logger?: SingletonLogger;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
export declare function stopDaemonFromPidFileForRestart(opts?: {
|
|
14
|
+
pidPath?: string;
|
|
15
|
+
currentPid?: number;
|
|
16
|
+
logger?: SingletonLogger;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
export declare function ensureNoOtherDaemonFromPidFile(opts?: {
|
|
19
|
+
pidPath?: string;
|
|
20
|
+
currentPid?: number;
|
|
21
|
+
}): number | null;
|
|
22
|
+
export declare function writeCurrentPid(opts?: {
|
|
23
|
+
pidPath?: string;
|
|
24
|
+
currentPid?: number;
|
|
25
|
+
}): void;
|
|
26
|
+
export declare function removePidFile(pidPath?: string): void;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { PID_PATH } from "./config.js";
|
|
3
|
+
const noopLogger = {
|
|
4
|
+
info() {
|
|
5
|
+
// noop
|
|
6
|
+
},
|
|
7
|
+
warn() {
|
|
8
|
+
// noop
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
export function readPid(pidPath = PID_PATH) {
|
|
12
|
+
if (!existsSync(pidPath))
|
|
13
|
+
return null;
|
|
14
|
+
const raw = readFileSync(pidPath, "utf8").trim();
|
|
15
|
+
const pid = Number(raw);
|
|
16
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
17
|
+
}
|
|
18
|
+
export function pidAlive(pid) {
|
|
19
|
+
try {
|
|
20
|
+
process.kill(pid, 0);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function waitForPidExit(pid, timeoutMs) {
|
|
28
|
+
const deadline = Date.now() + timeoutMs;
|
|
29
|
+
while (Date.now() < deadline) {
|
|
30
|
+
if (!pidAlive(pid))
|
|
31
|
+
return true;
|
|
32
|
+
await delay(100);
|
|
33
|
+
}
|
|
34
|
+
return !pidAlive(pid);
|
|
35
|
+
}
|
|
36
|
+
export async function stopExistingDaemonForRestart(pid, opts = {}) {
|
|
37
|
+
const pidPath = opts.pidPath ?? PID_PATH;
|
|
38
|
+
const currentPid = opts.currentPid ?? process.pid;
|
|
39
|
+
const logger = opts.logger ?? noopLogger;
|
|
40
|
+
if (pid === currentPid)
|
|
41
|
+
return;
|
|
42
|
+
logger.info("existing daemon found; restarting", { pid });
|
|
43
|
+
try {
|
|
44
|
+
process.kill(pid, "SIGTERM");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
removePidFile(pidPath);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!(await waitForPidExit(pid, 5_000))) {
|
|
51
|
+
logger.warn("existing daemon did not stop after SIGTERM; sending SIGKILL", { pid });
|
|
52
|
+
try {
|
|
53
|
+
process.kill(pid, "SIGKILL");
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// ignore
|
|
57
|
+
}
|
|
58
|
+
await waitForPidExit(pid, 2_000);
|
|
59
|
+
}
|
|
60
|
+
removePidFile(pidPath);
|
|
61
|
+
}
|
|
62
|
+
export async function stopDaemonFromPidFileForRestart(opts = {}) {
|
|
63
|
+
const pidPath = opts.pidPath ?? PID_PATH;
|
|
64
|
+
const existing = readPid(pidPath);
|
|
65
|
+
if (existing && pidAlive(existing)) {
|
|
66
|
+
await stopExistingDaemonForRestart(existing, opts);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function ensureNoOtherDaemonFromPidFile(opts = {}) {
|
|
70
|
+
const pidPath = opts.pidPath ?? PID_PATH;
|
|
71
|
+
const currentPid = opts.currentPid ?? process.pid;
|
|
72
|
+
const existing = readPid(pidPath);
|
|
73
|
+
if (existing && existing !== currentPid && pidAlive(existing)) {
|
|
74
|
+
return existing;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
export function writeCurrentPid(opts = {}) {
|
|
79
|
+
writeFileSync(opts.pidPath ?? PID_PATH, String(opts.currentPid ?? process.pid), { mode: 0o600 });
|
|
80
|
+
}
|
|
81
|
+
export function removePidFile(pidPath = PID_PATH) {
|
|
82
|
+
try {
|
|
83
|
+
unlinkSync(pidPath);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// ignore
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function delay(ms) {
|
|
90
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
91
|
+
}
|
package/dist/daemon.d.ts
CHANGED
|
@@ -64,7 +64,7 @@ export interface RuntimeSnapshotSink {
|
|
|
64
64
|
* failure is non-fatal (the Hub will re-query via `list_runtimes` on demand
|
|
65
65
|
* or wait for the next daemon restart). Exported for unit tests.
|
|
66
66
|
*/
|
|
67
|
-
export declare function pushRuntimeSnapshot(sink: RuntimeSnapshotSink): boolean;
|
|
67
|
+
export declare function pushRuntimeSnapshot(sink: RuntimeSnapshotSink, liveSnapshot?: GatewayRuntimeSnapshot): boolean;
|
|
68
68
|
/** Options accepted by {@link startDaemon} — the P0.5 compatibility shim. */
|
|
69
69
|
export interface DaemonRuntimeOptions {
|
|
70
70
|
config: DaemonConfig;
|
package/dist/daemon.js
CHANGED
|
@@ -7,7 +7,7 @@ import { ensureAgentWorkspace } from "./agent-workspace.js";
|
|
|
7
7
|
import { ControlChannel } from "./control-channel.js";
|
|
8
8
|
import { toGatewayConfig } from "./daemon-config-map.js";
|
|
9
9
|
import { log as daemonLog } from "./log.js";
|
|
10
|
-
import { adoptDiscoveredOpenclawAgents, collectRuntimeSnapshot, createProvisioner, } from "./provision.js";
|
|
10
|
+
import { adoptDiscoveredOpenclawAgents, attachRuntimeHealth, collectRuntimeSnapshot, createProvisioner, } from "./provision.js";
|
|
11
11
|
import { openclawAutoProvisionEnabled } from "./openclaw-discovery.js";
|
|
12
12
|
import { SnapshotWriter } from "./snapshot-writer.js";
|
|
13
13
|
import { createDaemonSystemContextBuilder } from "./system-context.js";
|
|
@@ -146,8 +146,9 @@ export function createDaemonChannel(chCfg, deps) {
|
|
|
146
146
|
* failure is non-fatal (the Hub will re-query via `list_runtimes` on demand
|
|
147
147
|
* or wait for the next daemon restart). Exported for unit tests.
|
|
148
148
|
*/
|
|
149
|
-
export function pushRuntimeSnapshot(sink) {
|
|
150
|
-
const
|
|
149
|
+
export function pushRuntimeSnapshot(sink, liveSnapshot) {
|
|
150
|
+
const base = collectRuntimeSnapshot();
|
|
151
|
+
const snap = liveSnapshot ? attachRuntimeHealth(base, liveSnapshot) : base;
|
|
151
152
|
const ok = sink.send({
|
|
152
153
|
id: `rt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
153
154
|
type: CONTROL_FRAME_TYPES.RUNTIME_SNAPSHOT,
|
|
@@ -353,7 +354,15 @@ export async function startDaemon(opts) {
|
|
|
353
354
|
}));
|
|
354
355
|
}
|
|
355
356
|
};
|
|
356
|
-
|
|
357
|
+
let controlChannel = null;
|
|
358
|
+
let gateway;
|
|
359
|
+
const pushLiveRuntimeSnapshot = () => {
|
|
360
|
+
if (!controlChannel)
|
|
361
|
+
return;
|
|
362
|
+
const pushed = pushRuntimeSnapshot(controlChannel, gateway.snapshot());
|
|
363
|
+
logger.info("control-channel: live runtime_snapshot push", { ok: pushed });
|
|
364
|
+
};
|
|
365
|
+
gateway = new Gateway({
|
|
357
366
|
config: gwConfig,
|
|
358
367
|
sessionStorePath: opts.sessionStorePath ?? SESSIONS_PATH,
|
|
359
368
|
createChannel: (chCfg) => createDaemonChannel(chCfg, {
|
|
@@ -367,6 +376,7 @@ export async function startDaemon(opts) {
|
|
|
367
376
|
buildMemoryContext,
|
|
368
377
|
onInbound,
|
|
369
378
|
onOutbound,
|
|
379
|
+
onRuntimeCircuitBreakerChange: pushLiveRuntimeSnapshot,
|
|
370
380
|
composeUserTurn: composeBotCordUserTurn,
|
|
371
381
|
attentionGate,
|
|
372
382
|
resolveHubUrl,
|
|
@@ -416,7 +426,6 @@ export async function startDaemon(opts) {
|
|
|
416
426
|
// Control channel is optional — daemon still runs (data-plane only)
|
|
417
427
|
// when user-auth hasn't been set up yet. Operators can `login` later
|
|
418
428
|
// without restarting, but for P0 we require a restart to pick it up.
|
|
419
|
-
let controlChannel = null;
|
|
420
429
|
if (userAuth?.current && !opts.disableControlChannel) {
|
|
421
430
|
logger.info("control-channel: enabling", {
|
|
422
431
|
userId: userAuth.current.userId,
|
|
@@ -455,7 +464,7 @@ export async function startDaemon(opts) {
|
|
|
455
464
|
// Plan §8.5 P0 — push one runtime snapshot immediately after connect
|
|
456
465
|
// so Hub's `daemon_instances.runtimes_json` is populated for the
|
|
457
466
|
// dashboard even before any user action. No periodic refresh in P0.
|
|
458
|
-
const pushed = pushRuntimeSnapshot(controlChannel);
|
|
467
|
+
const pushed = pushRuntimeSnapshot(controlChannel, gateway.snapshot());
|
|
459
468
|
logger.info("control-channel: initial runtime_snapshot push", {
|
|
460
469
|
ok: pushed,
|
|
461
470
|
});
|
package/dist/doctor.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { RuntimeProbeEntry } from "./adapters/runtimes.js";
|
|
2
2
|
import type { DaemonConfig } from "./config.js";
|
|
3
|
+
import { type ClaudeAuthProbeResult } from "./gateway/runtimes/claude-code.js";
|
|
3
4
|
/** Summary of a single channel's readiness, printable by the doctor command. */
|
|
4
5
|
export interface ChannelProbeResult {
|
|
5
6
|
id: string;
|
|
@@ -50,6 +51,7 @@ export interface DoctorRuntimeEndpoint {
|
|
|
50
51
|
/** Augmented runtime entry that may carry endpoint probe results. */
|
|
51
52
|
export interface DoctorRuntimeEntry extends RuntimeProbeEntry {
|
|
52
53
|
endpoints?: DoctorRuntimeEndpoint[];
|
|
54
|
+
auth?: ClaudeAuthProbeResult;
|
|
53
55
|
}
|
|
54
56
|
/** Input for the rendered doctor output. */
|
|
55
57
|
export interface DoctorInput {
|
|
@@ -107,9 +109,10 @@ export declare function renderDoctor(input: DoctorInput): string;
|
|
|
107
109
|
* Thin orchestrator: runs runtime + channel probes and returns the rendered
|
|
108
110
|
* text. Keeps `index.ts` free of probe wiring.
|
|
109
111
|
*/
|
|
110
|
-
export declare function runDoctor(runtimes:
|
|
112
|
+
export declare function runDoctor(runtimes: DoctorRuntimeEntry[], channels: ChannelProbeConfig[], opts: {
|
|
111
113
|
credentialsPath: (accountId: string) => string;
|
|
112
114
|
fileReader: DoctorFileReader;
|
|
113
115
|
fetcher: DoctorHttpFetcher;
|
|
114
116
|
timeoutMs?: number;
|
|
117
|
+
authCheck?: boolean;
|
|
115
118
|
}): Promise<DoctorInput>;
|
package/dist/doctor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveBootAgents } from "./agent-discovery.js";
|
|
2
|
+
import { probeClaudeAuth } from "./gateway/runtimes/claude-code.js";
|
|
2
3
|
/**
|
|
3
4
|
* Build the implicit channel list for a daemon config. One channel per
|
|
4
5
|
* configured or discovered agent, keyed by agentId (matches
|
|
@@ -74,12 +75,11 @@ export async function probeChannel(ch, opts) {
|
|
|
74
75
|
result.hubMessage = "skipped (no hub URL)";
|
|
75
76
|
return result;
|
|
76
77
|
}
|
|
77
|
-
// Probe `/` — the hub is ASGI and
|
|
78
|
-
//
|
|
79
|
-
// through to hubOk=false.
|
|
78
|
+
// Probe `/` — the hub is ASGI and may answer 404 when the host is still
|
|
79
|
+
// reachable. Network errors fall through to hubOk=false.
|
|
80
80
|
const probeUrl = `${result.hubUrl.replace(/\/+$/, "")}/`;
|
|
81
81
|
const http = await opts.fetcher(probeUrl, opts.timeoutMs);
|
|
82
|
-
if (http.ok) {
|
|
82
|
+
if (http.ok || http.status === 404) {
|
|
83
83
|
result.hubOk = true;
|
|
84
84
|
result.hubMessage = `reachable (HTTP ${http.status})`;
|
|
85
85
|
}
|
|
@@ -159,6 +159,10 @@ export function renderDoctor(input) {
|
|
|
159
159
|
if (!e.result.available && e.installHint) {
|
|
160
160
|
lines.push(` → ${e.installHint}`);
|
|
161
161
|
}
|
|
162
|
+
if (e.auth) {
|
|
163
|
+
const authStatus = e.auth.checked ? (e.auth.ok ? "ok" : "failed") : "skipped";
|
|
164
|
+
lines.push(` auth ${authStatus}: ${e.auth.message}`);
|
|
165
|
+
}
|
|
162
166
|
if (e.endpoints && e.endpoints.length > 0) {
|
|
163
167
|
for (const ep of e.endpoints) {
|
|
164
168
|
const mark = ep.reachable ? "✓" : "✗";
|
|
@@ -204,6 +208,13 @@ export function renderDoctor(input) {
|
|
|
204
208
|
* text. Keeps `index.ts` free of probe wiring.
|
|
205
209
|
*/
|
|
206
210
|
export async function runDoctor(runtimes, channels, opts) {
|
|
211
|
+
if (opts.authCheck) {
|
|
212
|
+
for (const runtime of runtimes) {
|
|
213
|
+
if (runtime.id === "claude-code" && runtime.result.available) {
|
|
214
|
+
runtime.auth = probeClaudeAuth();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
207
218
|
const channelResults = await probeChannels({
|
|
208
219
|
channels,
|
|
209
220
|
credentialsPath: opts.credentialsPath,
|
|
@@ -51,7 +51,7 @@ export interface BotCordChannelOptions {
|
|
|
51
51
|
credentialsPath?: string;
|
|
52
52
|
/** Override the Hub base URL. Defaults to the `hubUrl` stored in credentials. */
|
|
53
53
|
hubBaseUrl?: string;
|
|
54
|
-
/**
|
|
54
|
+
/** Periodic inbox polling fallback. Set to 0 to disable. Defaults to 30s. */
|
|
55
55
|
pollIntervalMs?: number;
|
|
56
56
|
/** Test hook: supply a pre-built client instead of loading credentials from disk. */
|
|
57
57
|
client?: BotCordChannelClient;
|