@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.
Files changed (62) hide show
  1. package/dist/cloud-auth.d.ts +47 -0
  2. package/dist/cloud-auth.js +51 -0
  3. package/dist/cloud-daemon.d.ts +43 -0
  4. package/dist/cloud-daemon.js +252 -0
  5. package/dist/cloud-mode.d.ts +45 -0
  6. package/dist/cloud-mode.js +55 -0
  7. package/dist/cloud-settle.d.ts +81 -0
  8. package/dist/cloud-settle.js +100 -0
  9. package/dist/daemon-singleton.d.ts +26 -0
  10. package/dist/daemon-singleton.js +91 -0
  11. package/dist/daemon.d.ts +1 -1
  12. package/dist/daemon.js +15 -6
  13. package/dist/doctor.d.ts +4 -1
  14. package/dist/doctor.js +15 -4
  15. package/dist/gateway/channels/botcord.d.ts +1 -1
  16. package/dist/gateway/channels/botcord.js +280 -52
  17. package/dist/gateway/dispatcher.d.ts +34 -1
  18. package/dist/gateway/dispatcher.js +277 -20
  19. package/dist/gateway/gateway.d.ts +9 -1
  20. package/dist/gateway/gateway.js +4 -1
  21. package/dist/gateway/runtime-errors.d.ts +6 -0
  22. package/dist/gateway/runtime-errors.js +14 -0
  23. package/dist/gateway/runtimes/claude-code.d.ts +8 -0
  24. package/dist/gateway/runtimes/claude-code.js +92 -4
  25. package/dist/gateway/runtimes/deepseek-tui.js +19 -5
  26. package/dist/gateway/transcript.d.ts +1 -1
  27. package/dist/gateway/types.d.ts +33 -0
  28. package/dist/index.js +71 -80
  29. package/dist/provision.d.ts +2 -0
  30. package/dist/provision.js +39 -1
  31. package/dist/status-render.js +17 -0
  32. package/package.json +2 -2
  33. package/src/__tests__/cloud-auth.test.ts +42 -0
  34. package/src/__tests__/cloud-daemon.test.ts +237 -0
  35. package/src/__tests__/cloud-mode.test.ts +65 -0
  36. package/src/__tests__/cloud-settle.test.ts +287 -0
  37. package/src/__tests__/daemon-singleton.test.ts +89 -0
  38. package/src/__tests__/doctor.test.ts +34 -0
  39. package/src/__tests__/runtime-discovery.test.ts +90 -0
  40. package/src/__tests__/status-render.test.ts +34 -0
  41. package/src/cloud-auth.ts +78 -0
  42. package/src/cloud-daemon.ts +338 -0
  43. package/src/cloud-mode.ts +70 -0
  44. package/src/cloud-settle.ts +182 -0
  45. package/src/daemon-singleton.ts +122 -0
  46. package/src/daemon.ts +18 -5
  47. package/src/doctor.ts +18 -5
  48. package/src/gateway/__tests__/botcord-channel.test.ts +98 -0
  49. package/src/gateway/__tests__/claude-code-adapter.test.ts +101 -1
  50. package/src/gateway/__tests__/deepseek-tui-adapter.test.ts +19 -0
  51. package/src/gateway/__tests__/dispatcher.test.ts +120 -0
  52. package/src/gateway/channels/botcord.ts +299 -43
  53. package/src/gateway/dispatcher.ts +354 -21
  54. package/src/gateway/gateway.ts +16 -1
  55. package/src/gateway/runtime-errors.ts +15 -0
  56. package/src/gateway/runtimes/claude-code.ts +98 -2
  57. package/src/gateway/runtimes/deepseek-tui.ts +23 -5
  58. package/src/gateway/transcript.ts +1 -1
  59. package/src/gateway/types.ts +34 -0
  60. package/src/index.ts +83 -74
  61. package/src/provision.ts +45 -1
  62. 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 snap = collectRuntimeSnapshot();
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
- const gateway = new Gateway({
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: RuntimeProbeEntry[], channels: ChannelProbeConfig[], opts: {
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 responds 2xx/3xx/404 which is fine for
78
- // "reachable". We treat any response as reachable; network errors fall
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
- /** Not used by the WS-only loop today; kept for future polling fallback. */
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;