@openclaw-cloud/agent-controller 0.2.6 → 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/dist/commands/install.js +3 -0
  2. package/dist/commands/install.js.map +1 -1
  3. package/dist/config-file.d.ts +9 -0
  4. package/dist/config-file.js +47 -0
  5. package/dist/config-file.js.map +1 -0
  6. package/dist/connection.d.ts +1 -0
  7. package/dist/connection.js +27 -13
  8. package/dist/connection.js.map +1 -1
  9. package/dist/handlers/backup.js +7 -2
  10. package/dist/handlers/backup.js.map +1 -1
  11. package/dist/handlers/knowledge-sync.d.ts +2 -0
  12. package/dist/handlers/knowledge-sync.js +51 -0
  13. package/dist/handlers/knowledge-sync.js.map +1 -0
  14. package/dist/heartbeat.d.ts +1 -0
  15. package/dist/heartbeat.js +30 -0
  16. package/dist/heartbeat.js.map +1 -1
  17. package/dist/index.js +7 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/types.d.ts +2 -1
  20. package/package.json +6 -1
  21. package/.claude/cc-notify.sh +0 -32
  22. package/.claude/settings.json +0 -31
  23. package/.husky/pre-commit +0 -1
  24. package/BIZPLAN.md +0 -530
  25. package/CLAUDE.md +0 -172
  26. package/Dockerfile +0 -9
  27. package/__tests__/api.test.ts +0 -183
  28. package/__tests__/backup.test.ts +0 -145
  29. package/__tests__/board-handler.test.ts +0 -323
  30. package/__tests__/chat.test.ts +0 -191
  31. package/__tests__/config.test.ts +0 -100
  32. package/__tests__/connection.test.ts +0 -289
  33. package/__tests__/file-delete.test.ts +0 -90
  34. package/__tests__/file-write.test.ts +0 -119
  35. package/__tests__/gateway-adapter.test.ts +0 -366
  36. package/__tests__/gateway-client.test.ts +0 -272
  37. package/__tests__/handlers.test.ts +0 -150
  38. package/__tests__/heartbeat.test.ts +0 -124
  39. package/__tests__/onboarding.test.ts +0 -55
  40. package/__tests__/package-install.test.ts +0 -109
  41. package/__tests__/pair.test.ts +0 -60
  42. package/__tests__/self-update.test.ts +0 -123
  43. package/__tests__/stop.test.ts +0 -38
  44. package/jest.config.ts +0 -16
  45. package/src/api.ts +0 -62
  46. package/src/commands/install.ts +0 -68
  47. package/src/commands/self-update.ts +0 -43
  48. package/src/commands/uninstall.ts +0 -19
  49. package/src/config-file.ts +0 -56
  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
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 };
package/src/index.ts DELETED
@@ -1,97 +0,0 @@
1
- import { createRequire } from 'node:module';
2
- import { fileURLToPath } from 'node:url';
3
- import { dirname, join } from 'node:path';
4
- import { createConnection } from './connection.js';
5
- import { startHeartbeat } from './heartbeat.js';
6
- import { createAgentApi } from './api.js';
7
- import { BoardHandler } from './handlers/board-handler.js';
8
- import { createChatProvider } from './openclaw/index.js';
9
- import { DEBUG } from './debug.js';
10
- import type { BoardEvent } from './types.js';
11
-
12
- const _require = createRequire(import.meta.url);
13
- const { version: CONTROLLER_VERSION } = _require(join(dirname(fileURLToPath(import.meta.url)), '../package.json')) as { version: string };
14
-
15
- function requireEnv(name: string): string {
16
- const value = process.env[name];
17
- if (!value) {
18
- console.error(`Missing required environment variable: ${name}`);
19
- process.exit(1);
20
- }
21
- return value;
22
- }
23
-
24
- export function main(): void {
25
- const rawUrl = requireEnv('CENTRIFUGO_URL');
26
- // Centrifuge client requires ws:// scheme
27
- const url = rawUrl.replace(/^http:\/\//, 'ws://').replace(/^https:\/\//, 'wss://') + '/connection/websocket';
28
- const token = requireEnv('AGENT_TOKEN');
29
- const agentId = requireEnv('AGENT_ID');
30
- const backendUrl = requireEnv('BACKEND_INTERNAL_URL');
31
- if (DEBUG) console.log('[debug] Debug mode enabled');
32
- console.log(`Starting agent-controller for agent: ${agentId}`);
33
- console.log(`Backend URL: ${backendUrl} (JWT mode)`);
34
-
35
- const api = createAgentApi(backendUrl, token);
36
- const { client } = createConnection({ url, token, agentId, backendUrl, api, version: CONTROLLER_VERSION });
37
-
38
- // Chat provider via OpenClaw Gateway WS
39
- const gatewayWsUrl = process.env.OPENCLAW_GATEWAY_WS || 'ws://localhost:18789';
40
- const gatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';
41
-
42
- if (gatewayToken) {
43
- const chatProvider = createChatProvider(gatewayWsUrl, gatewayToken);
44
- chatProvider.connect().then(() => {
45
- console.log('OpenClaw Gateway WS connected (chat provider ready)');
46
- }).catch((e) => {
47
- console.warn('Chat provider connect failed:', e instanceof Error ? e.message : e);
48
- });
49
- } else {
50
- console.warn('OPENCLAW_GATEWAY_TOKEN not set, chat provider disabled');
51
- }
52
-
53
- // Board handler
54
- const boardHandler = new BoardHandler(api);
55
-
56
- // Initialize board handler after connection is established
57
- boardHandler.initialize().then((workspaceId) => {
58
- if (!workspaceId) {
59
- console.log('No board found for this agent, board handler dormant');
60
- return;
61
- }
62
-
63
- const boardChannel = `board:${workspaceId}`;
64
- const boardSub = client.newSubscription(boardChannel);
65
-
66
- boardSub.on('publication', (ctx) => {
67
- const event = ctx.data as BoardEvent;
68
- if (!event || !event.event) return;
69
- boardHandler.onBoardEvent(event).catch((err) => {
70
- console.error('Board event handler error:', err instanceof Error ? err.message : err);
71
- });
72
- });
73
-
74
- boardSub.subscribe();
75
- console.log(`Subscribed to board channel: ${boardChannel}`);
76
- }).catch((err) => {
77
- console.error('Board handler init error:', err instanceof Error ? err.message : err);
78
- });
79
-
80
- console.log('Heartbeat via backend API');
81
- const heartbeatTimer = startHeartbeat({
82
- api,
83
- agentId,
84
- version: CONTROLLER_VERSION,
85
- getBoardStatus: () => boardHandler.getBoardStatus(),
86
- });
87
-
88
- const shutdown = () => {
89
- console.log('Shutting down...');
90
- if (heartbeatTimer) clearInterval(heartbeatTimer);
91
- client.disconnect();
92
- process.exit(0);
93
- };
94
-
95
- process.on('SIGINT', shutdown);
96
- process.on('SIGTERM', shutdown);
97
- }
@@ -1,129 +0,0 @@
1
- import { GatewayClient } from './gateway-client.js';
2
- import type { IChatProvider, ChatSession, ChatMessage, ChatAttachment } from './types.js';
3
-
4
- export class OpenclawGatewayAdapter implements IChatProvider {
5
- private client: GatewayClient;
6
- private streamCallbacks = new Map<string, {
7
- onDelta?: (text: string) => void;
8
- onDone?: (text: string) => void;
9
- onError?: (error: string) => void;
10
- lastText: string;
11
- }>();
12
-
13
- constructor(url: string, token: string) {
14
- this.client = new GatewayClient(url, token);
15
- this.client.setEventHandler(this.handleEvent.bind(this));
16
- }
17
-
18
- async connect(): Promise<void> {
19
- await this.client.connect();
20
- }
21
-
22
- disconnect(): void {
23
- this.client.disconnect();
24
- }
25
-
26
- isConnected(): boolean {
27
- return this.client.isConnected();
28
- }
29
-
30
- async listSessions(): Promise<ChatSession[]> {
31
- const result = await this.client.request('sessions.list', {});
32
- return (result?.sessions ?? []).map((s: any) => ({
33
- key: s.key,
34
- sessionId: s.sessionId ?? '',
35
- kind: s.kind ?? 'direct',
36
- updatedAt: s.updatedAt ?? 0,
37
- model: s.model,
38
- }));
39
- }
40
-
41
- async getHistory(sessionKey: string, limit = 200): Promise<ChatMessage[]> {
42
- const result = await this.client.request('chat.history', { sessionKey, limit });
43
- return (result?.messages ?? [])
44
- .filter((m: any) => m.role === 'user' || m.role === 'assistant')
45
- .map((m: any) => {
46
- const content = Array.isArray(m.content)
47
- ? m.content.filter((c: any) => c.type === 'text').map((c: any) => c.text).join('')
48
- : String(m.content ?? '');
49
- return {
50
- role: m.role,
51
- content,
52
- timestamp: typeof m.timestamp === 'number'
53
- ? new Date(m.timestamp).toISOString()
54
- : m.timestamp ?? new Date().toISOString(),
55
- };
56
- });
57
- }
58
-
59
- async sendMessage(params: {
60
- sessionKey: string;
61
- text: string;
62
- idempotencyKey: string;
63
- attachments?: ChatAttachment[];
64
- onDelta?: (text: string) => void;
65
- onDone?: (text: string) => void;
66
- onError?: (error: string) => void;
67
- }): Promise<void> {
68
- // Register stream callbacks BEFORE sending
69
- this.streamCallbacks.set(params.idempotencyKey, {
70
- onDelta: params.onDelta,
71
- onDone: params.onDone,
72
- onError: params.onError,
73
- lastText: '',
74
- });
75
-
76
- const payload: any = {
77
- sessionKey: params.sessionKey,
78
- message: params.text,
79
- deliver: false,
80
- idempotencyKey: params.idempotencyKey,
81
- };
82
- if (params.attachments?.length) {
83
- payload.attachments = params.attachments;
84
- }
85
-
86
- try {
87
- await this.client.request('chat.send', payload);
88
- // Request accepted — response comes via events
89
- } catch (err) {
90
- this.streamCallbacks.delete(params.idempotencyKey);
91
- params.onError?.(err instanceof Error ? err.message : String(err));
92
- }
93
- }
94
-
95
- async abort(sessionKey: string, runId?: string): Promise<void> {
96
- await this.client.request('chat.abort', { sessionKey, runId }).catch(() => {});
97
- }
98
-
99
- private handleEvent(event: { event: string; payload: any }): void {
100
- const data = event.payload;
101
- if (!data || !data.sessionKey) return;
102
-
103
- // Stream events have runId matching idempotencyKey
104
- const runId = data.runId;
105
- const cb = runId ? this.streamCallbacks.get(runId) : null;
106
- if (!cb) return;
107
-
108
- if (data.state === 'delta' && data.message != null) {
109
- // message is accumulated text
110
- const text = typeof data.message === 'string'
111
- ? data.message
112
- : Array.isArray(data.message)
113
- ? data.message.filter((c: any) => c.type === 'text').map((c: any) => c.text).join('')
114
- : '';
115
- cb.lastText = text;
116
- cb.onDelta?.(text);
117
- } else if (data.state === 'final') {
118
- this.streamCallbacks.delete(runId!);
119
- cb.onDone?.(cb.lastText);
120
- } else if (data.state === 'aborted') {
121
- this.streamCallbacks.delete(runId!);
122
- cb.onDone?.(cb.lastText); // still send whatever we have
123
- } else if (data.state === 'error') {
124
- this.streamCallbacks.delete(runId!);
125
- const errMsg = typeof data.error === 'string' ? data.error : data.error?.message ?? 'Unknown error';
126
- cb.onError?.(errMsg);
127
- }
128
- }
129
- }
@@ -1,131 +0,0 @@
1
- import WebSocket from 'ws';
2
- import { randomUUID } from 'crypto';
3
-
4
- interface PendingRequest {
5
- resolve: (payload: any) => void;
6
- reject: (err: Error) => void;
7
- }
8
-
9
- export type EventHandler = (event: { event: string; payload: any; seq?: number }) => void;
10
-
11
- export class GatewayClient {
12
- private ws: WebSocket | null = null;
13
- private pending = new Map<string, PendingRequest>();
14
- private onEvent: EventHandler | null = null;
15
- private connected = false;
16
- private token: string;
17
- private url: string;
18
-
19
- constructor(url: string, token: string) {
20
- this.url = url;
21
- this.token = token;
22
- }
23
-
24
- setEventHandler(handler: EventHandler): void {
25
- this.onEvent = handler;
26
- }
27
-
28
- connect(): Promise<void> {
29
- return new Promise((resolve, reject) => {
30
- this.ws = new WebSocket(this.url);
31
-
32
- this.ws.on('open', () => {
33
- // Wait for connect.challenge event, then authenticate
34
- });
35
-
36
- this.ws.on('message', (raw) => {
37
- try {
38
- const msg = JSON.parse(raw.toString());
39
- this.handleMessage(msg);
40
- } catch {}
41
- });
42
-
43
- this.ws.on('error', (e) => {
44
- if (!this.connected) reject(e);
45
- });
46
-
47
- this.ws.on('close', () => {
48
- this.connected = false;
49
- this.flushPending(new Error('Connection closed'));
50
- });
51
-
52
- // Wait for connect.challenge → authenticate → resolve
53
- const origHandler = this.onEvent;
54
- this.onEvent = async (ev) => {
55
- if (ev.event === 'connect.challenge') {
56
- try {
57
- await this.request('connect', {
58
- auth: { token: this.token },
59
- scopes: ['operator.admin'],
60
- caps: [],
61
- });
62
- this.connected = true;
63
- this.onEvent = origHandler;
64
- resolve();
65
- } catch (e) {
66
- reject(e);
67
- }
68
- }
69
- };
70
-
71
- setTimeout(() => {
72
- if (!this.connected) reject(new Error('Connect timeout'));
73
- }, 10_000);
74
- });
75
- }
76
-
77
- disconnect(): void {
78
- this.ws?.close();
79
- this.ws = null;
80
- this.connected = false;
81
- this.flushPending(new Error('Disconnected'));
82
- }
83
-
84
- isConnected(): boolean {
85
- return this.connected && this.ws?.readyState === WebSocket.OPEN;
86
- }
87
-
88
- request(method: string, params: any): Promise<any> {
89
- return new Promise((resolve, reject) => {
90
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
91
- return reject(new Error('Not connected'));
92
- }
93
-
94
- const id = randomUUID();
95
- this.pending.set(id, { resolve, reject });
96
- this.ws.send(JSON.stringify({ type: 'req', id, method, params }));
97
-
98
- // Timeout per request
99
- setTimeout(() => {
100
- if (this.pending.has(id)) {
101
- this.pending.delete(id);
102
- reject(new Error(`Request timeout: ${method}`));
103
- }
104
- }, 120_000);
105
- });
106
- }
107
-
108
- private handleMessage(msg: any): void {
109
- if (msg.type === 'event') {
110
- this.onEvent?.(msg);
111
- return;
112
- }
113
- if (msg.type === 'res') {
114
- const req = this.pending.get(msg.id);
115
- if (!req) return;
116
- this.pending.delete(msg.id);
117
- if (msg.ok) {
118
- req.resolve(msg.payload);
119
- } else {
120
- req.reject(new Error(msg.error?.message ?? 'Request failed'));
121
- }
122
- }
123
- }
124
-
125
- private flushPending(err: Error): void {
126
- for (const [, req] of this.pending) {
127
- req.reject(err);
128
- }
129
- this.pending.clear();
130
- }
131
- }
@@ -1,17 +0,0 @@
1
- export { OpenclawGatewayAdapter } from './gateway-adapter.js';
2
- export type { IChatProvider, ChatSession, ChatMessage, ChatAttachment } from './types.js';
3
-
4
- import { OpenclawGatewayAdapter } from './gateway-adapter.js';
5
- import type { IChatProvider } from './types.js';
6
-
7
- let _provider: IChatProvider | null = null;
8
-
9
- export function createChatProvider(url: string, token: string): IChatProvider & { connect(): Promise<void>; disconnect(): void } {
10
- const adapter = new OpenclawGatewayAdapter(url, token);
11
- _provider = adapter;
12
- return adapter;
13
- }
14
-
15
- export function getChatProvider(): IChatProvider | null {
16
- return _provider;
17
- }
@@ -1,41 +0,0 @@
1
- export interface ChatSession {
2
- key: string;
3
- sessionId: string;
4
- kind: string;
5
- updatedAt: number;
6
- model?: string;
7
- }
8
-
9
- export interface ChatMessage {
10
- role: 'user' | 'assistant';
11
- content: string; // normalized to plain text
12
- timestamp: string; // ISO 8601
13
- }
14
-
15
- export interface ChatAttachment {
16
- type: 'image';
17
- mimeType: string;
18
- content: string; // base64
19
- }
20
-
21
- export interface IChatProvider {
22
- listSessions(): Promise<ChatSession[]>;
23
- getHistory(sessionKey: string, limit?: number): Promise<ChatMessage[]>;
24
-
25
- /**
26
- * Send message. Returns after the agent finishes (full response).
27
- * Calls onDelta with accumulated text during streaming.
28
- * Calls onDone when agent response is complete.
29
- */
30
- sendMessage(params: {
31
- sessionKey: string;
32
- text: string;
33
- idempotencyKey: string;
34
- attachments?: ChatAttachment[];
35
- onDelta?: (accumulatedText: string) => void;
36
- onDone?: (finalText: string) => void;
37
- onError?: (error: string) => void;
38
- }): Promise<void>;
39
-
40
- abort(sessionKey: string, runId?: string): Promise<void>;
41
- }
@@ -1,108 +0,0 @@
1
- import { exec } from 'node:child_process';
2
- import { writeFileSync, mkdirSync } from 'node:fs';
3
- import { homedir } from 'node:os';
4
- import { join } from 'node:path';
5
-
6
- export interface PlatformConfig {
7
- centrifugoUrl: string;
8
- agentToken: string;
9
- agentId: string;
10
- backendInternalUrl: string;
11
- }
12
-
13
- function execAsync(cmd: string): Promise<string> {
14
- return new Promise((resolve, reject) => {
15
- exec(cmd, (err, stdout) => {
16
- if (err) reject(err);
17
- else resolve(stdout.toString().trim());
18
- });
19
- });
20
- }
21
-
22
- async function which(bin: string): Promise<string> {
23
- try {
24
- return await execAsync(`which "${bin}"`);
25
- } catch {
26
- return '';
27
- }
28
- }
29
-
30
- export async function installLinux(config: PlatformConfig): Promise<void> {
31
- const home = homedir();
32
- const systemdDir = join(home, '.config', 'systemd', 'user');
33
- const servicePath = join(systemdDir, 'agent-controller.service');
34
-
35
- const nodePath = process.execPath;
36
- let agentControllerPath = await which('agent-controller');
37
- if (!agentControllerPath) {
38
- agentControllerPath = join(nodePath, '..', 'agent-controller');
39
- }
40
-
41
- mkdirSync(systemdDir, { recursive: true });
42
-
43
- const service = `[Unit]
44
- Description=OpenClaw Agent Controller
45
- After=network.target
46
-
47
- [Service]
48
- Type=simple
49
- Restart=always
50
- RestartSec=10
51
- Environment=CENTRIFUGO_URL=${config.centrifugoUrl}
52
- Environment=AGENT_TOKEN=${config.agentToken}
53
- Environment=AGENT_ID=${config.agentId}
54
- Environment=BACKEND_INTERNAL_URL=${config.backendInternalUrl}
55
- ExecStart="${nodePath}" "${agentControllerPath}"
56
-
57
- [Install]
58
- WantedBy=default.target
59
- `;
60
-
61
- writeFileSync(servicePath, service, 'utf8');
62
- console.log(`Written: ${servicePath}`);
63
-
64
- try {
65
- await execAsync('systemctl --user daemon-reload');
66
- console.log('systemd daemon reloaded.');
67
- await execAsync('systemctl --user enable --now agent-controller');
68
- console.log('Service enabled and started.');
69
- } catch (err) {
70
- console.warn('systemctl command failed:', err instanceof Error ? err.message : err);
71
- }
72
-
73
- console.log('');
74
- console.log('Agent Controller installed as a systemd user service.');
75
- console.log('');
76
- console.log('Useful commands:');
77
- console.log(' Start: systemctl --user start agent-controller');
78
- console.log(' Stop: systemctl --user stop agent-controller');
79
- console.log(' Status: systemctl --user status agent-controller');
80
- console.log(' Logs: journalctl --user -u agent-controller -f');
81
- console.log(' Uninstall: agent-controller uninstall');
82
- }
83
-
84
- export async function uninstallLinux(): Promise<void> {
85
- try {
86
- await execAsync('systemctl --user disable --now agent-controller');
87
- console.log('Service disabled and stopped.');
88
- } catch (err) {
89
- console.warn('systemctl disable failed (may not be enabled):', err instanceof Error ? err.message : err);
90
- }
91
-
92
- const servicePath = join(homedir(), '.config', 'systemd', 'user', 'agent-controller.service');
93
- const { unlinkSync, existsSync } = await import('node:fs');
94
- if (existsSync(servicePath)) {
95
- unlinkSync(servicePath);
96
- console.log(`Removed: ${servicePath}`);
97
- } else {
98
- console.log(`Service file not found: ${servicePath}`);
99
- }
100
-
101
- try {
102
- await execAsync('systemctl --user daemon-reload');
103
- } catch {
104
- // best-effort
105
- }
106
-
107
- console.log('Agent Controller uninstalled.');
108
- }