@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.
- package/dist/commands/install.js +3 -0
- package/dist/commands/install.js.map +1 -1
- package/dist/config-file.d.ts +9 -0
- package/dist/config-file.js +47 -0
- package/dist/config-file.js.map +1 -0
- package/dist/connection.d.ts +1 -0
- package/dist/connection.js +27 -13
- package/dist/connection.js.map +1 -1
- package/dist/handlers/backup.js +7 -2
- package/dist/handlers/backup.js.map +1 -1
- package/dist/handlers/knowledge-sync.d.ts +2 -0
- package/dist/handlers/knowledge-sync.js +51 -0
- package/dist/handlers/knowledge-sync.js.map +1 -0
- package/dist/heartbeat.d.ts +1 -0
- package/dist/heartbeat.js +30 -0
- package/dist/heartbeat.js.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/package.json +6 -1
- package/.claude/cc-notify.sh +0 -32
- package/.claude/settings.json +0 -31
- package/.husky/pre-commit +0 -1
- package/BIZPLAN.md +0 -530
- package/CLAUDE.md +0 -172
- package/Dockerfile +0 -9
- package/__tests__/api.test.ts +0 -183
- package/__tests__/backup.test.ts +0 -145
- package/__tests__/board-handler.test.ts +0 -323
- package/__tests__/chat.test.ts +0 -191
- package/__tests__/config.test.ts +0 -100
- package/__tests__/connection.test.ts +0 -289
- package/__tests__/file-delete.test.ts +0 -90
- package/__tests__/file-write.test.ts +0 -119
- package/__tests__/gateway-adapter.test.ts +0 -366
- package/__tests__/gateway-client.test.ts +0 -272
- package/__tests__/handlers.test.ts +0 -150
- package/__tests__/heartbeat.test.ts +0 -124
- package/__tests__/onboarding.test.ts +0 -55
- package/__tests__/package-install.test.ts +0 -109
- package/__tests__/pair.test.ts +0 -60
- package/__tests__/self-update.test.ts +0 -123
- package/__tests__/stop.test.ts +0 -38
- package/jest.config.ts +0 -16
- package/src/api.ts +0 -62
- package/src/commands/install.ts +0 -68
- package/src/commands/self-update.ts +0 -43
- package/src/commands/uninstall.ts +0 -19
- package/src/config-file.ts +0 -56
- package/src/connection.ts +0 -203
- package/src/debug.ts +0 -11
- package/src/handlers/backup.ts +0 -101
- package/src/handlers/board-handler.ts +0 -155
- package/src/handlers/chat.ts +0 -79
- package/src/handlers/config.ts +0 -48
- package/src/handlers/deploy.ts +0 -32
- package/src/handlers/exec.ts +0 -32
- package/src/handlers/file-delete.ts +0 -46
- package/src/handlers/file-write.ts +0 -65
- package/src/handlers/knowledge-sync.ts +0 -53
- package/src/handlers/onboarding.ts +0 -19
- package/src/handlers/package-install.ts +0 -69
- package/src/handlers/pair.ts +0 -26
- package/src/handlers/restart.ts +0 -19
- package/src/handlers/stop.ts +0 -17
- package/src/heartbeat.ts +0 -110
- package/src/index.ts +0 -97
- package/src/openclaw/gateway-adapter.ts +0 -129
- package/src/openclaw/gateway-client.ts +0 -131
- package/src/openclaw/index.ts +0 -17
- package/src/openclaw/types.ts +0 -41
- package/src/platform/linux.ts +0 -108
- package/src/platform/macos.ts +0 -122
- package/src/platform/windows.ts +0 -92
- package/src/types.ts +0 -94
- package/tsconfig.json +0 -18
package/src/api.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { DEBUG, debugLog } from './debug.js';
|
|
2
|
-
|
|
3
|
-
const TIMEOUT = 10_000;
|
|
4
|
-
|
|
5
|
-
export interface AgentApi {
|
|
6
|
-
get(path: string): Promise<unknown>;
|
|
7
|
-
post(path: string, body?: unknown): Promise<Response>;
|
|
8
|
-
publishResponse(agentId: string, data: unknown): Promise<void>;
|
|
9
|
-
publishHeartbeat(agentId: string, payload: unknown): Promise<void>;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function createAgentApi(backendUrl: string, agentToken: string): AgentApi {
|
|
13
|
-
async function request(method: string, path: string, body?: unknown): Promise<Response> {
|
|
14
|
-
const controller = new AbortController();
|
|
15
|
-
const timeout = setTimeout(() => controller.abort(), TIMEOUT);
|
|
16
|
-
try {
|
|
17
|
-
debugLog('api', '→', method, path, body ? JSON.stringify(body).slice(0, 500) : '');
|
|
18
|
-
const res = await fetch(`${backendUrl}${path}`, {
|
|
19
|
-
method,
|
|
20
|
-
headers: {
|
|
21
|
-
'Content-Type': 'application/json',
|
|
22
|
-
'Authorization': `Bearer ${agentToken}`,
|
|
23
|
-
},
|
|
24
|
-
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
25
|
-
signal: controller.signal,
|
|
26
|
-
});
|
|
27
|
-
if (DEBUG) {
|
|
28
|
-
const clone = res.clone();
|
|
29
|
-
const bodyText = await clone.text().catch(() => '');
|
|
30
|
-
debugLog('api', '←', method, path, res.status, bodyText.slice(0, 1000));
|
|
31
|
-
}
|
|
32
|
-
return res;
|
|
33
|
-
} finally {
|
|
34
|
-
clearTimeout(timeout);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
async get(path: string): Promise<unknown> {
|
|
40
|
-
const res = await request('GET', path);
|
|
41
|
-
if (!res.ok) {
|
|
42
|
-
throw new Error(`GET ${path} failed: HTTP ${res.status}`);
|
|
43
|
-
}
|
|
44
|
-
return res.json();
|
|
45
|
-
},
|
|
46
|
-
async post(path: string, body?: unknown): Promise<Response> {
|
|
47
|
-
return request('POST', path, body);
|
|
48
|
-
},
|
|
49
|
-
async publishResponse(agentId: string, data: unknown): Promise<void> {
|
|
50
|
-
const res = await request('POST', '/api/internal/agent-response', { agentId, data });
|
|
51
|
-
if (!res.ok) {
|
|
52
|
-
throw new Error(`publishResponse failed: HTTP ${res.status}`);
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
async publishHeartbeat(agentId: string, payload: unknown): Promise<void> {
|
|
56
|
-
const res = await request('POST', '/api/internal/agent-heartbeat', { agentId, payload });
|
|
57
|
-
if (!res.ok) {
|
|
58
|
-
throw new Error(`publishHeartbeat failed: HTTP ${res.status}`);
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
}
|
package/src/commands/install.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { createInterface } from 'node:readline';
|
|
2
|
-
import { saveEnvFile } from '../config-file.js';
|
|
3
|
-
|
|
4
|
-
async function prompt(rl: ReturnType<typeof createInterface>, question: string): Promise<string> {
|
|
5
|
-
return new Promise((resolve) => {
|
|
6
|
-
rl.question(question, (answer) => {
|
|
7
|
-
resolve(answer.trim());
|
|
8
|
-
});
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async function getOrPrompt(
|
|
13
|
-
rl: ReturnType<typeof createInterface>,
|
|
14
|
-
envKey: string,
|
|
15
|
-
question: string,
|
|
16
|
-
): Promise<string> {
|
|
17
|
-
const fromEnv = process.env[envKey];
|
|
18
|
-
if (fromEnv) {
|
|
19
|
-
const display = envKey.includes('TOKEN') ? '***' : fromEnv;
|
|
20
|
-
console.log(` ${envKey}: ${display} (from environment)`);
|
|
21
|
-
return fromEnv;
|
|
22
|
-
}
|
|
23
|
-
return prompt(rl, question);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function install(): Promise<void> {
|
|
27
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
28
|
-
|
|
29
|
-
console.log('OpenClaw Agent Controller — Installation Wizard');
|
|
30
|
-
console.log('================================================');
|
|
31
|
-
console.log('');
|
|
32
|
-
|
|
33
|
-
const centrifugoUrl = await getOrPrompt(rl, 'CENTRIFUGO_URL', 'CENTRIFUGO_URL (e.g. wss://ws.openclaw-cloud.io): ');
|
|
34
|
-
const agentToken = await getOrPrompt(rl, 'AGENT_TOKEN', 'AGENT_TOKEN: ');
|
|
35
|
-
const agentId = await getOrPrompt(rl, 'AGENT_ID', 'AGENT_ID: ');
|
|
36
|
-
const backendInternalUrl = await getOrPrompt(rl, 'BACKEND_INTERNAL_URL', 'BACKEND_INTERNAL_URL (e.g. https://api.openclaw-cloud.io): ');
|
|
37
|
-
|
|
38
|
-
rl.close();
|
|
39
|
-
|
|
40
|
-
if (!centrifugoUrl || !agentToken || !agentId || !backendInternalUrl) {
|
|
41
|
-
console.error('All fields are required. Aborting.');
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const config = { centrifugoUrl, agentToken, agentId, backendInternalUrl };
|
|
46
|
-
|
|
47
|
-
// Save env vars for future CLI usage (backup, knowledge_sync, etc.)
|
|
48
|
-
await saveEnvFile(config);
|
|
49
|
-
|
|
50
|
-
const platform = process.platform;
|
|
51
|
-
console.log('');
|
|
52
|
-
console.log(`Detected platform: ${platform}`);
|
|
53
|
-
console.log('');
|
|
54
|
-
|
|
55
|
-
if (platform === 'darwin') {
|
|
56
|
-
const { installMacOS } = await import('../platform/macos.js');
|
|
57
|
-
await installMacOS(config);
|
|
58
|
-
} else if (platform === 'linux') {
|
|
59
|
-
const { installLinux } = await import('../platform/linux.js');
|
|
60
|
-
await installLinux(config);
|
|
61
|
-
} else if (platform === 'win32') {
|
|
62
|
-
const { installWindows } = await import('../platform/windows.js');
|
|
63
|
-
await installWindows(config);
|
|
64
|
-
} else {
|
|
65
|
-
console.error(`Unsupported platform: ${platform}`);
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { exec } from 'node:child_process';
|
|
2
|
-
|
|
3
|
-
const PACKAGE_NAME = '@openclaw-cloud/agent-controller';
|
|
4
|
-
|
|
5
|
-
function execAsync(cmd: string, timeout: number): Promise<string> {
|
|
6
|
-
return new Promise((resolve, reject) => {
|
|
7
|
-
exec(cmd, { timeout }, (err, stdout) => {
|
|
8
|
-
if (err) reject(err);
|
|
9
|
-
else resolve(stdout.toString().trim());
|
|
10
|
-
});
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function selfUpdate(currentVersion: string): Promise<void> {
|
|
15
|
-
console.log(`Current version: ${currentVersion}`);
|
|
16
|
-
|
|
17
|
-
let latestVersion: string;
|
|
18
|
-
try {
|
|
19
|
-
latestVersion = await execAsync(`npm view ${PACKAGE_NAME} version`, 10_000);
|
|
20
|
-
} catch (err) {
|
|
21
|
-
console.error('Failed to fetch latest version:', err instanceof Error ? err.message : err);
|
|
22
|
-
process.exit(1);
|
|
23
|
-
return; // unreachable in production; guards test flow when process.exit is mocked
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
console.log(`Latest version: ${latestVersion}`);
|
|
27
|
-
|
|
28
|
-
if (currentVersion === latestVersion) {
|
|
29
|
-
console.log('Already up to date.');
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
console.log(`Updating from ${currentVersion} to ${latestVersion}...`);
|
|
34
|
-
try {
|
|
35
|
-
await execAsync(`npm install -g ${PACKAGE_NAME}@latest`, 120_000);
|
|
36
|
-
console.log(`Successfully updated to ${latestVersion}. Restarting...`);
|
|
37
|
-
process.exit(0);
|
|
38
|
-
return; // unreachable in production; guards test flow when process.exit is mocked
|
|
39
|
-
} catch (err) {
|
|
40
|
-
console.error('Update failed:', err instanceof Error ? err.message : err);
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export async function uninstall(): Promise<void> {
|
|
2
|
-
const platform = process.platform;
|
|
3
|
-
console.log(`Uninstalling Agent Controller on platform: ${platform}`);
|
|
4
|
-
console.log('');
|
|
5
|
-
|
|
6
|
-
if (platform === 'darwin') {
|
|
7
|
-
const { uninstallMacOS } = await import('../platform/macos.js');
|
|
8
|
-
await uninstallMacOS();
|
|
9
|
-
} else if (platform === 'linux') {
|
|
10
|
-
const { uninstallLinux } = await import('../platform/linux.js');
|
|
11
|
-
await uninstallLinux();
|
|
12
|
-
} else if (platform === 'win32') {
|
|
13
|
-
const { uninstallWindows } = await import('../platform/windows.js');
|
|
14
|
-
await uninstallWindows();
|
|
15
|
-
} else {
|
|
16
|
-
console.error(`Unsupported platform: ${platform}`);
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
}
|
package/src/config-file.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import { debugLog } from './debug.js';
|
|
2
|
-
import type { AgentApi } from './api.js';
|
|
3
|
-
import { Centrifuge, Subscription } from 'centrifuge';
|
|
4
|
-
import WebSocket from 'ws';
|
|
5
|
-
import type { AgentCommand, AgentResponse } from './types.js';
|
|
6
|
-
import { handleExec } from './handlers/exec.js';
|
|
7
|
-
import { handleRestart } from './handlers/restart.js';
|
|
8
|
-
import { handleDeploy } from './handlers/deploy.js';
|
|
9
|
-
import { handleConfig } from './handlers/config.js';
|
|
10
|
-
import { handlePair } from './handlers/pair.js';
|
|
11
|
-
import { handleStop } from './handlers/stop.js';
|
|
12
|
-
import { handleBackup } from './handlers/backup.js';
|
|
13
|
-
import { handleChatListSessions, handleChatHistory, handleChatSend } from './handlers/chat.js';
|
|
14
|
-
import { handlePackageInstall } from './handlers/package-install.js';
|
|
15
|
-
import { handleFileWrite } from './handlers/file-write.js';
|
|
16
|
-
import { handleFileDelete } from './handlers/file-delete.js';
|
|
17
|
-
import { handleOnboardingComplete } from './handlers/onboarding.js';
|
|
18
|
-
import { handleKnowledgeSync } from './handlers/knowledge-sync.js';
|
|
19
|
-
import { selfUpdate } from './commands/self-update.js';
|
|
20
|
-
|
|
21
|
-
export interface ConnectionOptions {
|
|
22
|
-
url: string;
|
|
23
|
-
token: string;
|
|
24
|
-
agentId: string;
|
|
25
|
-
backendUrl: string;
|
|
26
|
-
api: AgentApi;
|
|
27
|
-
version: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
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
|
-
}
|
|
55
|
-
|
|
56
|
-
async function fetchCentrifugoToken(backendUrl: string, agentToken: string, agentId: string): Promise<string> {
|
|
57
|
-
const controller = new AbortController();
|
|
58
|
-
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
59
|
-
try {
|
|
60
|
-
const res = await fetch(`${backendUrl}/api/internal/centrifugo-token`, {
|
|
61
|
-
method: 'POST',
|
|
62
|
-
headers: {
|
|
63
|
-
'Content-Type': 'application/json',
|
|
64
|
-
'Authorization': `Bearer ${agentToken}`,
|
|
65
|
-
},
|
|
66
|
-
body: JSON.stringify({ agentId }),
|
|
67
|
-
signal: controller.signal,
|
|
68
|
-
});
|
|
69
|
-
if (!res.ok) {
|
|
70
|
-
throw new Error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
|
71
|
-
}
|
|
72
|
-
const data = await res.json() as { token: string };
|
|
73
|
-
return data.token;
|
|
74
|
-
} finally {
|
|
75
|
-
clearTimeout(timeout);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function createConnection(opts: ConnectionOptions): {
|
|
80
|
-
client: Centrifuge;
|
|
81
|
-
commandSub: Subscription;
|
|
82
|
-
} {
|
|
83
|
-
// Dynamic JWT: fetch fresh token on every connect/reconnect
|
|
84
|
-
const getToken = async (): Promise<string> => {
|
|
85
|
-
console.log('Fetching Centrifugo JWT from backend...');
|
|
86
|
-
try {
|
|
87
|
-
const token = await fetchCentrifugoToken(opts.backendUrl, opts.token, opts.agentId);
|
|
88
|
-
console.log('Got Centrifugo JWT');
|
|
89
|
-
return token;
|
|
90
|
-
} catch (err) {
|
|
91
|
-
console.error('Failed to fetch Centrifugo JWT:', err instanceof Error ? err.message : err);
|
|
92
|
-
// Retry once before giving up — centrifuge will handle reconnect backoff
|
|
93
|
-
try {
|
|
94
|
-
const token = await fetchCentrifugoToken(opts.backendUrl, opts.token, opts.agentId);
|
|
95
|
-
console.log('Got Centrifugo JWT (retry)');
|
|
96
|
-
return token;
|
|
97
|
-
} catch (retryErr) {
|
|
98
|
-
console.error('JWT fetch retry failed:', retryErr instanceof Error ? retryErr.message : retryErr);
|
|
99
|
-
throw retryErr;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const centrifugeOpts: Record<string, unknown> = {
|
|
105
|
-
websocket: WebSocket,
|
|
106
|
-
name: 'agent-controller',
|
|
107
|
-
getToken,
|
|
108
|
-
minReconnectDelay: 1000,
|
|
109
|
-
maxReconnectDelay: 30000,
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const client = new Centrifuge(opts.url, centrifugeOpts);
|
|
113
|
-
|
|
114
|
-
const commandChannel = `agent:${opts.agentId}`;
|
|
115
|
-
const commandSub = client.newSubscription(commandChannel);
|
|
116
|
-
const handlers = buildHandlers(opts.version);
|
|
117
|
-
|
|
118
|
-
commandSub.on('publication', async (ctx) => {
|
|
119
|
-
console.log(`[WS] Received message on ${commandChannel}:`, JSON.stringify(ctx.data));
|
|
120
|
-
debugLog('connection', 'publication', commandChannel, JSON.stringify(ctx.data).slice(0, 500));
|
|
121
|
-
const command = ctx.data as AgentCommand;
|
|
122
|
-
if (!command || !command.type) {
|
|
123
|
-
console.warn('[WS] Ignoring message without type:', JSON.stringify(ctx.data));
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Chat commands — handle before generic dispatch
|
|
128
|
-
if (command.type === 'chat_list_sessions' || command.type === 'chat_history' || command.type === 'chat_send') {
|
|
129
|
-
const publishFn = async (data: unknown): Promise<void> => { await opts.api.publishResponse(opts.agentId, data); };
|
|
130
|
-
console.log(`[WS] Handling chat command: type=${command.type} id=${command.id}`);
|
|
131
|
-
switch (command.type) {
|
|
132
|
-
case 'chat_list_sessions':
|
|
133
|
-
handleChatListSessions(command, publishFn).catch(console.error);
|
|
134
|
-
break;
|
|
135
|
-
case 'chat_history':
|
|
136
|
-
handleChatHistory(command, publishFn).catch(console.error);
|
|
137
|
-
break;
|
|
138
|
-
case 'chat_send':
|
|
139
|
-
handleChatSend(command, publishFn, opts.agentId).catch(console.error);
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const handler = handlers[command.type];
|
|
146
|
-
if (!handler) {
|
|
147
|
-
console.error(`[WS] Unknown command type: ${command.type}`);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
console.log(`[WS] Handling command: type=${command.type} id=${command.id}`);
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
const response = await handler(command);
|
|
154
|
-
await opts.api.publishResponse(opts.agentId, response);
|
|
155
|
-
} catch (err) {
|
|
156
|
-
const errorResponse: AgentResponse = {
|
|
157
|
-
id: command.id,
|
|
158
|
-
type: command.type,
|
|
159
|
-
success: false,
|
|
160
|
-
error: err instanceof Error ? err.message : String(err),
|
|
161
|
-
};
|
|
162
|
-
await opts.api.publishResponse(opts.agentId, errorResponse);
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
client.on('connected', (ctx) => {
|
|
167
|
-
console.log(`Connected to Centrifugo via ${ctx.transport}`);
|
|
168
|
-
debugLog('connection', 'connected', ctx);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
client.on('disconnected', (ctx) => {
|
|
172
|
-
console.log(`Disconnected: ${ctx.reason} (code ${ctx.code})`);
|
|
173
|
-
debugLog('connection', 'disconnected', ctx);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
client.on('connecting', (ctx) => {
|
|
177
|
-
console.log(`Connecting: ${ctx.reason} (code ${ctx.code})`);
|
|
178
|
-
debugLog('connection', 'connecting', ctx);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
commandSub.on('subscribed', (ctx) => {
|
|
182
|
-
console.log(`[WS] Subscribed to ${commandChannel} (recovered=${ctx.wasRecovering})`);
|
|
183
|
-
debugLog('connection', 'subscribed', commandChannel, ctx);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
commandSub.on('subscribing', (ctx) => {
|
|
187
|
-
console.log(`[WS] Subscribing to ${commandChannel}: ${ctx.reason} (code ${ctx.code})`);
|
|
188
|
-
debugLog('connection', 'subscribing', commandChannel, ctx);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
commandSub.on('error', (ctx) => {
|
|
192
|
-
console.error(`[WS] Subscription error on ${commandChannel}:`, ctx.error);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
commandSub.on('unsubscribed', (ctx) => {
|
|
196
|
-
console.log(`[WS] Unsubscribed from ${commandChannel}: ${ctx.reason} (code ${ctx.code})`);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
commandSub.subscribe();
|
|
200
|
-
client.connect();
|
|
201
|
-
|
|
202
|
-
return { client, commandSub };
|
|
203
|
-
}
|
package/src/debug.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export const DEBUG = !!(process.env.AC_DEBUG || process.env.DEBUG);
|
|
2
|
-
|
|
3
|
-
export function debugLog(prefix: string, ...args: unknown[]): void {
|
|
4
|
-
if (!DEBUG) return;
|
|
5
|
-
console.log(`[debug][${prefix}]`, ...args);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function maskToken(token: string): string {
|
|
9
|
-
if (!token) return '';
|
|
10
|
-
return token.slice(0, 10) + '...';
|
|
11
|
-
}
|
package/src/handlers/backup.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { exec } from 'node:child_process';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import type { AgentCommand, AgentResponse } from '../types.js';
|
|
6
|
-
|
|
7
|
-
function getWorkspaceDir(): string {
|
|
8
|
-
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() ?? path.join(os.homedir(), '.openclaw');
|
|
9
|
-
return path.join(stateDir, 'workspace');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const BACKUP_DIR = '/tmp';
|
|
13
|
-
const TAR_TIMEOUT = 120_000;
|
|
14
|
-
const UPLOAD_TIMEOUT = 120_000;
|
|
15
|
-
|
|
16
|
-
export function handleBackup(command: AgentCommand): Promise<AgentResponse> {
|
|
17
|
-
const uploadUrl = command.payload.uploadUrl as string;
|
|
18
|
-
const token = command.payload.token as string;
|
|
19
|
-
if (!uploadUrl) {
|
|
20
|
-
return Promise.resolve({
|
|
21
|
-
id: command.id,
|
|
22
|
-
type: 'backup',
|
|
23
|
-
success: false,
|
|
24
|
-
error: 'Missing "uploadUrl" in payload',
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const timestamp = Date.now();
|
|
29
|
-
const filename = `backup-${timestamp}.tar.gz`;
|
|
30
|
-
const archivePath = path.join(BACKUP_DIR, filename);
|
|
31
|
-
|
|
32
|
-
return createArchive(archivePath)
|
|
33
|
-
.then(() => uploadArchive(archivePath, uploadUrl, token))
|
|
34
|
-
.then(async (size) => {
|
|
35
|
-
await cleanup(archivePath);
|
|
36
|
-
return {
|
|
37
|
-
id: command.id,
|
|
38
|
-
type: 'backup' as const,
|
|
39
|
-
success: true,
|
|
40
|
-
data: { size, filename },
|
|
41
|
-
};
|
|
42
|
-
})
|
|
43
|
-
.catch(async (err) => {
|
|
44
|
-
await cleanup(archivePath);
|
|
45
|
-
return {
|
|
46
|
-
id: command.id,
|
|
47
|
-
type: 'backup' as const,
|
|
48
|
-
success: false,
|
|
49
|
-
error: err instanceof Error ? err.message : String(err),
|
|
50
|
-
};
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function createArchive(archivePath: string): Promise<void> {
|
|
55
|
-
const workspaceDir = getWorkspaceDir();
|
|
56
|
-
return new Promise((resolve, reject) => {
|
|
57
|
-
exec(
|
|
58
|
-
`tar -czf ${archivePath} -C ${path.dirname(workspaceDir)} ${path.basename(workspaceDir)}`,
|
|
59
|
-
{ timeout: TAR_TIMEOUT },
|
|
60
|
-
(error, _stdout, stderr) => {
|
|
61
|
-
if (error) {
|
|
62
|
-
reject(new Error(`tar failed: ${stderr.toString() || error.message}`));
|
|
63
|
-
} else {
|
|
64
|
-
resolve();
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
);
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function uploadArchive(archivePath: string, uploadUrl: string, token?: string): Promise<number> {
|
|
72
|
-
const stat = await fs.stat(archivePath);
|
|
73
|
-
const fileBuffer = await fs.readFile(archivePath);
|
|
74
|
-
|
|
75
|
-
const headers: Record<string, string> = { 'Content-Type': 'application/gzip' };
|
|
76
|
-
if (token) {
|
|
77
|
-
headers['Authorization'] = `Bearer ${token}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const response = await fetch(uploadUrl, {
|
|
81
|
-
method: 'POST',
|
|
82
|
-
headers,
|
|
83
|
-
body: fileBuffer,
|
|
84
|
-
signal: AbortSignal.timeout(UPLOAD_TIMEOUT),
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
if (!response.ok) {
|
|
88
|
-
const text = await response.text().catch(() => '');
|
|
89
|
-
throw new Error(`Upload failed: ${response.status} ${response.statusText} ${text}`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return stat.size;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function cleanup(archivePath: string): Promise<void> {
|
|
96
|
-
try {
|
|
97
|
-
await fs.unlink(archivePath);
|
|
98
|
-
} catch {
|
|
99
|
-
// ignore cleanup errors
|
|
100
|
-
}
|
|
101
|
-
}
|