@openclaw-cloud/agent-controller 0.2.5 → 0.2.7

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 (76) hide show
  1. package/bin/agent-controller.js +5 -0
  2. package/dist/commands/install.js +3 -0
  3. package/dist/commands/install.js.map +1 -1
  4. package/dist/config-file.d.ts +9 -0
  5. package/dist/config-file.js +47 -0
  6. package/dist/config-file.js.map +1 -0
  7. package/dist/connection.d.ts +1 -0
  8. package/dist/connection.js +27 -13
  9. package/dist/connection.js.map +1 -1
  10. package/dist/handlers/backup.js +7 -2
  11. package/dist/handlers/backup.js.map +1 -1
  12. package/dist/handlers/knowledge-sync.d.ts +2 -0
  13. package/dist/handlers/knowledge-sync.js +51 -0
  14. package/dist/handlers/knowledge-sync.js.map +1 -0
  15. package/dist/heartbeat.d.ts +1 -0
  16. package/dist/heartbeat.js +30 -0
  17. package/dist/heartbeat.js.map +1 -1
  18. package/dist/index.js +7 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/types.d.ts +2 -1
  21. package/package.json +6 -1
  22. package/.claude/cc-notify.sh +0 -32
  23. package/.claude/settings.json +0 -31
  24. package/.husky/pre-commit +0 -1
  25. package/BIZPLAN.md +0 -530
  26. package/CLAUDE.md +0 -172
  27. package/Dockerfile +0 -9
  28. package/__tests__/api.test.ts +0 -183
  29. package/__tests__/backup.test.ts +0 -145
  30. package/__tests__/board-handler.test.ts +0 -323
  31. package/__tests__/chat.test.ts +0 -191
  32. package/__tests__/config.test.ts +0 -100
  33. package/__tests__/connection.test.ts +0 -289
  34. package/__tests__/file-delete.test.ts +0 -90
  35. package/__tests__/file-write.test.ts +0 -119
  36. package/__tests__/gateway-adapter.test.ts +0 -366
  37. package/__tests__/gateway-client.test.ts +0 -272
  38. package/__tests__/handlers.test.ts +0 -150
  39. package/__tests__/heartbeat.test.ts +0 -124
  40. package/__tests__/onboarding.test.ts +0 -55
  41. package/__tests__/package-install.test.ts +0 -109
  42. package/__tests__/pair.test.ts +0 -60
  43. package/__tests__/self-update.test.ts +0 -123
  44. package/__tests__/stop.test.ts +0 -38
  45. package/jest.config.ts +0 -16
  46. package/src/api.ts +0 -62
  47. package/src/commands/install.ts +0 -64
  48. package/src/commands/self-update.ts +0 -43
  49. package/src/commands/uninstall.ts +0 -19
  50. package/src/connection.ts +0 -203
  51. package/src/debug.ts +0 -11
  52. package/src/handlers/backup.ts +0 -101
  53. package/src/handlers/board-handler.ts +0 -155
  54. package/src/handlers/chat.ts +0 -79
  55. package/src/handlers/config.ts +0 -48
  56. package/src/handlers/deploy.ts +0 -32
  57. package/src/handlers/exec.ts +0 -32
  58. package/src/handlers/file-delete.ts +0 -46
  59. package/src/handlers/file-write.ts +0 -65
  60. package/src/handlers/knowledge-sync.ts +0 -53
  61. package/src/handlers/onboarding.ts +0 -19
  62. package/src/handlers/package-install.ts +0 -69
  63. package/src/handlers/pair.ts +0 -26
  64. package/src/handlers/restart.ts +0 -19
  65. package/src/handlers/stop.ts +0 -17
  66. package/src/heartbeat.ts +0 -110
  67. package/src/index.ts +0 -97
  68. package/src/openclaw/gateway-adapter.ts +0 -129
  69. package/src/openclaw/gateway-client.ts +0 -131
  70. package/src/openclaw/index.ts +0 -17
  71. package/src/openclaw/types.ts +0 -41
  72. package/src/platform/linux.ts +0 -108
  73. package/src/platform/macos.ts +0 -122
  74. package/src/platform/windows.ts +0 -92
  75. package/src/types.ts +0 -94
  76. package/tsconfig.json +0 -18
