@openclaw-cloud/agent-controller 0.1.4 → 0.1.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/.husky/pre-commit +1 -0
- package/BIZPLAN.md +530 -0
- package/CLAUDE.md +64 -0
- package/__tests__/api.test.ts +123 -0
- package/__tests__/chat.test.ts +191 -0
- package/__tests__/config.test.ts +100 -0
- package/__tests__/connection.test.ts +171 -14
- package/__tests__/file-delete.test.ts +90 -0
- package/__tests__/file-write.test.ts +119 -0
- package/__tests__/gateway-adapter.test.ts +366 -0
- package/__tests__/gateway-client.test.ts +272 -0
- package/__tests__/onboarding.test.ts +55 -0
- package/__tests__/package-install.test.ts +109 -0
- package/__tests__/pair.test.ts +60 -0
- package/__tests__/self-update.test.ts +123 -0
- package/__tests__/stop.test.ts +38 -0
- package/bin/agent-controller.js +15 -2
- package/dist/commands/self-update.d.ts +1 -0
- package/dist/commands/self-update.js +41 -0
- package/dist/commands/self-update.js.map +1 -0
- package/dist/connection.d.ts +1 -1
- package/dist/connection.js +49 -29
- package/dist/connection.js.map +1 -1
- package/dist/handlers/board-handler.d.ts +1 -0
- package/dist/handlers/board-handler.js +5 -2
- package/dist/handlers/board-handler.js.map +1 -1
- package/dist/handlers/chat.d.ts +4 -0
- package/dist/handlers/chat.js +62 -0
- package/dist/handlers/chat.js.map +1 -0
- package/dist/handlers/file-delete.d.ts +2 -0
- package/dist/handlers/file-delete.js +41 -0
- package/dist/handlers/file-delete.js.map +1 -0
- package/dist/handlers/file-write.d.ts +2 -0
- package/dist/handlers/file-write.js +59 -0
- package/dist/handlers/file-write.js.map +1 -0
- package/dist/handlers/onboarding.d.ts +2 -0
- package/dist/handlers/onboarding.js +18 -0
- package/dist/handlers/onboarding.js.map +1 -0
- package/dist/handlers/package-install.d.ts +2 -0
- package/dist/handlers/package-install.js +59 -0
- package/dist/handlers/package-install.js.map +1 -0
- package/dist/index.js +20 -7
- package/dist/index.js.map +1 -1
- package/dist/openclaw/gateway-adapter.d.ts +22 -0
- package/dist/openclaw/gateway-adapter.js +108 -0
- package/dist/openclaw/gateway-adapter.js.map +1 -0
- package/dist/openclaw/gateway-client.d.ts +21 -0
- package/dist/openclaw/gateway-client.js +114 -0
- package/dist/openclaw/gateway-client.js.map +1 -0
- package/dist/openclaw/index.d.ts +8 -0
- package/dist/openclaw/index.js +12 -0
- package/dist/openclaw/index.js.map +1 -0
- package/dist/openclaw/types.d.ts +36 -0
- package/dist/openclaw/types.js +2 -0
- package/dist/openclaw/types.js.map +1 -0
- package/dist/types.d.ts +2 -1
- package/package.json +4 -2
- package/src/commands/self-update.ts +43 -0
- package/src/connection.ts +31 -28
- package/src/handlers/file-delete.ts +46 -0
- package/src/handlers/file-write.ts +65 -0
- package/src/handlers/onboarding.ts +19 -0
- package/src/handlers/package-install.ts +69 -0
- package/src/index.ts +3 -5
- package/src/types.ts +2 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IChatProvider, ChatSession, ChatMessage, ChatAttachment } from './types.js';
|
|
2
|
+
export declare class OpenclawGatewayAdapter implements IChatProvider {
|
|
3
|
+
private client;
|
|
4
|
+
private streamCallbacks;
|
|
5
|
+
constructor(url: string, token: string);
|
|
6
|
+
connect(): Promise<void>;
|
|
7
|
+
disconnect(): void;
|
|
8
|
+
isConnected(): boolean;
|
|
9
|
+
listSessions(): Promise<ChatSession[]>;
|
|
10
|
+
getHistory(sessionKey: string, limit?: number): Promise<ChatMessage[]>;
|
|
11
|
+
sendMessage(params: {
|
|
12
|
+
sessionKey: string;
|
|
13
|
+
text: string;
|
|
14
|
+
idempotencyKey: string;
|
|
15
|
+
attachments?: ChatAttachment[];
|
|
16
|
+
onDelta?: (text: string) => void;
|
|
17
|
+
onDone?: (text: string) => void;
|
|
18
|
+
onError?: (error: string) => void;
|
|
19
|
+
}): Promise<void>;
|
|
20
|
+
abort(sessionKey: string, runId?: string): Promise<void>;
|
|
21
|
+
private handleEvent;
|
|
22
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { GatewayClient } from './gateway-client.js';
|
|
2
|
+
export class OpenclawGatewayAdapter {
|
|
3
|
+
client;
|
|
4
|
+
streamCallbacks = new Map();
|
|
5
|
+
constructor(url, token) {
|
|
6
|
+
this.client = new GatewayClient(url, token);
|
|
7
|
+
this.client.setEventHandler(this.handleEvent.bind(this));
|
|
8
|
+
}
|
|
9
|
+
async connect() {
|
|
10
|
+
await this.client.connect();
|
|
11
|
+
}
|
|
12
|
+
disconnect() {
|
|
13
|
+
this.client.disconnect();
|
|
14
|
+
}
|
|
15
|
+
isConnected() {
|
|
16
|
+
return this.client.isConnected();
|
|
17
|
+
}
|
|
18
|
+
async listSessions() {
|
|
19
|
+
const result = await this.client.request('sessions.list', {});
|
|
20
|
+
return (result?.sessions ?? []).map((s) => ({
|
|
21
|
+
key: s.key,
|
|
22
|
+
sessionId: s.sessionId ?? '',
|
|
23
|
+
kind: s.kind ?? 'direct',
|
|
24
|
+
updatedAt: s.updatedAt ?? 0,
|
|
25
|
+
model: s.model,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
async getHistory(sessionKey, limit = 200) {
|
|
29
|
+
const result = await this.client.request('chat.history', { sessionKey, limit });
|
|
30
|
+
return (result?.messages ?? [])
|
|
31
|
+
.filter((m) => m.role === 'user' || m.role === 'assistant')
|
|
32
|
+
.map((m) => {
|
|
33
|
+
const content = Array.isArray(m.content)
|
|
34
|
+
? m.content.filter((c) => c.type === 'text').map((c) => c.text).join('')
|
|
35
|
+
: String(m.content ?? '');
|
|
36
|
+
return {
|
|
37
|
+
role: m.role,
|
|
38
|
+
content,
|
|
39
|
+
timestamp: typeof m.timestamp === 'number'
|
|
40
|
+
? new Date(m.timestamp).toISOString()
|
|
41
|
+
: m.timestamp ?? new Date().toISOString(),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async sendMessage(params) {
|
|
46
|
+
// Register stream callbacks BEFORE sending
|
|
47
|
+
this.streamCallbacks.set(params.idempotencyKey, {
|
|
48
|
+
onDelta: params.onDelta,
|
|
49
|
+
onDone: params.onDone,
|
|
50
|
+
onError: params.onError,
|
|
51
|
+
lastText: '',
|
|
52
|
+
});
|
|
53
|
+
const payload = {
|
|
54
|
+
sessionKey: params.sessionKey,
|
|
55
|
+
message: params.text,
|
|
56
|
+
deliver: false,
|
|
57
|
+
idempotencyKey: params.idempotencyKey,
|
|
58
|
+
};
|
|
59
|
+
if (params.attachments?.length) {
|
|
60
|
+
payload.attachments = params.attachments;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
await this.client.request('chat.send', payload);
|
|
64
|
+
// Request accepted — response comes via events
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
this.streamCallbacks.delete(params.idempotencyKey);
|
|
68
|
+
params.onError?.(err instanceof Error ? err.message : String(err));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async abort(sessionKey, runId) {
|
|
72
|
+
await this.client.request('chat.abort', { sessionKey, runId }).catch(() => { });
|
|
73
|
+
}
|
|
74
|
+
handleEvent(event) {
|
|
75
|
+
const data = event.payload;
|
|
76
|
+
if (!data || !data.sessionKey)
|
|
77
|
+
return;
|
|
78
|
+
// Stream events have runId matching idempotencyKey
|
|
79
|
+
const runId = data.runId;
|
|
80
|
+
const cb = runId ? this.streamCallbacks.get(runId) : null;
|
|
81
|
+
if (!cb)
|
|
82
|
+
return;
|
|
83
|
+
if (data.state === 'delta' && data.message != null) {
|
|
84
|
+
// message is accumulated text
|
|
85
|
+
const text = typeof data.message === 'string'
|
|
86
|
+
? data.message
|
|
87
|
+
: Array.isArray(data.message)
|
|
88
|
+
? data.message.filter((c) => c.type === 'text').map((c) => c.text).join('')
|
|
89
|
+
: '';
|
|
90
|
+
cb.lastText = text;
|
|
91
|
+
cb.onDelta?.(text);
|
|
92
|
+
}
|
|
93
|
+
else if (data.state === 'final') {
|
|
94
|
+
this.streamCallbacks.delete(runId);
|
|
95
|
+
cb.onDone?.(cb.lastText);
|
|
96
|
+
}
|
|
97
|
+
else if (data.state === 'aborted') {
|
|
98
|
+
this.streamCallbacks.delete(runId);
|
|
99
|
+
cb.onDone?.(cb.lastText); // still send whatever we have
|
|
100
|
+
}
|
|
101
|
+
else if (data.state === 'error') {
|
|
102
|
+
this.streamCallbacks.delete(runId);
|
|
103
|
+
const errMsg = typeof data.error === 'string' ? data.error : data.error?.message ?? 'Unknown error';
|
|
104
|
+
cb.onError?.(errMsg);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=gateway-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway-adapter.js","sourceRoot":"","sources":["../../src/openclaw/gateway-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type EventHandler = (event: {
|
|
2
|
+
event: string;
|
|
3
|
+
payload: any;
|
|
4
|
+
seq?: number;
|
|
5
|
+
}) => void;
|
|
6
|
+
export declare class GatewayClient {
|
|
7
|
+
private ws;
|
|
8
|
+
private pending;
|
|
9
|
+
private onEvent;
|
|
10
|
+
private connected;
|
|
11
|
+
private token;
|
|
12
|
+
private url;
|
|
13
|
+
constructor(url: string, token: string);
|
|
14
|
+
setEventHandler(handler: EventHandler): void;
|
|
15
|
+
connect(): Promise<void>;
|
|
16
|
+
disconnect(): void;
|
|
17
|
+
isConnected(): boolean;
|
|
18
|
+
request(method: string, params: any): Promise<any>;
|
|
19
|
+
private handleMessage;
|
|
20
|
+
private flushPending;
|
|
21
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
export class GatewayClient {
|
|
4
|
+
ws = null;
|
|
5
|
+
pending = new Map();
|
|
6
|
+
onEvent = null;
|
|
7
|
+
connected = false;
|
|
8
|
+
token;
|
|
9
|
+
url;
|
|
10
|
+
constructor(url, token) {
|
|
11
|
+
this.url = url;
|
|
12
|
+
this.token = token;
|
|
13
|
+
}
|
|
14
|
+
setEventHandler(handler) {
|
|
15
|
+
this.onEvent = handler;
|
|
16
|
+
}
|
|
17
|
+
connect() {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
this.ws = new WebSocket(this.url);
|
|
20
|
+
this.ws.on('open', () => {
|
|
21
|
+
// Wait for connect.challenge event, then authenticate
|
|
22
|
+
});
|
|
23
|
+
this.ws.on('message', (raw) => {
|
|
24
|
+
try {
|
|
25
|
+
const msg = JSON.parse(raw.toString());
|
|
26
|
+
this.handleMessage(msg);
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
});
|
|
30
|
+
this.ws.on('error', (e) => {
|
|
31
|
+
if (!this.connected)
|
|
32
|
+
reject(e);
|
|
33
|
+
});
|
|
34
|
+
this.ws.on('close', () => {
|
|
35
|
+
this.connected = false;
|
|
36
|
+
this.flushPending(new Error('Connection closed'));
|
|
37
|
+
});
|
|
38
|
+
// Wait for connect.challenge → authenticate → resolve
|
|
39
|
+
const origHandler = this.onEvent;
|
|
40
|
+
this.onEvent = async (ev) => {
|
|
41
|
+
if (ev.event === 'connect.challenge') {
|
|
42
|
+
try {
|
|
43
|
+
await this.request('connect', {
|
|
44
|
+
auth: { token: this.token },
|
|
45
|
+
scopes: ['operator.admin'],
|
|
46
|
+
caps: [],
|
|
47
|
+
});
|
|
48
|
+
this.connected = true;
|
|
49
|
+
this.onEvent = origHandler;
|
|
50
|
+
resolve();
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
reject(e);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
if (!this.connected)
|
|
59
|
+
reject(new Error('Connect timeout'));
|
|
60
|
+
}, 10_000);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
disconnect() {
|
|
64
|
+
this.ws?.close();
|
|
65
|
+
this.ws = null;
|
|
66
|
+
this.connected = false;
|
|
67
|
+
this.flushPending(new Error('Disconnected'));
|
|
68
|
+
}
|
|
69
|
+
isConnected() {
|
|
70
|
+
return this.connected && this.ws?.readyState === WebSocket.OPEN;
|
|
71
|
+
}
|
|
72
|
+
request(method, params) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
75
|
+
return reject(new Error('Not connected'));
|
|
76
|
+
}
|
|
77
|
+
const id = randomUUID();
|
|
78
|
+
this.pending.set(id, { resolve, reject });
|
|
79
|
+
this.ws.send(JSON.stringify({ type: 'req', id, method, params }));
|
|
80
|
+
// Timeout per request
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
if (this.pending.has(id)) {
|
|
83
|
+
this.pending.delete(id);
|
|
84
|
+
reject(new Error(`Request timeout: ${method}`));
|
|
85
|
+
}
|
|
86
|
+
}, 120_000);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
handleMessage(msg) {
|
|
90
|
+
if (msg.type === 'event') {
|
|
91
|
+
this.onEvent?.(msg);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (msg.type === 'res') {
|
|
95
|
+
const req = this.pending.get(msg.id);
|
|
96
|
+
if (!req)
|
|
97
|
+
return;
|
|
98
|
+
this.pending.delete(msg.id);
|
|
99
|
+
if (msg.ok) {
|
|
100
|
+
req.resolve(msg.payload);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
req.reject(new Error(msg.error?.message ?? 'Request failed'));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
flushPending(err) {
|
|
108
|
+
for (const [, req] of this.pending) {
|
|
109
|
+
req.reject(err);
|
|
110
|
+
}
|
|
111
|
+
this.pending.clear();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=gateway-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway-client.js","sourceRoot":"","sources":["../../src/openclaw/gateway-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { OpenclawGatewayAdapter } from './gateway-adapter.js';
|
|
2
|
+
export type { IChatProvider, ChatSession, ChatMessage, ChatAttachment } from './types.js';
|
|
3
|
+
import type { IChatProvider } from './types.js';
|
|
4
|
+
export declare function createChatProvider(url: string, token: string): IChatProvider & {
|
|
5
|
+
connect(): Promise<void>;
|
|
6
|
+
disconnect(): void;
|
|
7
|
+
};
|
|
8
|
+
export declare function getChatProvider(): IChatProvider | null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { OpenclawGatewayAdapter } from './gateway-adapter.js';
|
|
2
|
+
import { OpenclawGatewayAdapter } from './gateway-adapter.js';
|
|
3
|
+
let _provider = null;
|
|
4
|
+
export function createChatProvider(url, token) {
|
|
5
|
+
const adapter = new OpenclawGatewayAdapter(url, token);
|
|
6
|
+
_provider = adapter;
|
|
7
|
+
return adapter;
|
|
8
|
+
}
|
|
9
|
+
export function getChatProvider() {
|
|
10
|
+
return _provider;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/openclaw/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAG9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAG9D,IAAI,SAAS,GAAyB,IAAI,CAAC;AAE3C,MAAM,UAAU,kBAAkB,CAAC,GAAW,EAAE,KAAa;IAC3D,MAAM,OAAO,GAAG,IAAI,sBAAsB,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACvD,SAAS,GAAG,OAAO,CAAC;IACpB,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface ChatSession {
|
|
2
|
+
key: string;
|
|
3
|
+
sessionId: string;
|
|
4
|
+
kind: string;
|
|
5
|
+
updatedAt: number;
|
|
6
|
+
model?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ChatMessage {
|
|
9
|
+
role: 'user' | 'assistant';
|
|
10
|
+
content: string;
|
|
11
|
+
timestamp: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ChatAttachment {
|
|
14
|
+
type: 'image';
|
|
15
|
+
mimeType: string;
|
|
16
|
+
content: string;
|
|
17
|
+
}
|
|
18
|
+
export interface IChatProvider {
|
|
19
|
+
listSessions(): Promise<ChatSession[]>;
|
|
20
|
+
getHistory(sessionKey: string, limit?: number): Promise<ChatMessage[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Send message. Returns after the agent finishes (full response).
|
|
23
|
+
* Calls onDelta with accumulated text during streaming.
|
|
24
|
+
* Calls onDone when agent response is complete.
|
|
25
|
+
*/
|
|
26
|
+
sendMessage(params: {
|
|
27
|
+
sessionKey: string;
|
|
28
|
+
text: string;
|
|
29
|
+
idempotencyKey: string;
|
|
30
|
+
attachments?: ChatAttachment[];
|
|
31
|
+
onDelta?: (accumulatedText: string) => void;
|
|
32
|
+
onDone?: (finalText: string) => void;
|
|
33
|
+
onError?: (error: string) => void;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
abort(sessionKey: string, runId?: string): Promise<void>;
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/openclaw/types.ts"],"names":[],"mappings":""}
|
package/dist/types.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* agent:<agentId> — commands from backend, responses from agent
|
|
4
4
|
* heartbeat:<agentId> — heartbeat publications from agent
|
|
5
5
|
*/
|
|
6
|
-
export type CommandType = 'exec' | 'restart' | 'deploy' | 'config' | 'pair' | 'stop' | 'backup';
|
|
6
|
+
export type CommandType = 'exec' | 'restart' | 'deploy' | 'config' | 'pair' | 'stop' | 'backup' | 'chat_list_sessions' | 'chat_history' | 'chat_send' | 'package_install' | 'file_write' | 'file_delete' | 'onboarding_complete';
|
|
7
7
|
export interface AgentCommand {
|
|
8
8
|
id: string;
|
|
9
9
|
type: CommandType;
|
|
@@ -49,6 +49,7 @@ export interface BoardEvent {
|
|
|
49
49
|
export interface BoardInfo {
|
|
50
50
|
board: {
|
|
51
51
|
id: string;
|
|
52
|
+
workspaceId: string;
|
|
52
53
|
columns: Array<{
|
|
53
54
|
id: string;
|
|
54
55
|
name: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw-cloud/agent-controller",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
|
12
|
-
"start": "node bin/agent-controller.js"
|
|
12
|
+
"start": "node bin/agent-controller.js",
|
|
13
|
+
"prepare": "[ -d .git ] && husky || true"
|
|
13
14
|
},
|
|
14
15
|
"dependencies": {
|
|
15
16
|
"centrifuge": "^5.5.3",
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
"@types/jest": "^30.0.0",
|
|
20
21
|
"@types/node": "^25.2.3",
|
|
21
22
|
"@types/ws": "^8.18.1",
|
|
23
|
+
"husky": "^9.1.7",
|
|
22
24
|
"jest": "^30.2.0",
|
|
23
25
|
"ts-jest": "^29.4.6",
|
|
24
26
|
"typescript": "^5.9.3"
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
}
|
package/src/connection.ts
CHANGED
|
@@ -9,12 +9,16 @@ import { handlePair } from './handlers/pair.js';
|
|
|
9
9
|
import { handleStop } from './handlers/stop.js';
|
|
10
10
|
import { handleBackup } from './handlers/backup.js';
|
|
11
11
|
import { handleChatListSessions, handleChatHistory, handleChatSend } from './handlers/chat.js';
|
|
12
|
+
import { handlePackageInstall } from './handlers/package-install.js';
|
|
13
|
+
import { handleFileWrite } from './handlers/file-write.js';
|
|
14
|
+
import { handleFileDelete } from './handlers/file-delete.js';
|
|
15
|
+
import { handleOnboardingComplete } from './handlers/onboarding.js';
|
|
12
16
|
|
|
13
17
|
export interface ConnectionOptions {
|
|
14
18
|
url: string;
|
|
15
19
|
token: string;
|
|
16
20
|
agentId: string;
|
|
17
|
-
backendUrl
|
|
21
|
+
backendUrl: string;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
const handlers: Record<string, (cmd: AgentCommand) => Promise<AgentResponse>> = {
|
|
@@ -25,6 +29,10 @@ const handlers: Record<string, (cmd: AgentCommand) => Promise<AgentResponse>> =
|
|
|
25
29
|
pair: handlePair,
|
|
26
30
|
stop: handleStop,
|
|
27
31
|
backup: handleBackup,
|
|
32
|
+
package_install: handlePackageInstall,
|
|
33
|
+
file_write: handleFileWrite,
|
|
34
|
+
file_delete: handleFileDelete,
|
|
35
|
+
onboarding_complete: handleOnboardingComplete,
|
|
28
36
|
};
|
|
29
37
|
|
|
30
38
|
async function fetchCentrifugoToken(backendUrl: string, agentToken: string, agentId: string): Promise<string> {
|
|
@@ -54,40 +62,35 @@ export function createConnection(opts: ConnectionOptions): {
|
|
|
54
62
|
client: Centrifuge;
|
|
55
63
|
commandSub: Subscription;
|
|
56
64
|
} {
|
|
65
|
+
// Dynamic JWT: fetch fresh token on every connect/reconnect
|
|
66
|
+
const getToken = async (): Promise<string> => {
|
|
67
|
+
console.log('Fetching Centrifugo JWT from backend...');
|
|
68
|
+
try {
|
|
69
|
+
const token = await fetchCentrifugoToken(opts.backendUrl, opts.token, opts.agentId);
|
|
70
|
+
console.log('Got Centrifugo JWT');
|
|
71
|
+
return token;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error('Failed to fetch Centrifugo JWT:', err instanceof Error ? err.message : err);
|
|
74
|
+
// Retry once before giving up — centrifuge will handle reconnect backoff
|
|
75
|
+
try {
|
|
76
|
+
const token = await fetchCentrifugoToken(opts.backendUrl, opts.token, opts.agentId);
|
|
77
|
+
console.log('Got Centrifugo JWT (retry)');
|
|
78
|
+
return token;
|
|
79
|
+
} catch (retryErr) {
|
|
80
|
+
console.error('JWT fetch retry failed:', retryErr instanceof Error ? retryErr.message : retryErr);
|
|
81
|
+
throw retryErr;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
57
86
|
const centrifugeOpts: Record<string, unknown> = {
|
|
58
87
|
websocket: WebSocket,
|
|
59
88
|
name: 'agent-controller',
|
|
89
|
+
getToken,
|
|
60
90
|
minReconnectDelay: 1000,
|
|
61
91
|
maxReconnectDelay: 30000,
|
|
62
92
|
};
|
|
63
93
|
|
|
64
|
-
if (opts.backendUrl) {
|
|
65
|
-
// Dynamic JWT: fetch fresh token on every connect/reconnect
|
|
66
|
-
centrifugeOpts.getToken = async () => {
|
|
67
|
-
console.log('Fetching Centrifugo JWT from backend...');
|
|
68
|
-
try {
|
|
69
|
-
const token = await fetchCentrifugoToken(opts.backendUrl!, opts.token, opts.agentId);
|
|
70
|
-
console.log('Got Centrifugo JWT');
|
|
71
|
-
return token;
|
|
72
|
-
} catch (err) {
|
|
73
|
-
console.error('Failed to fetch Centrifugo JWT:', err instanceof Error ? err.message : err);
|
|
74
|
-
// Retry once
|
|
75
|
-
try {
|
|
76
|
-
const token = await fetchCentrifugoToken(opts.backendUrl!, opts.token, opts.agentId);
|
|
77
|
-
console.log('Got Centrifugo JWT (retry)');
|
|
78
|
-
return token;
|
|
79
|
-
} catch (retryErr) {
|
|
80
|
-
console.error('JWT fetch retry failed:', retryErr instanceof Error ? retryErr.message : retryErr);
|
|
81
|
-
throw retryErr;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
} else {
|
|
86
|
-
// Fallback: use raw token (backward compat, won't work with Centrifugo JWT validation)
|
|
87
|
-
console.warn('BACKEND_INTERNAL_URL not set, using AGENT_TOKEN directly (may not work)');
|
|
88
|
-
centrifugeOpts.token = opts.token;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
94
|
const client = new Centrifuge(opts.url, centrifugeOpts);
|
|
92
95
|
|
|
93
96
|
const commandChannel = `agent:${opts.agentId}`;
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
}
|