@openclaw-cloud/agent-controller 0.1.2 → 0.1.4

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/CLAUDE.md CHANGED
@@ -1,5 +1,64 @@
1
1
  # CLAUDE.md — Agent Controller Context
2
2
 
3
+ Review this plan thoroughly before making any code changes. For every issue or recommendation, explain the concrete tradeoffs, give me an opinionated recommendation, and ask for my input before assuming a direction.
4
+
5
+ My engineering preferences (use these to guide your recommendations):
6
+
7
+ • DRY is important—flag repetition aggressively.
8
+ • Well-tested code is non-negotiable; I'd rather have too many tests than too few.
9
+ • I want code that's "engineered enough" — not under-engineered (fragile, hacky) and not over-engineered (premature abstraction, unnecessary complexity).
10
+ • I err on the side of handling more edge cases, not fewer; thoughtfulness > speed.
11
+ • Bias toward explicit over clever.
12
+
13
+ 1. Architecture review
14
+ Evaluate:
15
+
16
+ • Overall system design and component boundaries.
17
+ • Dependency graph and coupling concerns.
18
+ • Data flow patterns and potential bottlenecks.
19
+ • Scaling characteristics and single points of failure.
20
+ • Security architecture (auth, data access, API boundaries).
21
+ 2. Code quality review
22
+ Evaluate:
23
+
24
+ • Code organization and module structure.
25
+ • DRY violations—be aggressive here.
26
+ • Error handling patterns and missing edge cases (call these out explicitly).
27
+ • Technical debt hotspots.
28
+ • Areas that are over-engineered or under-engineered relative to my preferences.
29
+ 3. Test review
30
+ Evaluate:
31
+
32
+ • Test coverage gaps (unit, integration, e2e).
33
+ • Test quality and assertion strength.
34
+ • Missing edge case coverage—be thorough.
35
+ • Untested failure modes and error paths.
36
+ 4. Performance review
37
+ Evaluate:
38
+
39
+ • N+1 queries and database access patterns.
40
+ • Memory-usage concerns.
41
+ • Caching opportunities.
42
+ • Slow or high-complexity code paths.
43
+ For each issue you find
44
+ For every specific issue (bug, smell, design concern, or risk):
45
+
46
+ • Describe the problem concretely, with file and line references.
47
+ • Present 2–3 options, including "do nothing" where that's reasonable.
48
+ • For each option, specify: implementation effort, risk, impact on other code, and maintenance burden.
49
+ • Give me your recommended option and why, mapped to my preferences above.
50
+ • Then explicitly ask whether I agree or want to choose a different direction before proceeding.
51
+ Workflow and interaction
52
+
53
+ • Do not assume my priorities on timeline or scale.
54
+ • After each section, pause and ask for my feedback before moving on.
55
+ BEFORE YOU START:
56
+ Ask if I want one of two options:
57
+ 1/ BIG CHANGE: Work through this interactively, one section at a time (Architecture → Code Quality → Tests → Performance) with at most 4 top issues in each section.
58
+ 2/ SMALL CHANGE: Work through interactively ONE question per review section
59
+
60
+ FOR EACH STAGE OF REVIEW: output the explanation and pros and cons of each stage's questions AND your opinionated recommendation and why, and then use AskUserQuestion. Also NUMBER issues and then give LETTERS for options and when using AskUserQuestion make sure each option clearly labels the issue NUMBER and option LETTER so the user doesn't get confused. Make the recommended option always the 1st option.
61
+
3
62
  ## Quick Reference
4
63
 