@@ -1,79 +0,0 @@
1
- import type { AgentCommand } from '../types.js';
2
- import { getChatProvider } from '../openclaw/index.js';
3
-
4
- export async function handleChatListSessions(
5
- command: AgentCommand,
6
- publish: (data: unknown) => Promise<void>,
7
- ): Promise<void> {
8
- const provider = getChatProvider();
9
- if (!provider) {
10
- await publish({ type: 'chat_sessions_response', correlationId: command.id, sessions: [], error: 'Chat provider not initialized' });
11
- return;
12
- }
13
- try {
14
- const sessions = await provider.listSessions();
15
- await publish({ type: 'chat_sessions_response', correlationId: command.id, sessions });
16
- } catch (err) {
17
- await publish({ type: 'chat_sessions_response', correlationId: command.id, sessions: [], error: err instanceof Error ? err.message : String(err) });
18
- }
19
- }
20
-
21
- export async function handleChatHistory(
22
- command: AgentCommand,
23
- publish: (data: unknown) => Promise<void>,
24
- ): Promise<void> {
25
- const { sessionKey, limit } = command.payload as { sessionKey: string; limit?: number };
26
- const provider = getChatProvider();
27
- if (!provider) {
28
- await publish({ type: 'chat_history_response', correlationId: command.id, messages: [], error: 'Chat provider not initialized' });
29
- return;
30
- }
31
- try {
32
- const messages = await provider.getHistory(sessionKey, limit ?? 200);
33
- await publish({ type: 'chat_history_response', correlationId: command.id, messages });
34
- } catch (err) {
35
- await publish({ type: 'chat_history_response', correlationId: command.id, messages: [], error: err instanceof Error ? err.message : String(err) });
36
- }
37
- }
38
-
39
- export async function handleChatSend(
40
- command: AgentCommand,
41
- publish: (data: unknown) => Promise<void>,
42
- agentId: string,
43
- ): Promise<void> {
44
- const { sessionKey, text, attachments } = command.payload as {
45
- sessionKey: string;
46
- text: string;
47
- attachments?: Array<{ type: 'image'; mimeType: string; content: string }>;
48
- };
49
- const correlationId = command.id;
50
-
51
- // Typing indicator
52
- await publish({ type: 'chat_typing', agentId, state: true }).catch(() => {});
53
-
54
- const provider = getChatProvider();
55
- if (!provider) {
56
- await publish({ type: 'chat_response', correlationId, sessionKey, text: '', error: 'Chat provider not initialized' });
57
- return;
58
- }
59
-
60
- try {
61
- await provider.sendMessage({
62
- sessionKey,
63
- text,
64
- idempotencyKey: correlationId,
65
- attachments, // base64 inline — Centrifugo limit raised to 10MB
66
- onDelta: async (accumulated) => {
67
- await publish({ type: 'chat_delta', correlationId, sessionKey, text: accumulated }).catch(() => {});
68
- },
69
- onDone: async (finalText) => {
70
- await publish({ type: 'chat_response', correlationId, sessionKey, text: finalText }).catch(() => {});
71
- },
72
- onError: async (error) => {
73
- await publish({ type: 'chat_response', correlationId, sessionKey, text: '', error }).catch(() => {});
74
- },
75
- });
76
- } catch (err) {
77
- await publish({ type: 'chat_response', correlationId, sessionKey, text: '', error: err instanceof Error ? err.message : String(err) }).catch(() => {});
78
- }
79
- }
@@ -1,48 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { exec } from 'node:child_process';
4
- import type { AgentCommand, AgentResponse } from '../types.js';
5
-
6
- const CONFIG_DIR = '/etc/openclaw';
7
-
8
- export function handleConfig(command: AgentCommand): Promise<AgentResponse> {
9
- const filename = command.payload.filename as string;
10
- const content = command.payload.content as string;
11
-
12
- if (!filename || content === undefined) {
13
- return Promise.resolve({
14
- id: command.id,
15
- type: 'config',
16
- success: false,
17
- error: 'Missing "filename" or "content" in payload',
18
- });
19
- }
20
-
21
- const configPath = path.join(CONFIG_DIR, path.basename(filename));
22
-
23
- return fs.mkdir(CONFIG_DIR, { recursive: true })
24
- .then(() => fs.writeFile(configPath, content, 'utf-8'))
25
- .then(() => {
26
- return new Promise<AgentResponse>((resolve) => {
27
- exec('openclaw gateway restart', { timeout: 30_000 }, (error, stdout, stderr) => {
28
- resolve({
29
- id: command.id,
30
- type: 'config',
31
- success: !error,
32
- data: {
33
- path: configPath,
34
- stdout: stdout.toString(),
35
- stderr: stderr.toString(),
36
- },
37
- ...(error ? { error: error.message } : {}),
38
- });
39
- });
40
- });
41
- })
42
- .catch((err) => ({
43
- id: command.id,
44
- type: 'config',
45
- success: false,
46
- error: err instanceof Error ? err.message : String(err),
47
- }));
48
- }
@@ -1,32 +0,0 @@
1
- import { exec } from 'node:child_process';
2
- import type { AgentCommand, AgentResponse } from '../types.js';
3
-
4
- export function handleDeploy(command: AgentCommand): Promise<AgentResponse> {
5
- return new Promise((resolve) => {
6
- exec('npm i -g openclaw', { timeout: 120_000 }, (installErr, installOut, installStderr) => {
7
- if (installErr) {
8
- resolve({
9
- id: command.id,
10
- type: 'deploy',
11
- success: false,
12
- data: { stdout: installOut.toString(), stderr: installStderr.toString() },
13
- error: `Install failed: ${installErr.message}`,
14
- });
15
- return;
16
- }
17
-
18
- exec('openclaw gateway restart', { timeout: 30_000 }, (restartErr, restartOut, restartStderr) => {
19
- resolve({
20
- id: command.id,
21
- type: 'deploy',
22
- success: !restartErr,
23
- data: {
24
- install: { stdout: installOut.toString(), stderr: installStderr.toString() },
25
- restart: { stdout: restartOut.toString(), stderr: restartStderr.toString() },
26
- },
27
- ...(restartErr ? { error: `Restart failed: ${restartErr.message}` } : {}),
28
- });
29
- });
30
- });
31
- });
32
- }
@@ -1,32 +0,0 @@
1
- import { exec } from 'node:child_process';
2
- import type { AgentCommand, AgentResponse } from '../types.js';
3
-
4
- const EXEC_TIMEOUT = 60_000;
5
-
6
- export function handleExec(command: AgentCommand): Promise<AgentResponse> {
7
- const cmd = command.payload.command as string;
8
- if (!cmd) {
9
- return Promise.resolve({
10
- id: command.id,
11
- type: 'exec',
12
- success: false,
13
- error: 'Missing "command" in payload',
14
- });
15
- }
16
-
17
- return new Promise((resolve) => {
18
- exec(cmd, { timeout: EXEC_TIMEOUT }, (error, stdout, stderr) => {
19
- resolve({
20
- id: command.id,
21
- type: 'exec',
22
- success: !error,
23
- data: {
24
- exitCode: error ? (error as NodeJS.ErrnoException & { code?: number }).code ?? 1 : 0,
25
- stdout: stdout.toString(),
26
- stderr: stderr.toString(),
27
- },
28
- ...(error ? { error: error.message } : {}),
29
- });
30
- });
31
- });
32
- }
@@ -1,46 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import type { AgentCommand, AgentResponse } from '../types.js';
4
-
5
- const WORKSPACE_DIR = process.env.WORKSPACE_DIR ?? '/etc/openclaw/workspace';
6
-
7
- export async function handleFileDelete(command: AgentCommand): Promise<AgentResponse> {
8
- const filePath = command.payload.path as string;
9
-
10
- if (!filePath) {
11
- return {
12
- id: command.id,
13
- type: 'file_delete',
14
- success: false,
15
- error: 'Missing "path" in payload',
16
- };
17
- }
18
-
19
- if (filePath.includes('..') || filePath.startsWith('/')) {
20
- return {
21
- id: command.id,
22
- type: 'file_delete',
23
- success: false,
24
- error: 'Invalid path: must not contain ".." or start with "/"',
25
- };
26
- }
27
-
28
- const resolvedPath = path.join(WORKSPACE_DIR, filePath);
29
-
30
- try {
31
- await fs.unlink(resolvedPath);
32
- return {
33
- id: command.id,
34
- type: 'file_delete',
35
- success: true,
36
- data: { path: resolvedPath },
37
- };
38
- } catch (err) {
39
- return {
40
- id: command.id,
41
- type: 'file_delete',
42
- success: false,
43
- error: err instanceof Error ? err.message : String(err),
44
- };
45
- }
46
- }
@@ -1,65 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import type { AgentCommand, AgentResponse } from '../types.js';
4
-
5
- const WORKSPACE_DIR = process.env.WORKSPACE_DIR ?? '/etc/openclaw/workspace';
6
-
7
- export async function handleFileWrite(command: AgentCommand): Promise<AgentResponse> {
8
- const filePath = command.payload.path as string;
9
- const content = command.payload.content;
10
- const overwrite = (command.payload.overwrite as boolean | undefined) ?? true;
11
-
12
- if (!filePath || content === undefined) {
13
- return {
14
- id: command.id,
15
- type: 'file_write',
16
- success: false,
17
- error: 'Missing "path" or "content" in payload',
18
- };
19
- }
20
-
21
- if (filePath.includes('..') || filePath.startsWith('/')) {
22
- return {
23
- id: command.id,
24
- type: 'file_write',
25
- success: false,
26
- error: 'Invalid path: must not contain ".." or start with "/"',
27
- };
28
- }
29
-
30
- const resolvedPath = path.join(WORKSPACE_DIR, filePath);
31
-
32
- try {
33
- if (!overwrite) {
34
- try {
35
- await fs.access(resolvedPath);
36
- // File exists — skip
37
- return {
38
- id: command.id,
39
- type: 'file_write',
40
- success: true,
41
- data: { path: resolvedPath, written: false, skipped: true },
42
- };
43
- } catch {
44
- // File does not exist — proceed with write
45
- }
46
- }
47
-
48
- await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
49
- await fs.writeFile(resolvedPath, content as string, 'utf-8');
50
-
51
- return {
52
- id: command.id,
53
- type: 'file_write',
54
- success: true,
55
- data: { path: resolvedPath, written: true },
56
- };
57
- } catch (err) {
58
- return {
59
- id: command.id,
60
- type: 'file_write',
61
- success: false,
62
- error: err instanceof Error ? err.message : String(err),
63
- };
64
- }
65
- }
@@ -1,53 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import type { AgentCommand, AgentResponse } from '../types.js';
5
-
6
- function getWorkspaceDir(): string {
7
- const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() ?? path.join(os.homedir(), '.openclaw');
8
- return path.join(stateDir, 'workspace');
9
- }
10
-
11
- export async function handleKnowledgeSync(command: AgentCommand): Promise<AgentResponse> {
12
- const requestedPaths = command.payload.paths as string[];
13
- if (!Array.isArray(requestedPaths)) {
14
- return { id: command.id, type: 'knowledge_sync', success: false, error: 'Missing "paths" in payload' };
15
- }
16
-
17
- const workspaceDir = getWorkspaceDir();
18
- const resolvedPaths: string[] = [];
19
-
20
- for (const p of requestedPaths) {
21
- if (p.endsWith('/')) {
22
- // Directory — scan for .md files
23
- const dir = path.join(workspaceDir, p);
24
- try {
25
- const entries = await fs.readdir(dir);
26
- for (const entry of entries) {
27
- if (entry.endsWith('.md')) resolvedPaths.push(path.join(p, entry));
28
- }
29
- } catch {
30
- // Directory doesn't exist — skip
31
- }
32
- } else {
33
- resolvedPaths.push(p);
34
- }
35
- }
36
-
37
- const files: { path: string; content: string }[] = [];
38
- for (const filePath of resolvedPaths) {
39
- try {
40
- const content = await fs.readFile(path.join(workspaceDir, filePath), 'utf-8');
41
- files.push({ path: filePath, content });
42
- } catch {
43
- // File doesn't exist or unreadable — skip
44
- }
45
- }
46
-
47
- return {
48
- id: command.id,
49
- type: 'knowledge_sync',
50
- success: true,
51
- data: { files },
52
- };
53
- }
@@ -1,19 +0,0 @@
1
- import { exec } from 'node:child_process';
2
- import type { AgentCommand, AgentResponse } from '../types.js';
3
-
4
- export function handleOnboardingComplete(command: AgentCommand): Promise<AgentResponse> {
5
- return new Promise((resolve) => {
6
- exec('openclaw gateway restart', { timeout: 30_000 }, (error, stdout, stderr) => {
7
- resolve({
8
- id: command.id,
9
- type: 'onboarding_complete',
10
- success: !error,
11
- data: {
12
- stdout: stdout.toString(),
13
- stderr: stderr.toString(),
14
- },
15
- ...(error ? { error: error.message } : {}),
16
- });
17
- });
18
- });
19
- }
@@ -1,69 +0,0 @@
1
- import { exec } from 'node:child_process';
2
- import type { AgentCommand, AgentResponse } from '../types.js';
3
-
4
- const INSTALL_TIMEOUT_MS = 300_000; // 5 minutes total
5
-
6
- function execPackage(cmd: string, timeoutMs: number): Promise<void> {
7
- return new Promise((resolve, reject) => {
8
- exec(cmd, { timeout: timeoutMs }, (error) => {
9
- if (error) reject(error);
10
- else resolve();
11
- });
12
- });
13
- }
14
-
15
- export async function handlePackageInstall(command: AgentCommand): Promise<AgentResponse> {
16
- const packages = command.payload.packages as {
17
- apt?: string[];
18
- npm?: string[];
19
- pip?: string[];
20
- } | undefined;
21
-
22
- const apt = packages?.apt ?? [];
23
- const npm = packages?.npm ?? [];
24
- const pip = packages?.pip ?? [];
25
-
26
- if (apt.length === 0 && npm.length === 0 && pip.length === 0) {
27
- return {
28
- id: command.id,
29
- type: 'package_install',
30
- success: true,
31
- data: { installed: { apt: [], npm: [], pip: [] }, errors: [] },
32
- };
33
- }
34
-
35
- const installed: { apt: string[]; npm: string[]; pip: string[] } = { apt: [], npm: [], pip: [] };
36
- const errors: Array<{ name: string; error: string }> = [];
37
- const deadline = Date.now() + INSTALL_TIMEOUT_MS;
38
-
39
- async function tryInstall(pkg: string, cmd: string, type: 'apt' | 'npm' | 'pip'): Promise<void> {
40
- const remaining = deadline - Date.now();
41
- if (remaining <= 0) {
42
- errors.push({ name: pkg, error: 'Install timeout exceeded' });
43
- return;
44
- }
45
- try {
46
- await execPackage(cmd, remaining);
47
- installed[type].push(pkg);
48
- } catch (err) {
49
- errors.push({ name: pkg, error: err instanceof Error ? err.message : String(err) });
50
- }
51
- }
52
-
53
- for (const pkg of apt) {
54
- await tryInstall(pkg, `apt-get install -y ${pkg}`, 'apt');
55
- }
56
- for (const pkg of npm) {
57
- await tryInstall(pkg, `npm install -g ${pkg}`, 'npm');
58
- }
59
- for (const pkg of pip) {
60
- await tryInstall(pkg, `pip install ${pkg}`, 'pip');
61
- }
62
-
63
- return {
64
- id: command.id,
65
- type: 'package_install',
66
- success: errors.length === 0,
67
- data: { installed, errors },
68
- };
69
- }
@@ -1,26 +0,0 @@
1
- import type { AgentCommand, AgentResponse } from '../types.js';
2
-
3
- export function handlePair(command: AgentCommand): Promise<AgentResponse> {
4
- const pairToken = command.payload.token as string;
5
- const targetId = command.payload.targetId as string;
6
-
7
- if (!pairToken || !targetId) {
8
- return Promise.resolve({
9
- id: command.id,
10
- type: 'pair',
11
- success: false,
12
- error: 'Missing "token" or "targetId" in payload',
13
- });
14
- }
15
-
16
- return Promise.resolve({
17
- id: command.id,
18
- type: 'pair',
19
- success: true,
20
- data: {
21
- paired: true,
22
- targetId,
23
- agentId: process.env.AGENT_ID,
24
- },
25
- });
26
- }
@@ -1,19 +0,0 @@
1
- import { exec } from 'node:child_process';
2
- import type { AgentCommand, AgentResponse } from '../types.js';
3
-
4
- export function handleRestart(command: AgentCommand): Promise<AgentResponse> {
5
- return new Promise((resolve) => {
6
- exec('openclaw gateway restart', { timeout: 30_000 }, (error, stdout, stderr) => {
7
- resolve({
8
- id: command.id,
9
- type: 'restart',
10
- success: !error,
11
- data: {
12
- stdout: stdout.toString(),
13
- stderr: stderr.toString(),
14
- },
15
- ...(error ? { error: error.message } : {}),
16
- });
17
- });
18
- });
19
- }
@@ -1,17 +0,0 @@
1
- import type { AgentCommand, AgentResponse } from '../types.js';
2
-
3
- export function handleStop(command: AgentCommand): Promise<AgentResponse> {
4
- const response: AgentResponse = {
5
- id: command.id,
6
- type: 'stop',
7
- success: true,
8
- data: { message: 'Shutting down gracefully' },
9
- };
10
-
11
- setTimeout(() => {
12
- console.log('Agent stopping gracefully...');
13
- process.exit(0);
14
- }, 500);
15
-
16
- return Promise.resolve(response);
17
- }
package/src/heartbeat.ts DELETED
@@ -1,110 +0,0 @@
1
- import os from 'node:os';
2
- import path from 'node:path';
3
- import fs from 'node:fs/promises';
4
- import { exec } from 'node:child_process';
5
- import { debugLog } from './debug.js';
6
- import type { AgentApi } from './api.js';
7
- import type { BoardState, HeartbeatPayload } from './types.js';
8
- import { selfUpdate } from './commands/self-update.js';
9
-
10
- const HEARTBEAT_INTERVAL = 30_000;
11
- const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24h
12
- const PACKAGE_NAME = '@openclaw-cloud/agent-controller';
13
-
14
- function execAsync(cmd: string, timeout: number): Promise<string> {
15
- return new Promise((resolve, reject) => {
16
- exec(cmd, { timeout }, (err, stdout) => {
17
- if (err) reject(err);
18
- else resolve(stdout.toString().trim());
19
- });
20
- });
21
- }
22
-
23
- async function checkForUpdate(currentVersion: string): Promise<void> {
24
- try {
25
- const latest = await execAsync(`npm view ${PACKAGE_NAME} version`, 10_000);
26
- if (latest && latest !== currentVersion) {
27
- console.log(`[update] New version available: ${latest} (current: ${currentVersion}). Updating...`);
28
- await selfUpdate(currentVersion);
29
- }
30
- } catch {
31
- // No internet, npm unavailable, or already up to date — silently ignore
32
- }
33
- }
34
-
35
- export async function getLastMessageAt(): Promise<string | null> {
36
- const stateDir = process.env.OPENCLAW_STATE_DIR?.trim()
37
- ?? path.join(os.homedir(), '.openclaw');
38
- const sessionsDir = path.join(stateDir, 'agents', 'main', 'sessions');
39
- try {
40
- const files = await fs.readdir(sessionsDir);
41
- const jsonlFiles = files.filter((f) => f.endsWith('.jsonl'));
42
- if (jsonlFiles.length === 0) return null;
43
- const mtimes = await Promise.all(
44
- jsonlFiles.map((f) =>
45
- fs.stat(path.join(sessionsDir, f)).then((s) => s.mtime.getTime())
46
- )
47
- );
48
- return new Date(Math.max(...mtimes)).toISOString();
49
- } catch {
50
- return null;
51
- }
52
- }
53
-
54
- export interface HeartbeatOptions {
55
- api: AgentApi;
56
- agentId: string;
57
- version: string;
58
- getBoardStatus?: () => BoardState;
59
- }
60
-
61
- function getMetrics(): HeartbeatPayload['metrics'] {
62
- const cpus = os.cpus();
63
- const cpuUsage = cpus.reduce((acc, cpu) => {
64
- const total = Object.values(cpu.times).reduce((a, b) => a + b, 0);
65
- return acc + (1 - cpu.times.idle / total);
66
- }, 0) / cpus.length;
67
-
68
- const totalMem = os.totalmem();
69
- const freeMem = os.freemem();
70
-
71
- return {
72
- cpu: Math.round(cpuUsage * 100) / 100,
73
- mem: Math.round(((totalMem - freeMem) / totalMem) * 100) / 100,
74
- uptime: os.uptime(),
75
- };
76
- }
77
-
78
- async function publishHeartbeat(opts: HeartbeatOptions): Promise<void> {
79
- const payload: HeartbeatPayload = {
80
- type: 'heartbeat',
81
- agentId: opts.agentId,
82
- ts: Date.now(),
83
- version: opts.version,
84
- metrics: getMetrics(),
85
- ...(opts.getBoardStatus ? { boardStatus: opts.getBoardStatus() } : {}),
86
- lastMessageAt: await getLastMessageAt(),
87
- };
88
-
89
- try {
90
- debugLog('heartbeat', '→ sending', opts.agentId, JSON.stringify(payload).slice(0, 500));
91
- await opts.api.publishHeartbeat(opts.agentId, payload);
92
- debugLog('heartbeat', '← ok');
93
- } catch (err) {
94
- console.error('Heartbeat publish failed:', err instanceof Error ? err.message : err);
95
- debugLog('heartbeat', '← error', err);
96
- }
97
- }
98
-
99
- export function startHeartbeat(opts: HeartbeatOptions): NodeJS.Timeout {
100
- const send = () => { publishHeartbeat(opts).catch(() => {}); };
101
- send();
102
-
103
- // Auto-update check: immediately on start, then every 24h
104
- checkForUpdate(opts.version).catch(() => {});
105
- setInterval(() => { checkForUpdate(opts.version).catch(() => {}); }, UPDATE_CHECK_INTERVAL);
106
-
107
- return setInterval(send, HEARTBEAT_INTERVAL);
108
- }
109
-
110
- export { getMetrics };