@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.
Files changed (65) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/BIZPLAN.md +530 -0
  3. package/CLAUDE.md +64 -0
  4. package/__tests__/api.test.ts +123 -0
  5. package/__tests__/chat.test.ts +191 -0
  6. package/__tests__/config.test.ts +100 -0
  7. package/__tests__/connection.test.ts +171 -14
  8. package/__tests__/file-delete.test.ts +90 -0
  9. package/__tests__/file-write.test.ts +119 -0
  10. package/__tests__/gateway-adapter.test.ts +366 -0
  11. package/__tests__/gateway-client.test.ts +272 -0
  12. package/__tests__/onboarding.test.ts +55 -0
  13. package/__tests__/package-install.test.ts +109 -0
  14. package/__tests__/pair.test.ts +60 -0
  15. package/__tests__/self-update.test.ts +123 -0
  16. package/__tests__/stop.test.ts +38 -0
  17. package/bin/agent-controller.js +15 -2
  18. package/dist/commands/self-update.d.ts +1 -0
  19. package/dist/commands/self-update.js +41 -0
  20. package/dist/commands/self-update.js.map +1 -0
  21. package/dist/connection.d.ts +1 -1
  22. package/dist/connection.js +49 -29
  23. package/dist/connection.js.map +1 -1
  24. package/dist/handlers/board-handler.d.ts +1 -0
  25. package/dist/handlers/board-handler.js +5 -2
  26. package/dist/handlers/board-handler.js.map +1 -1
  27. package/dist/handlers/chat.d.ts +4 -0
  28. package/dist/handlers/chat.js +62 -0
  29. package/dist/handlers/chat.js.map +1 -0
  30. package/dist/handlers/file-delete.d.ts +2 -0
  31. package/dist/handlers/file-delete.js +41 -0
  32. package/dist/handlers/file-delete.js.map +1 -0
  33. package/dist/handlers/file-write.d.ts +2 -0
  34. package/dist/handlers/file-write.js +59 -0
  35. package/dist/handlers/file-write.js.map +1 -0
  36. package/dist/handlers/onboarding.d.ts +2 -0
  37. package/dist/handlers/onboarding.js +18 -0
  38. package/dist/handlers/onboarding.js.map +1 -0
  39. package/dist/handlers/package-install.d.ts +2 -0
  40. package/dist/handlers/package-install.js +59 -0
  41. package/dist/handlers/package-install.js.map +1 -0
  42. package/dist/index.js +20 -7
  43. package/dist/index.js.map +1 -1
  44. package/dist/openclaw/gateway-adapter.d.ts +22 -0
  45. package/dist/openclaw/gateway-adapter.js +108 -0
  46. package/dist/openclaw/gateway-adapter.js.map +1 -0
  47. package/dist/openclaw/gateway-client.d.ts +21 -0
  48. package/dist/openclaw/gateway-client.js +114 -0
  49. package/dist/openclaw/gateway-client.js.map +1 -0
  50. package/dist/openclaw/index.d.ts +8 -0
  51. package/dist/openclaw/index.js +12 -0
  52. package/dist/openclaw/index.js.map +1 -0
  53. package/dist/openclaw/types.d.ts +36 -0
  54. package/dist/openclaw/types.js +2 -0
  55. package/dist/openclaw/types.js.map +1 -0
  56. package/dist/types.d.ts +2 -1
  57. package/package.json +4 -2
  58. package/src/commands/self-update.ts +43 -0
  59. package/src/connection.ts +31 -28
  60. package/src/handlers/file-delete.ts +46 -0
  61. package/src/handlers/file-write.ts +65 -0
  62. package/src/handlers/onboarding.ts +19 -0
  63. package/src/handlers/package-install.ts +69 -0
  64. package/src/index.ts +3 -5
  65. 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":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGpD,MAAM,OAAO,sBAAsB;IACzB,MAAM,CAAgB;IACtB,eAAe,GAAG,IAAI,GAAG,EAK7B,CAAC;IAEL,YAAY,GAAW,EAAE,KAAa;QACpC,IAAI,CAAC,MAAM,GAAG,IAAI,aAAa,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC5C,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3D,CAAC;IAED,KAAK,CAAC,OAAO;QACX,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;IAC9B,CAAC;IAED,UAAU;QACR,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;IAC3B,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,YAAY;QAChB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;QAC9D,OAAO,CAAC,MAAM,EAAE,QAAQ,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC;YAC/C,GAAG,EAAE,CAAC,CAAC,GAAG;YACV,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,EAAE;YAC5B,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,QAAQ;YACxB,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,CAAC;YAC3B,KAAK,EAAE,CAAC,CAAC,KAAK;SACf,CAAC,CAAC,CAAC;IACN,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,UAAkB,EAAE,KAAK,GAAG,GAAG;QAC9C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;QAChF,OAAO,CAAC,MAAM,EAAE,QAAQ,IAAI,EAAE,CAAC;aAC5B,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC;aAC/D,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE;YACd,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;gBACtC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;gBAClF,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;YAC5B,OAAO;gBACL,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,OAAO;gBACP,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ;oBACxC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE;oBACrC,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aAC5C,CAAC;QACJ,CAAC,CAAC,CAAC;IACP,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,MAQjB;QACC,2CAA2C;QAC3C,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,cAAc,EAAE;YAC9C,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,QAAQ,EAAE,EAAE;SACb,CAAC,CAAC;QAEH,MAAM,OAAO,GAAQ;YACnB,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,OAAO,EAAE,MAAM,CAAC,IAAI;YACpB,OAAO,EAAE,KAAK;YACd,cAAc,EAAE,MAAM,CAAC,cAAc;SACtC,CAAC;QACF,IAAI,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC;YAC/B,OAAO,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;QAC3C,CAAC;QAED,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;YAChD,+CAA+C;QACjD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;YACnD,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACrE,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,UAAkB,EAAE,KAAc;QAC5C,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACjF,CAAC;IAEO,WAAW,CAAC,KAAsC;QACxD,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC;QAC3B,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU;YAAE,OAAO;QAEtC,mDAAmD;QACnD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACzB,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC1D,IAAI,CAAC,EAAE;YAAE,OAAO;QAEhB,IAAI,IAAI,CAAC,KAAK,KAAK,OAAO,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC;YACnD,8BAA8B;YAC9B,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ;gBAC3C,CAAC,CAAC,IAAI,CAAC,OAAO;gBACd,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;oBAC3B,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;oBACrF,CAAC,CAAC,EAAE,CAAC;YACT,EAAE,CAAC,QAAQ,GAAG,IAAI,CAAC;YACnB,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;aAAM,IAAI,IAAI,CAAC,KAAK,KAAK,OAAO,EAAE,CAAC;YAClC,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,KAAM,CAAC,CAAC;YACpC,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;QAC3B,CAAC;aAAM,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YACpC,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,KAAM,CAAC,CAAC;YACpC,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,8BAA8B;QAC1D,CAAC;aAAM,IAAI,IAAI,CAAC,KAAK,KAAK,OAAO,EAAE,CAAC;YAClC,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,KAAM,CAAC,CAAC;YACpC,MAAM,MAAM,GAAG,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,IAAI,eAAe,CAAC;YACpG,EAAE,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;CACF"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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.4",
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?: string;
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
+ }