@openclaw-cloud/agent-controller 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/bin/agent-controller.js +6 -1
- package/package.json +1 -1
- package/src/commands/install.ts +4 -0
- package/src/config-file.ts +56 -0
- package/src/connection.ts +29 -13
- package/src/handlers/knowledge-sync.ts +53 -0
- package/src/heartbeat.ts +32 -0
- package/src/index.ts +8 -1
- package/src/types.ts +3 -1
package/bin/agent-controller.js
CHANGED
|
@@ -7,11 +7,16 @@ import { dirname, join } from 'node:path';
|
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
8
8
|
const pkg = require(join(dirname(fileURLToPath(import.meta.url)), '../package.json'));
|
|
9
9
|
|
|
10
|
+
// Load saved env file before dispatching any command
|
|
11
|
+
// This allows `agent-controller backup`, `knowledge_sync`, etc. to work without manual env vars
|
|
12
|
+
const { loadEnvFile } = await import('../dist/config-file.js');
|
|
13
|
+
await loadEnvFile();
|
|
14
|
+
|
|
10
15
|
const [,, command] = process.argv;
|
|
11
16
|
|
|
12
17
|
if (command === '--version' || command === '-v') {
|
|
13
18
|
console.log(pkg.version);
|
|
14
|
-
} else if (command === 'self-update') {
|
|
19
|
+
} else if (command === 'self-update' || command === 'update') {
|
|
15
20
|
const { selfUpdate } = await import('../dist/commands/self-update.js');
|
|
16
21
|
await selfUpdate(pkg.version);
|
|
17
22
|
} else if (command === 'install') {
|
package/package.json
CHANGED
package/src/commands/install.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
|
+
import { saveEnvFile } from '../config-file.js';
|
|
2
3
|
|
|
3
4
|
async function prompt(rl: ReturnType<typeof createInterface>, question: string): Promise<string> {
|
|
4
5
|
return new Promise((resolve) => {
|
|
@@ -43,6 +44,9 @@ export async function install(): Promise<void> {
|
|
|
43
44
|
|
|
44
45
|
const config = { centrifugoUrl, agentToken, agentId, backendInternalUrl };
|
|
45
46
|
|
|
47
|
+
// Save env vars for future CLI usage (backup, knowledge_sync, etc.)
|
|
48
|
+
await saveEnvFile(config);
|
|
49
|
+
|
|
46
50
|
const platform = process.platform;
|
|
47
51
|
console.log('');
|
|
48
52
|
console.log(`Detected platform: ${platform}`);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const ENV_KEYS = ['CENTRIFUGO_URL', 'AGENT_TOKEN', 'AGENT_ID', 'BACKEND_INTERNAL_URL'] as const;
|
|
6
|
+
|
|
7
|
+
export function getConfigDir(): string {
|
|
8
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() ?? path.join(os.homedir(), '.openclaw');
|
|
9
|
+
return path.join(stateDir, 'agent-controller');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getConfigPath(): string {
|
|
13
|
+
return path.join(getConfigDir(), 'agent.env');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function saveEnvFile(config: {
|
|
17
|
+
centrifugoUrl: string;
|
|
18
|
+
agentToken: string;
|
|
19
|
+
agentId: string;
|
|
20
|
+
backendInternalUrl: string;
|
|
21
|
+
}): Promise<void> {
|
|
22
|
+
const dir = getConfigDir();
|
|
23
|
+
await fs.mkdir(dir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
const content = [
|
|
26
|
+
`CENTRIFUGO_URL=${config.centrifugoUrl}`,
|
|
27
|
+
`AGENT_TOKEN=${config.agentToken}`,
|
|
28
|
+
`AGENT_ID=${config.agentId}`,
|
|
29
|
+
`BACKEND_INTERNAL_URL=${config.backendInternalUrl}`,
|
|
30
|
+
].join('\n') + '\n';
|
|
31
|
+
|
|
32
|
+
await fs.writeFile(getConfigPath(), content, { mode: 0o600 });
|
|
33
|
+
console.log(` Config saved to ${getConfigPath()}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function loadEnvFile(): Promise<void> {
|
|
37
|
+
const filePath = getConfigPath();
|
|
38
|
+
let content: string;
|
|
39
|
+
try {
|
|
40
|
+
content = await fs.readFile(filePath, 'utf-8');
|
|
41
|
+
} catch {
|
|
42
|
+
return; // No config file — skip
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const line of content.split('\n')) {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
48
|
+
const eq = trimmed.indexOf('=');
|
|
49
|
+
if (eq === -1) continue;
|
|
50
|
+
const key = trimmed.slice(0, eq).trim();
|
|
51
|
+
const value = trimmed.slice(eq + 1).trim();
|
|
52
|
+
if (ENV_KEYS.includes(key as typeof ENV_KEYS[number]) && !process.env[key]) {
|
|
53
|
+
process.env[key] = value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/connection.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { handlePackageInstall } from './handlers/package-install.js';
|
|
|
15
15
|
import { handleFileWrite } from './handlers/file-write.js';
|
|
16
16
|
import { handleFileDelete } from './handlers/file-delete.js';
|
|
17
17
|
import { handleOnboardingComplete } from './handlers/onboarding.js';
|
|
18
|
+
import { handleKnowledgeSync } from './handlers/knowledge-sync.js';
|
|
19
|
+
import { selfUpdate } from './commands/self-update.js';
|
|
18
20
|
|
|
19
21
|
export interface ConnectionOptions {
|
|
20
22
|
url: string;
|
|
@@ -22,21 +24,34 @@ export interface ConnectionOptions {
|
|
|
22
24
|
agentId: string;
|
|
23
25
|
backendUrl: string;
|
|
24
26
|
api: AgentApi;
|
|
27
|
+
version: string;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
function buildHandlers(version: string): Record<string, (cmd: AgentCommand) => Promise<AgentResponse>> {
|
|
31
|
+
return {
|
|
32
|
+
exec: handleExec,
|
|
33
|
+
restart: handleRestart,
|
|
34
|
+
deploy: handleDeploy,
|
|
35
|
+
config: handleConfig,
|
|
36
|
+
pair: handlePair,
|
|
37
|
+
stop: handleStop,
|
|
38
|
+
backup: handleBackup,
|
|
39
|
+
package_install: handlePackageInstall,
|
|
40
|
+
file_write: handleFileWrite,
|
|
41
|
+
file_delete: handleFileDelete,
|
|
42
|
+
onboarding_complete: handleOnboardingComplete,
|
|
43
|
+
knowledge_sync: handleKnowledgeSync,
|
|
44
|
+
self_update: (cmd: AgentCommand) =>
|
|
45
|
+
selfUpdate(version)
|
|
46
|
+
.then(() => ({ id: cmd.id, type: cmd.type, success: true, data: {} }))
|
|
47
|
+
.catch((err: unknown) => ({
|
|
48
|
+
id: cmd.id,
|
|
49
|
+
type: cmd.type,
|
|
50
|
+
success: false,
|
|
51
|
+
error: err instanceof Error ? err.message : String(err),
|
|
52
|
+
})),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
40
55
|
|
|
41
56
|
async function fetchCentrifugoToken(backendUrl: string, agentToken: string, agentId: string): Promise<string> {
|
|
42
57
|
const controller = new AbortController();
|
|
@@ -98,6 +113,7 @@ export function createConnection(opts: ConnectionOptions): {
|
|
|
98
113
|
|
|
99
114
|
const commandChannel = `agent:${opts.agentId}`;
|
|
100
115
|
const commandSub = client.newSubscription(commandChannel);
|
|
116
|
+
const handlers = buildHandlers(opts.version);
|
|
101
117
|
|
|
102
118
|
commandSub.on('publication', async (ctx) => {
|
|
103
119
|
console.log(`[WS] Received message on ${commandChannel}:`, JSON.stringify(ctx.data));
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
}
|
package/src/heartbeat.ts
CHANGED
|
@@ -1,11 +1,36 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
|
+
import { exec } from 'node:child_process';
|
|
4
5
|
import { debugLog } from './debug.js';
|
|
5
6
|
import type { AgentApi } from './api.js';
|
|
6
7
|
import type { BoardState, HeartbeatPayload } from './types.js';
|
|
8
|
+
import { selfUpdate } from './commands/self-update.js';
|
|
7
9
|
|
|
8
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
|
+
}
|
|
9
34
|
|
|
10
35
|
export async function getLastMessageAt(): Promise<string | null> {
|
|
11
36
|
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim()
|
|
@@ -29,6 +54,7 @@ export async function getLastMessageAt(): Promise<string | null> {
|
|
|
29
54
|
export interface HeartbeatOptions {
|
|
30
55
|
api: AgentApi;
|
|
31
56
|
agentId: string;
|
|
57
|
+
version: string;
|
|
32
58
|
getBoardStatus?: () => BoardState;
|
|
33
59
|
}
|
|
34
60
|
|
|
@@ -54,6 +80,7 @@ async function publishHeartbeat(opts: HeartbeatOptions): Promise<void> {
|
|
|
54
80
|
type: 'heartbeat',
|
|
55
81
|
agentId: opts.agentId,
|
|
56
82
|
ts: Date.now(),
|
|
83
|
+
version: opts.version,
|
|
57
84
|
metrics: getMetrics(),
|
|
58
85
|
...(opts.getBoardStatus ? { boardStatus: opts.getBoardStatus() } : {}),
|
|
59
86
|
lastMessageAt: await getLastMessageAt(),
|
|
@@ -72,6 +99,11 @@ async function publishHeartbeat(opts: HeartbeatOptions): Promise<void> {
|
|
|
72
99
|
export function startHeartbeat(opts: HeartbeatOptions): NodeJS.Timeout {
|
|
73
100
|
const send = () => { publishHeartbeat(opts).catch(() => {}); };
|
|
74
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
|
+
|
|
75
107
|
return setInterval(send, HEARTBEAT_INTERVAL);
|
|
76
108
|
}
|
|
77
109
|
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
1
4
|
import { createConnection } from './connection.js';
|
|
2
5
|
import { startHeartbeat } from './heartbeat.js';
|
|
3
6
|
import { createAgentApi } from './api.js';
|
|
@@ -6,6 +9,9 @@ import { createChatProvider } from './openclaw/index.js';
|
|
|
6
9
|
import { DEBUG } from './debug.js';
|
|
7
10
|
import type { BoardEvent } from './types.js';
|
|
8
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
|
+
|
|
9
15
|
function requireEnv(name: string): string {
|
|
10
16
|
const value = process.env[name];
|
|
11
17
|
if (!value) {
|
|
@@ -27,7 +33,7 @@ export function main(): void {
|
|
|
27
33
|
console.log(`Backend URL: ${backendUrl} (JWT mode)`);
|
|
28
34
|
|
|
29
35
|
const api = createAgentApi(backendUrl, token);
|
|
30
|
-
const { client } = createConnection({ url, token, agentId, backendUrl, api });
|
|
36
|
+
const { client } = createConnection({ url, token, agentId, backendUrl, api, version: CONTROLLER_VERSION });
|
|
31
37
|
|
|
32
38
|
// Chat provider via OpenClaw Gateway WS
|
|
33
39
|
const gatewayWsUrl = process.env.OPENCLAW_GATEWAY_WS || 'ws://localhost:18789';
|
|
@@ -75,6 +81,7 @@ export function main(): void {
|
|
|
75
81
|
const heartbeatTimer = startHeartbeat({
|
|
76
82
|
api,
|
|
77
83
|
agentId,
|
|
84
|
+
version: CONTROLLER_VERSION,
|
|
78
85
|
getBoardStatus: () => boardHandler.getBoardStatus(),
|
|
79
86
|
});
|
|
80
87
|
|
package/src/types.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
export type CommandType =
|
|
8
8
|
| 'exec' | 'restart' | 'deploy' | 'config' | 'pair' | 'stop' | 'backup'
|
|
9
9
|
| 'chat_list_sessions' | 'chat_history' | 'chat_send'
|
|
10
|
-
| 'package_install' | 'file_write' | 'file_delete' | 'onboarding_complete'
|
|
10
|
+
| 'package_install' | 'file_write' | 'file_delete' | 'onboarding_complete'
|
|
11
|
+
| 'knowledge_sync' | 'self_update';
|
|
11
12
|
|
|
12
13
|
export interface AgentCommand {
|
|
13
14
|
id: string;
|
|
@@ -27,6 +28,7 @@ export interface HeartbeatPayload {
|
|
|
27
28
|
type: 'heartbeat';
|
|
28
29
|
agentId: string;
|
|
29
30
|
ts: number;
|
|
31
|
+
version: string;
|
|
30
32
|
metrics: {
|
|
31
33
|
cpu: number;
|
|
32
34
|
mem: number;
|