@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw-cloud/agent-controller",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -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
- const handlers: Record<string, (cmd: AgentCommand) => Promise<AgentResponse>> = {
28
- exec: handleExec,
29
- restart: handleRestart,
30
- deploy: handleDeploy,
31
- config: handleConfig,
32
- pair: handlePair,
33
- stop: handleStop,
34
- backup: handleBackup,
35
- package_install: handlePackageInstall,
36
- file_write: handleFileWrite,
37
- file_delete: handleFileDelete,
38
- onboarding_complete: handleOnboardingComplete,
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;