5
64
  ```bash
@@ -16,12 +16,14 @@ function mockApi(overrides: Partial<AgentApi> = {}): AgentApi {
16
16
 
17
17
  function makeBoardInfo(opts: Partial<{
18
18
  boardId: string;
19
+ workspaceId: string;
19
20
  myColumnIds: string[];
20
21
  columns: BoardInfo['board']['columns'];
21
22
  }> = {}): BoardInfo {
22
23
  return {
23
24
  board: {
24
25
  id: opts.boardId ?? 'board-1',
26
+ workspaceId: opts.workspaceId ?? 'ws-1',
25
27
  myColumnIds: opts.myColumnIds ?? ['col-1'],
26
28
  columns: opts.columns ?? [{
27
29
  id: 'col-1',
@@ -70,13 +72,14 @@ describe('BoardHandler', () => {
70
72
  });
71
73
 
72
74
  describe('initialize', () => {
73
- it('sets boardId and myColumnIds from API response', async () => {
74
- const info = makeBoardInfo({ boardId: 'b-42', myColumnIds: ['c-1', 'c-2'] });
75
+ it('sets boardId, workspaceId and myColumnIds from API response', async () => {
76
+ const info = makeBoardInfo({ boardId: 'b-42', workspaceId: 'ws-99', myColumnIds: ['c-1', 'c-2'] });
75
77
  (api.get as jest.Mock).mockResolvedValue(info);
76
78
 
77
79
  const result = await handler.initialize();
78
80
 
79
- expect(result).toBe('b-42');
81
+ // initialize() returns workspaceId for channel subscription
82
+ expect(result).toBe('ws-99');
80
83
  expect(handler.getBoardId()).toBe('b-42');
81
84
  expect(api.get).toHaveBeenCalledWith('/api/agent/board');
82
85
  });
@@ -79,14 +79,18 @@ export function createConnection(opts) {
79
79
  const commandChannel = `agent:${opts.agentId}`;
80
80
  const commandSub = client.newSubscription(commandChannel);
81
81
  commandSub.on('publication', async (ctx) => {
82
+ console.log(`[WS] Received message on ${commandChannel}:`, JSON.stringify(ctx.data));
82
83
  const command = ctx.data;
83
- if (!command || !command.type)
84
+ if (!command || !command.type) {
85
+ console.warn('[WS] Ignoring message without type:', JSON.stringify(ctx.data));
84
86
  return;
87
+ }
85
88
  const handler = handlers[command.type];
86
89
  if (!handler) {
87
- console.error(`Unknown command type: ${command.type}`);
90
+ console.error(`[WS] Unknown command type: ${command.type}`);
88
91
  return;
89
92
  }
93
+ console.log(`[WS] Handling command: type=${command.type} id=${command.id}`);
90
94
  try {
91
95
  const response = await handler(command);
92
96
  await client.publish(commandChannel, response);
@@ -110,6 +114,18 @@ export function createConnection(opts) {
110
114
  client.on('connecting', (ctx) => {
111
115
  console.log(`Connecting: ${ctx.reason} (code ${ctx.code})`);
112
116
  });
117
+ commandSub.on('subscribed', (ctx) => {
118
+ console.log(`[WS] Subscribed to ${commandChannel} (recovered=${ctx.wasRecovering})`);
119
+ });
120
+ commandSub.on('subscribing', (ctx) => {
121
+ console.log(`[WS] Subscribing to ${commandChannel}: ${ctx.reason} (code ${ctx.code})`);
122
+ });
123
+ commandSub.on('error', (ctx) => {
124
+ console.error(`[WS] Subscription error on ${commandChannel}:`, ctx.error);
125
+ });
126
+ commandSub.on('unsubscribed', (ctx) => {
127
+ console.log(`[WS] Unsubscribed from ${commandChannel}: ${ctx.reason} (code ${ctx.code})`);
128
+ });
113
129
  commandSub.subscribe();
114
130
  client.connect();
115
131
  return { client, commandSub };
@@ -1 +1 @@
1
- {"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw-cloud/agent-controller",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/connection.ts CHANGED
@@ -8,6 +8,7 @@ import { handleConfig } from './handlers/config.js';
8
8
  import { handlePair } from './handlers/pair.js';
9
9
  import { handleStop } from './handlers/stop.js';
10
10
  import { handleBackup } from './handlers/backup.js';
11
+ import { handleChatListSessions, handleChatHistory, handleChatSend } from './handlers/chat.js';
11
12
 
12
13
  export interface ConnectionOptions {
13
14
  url: string;
@@ -93,14 +94,37 @@ export function createConnection(opts: ConnectionOptions): {
93
94
  const commandSub = client.newSubscription(commandChannel);
94
95
 
95
96
  commandSub.on('publication', async (ctx) => {
97
+ console.log(`[WS] Received message on ${commandChannel}:`, JSON.stringify(ctx.data));
96
98
  const command = ctx.data as AgentCommand;
97
- if (!command || !command.type) return;
99
+ if (!command || !command.type) {
100
+ console.warn('[WS] Ignoring message without type:', JSON.stringify(ctx.data));
101
+ return;
102
+ }
103
+
104
+ // Chat commands — handle before generic dispatch
105
+ if (command.type === 'chat_list_sessions' || command.type === 'chat_history' || command.type === 'chat_send') {
106
+ const publishFn = async (data: unknown): Promise<void> => { await client.publish(commandChannel, data); };
107
+ console.log(`[WS] Handling chat command: type=${command.type} id=${command.id}`);
108
+ switch (command.type) {
109
+ case 'chat_list_sessions':
110
+ handleChatListSessions(command, publishFn).catch(console.error);
111
+ break;
112
+ case 'chat_history':
113
+ handleChatHistory(command, publishFn).catch(console.error);
114
+ break;
115
+ case 'chat_send':
116
+ handleChatSend(command, publishFn, opts.agentId).catch(console.error);
117
+ break;
118
+ }
119
+ return;
120
+ }
98
121
 
99
122
  const handler = handlers[command.type];
100
123
  if (!handler) {
101
- console.error(`Unknown command type: ${command.type}`);
124
+ console.error(`[WS] Unknown command type: ${command.type}`);
102
125
  return;
103
126
  }
127
+ console.log(`[WS] Handling command: type=${command.type} id=${command.id}`);
104
128
 
105
129
  try {
106
130
  const response = await handler(command);
@@ -128,6 +152,22 @@ export function createConnection(opts: ConnectionOptions): {
128
152
  console.log(`Connecting: ${ctx.reason} (code ${ctx.code})`);
129
153
  });
130
154
 
155
+ commandSub.on('subscribed', (ctx) => {
156
+ console.log(`[WS] Subscribed to ${commandChannel} (recovered=${ctx.wasRecovering})`);
157
+ });
158
+
159
+ commandSub.on('subscribing', (ctx) => {
160
+ console.log(`[WS] Subscribing to ${commandChannel}: ${ctx.reason} (code ${ctx.code})`);
161
+ });
162
+
163
+ commandSub.on('error', (ctx) => {
164
+ console.error(`[WS] Subscription error on ${commandChannel}:`, ctx.error);
165
+ });
166
+
167
+ commandSub.on('unsubscribed', (ctx) => {
168
+ console.log(`[WS] Unsubscribed from ${commandChannel}: ${ctx.reason} (code ${ctx.code})`);
169
+ });
170
+
131
171
  commandSub.subscribe();
132
172
  client.connect();
133
173
 
@@ -8,6 +8,7 @@ export class BoardHandler {
8
8
  private boardState: BoardState = { state: 'idle', cardId: null };
9
9
  private myColumnIds: string[] = [];
10
10
  private boardId: string | null = null;
11
+ private workspaceId: string | null = null;
11
12
  private api: AgentApi;
12
13
 
13
14
  constructor(api: AgentApi) {
@@ -18,12 +19,14 @@ export class BoardHandler {
18
19
  try {
19
20
  const data = await this.api.get('/api/agent/board') as BoardInfo;
20
21
  this.boardId = data.board.id;
22
+ this.workspaceId = data.board.workspaceId;
21
23
  this.myColumnIds = data.board.myColumnIds;
22
- console.log(`Board initialized: ${this.boardId}, watching ${this.myColumnIds.length} column(s)`);
24
+ console.log(`Board initialized: ${this.boardId} (workspace: ${this.workspaceId}), watching ${this.myColumnIds.length} column(s)`);
23
25
 
24
26
  // Backfill: check for existing unassigned cards in my columns
25
27
  await this.checkQueue(data);
26
- return this.boardId;
28
+ // Return workspaceId for channel subscription (board:<workspaceId>)
29
+ return this.workspaceId;
27
30
  } catch (err) {
28
31
  console.error('Board initialization failed:', err instanceof Error ? err.message : err);
29
32
  return null;
@@ -0,0 +1,79 @@
1
+ import type { AgentCommand } from '../types.js';
2
+ import { getChatProvider } from '../openclaw/index.js';
3
+
4
+ export async function handleChatListSessions(
5
+ command: AgentCommand,
6
+ publish: (data: unknown) => Promise<void>,
7
+ ): Promise<void> {
8
+ const provider = getChatProvider();
9
+ if (!provider) {
10
+ await publish({ type: 'chat_sessions_response', correlationId: command.id, sessions: [], error: 'Chat provider not initialized' });
11
+ return;
12
+ }
13
+ try {
14
+ const sessions = await provider.listSessions();
15
+ await publish({ type: 'chat_sessions_response', correlationId: command.id, sessions });
16
+ } catch (err) {
17
+ await publish({ type: 'chat_sessions_response', correlationId: command.id, sessions: [], error: err instanceof Error ? err.message : String(err) });
18
+ }
19
+ }
20
+
21
+ export async function handleChatHistory(
22
+ command: AgentCommand,
23
+ publish: (data: unknown) => Promise<void>,
24
+ ): Promise<void> {
25
+ const { sessionKey, limit } = command.payload as { sessionKey: string; limit?: number };
26
+ const provider = getChatProvider();
27
+ if (!provider) {
28
+ await publish({ type: 'chat_history_response', correlationId: command.id, messages: [], error: 'Chat provider not initialized' });
29
+ return;
30
+ }
31
+ try {
32
+ const messages = await provider.getHistory(sessionKey, limit ?? 200);
33
+ await publish({ type: 'chat_history_response', correlationId: command.id, messages });
34
+ } catch (err) {
35
+ await publish({ type: 'chat_history_response', correlationId: command.id, messages: [], error: err instanceof Error ? err.message : String(err) });
36
+ }
37
+ }
38
+
39
+ export async function handleChatSend(
40
+ command: AgentCommand,
41
+ publish: (data: unknown) => Promise<void>,
42
+ agentId: string,
43
+ ): Promise<void> {
44
+ const { sessionKey, text, attachments } = command.payload as {
45
+ sessionKey: string;
46
+ text: string;
47
+ attachments?: Array<{ type: 'image'; mimeType: string; content: string }>;
48
+ };
49
+ const correlationId = command.id;
50
+
51
+ // Typing indicator
52
+ await publish({ type: 'chat_typing', agentId, state: true }).catch(() => {});
53
+
54
+ const provider = getChatProvider();
55
+ if (!provider) {
56
+ await publish({ type: 'chat_response', correlationId, sessionKey, text: '', error: 'Chat provider not initialized' });
57
+ return;
58
+ }
59
+
60
+ try {
61
+ await provider.sendMessage({
62
+ sessionKey,
63
+ text,
64
+ idempotencyKey: correlationId,
65
+ attachments, // base64 inline — Centrifugo limit raised to 10MB
66
+ onDelta: async (accumulated) => {
67
+ await publish({ type: 'chat_delta', correlationId, sessionKey, text: accumulated }).catch(() => {});
68
+ },
69
+ onDone: async (finalText) => {
70
+ await publish({ type: 'chat_response', correlationId, sessionKey, text: finalText }).catch(() => {});
71
+ },
72
+ onError: async (error) => {
73
+ await publish({ type: 'chat_response', correlationId, sessionKey, text: '', error }).catch(() => {});
74
+ },
75
+ });
76
+ } catch (err) {
77
+ await publish({ type: 'chat_response', correlationId, sessionKey, text: '', error: err instanceof Error ? err.message : String(err) }).catch(() => {});
78
+ }
79
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ import { createConnection } from './connection.js';
2
2
  import { startHeartbeat } from './heartbeat.js';
3
3
  import { createAgentApi } from './api.js';
4
4
  import { BoardHandler } from './handlers/board-handler.js';
5
+ import { createChatProvider } from './openclaw/index.js';
5
6
  import type { BoardEvent } from './types.js';
6
7
 
7
8
  function requireEnv(name: string): string {
@@ -30,6 +31,21 @@ export function main(): void {
30
31
 
31
32
  const { client } = createConnection({ url, token, agentId, backendUrl: backendUrl || undefined });
32
33
 
34
+ // Chat provider via OpenClaw Gateway WS
35
+ const gatewayWsUrl = process.env.OPENCLAW_GATEWAY_WS || 'ws://localhost:18789';
36
+ const gatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';
37
+
38
+ if (gatewayToken) {
39
+ const chatProvider = createChatProvider(gatewayWsUrl, gatewayToken);
40
+ chatProvider.connect().then(() => {
41
+ console.log('OpenClaw Gateway WS connected (chat provider ready)');
42
+ }).catch((e) => {
43
+ console.warn('Chat provider connect failed:', e instanceof Error ? e.message : e);
44
+ });
45
+ } else {
46
+ console.warn('OPENCLAW_GATEWAY_TOKEN not set, chat provider disabled');
47
+ }
48
+
33
49
  // Board handler (optional — requires BACKEND_URL and AGENT_TOKEN)
34
50
  let boardHandler: BoardHandler | null = null;
35
51
  const apiBaseUrl = process.env.BACKEND_URL || backendUrl;
@@ -38,13 +54,13 @@ export function main(): void {
38
54
  boardHandler = new BoardHandler(api);
39
55
 
40
56
  // Initialize board handler after connection is established
41
- boardHandler.initialize().then((boardId) => {
42
- if (!boardId) {
57
+ boardHandler.initialize().then((workspaceId) => {
58
+ if (!workspaceId) {
43
59
  console.log('No board found for this agent, board handler dormant');
44
60
  return;
45
61
  }
46
62
 
47
- const boardChannel = `board:${boardId}`;
63
+ const boardChannel = `board:${workspaceId}`;
48
64
  const boardSub = client.newSubscription(boardChannel);
49
65
 
50
66
  boardSub.on('publication', (ctx) => {
@@ -0,0 +1,129 @@
1
+ import { GatewayClient } from './gateway-client.js';
2
+ import type { IChatProvider, ChatSession, ChatMessage, ChatAttachment } from './types.js';
3
+
4
+ export class OpenclawGatewayAdapter implements IChatProvider {
5
+ private client: GatewayClient;
6
+ private streamCallbacks = new Map<string, {
7
+ onDelta?: (text: string) => void;
8
+ onDone?: (text: string) => void;
9
+ onError?: (error: string) => void;
10
+ lastText: string;
11
+ }>();
12
+
13
+ constructor(url: string, token: string) {
14
+ this.client = new GatewayClient(url, token);
15
+ this.client.setEventHandler(this.handleEvent.bind(this));
16
+ }
17
+
18
+ async connect(): Promise<void> {
19
+ await this.client.connect();
20
+ }
21
+
22
+ disconnect(): void {
23
+ this.client.disconnect();
24
+ }
25
+
26
+ isConnected(): boolean {
27
+ return this.client.isConnected();
28
+ }
29
+
30
+ async listSessions(): Promise<ChatSession[]> {
31
+ const result = await this.client.request('sessions.list', {});
32
+ return (result?.sessions ?? []).map((s: any) => ({
33
+ key: s.key,
34
+ sessionId: s.sessionId ?? '',
35
+ kind: s.kind ?? 'direct',
36
+ updatedAt: s.updatedAt ?? 0,
37
+ model: s.model,
38
+ }));
39
+ }
40
+
41
+ async getHistory(sessionKey: string, limit = 200): Promise<ChatMessage[]> {
42
+ const result = await this.client.request('chat.history', { sessionKey, limit });
43
+ return (result?.messages ?? [])
44
+ .filter((m: any) => m.role === 'user' || m.role === 'assistant')
45
+ .map((m: any) => {
46
+ const content = Array.isArray(m.content)
47
+ ? m.content.filter((c: any) => c.type === 'text').map((c: any) => c.text).join('')
48
+ : String(m.content ?? '');
49
+ return {
50
+ role: m.role,
51
+ content,
52
+ timestamp: typeof m.timestamp === 'number'
53
+ ? new Date(m.timestamp).toISOString()
54
+ : m.timestamp ?? new Date().toISOString(),
55
+ };
56
+ });
57
+ }
58
+
59
+ async sendMessage(params: {
60
+ sessionKey: string;
61
+ text: string;
62
+ idempotencyKey: string;
63
+ attachments?: ChatAttachment[];
64
+ onDelta?: (text: string) => void;
65
+ onDone?: (text: string) => void;
66
+ onError?: (error: string) => void;
67
+ }): Promise<void> {
68
+ // Register stream callbacks BEFORE sending
69
+ this.streamCallbacks.set(params.idempotencyKey, {
70
+ onDelta: params.onDelta,
71
+ onDone: params.onDone,
72
+ onError: params.onError,
73
+ lastText: '',
74
+ });
75
+
76
+ const payload: any = {
77
+ sessionKey: params.sessionKey,
78
+ message: params.text,
79
+ deliver: false,
80
+ idempotencyKey: params.idempotencyKey,
81
+ };
82
+ if (params.attachments?.length) {
83
+ payload.attachments = params.attachments;
84
+ }
85
+
86
+ try {
87
+ await this.client.request('chat.send', payload);
88
+ // Request accepted — response comes via events
89
+ } catch (err) {
90
+ this.streamCallbacks.delete(params.idempotencyKey);
91
+ params.onError?.(err instanceof Error ? err.message : String(err));
92
+ }
93
+ }
94
+
95
+ async abort(sessionKey: string, runId?: string): Promise<void> {
96
+ await this.client.request('chat.abort', { sessionKey, runId }).catch(() => {});
97
+ }
98
+
99
+ private handleEvent(event: { event: string; payload: any }): void {
100
+ const data = event.payload;
101
+ if (!data || !data.sessionKey) return;
102
+
103
+ // Stream events have runId matching idempotencyKey
104
+ const runId = data.runId;
105
+ const cb = runId ? this.streamCallbacks.get(runId) : null;
106
+ if (!cb) return;
107
+
108
+ if (data.state === 'delta' && data.message != null) {
109
+ // message is accumulated text
110
+ const text = typeof data.message === 'string'
111
+ ? data.message
112
+ : Array.isArray(data.message)
113
+ ? data.message.filter((c: any) => c.type === 'text').map((c: any) => c.text).join('')
114
+ : '';
115
+ cb.lastText = text;
116
+ cb.onDelta?.(text);
117
+ } else if (data.state === 'final') {
118
+ this.streamCallbacks.delete(runId!);
119
+ cb.onDone?.(cb.lastText);
120
+ } else if (data.state === 'aborted') {
121
+ this.streamCallbacks.delete(runId!);
122
+ cb.onDone?.(cb.lastText); // still send whatever we have
123
+ } else if (data.state === 'error') {
124
+ this.streamCallbacks.delete(runId!);
125
+ const errMsg = typeof data.error === 'string' ? data.error : data.error?.message ?? 'Unknown error';
126
+ cb.onError?.(errMsg);
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,131 @@
1
+ import WebSocket from 'ws';
2
+ import { randomUUID } from 'crypto';
3
+
4
+ interface PendingRequest {
5
+ resolve: (payload: any) => void;
6
+ reject: (err: Error) => void;
7
+ }
8
+
9
+ export type EventHandler = (event: { event: string; payload: any; seq?: number }) => void;
10
+
11
+ export class GatewayClient {
12
+ private ws: WebSocket | null = null;
13
+ private pending = new Map<string, PendingRequest>();
14
+ private onEvent: EventHandler | null = null;
15
+ private connected = false;
16
+ private token: string;
17
+ private url: string;
18
+
19
+ constructor(url: string, token: string) {
20
+ this.url = url;
21
+ this.token = token;
22
+ }
23
+
24
+ setEventHandler(handler: EventHandler): void {
25
+ this.onEvent = handler;
26
+ }
27
+
28
+ connect(): Promise<void> {
29
+ return new Promise((resolve, reject) => {
30
+ this.ws = new WebSocket(this.url);
31
+
32
+ this.ws.on('open', () => {
33
+ // Wait for connect.challenge event, then authenticate
34
+ });
35
+
36
+ this.ws.on('message', (raw) => {
37
+ try {
38
+ const msg = JSON.parse(raw.toString());
39
+ this.handleMessage(msg);
40
+ } catch {}
41
+ });
42
+
43
+ this.ws.on('error', (e) => {
44
+ if (!this.connected) reject(e);
45
+ });
46
+
47
+ this.ws.on('close', () => {
48
+ this.connected = false;
49
+ this.flushPending(new Error('Connection closed'));
50
+ });
51
+
52
+ // Wait for connect.challenge → authenticate → resolve
53
+ const origHandler = this.onEvent;
54
+ this.onEvent = async (ev) => {
55
+ if (ev.event === 'connect.challenge') {
56
+ try {
57
+ await this.request('connect', {
58
+ auth: { token: this.token },
59
+ scopes: ['operator.admin'],
60
+ caps: [],
61
+ });
62
+ this.connected = true;
63
+ this.onEvent = origHandler;
64
+ resolve();
65
+ } catch (e) {
66
+ reject(e);
67
+ }
68
+ }
69
+ };
70
+
71
+ setTimeout(() => {
72
+ if (!this.connected) reject(new Error('Connect timeout'));
73
+ }, 10_000);
74
+ });
75
+ }
76
+
77
+ disconnect(): void {
78
+ this.ws?.close();
79
+ this.ws = null;
80
+ this.connected = false;
81
+ this.flushPending(new Error('Disconnected'));
82
+ }
83
+
84
+ isConnected(): boolean {
85
+ return this.connected && this.ws?.readyState === WebSocket.OPEN;
86
+ }
87
+
88
+ request(method: string, params: any): Promise<any> {
89
+ return new Promise((resolve, reject) => {
90
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
91
+ return reject(new Error('Not connected'));
92
+ }
93
+
94
+ const id = randomUUID();
95
+ this.pending.set(id, { resolve, reject });
96
+ this.ws.send(JSON.stringify({ type: 'req', id, method, params }));
97
+
98
+ // Timeout per request
99
+ setTimeout(() => {
100
+ if (this.pending.has(id)) {
101
+ this.pending.delete(id);
102
+ reject(new Error(`Request timeout: ${method}`));
103
+ }
104
+ }, 120_000);
105
+ });
106
+ }
107
+
108
+ private handleMessage(msg: any): void {
109
+ if (msg.type === 'event') {
110
+ this.onEvent?.(msg);
111
+ return;
112
+ }
113
+ if (msg.type === 'res') {
114
+ const req = this.pending.get(msg.id);
115
+ if (!req) return;
116
+ this.pending.delete(msg.id);
117
+ if (msg.ok) {
118
+ req.resolve(msg.payload);
119
+ } else {
120
+ req.reject(new Error(msg.error?.message ?? 'Request failed'));
121
+ }
122
+ }
123
+ }
124
+
125
+ private flushPending(err: Error): void {
126
+ for (const [, req] of this.pending) {
127
+ req.reject(err);
128
+ }
129
+ this.pending.clear();
130
+ }
131
+ }
@@ -0,0 +1,17 @@
1
+ export { OpenclawGatewayAdapter } from './gateway-adapter.js';
2
+ export type { IChatProvider, ChatSession, ChatMessage, ChatAttachment } from './types.js';
3
+
4
+ import { OpenclawGatewayAdapter } from './gateway-adapter.js';
5
+ import type { IChatProvider } from './types.js';
6
+
7
+ let _provider: IChatProvider | null = null;
8
+
9
+ export function createChatProvider(url: string, token: string): IChatProvider & { connect(): Promise<void>; disconnect(): void } {
10
+ const adapter = new OpenclawGatewayAdapter(url, token);
11
+ _provider = adapter;
12
+ return adapter;
13
+ }
14
+
15
+ export function getChatProvider(): IChatProvider | null {
16
+ return _provider;
17
+ }
@@ -0,0 +1,41 @@
1
+ export interface ChatSession {
2
+ key: string;
3
+ sessionId: string;
4
+ kind: string;
5
+ updatedAt: number;
6
+ model?: string;
7
+ }
8
+
9
+ export interface ChatMessage {
10
+ role: 'user' | 'assistant';
11
+ content: string; // normalized to plain text
12
+ timestamp: string; // ISO 8601
13
+ }
14
+
15
+ export interface ChatAttachment {
16
+ type: 'image';
17
+ mimeType: string;
18
+ content: string; // base64
19
+ }
20
+
21
+ export interface IChatProvider {
22
+ listSessions(): Promise<ChatSession[]>;
23
+ getHistory(sessionKey: string, limit?: number): Promise<ChatMessage[]>;
24
+
25
+ /**
26
+ * Send message. Returns after the agent finishes (full response).
27
+ * Calls onDelta with accumulated text during streaming.
28
+ * Calls onDone when agent response is complete.
29
+ */
30
+ sendMessage(params: {
31
+ sessionKey: string;
32
+ text: string;
33
+ idempotencyKey: string;
34
+ attachments?: ChatAttachment[];
35
+ onDelta?: (accumulatedText: string) => void;
36
+ onDone?: (finalText: string) => void;
37
+ onError?: (error: string) => void;
38
+ }): Promise<void>;
39
+
40
+ abort(sessionKey: string, runId?: string): Promise<void>;
41
+ }
package/src/types.ts CHANGED
@@ -4,7 +4,9 @@
4
4
  * heartbeat:<agentId> — heartbeat publications from agent
5
5
  */
6
6
 
7
- export type CommandType = 'exec' | 'restart' | 'deploy' | 'config' | 'pair' | 'stop' | 'backup';
7
+ export type CommandType =
8
+ | 'exec' | 'restart' | 'deploy' | 'config' | 'pair' | 'stop' | 'backup'
9
+ | 'chat_list_sessions' | 'chat_history' | 'chat_send';
8
10
 
9
11
  export interface AgentCommand {
10
12
  id: string;
@@ -56,6 +58,7 @@ export interface BoardEvent {
56
58
  export interface BoardInfo {
57
59
  board: {
58
60
  id: string;
61
+ workspaceId: string;
59
62
  columns: Array<{
60
63
  id: string;
61
64
  name: string;