@openclaw-cloud/agent-controller 0.1.0 → 0.1.2

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.
@@ -0,0 +1,32 @@
1
+ #!/bin/bash
2
+ # Claude Code hook: notify OpenClaw on Stop/PermissionRequest
3
+ # Reads JSON from stdin, sends system event
4
+
5
+ EVENT_TYPE="${CC_HOOK_EVENT:-unknown}"
6
+ PROJECT="${CC_PROJECT:-unknown}"
7
+ INPUT=$(cat)
8
+
9
+ case "$EVENT_TYPE" in
10
+ Stop)
11
+ REASON=$(echo "$INPUT" | jq -r '.stop_reason // "completed"' 2>/dev/null)
12
+ TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_summary // ""' 2>/dev/null)
13
+ if [ -n "$TRANSCRIPT" ] && [ "$TRANSCRIPT" != "null" ]; then
14
+ MSG="🏁 CC [$PROJECT] finished ($REASON): $TRANSCRIPT"
15
+ else
16
+ MSG="🏁 CC [$PROJECT] finished ($REASON)"
17
+ fi
18
+ ;;
19
+ Notification)
20
+ NOTIF_TYPE=$(echo "$INPUT" | jq -r '.notification_type // "unknown"' 2>/dev/null)
21
+ MSG="⚠️ CC [$PROJECT] notification: $NOTIF_TYPE"
22
+ ;;
23
+ *)
24
+ MSG="🔔 CC [$PROJECT] hook: $EVENT_TYPE"
25
+ ;;
26
+ esac
27
+
28
+ # Truncate to 500 chars
29
+ MSG="${MSG:0:500}"
30
+
31
+ openclaw system event --text "$MSG" --mode now 2>/dev/null &
32
+ exit 0
@@ -0,0 +1,31 @@
1
+ {
2
+ "hooks": {
3
+ "Stop": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "CC_HOOK_EVENT=Stop CC_PROJECT=agent-controller .claude/cc-notify.sh"
9
+ }
10
+ ]
11
+ }
12
+ ],
13
+ "Notification": [
14
+ {
15
+ "matcher": "permission_prompt",
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "CC_HOOK_EVENT=Notification CC_PROJECT=agent-controller .claude/cc-notify.sh"
20
+ }
21
+ ]
22
+ }
23
+ ]
24
+ },
25
+ "instructions": [
26
+ "This is the agent-controller (Node.js + TypeScript). Runs inside agent containers/VMs.",
27
+ "Tests: npx jest --forceExit",
28
+ "Communicates with backend via Centrifugo WebSocket.",
29
+ "Push to origin after each commit. Target branch: master."
30
+ ]
31
+ }
package/CLAUDE.md ADDED
@@ -0,0 +1,48 @@
1
+ # CLAUDE.md — Agent Controller Context
2
+
3
+ ## Quick Reference
4
+
5
+ ```bash
6
+ npx jest --forceExit # Run tests
7
+ npx tsc --noEmit # Type check
8
+ npm run build # Build
9
+ ```
10
+
11
+ ## Architecture
12
+
13
+ Agent-controller is a Node.js process that runs inside every agent container (K8s pod) or VM. It's the single communication interface between the agent and the backend.
14
+
15
+ ### Communication Flow
16
+ ```
17
+ Backend → Centrifugo WS (port 8000) → agent-controller → local agent process
18
+ agent-controller → Centrifugo HTTP API (port 9000) → Backend (heartbeat processing)
19
+ ```
20
+
21
+ ### Key Components
22
+ - **WebSocket client** — Connects to Centrifugo, subscribes to agent channel
23
+ - **Heartbeat publisher** — Sends periodic heartbeats via Centrifugo HTTP API (server-side publish)
24
+ - **Command handlers** — Processes commands from backend (backup, reconfigure, etc.)
25
+ - **Board handler** — Kanban board integration (claim cards, move cards, report status)
26
+ - **JWT auth** — Fetches JWT from backend's `/api/internal/centrifugo-token` endpoint, auto-refreshes before expiry (24h TTL)
27
+
28
+ ### Environment Variables
29
+ - `AGENT_ID` — UUID of this agent
30
+ - `AGENT_TOKEN` — Internal auth token
31
+ - `CENTRIFUGO_URL` — WebSocket URL (port 8000, converted from http→ws)
32
+ - `CENTRIFUGO_INTERNAL_URL` — HTTP API URL (port 9000) for server-side publish
33
+ - `CENTRIFUGO_API_KEY` — API key for HTTP API auth
34
+ - `API_BASE_URL` — Backend API URL for JWT fetch
35
+
36
+ ### Deployment
37
+
38
+ Agent-controller is delivered via **npm** (not Docker image copy):
39
+
40
+ - **K8s pods**: `npm install -g @openclaw-cloud/agent-controller` in agent Dockerfile (`backend/charts/openclaw-kube/Dockerfile`)
41
+ - **VMs**: `npm install -g @openclaw-cloud/agent-controller` in EC2 user-data script (`backend/src/modules/vm-provisioner/user-data.ts`)
42
+ - Runs in same container/VM as OpenClaw agent (not sidecar)
43
+ - Started via bash wrapper in K8s StatefulSet command
44
+ - PID monitoring: if agent-controller dies, `kill 1` terminates container → K8s restarts pod
45
+
46
+ ### Docker
47
+
48
+ The `Dockerfile` in this repo is for **local development and testing only**. It is NOT used in production. Production installs agent-controller from npm registry.
@@ -92,7 +92,7 @@ describe('handleBackup', () => {
92
92
  fakeExecSuccess();
93
93
  mockStat.mockResolvedValue({ size: 512 });
94
94
  mockReadFile.mockResolvedValue(Buffer.from('fake'));
95
- mockFetch.mockResolvedValue({ ok: false, status: 500, statusText: 'Internal Server Error' });
95
+ mockFetch.mockResolvedValue({ ok: false, status: 500, statusText: 'Internal Server Error', text: () => Promise.resolve('') });
96
96
 
97
97
  const cmd: AgentCommand = {
98
98
  id: '4',
@@ -0,0 +1,318 @@
1
+ import { BoardHandler } from '../src/handlers/board-handler';
2
+ import type { AgentApi } from '../src/api';
3
+ import type { BoardInfo, CardDetail, BoardEvent } from '../src/types';
4
+ import childProcess from 'node:child_process';
5
+
6
+ jest.mock('node:child_process');
7
+ const mockExec = childProcess.exec as unknown as jest.Mock;
8
+
9
+ function mockApi(overrides: Partial<AgentApi> = {}): AgentApi {
10
+ return {
11
+ get: jest.fn(),
12
+ post: jest.fn(),
13
+ ...overrides,
14
+ };
15
+ }
16
+
17
+ function makeBoardInfo(opts: Partial<{
18
+ boardId: string;
19
+ myColumnIds: string[];
20
+ columns: BoardInfo['board']['columns'];
21
+ }> = {}): BoardInfo {
22
+ return {
23
+ board: {
24
+ id: opts.boardId ?? 'board-1',
25
+ myColumnIds: opts.myColumnIds ?? ['col-1'],
26
+ columns: opts.columns ?? [{
27
+ id: 'col-1',
28
+ name: 'To Do',
29
+ type: 'agent',
30
+ position: 0,
31
+ cards: [],
32
+ }],
33
+ },
34
+ };
35
+ }
36
+
37
+ function makeCardDetail(opts: Partial<CardDetail['card']> = {}): CardDetail {
38
+ return {
39
+ card: {
40
+ id: opts.id ?? 'card-1',
41
+ title: opts.title ?? 'Test task',
42
+ description: opts.description ?? 'Do the thing',
43
+ priority: opts.priority ?? 'medium',
44
+ columnId: opts.columnId ?? 'col-1',
45
+ columnName: opts.columnName ?? 'To Do',
46
+ assignedAgentId: opts.assignedAgentId ?? null,
47
+ },
48
+ };
49
+ }
50
+
51
+ function makeResponse(status: number, body: string = ''): Response {
52
+ return {
53
+ ok: status >= 200 && status < 300,
54
+ status,
55
+ text: () => Promise.resolve(body),
56
+ } as Response;
57
+ }
58
+
59
+ describe('BoardHandler', () => {
60
+ let api: AgentApi;
61
+ let handler: BoardHandler;
62
+
63
+ beforeEach(() => {
64
+ jest.clearAllMocks();
65
+ mockExec.mockImplementation((_cmd: string, _opts: unknown, cb: Function) => {
66
+ cb(null, Buffer.from('ok'), Buffer.from(''));
67
+ });
68
+ api = mockApi();
69
+ handler = new BoardHandler(api);
70
+ });
71
+
72
+ 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
+ (api.get as jest.Mock).mockResolvedValue(info);
76
+
77
+ const result = await handler.initialize();
78
+
79
+ expect(result).toBe('b-42');
80
+ expect(handler.getBoardId()).toBe('b-42');
81
+ expect(api.get).toHaveBeenCalledWith('/api/agent/board');
82
+ });
83
+
84
+ it('claims unassigned card in my column during init (backfill)', async () => {
85
+ const info = makeBoardInfo({
86
+ columns: [{
87
+ id: 'col-1', name: 'To Do', type: 'agent', position: 0,
88
+ cards: [{ id: 'card-99', title: 'Backfill', assignedAgentId: null }],
89
+ }],
90
+ });
91
+ (api.get as jest.Mock).mockResolvedValue(info);
92
+ (api.post as jest.Mock).mockResolvedValue(makeResponse(200));
93
+ // Mock card detail for startTask
94
+ (api.get as jest.Mock).mockResolvedValueOnce(info).mockResolvedValueOnce(makeCardDetail({ id: 'card-99' }));
95
+
96
+ await handler.initialize();
97
+
98
+ expect(api.post).toHaveBeenCalledWith('/api/agent/board/cards/card-99/claim');
99
+ });
100
+
101
+ it('returns null on API error', async () => {
102
+ (api.get as jest.Mock).mockRejectedValue(new Error('network fail'));
103
+
104
+ const result = await handler.initialize();
105
+
106
+ expect(result).toBeNull();
107
+ expect(handler.getBoardId()).toBeNull();
108
+ });
109
+ });
110
+
111
+ describe('onBoardEvent', () => {
112
+ beforeEach(async () => {
113
+ const info = makeBoardInfo();
114
+ (api.get as jest.Mock).mockResolvedValue(info);
115
+ await handler.initialize();
116
+ jest.clearAllMocks();
117
+ });
118
+
119
+ it('claims card when card:entered in my column and idle', async () => {
120
+ (api.post as jest.Mock).mockResolvedValue(makeResponse(200));
121
+ (api.get as jest.Mock).mockResolvedValue(makeCardDetail());
122
+
123
+ const event: BoardEvent = { event: 'card:entered', cardId: 'card-5', columnId: 'col-1' };
124
+ await handler.onBoardEvent(event);
125
+
126
+ expect(api.post).toHaveBeenCalledWith('/api/agent/board/cards/card-5/claim');
127
+ });
128
+
129
+ it('ignores card:entered for column not in myColumnIds', async () => {
130
+ const event: BoardEvent = { event: 'card:entered', cardId: 'card-5', columnId: 'other-col' };
131
+ await handler.onBoardEvent(event);
132
+
133
+ expect(api.post).not.toHaveBeenCalled();
134
+ });
135
+
136
+ it('ignores card:entered when already working', async () => {
137
+ // First: claim a card to set state to working
138
+ (api.post as jest.Mock).mockResolvedValue(makeResponse(200));
139
+ (api.get as jest.Mock).mockResolvedValue(makeCardDetail());
140
+ await handler.onBoardEvent({ event: 'card:entered', cardId: 'card-1', columnId: 'col-1' });
141
+
142
+ jest.clearAllMocks();
143
+
144
+ // Second: new card enters, should be ignored
145
+ await handler.onBoardEvent({ event: 'card:entered', cardId: 'card-2', columnId: 'col-1' });
146
+ expect(api.post).not.toHaveBeenCalled();
147
+ });
148
+
149
+ it('ignores card:claimed, card:moved, card:commented events', async () => {
150
+ for (const event of ['card:claimed', 'card:moved', 'card:commented'] as const) {
151
+ await handler.onBoardEvent({ event, cardId: 'card-1' });
152
+ }
153
+ expect(api.post).not.toHaveBeenCalled();
154
+ expect(api.get).not.toHaveBeenCalled();
155
+ });
156
+ });
157
+
158
+ describe('tryClaimCard', () => {
159
+ beforeEach(async () => {
160
+ (api.get as jest.Mock).mockResolvedValue(makeBoardInfo());
161
+ await handler.initialize();
162
+ jest.clearAllMocks();
163
+ });
164
+
165
+ it('sets state to working on successful claim (200)', async () => {
166
+ (api.post as jest.Mock).mockResolvedValue(makeResponse(200));
167
+ (api.get as jest.Mock).mockResolvedValue(makeCardDetail({ id: 'card-10' }));
168
+
169
+ await handler.tryClaimCard('card-10');
170
+
171
+ expect(handler.getBoardStatus()).toEqual({ state: 'working', cardId: 'card-10' });
172
+ });
173
+
174
+ it('stays idle on 409 (already claimed)', async () => {
175
+ (api.post as jest.Mock).mockResolvedValue(makeResponse(409));
176
+
177
+ await handler.tryClaimCard('card-10');
178
+
179
+ expect(handler.getBoardStatus()).toEqual({ state: 'idle', cardId: null });
180
+ });
181
+
182
+ it('stays idle on 500 error', async () => {
183
+ (api.post as jest.Mock).mockResolvedValue(makeResponse(500, 'Internal error'));
184
+
185
+ await handler.tryClaimCard('card-10');
186
+
187
+ expect(handler.getBoardStatus()).toEqual({ state: 'idle', cardId: null });
188
+ });
189
+
190
+ it('stays idle on network error', async () => {
191
+ (api.post as jest.Mock).mockRejectedValue(new Error('connection refused'));
192
+
193
+ await handler.tryClaimCard('card-10');
194
+
195
+ expect(handler.getBoardStatus()).toEqual({ state: 'idle', cardId: null });
196
+ });
197
+ });
198
+
199
+ describe('startTask', () => {
200
+ beforeEach(async () => {
201
+ (api.get as jest.Mock).mockResolvedValue(makeBoardInfo());
202
+ await handler.initialize();
203
+ jest.clearAllMocks();
204
+ });
205
+
206
+ it('fetches card detail and calls openclaw exec', async () => {
207
+ const detail = makeCardDetail({ id: 'card-20', title: 'Build feature', description: 'Some desc', priority: 'high' });
208
+ (api.get as jest.Mock).mockResolvedValue(detail);
209
+
210
+ await handler.startTask('card-20');
211
+
212
+ expect(api.get).toHaveBeenCalledWith('/api/agent/board/cards/card-20');
213
+ expect(mockExec).toHaveBeenCalledTimes(1);
214
+ const cmd = mockExec.mock.calls[0][0] as string;
215
+ expect(cmd).toContain('openclaw system event');
216
+ expect(cmd).toContain('Build feature');
217
+ expect(cmd).toContain('Priority: high');
218
+ });
219
+
220
+ it('handles API error gracefully', async () => {
221
+ (api.get as jest.Mock).mockRejectedValue(new Error('not found'));
222
+
223
+ await handler.startTask('card-missing');
224
+
225
+ expect(mockExec).not.toHaveBeenCalled();
226
+ });
227
+ });
228
+
229
+ describe('completeTask', () => {
230
+ it('resets state to idle and triggers checkQueue', async () => {
231
+ const info = makeBoardInfo();
232
+ (api.get as jest.Mock).mockResolvedValue(info);
233
+ await handler.initialize();
234
+
235
+ // Claim a card first
236
+ (api.post as jest.Mock).mockResolvedValue(makeResponse(200));
237
+ (api.get as jest.Mock).mockResolvedValueOnce(makeCardDetail()).mockResolvedValueOnce(info);
238
+
239
+ await handler.tryClaimCard('card-1');
240
+ expect(handler.getBoardStatus().state).toBe('working');
241
+
242
+ handler.completeTask('card-1');
243
+ expect(handler.getBoardStatus()).toEqual({ state: 'idle', cardId: null });
244
+ });
245
+ });
246
+
247
+ describe('checkQueue', () => {
248
+ beforeEach(async () => {
249
+ (api.get as jest.Mock).mockResolvedValue(makeBoardInfo());
250
+ await handler.initialize();
251
+ jest.clearAllMocks();
252
+ });
253
+
254
+ it('sorts candidates by priority then createdAt', async () => {
255
+ const info = makeBoardInfo({
256
+ columns: [{
257
+ id: 'col-1', name: 'To Do', type: 'agent', position: 0,
258
+ cards: [
259
+ { id: 'low-old', title: 'Low old', priority: 'low', assignedAgentId: null, createdAt: '2026-01-01' },
260
+ { id: 'critical-new', title: 'Critical', priority: 'critical', assignedAgentId: null, createdAt: '2026-02-01' },
261
+ { id: 'high-old', title: 'High old', priority: 'high', assignedAgentId: null, createdAt: '2026-01-15' },
262
+ ],
263
+ }],
264
+ });
265
+ (api.get as jest.Mock).mockResolvedValue(info);
266
+ (api.post as jest.Mock).mockResolvedValue(makeResponse(200));
267
+ (api.get as jest.Mock).mockResolvedValueOnce(info).mockResolvedValueOnce(makeCardDetail({ id: 'critical-new' }));
268
+
269
+ await handler.checkQueue();
270
+
271
+ // Should try to claim the critical card first
272
+ expect(api.post).toHaveBeenCalledWith('/api/agent/board/cards/critical-new/claim');
273
+ });
274
+
275
+ it('does nothing when no unassigned cards', async () => {
276
+ const info = makeBoardInfo({
277
+ columns: [{
278
+ id: 'col-1', name: 'To Do', type: 'agent', position: 0,
279
+ cards: [
280
+ { id: 'card-1', title: 'Taken', assignedAgentId: 'agent-1' },
281
+ ],
282
+ }],
283
+ });
284
+ (api.get as jest.Mock).mockResolvedValue(info);
285
+
286
+ await handler.checkQueue();
287
+
288
+ expect(api.post).not.toHaveBeenCalled();
289
+ });
290
+
291
+ it('skips cards in columns not in myColumnIds', async () => {
292
+ const info = makeBoardInfo({
293
+ myColumnIds: ['col-1'],
294
+ columns: [
295
+ { id: 'col-1', name: 'Mine', type: 'agent', position: 0, cards: [] },
296
+ { id: 'col-2', name: 'Not mine', type: 'human', position: 1, cards: [
297
+ { id: 'card-x', title: 'Other', assignedAgentId: null },
298
+ ]},
299
+ ],
300
+ });
301
+ (api.get as jest.Mock).mockResolvedValue(info);
302
+
303
+ await handler.checkQueue();
304
+
305
+ expect(api.post).not.toHaveBeenCalled();
306
+ });
307
+ });
308
+
309
+ describe('getters', () => {
310
+ it('getBoardStatus returns idle by default', () => {
311
+ expect(handler.getBoardStatus()).toEqual({ state: 'idle', cardId: null });
312
+ });
313
+
314
+ it('getBoardId returns null before initialize', () => {
315
+ expect(handler.getBoardId()).toBeNull();
316
+ });
317
+ });
318
+ });
package/dist/api.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export interface AgentApi {
2
+ get(path: string): Promise<unknown>;
3
+ post(path: string, body?: unknown): Promise<Response>;
4
+ }
5
+ export declare function createAgentApi(backendUrl: string, agentToken: string): AgentApi;
package/dist/api.js ADDED
@@ -0,0 +1,35 @@
1
+ const TIMEOUT = 10_000;
2
+ export function createAgentApi(backendUrl, agentToken) {
3
+ async function request(method, path, body) {
4
+ const controller = new AbortController();
5
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT);
6
+ try {
7
+ const res = await fetch(`${backendUrl}${path}`, {
8
+ method,
9
+ headers: {
10
+ 'Content-Type': 'application/json',
11
+ 'Authorization': `Bearer ${agentToken}`,
12
+ },
13
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
14
+ signal: controller.signal,
15
+ });
16
+ return res;
17
+ }
18
+ finally {
19
+ clearTimeout(timeout);
20
+ }
21
+ }
22
+ return {
23
+ async get(path) {
24
+ const res = await request('GET', path);
25
+ if (!res.ok) {
26
+ throw new Error(`GET ${path} failed: HTTP ${res.status}`);
27
+ }
28
+ return res.json();
29
+ },
30
+ async post(path, body) {
31
+ return request('POST', path, body);
32
+ },
33
+ };
34
+ }
35
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,GAAG,MAAM,CAAC;AAOvB,MAAM,UAAU,cAAc,CAAC,UAAkB,EAAE,UAAkB;IACnE,KAAK,UAAU,OAAO,CAAC,MAAc,EAAE,IAAY,EAAE,IAAc;QACjE,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,OAAO,CAAC,CAAC;QAC9D,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,UAAU,GAAG,IAAI,EAAE,EAAE;gBAC9C,MAAM;gBACN,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,eAAe,EAAE,UAAU,UAAU,EAAE;iBACxC;gBACD,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC7D,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YACH,OAAO,GAAG,CAAC;QACb,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,OAAO,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,IAAY;YACpB,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YACvC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,OAAO,IAAI,iBAAiB,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;YAC5D,CAAC;YACD,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;QACpB,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,IAAY,EAAE,IAAc;YACrC,OAAO,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QACrC,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -7,6 +7,7 @@ const TAR_TIMEOUT = 120_000;
7
7
  const UPLOAD_TIMEOUT = 120_000;
8
8
  export function handleBackup(command) {
9
9
  const uploadUrl = command.payload.uploadUrl;
10
+ const token = command.payload.token;
10
11
  if (!uploadUrl) {
11
12
  return Promise.resolve({
12
13
  id: command.id,
@@ -19,7 +20,7 @@ export function handleBackup(command) {
19
20
  const filename = `backup-${timestamp}.tar.gz`;
20
21
  const archivePath = path.join(BACKUP_DIR, filename);
21
22
  return createArchive(archivePath)
22
- .then(() => uploadArchive(archivePath, uploadUrl))
23
+ .then(() => uploadArchive(archivePath, uploadUrl, token))
23
24
  .then(async (size) => {
24
25
  await cleanup(archivePath);
25
26
  return {
@@ -51,22 +52,22 @@ function createArchive(archivePath) {
51
52
  });
52
53
  });
53
54
  }
54
- async function uploadArchive(archivePath, uploadUrl) {
55
+ async function uploadArchive(archivePath, uploadUrl, token) {
55
56
  const stat = await fs.stat(archivePath);
56
57
  const fileBuffer = await fs.readFile(archivePath);
57
- const boundary = `----backup-${Date.now()}`;
58
- const filename = path.basename(archivePath);
59
- const header = Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${filename}"\r\nContent-Type: application/gzip\r\n\r\n`);
60
- const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
61
- const body = Buffer.concat([header, fileBuffer, footer]);
58
+ const headers = { 'Content-Type': 'application/gzip' };
59
+ if (token) {
60
+ headers['Authorization'] = `Bearer ${token}`;
61
+ }
62
62
  const response = await fetch(uploadUrl, {
63
63
  method: 'POST',
64
- headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` },
65
- body,
64
+ headers,
65
+ body: fileBuffer,
66
66
  signal: AbortSignal.timeout(UPLOAD_TIMEOUT),
67
67
  });
68
68
  if (!response.ok) {
69
- throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
69
+ const text = await response.text().catch(() => '');
70
+ throw new Error(`Upload failed: ${response.status} ${response.statusText} ${text}`);
70
71
  }
71
72
  return stat.size;
72
73
  }
@@ -1 +1 @@
1
- {"version":3,"file":"backup.js","sourceRoot":"","sources":["../../src/handlers/backup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,MAAM,aAAa,GAAG,gCAAgC,CAAC;AACvD,MAAM,UAAU,GAAG,MAAM,CAAC;AAC1B,MAAM,WAAW,GAAG,OAAO,CAAC;AAC5B,MAAM,cAAc,GAAG,OAAO,CAAC;AAE/B,MAAM,UAAU,YAAY,CAAC,OAAqB;IAChD,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,SAAmB,CAAC;IACtD,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,OAAO,CAAC,OAAO,CAAC;YACrB,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,gCAAgC;SACxC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,MAAM,QAAQ,GAAG,UAAU,SAAS,SAAS,CAAC;IAC9C,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAEpD,OAAO,aAAa,CAAC,WAAW,CAAC;SAC9B,IAAI,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;SACjD,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QACnB,MAAM,OAAO,CAAC,WAAW,CAAC,CAAC;QAC3B,OAAO;YACL,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,IAAI,EAAE,QAAiB;YACvB,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;SACzB,CAAC;IACJ,CAAC,CAAC;SACD,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACnB,MAAM,OAAO,CAAC,WAAW,CAAC,CAAC;QAC3B,OAAO;YACL,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,IAAI,EAAE,QAAiB;YACvB,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;SACxD,CAAC;IACJ,CAAC,CAAC,CAAC;AACP,CAAC;AAED,SAAS,aAAa,CAAC,WAAmB;IACxC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,CACF,YAAY,WAAW,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,EAC3F,EAAE,OAAO,EAAE,WAAW,EAAE,EACxB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;YACzB,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,CAAC,IAAI,KAAK,CAAC,eAAe,MAAM,CAAC,QAAQ,EAAE,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YACzE,CAAC;iBAAM,CAAC;gBACN,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,WAAmB,EAAE,SAAiB;IACjE,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACxC,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAElD,MAAM,QAAQ,GAAG,cAAc,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;IAC5C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAE5C,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CACxB,KAAK,QAAQ,8DAA8D,QAAQ,6CAA6C,CACjI,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,QAAQ,QAAQ,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;IAEzD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE;QACtC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,iCAAiC,QAAQ,EAAE,EAAE;QACxE,IAAI;QACJ,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,cAAc,CAAC;KAC5C,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,kBAAkB,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,OAAO,IAAI,CAAC,IAAI,CAAC;AACnB,CAAC;AAED,KAAK,UAAU,OAAO,CAAC,WAAmB;IACxC,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,wBAAwB;IAC1B,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"backup.js","sourceRoot":"","sources":["../../src/handlers/backup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { AgentApi } from '../api.js';
2
+ import type { BoardState, BoardEvent, BoardInfo } from '../types.js';
3
+ export declare class BoardHandler {
4
+ private boardState;
5
+ private myColumnIds;
6
+ private boardId;
7
+ private api;
8
+ constructor(api: AgentApi);
9
+ initialize(): Promise<string | null>;
10
+ onBoardEvent(event: BoardEvent): Promise<void>;
11
+ tryClaimCard(cardId: string): Promise<void>;
12
+ startTask(cardId: string): Promise<void>;
13
+ completeTask(cardId: string): void;
14
+ checkQueue(existingData?: BoardInfo): Promise<void>;
15
+ getBoardStatus(): BoardState;
16
+ getBoardId(): string | null;
17
+ private execOpenclaw;
18
+ }
@@ -0,0 +1,141 @@
1
+ import { exec } from 'node:child_process';
2
+ const OPENCLAW_TIMEOUT = 30_000;
3
+ export class BoardHandler {
4
+ boardState = { state: 'idle', cardId: null };
5
+ myColumnIds = [];
6
+ boardId = null;
7
+ api;
8
+ constructor(api) {
9
+ this.api = api;
10
+ }
11
+ async initialize() {
12
+ try {
13
+ const data = await this.api.get('/api/agent/board');
14
+ this.boardId = data.board.id;
15
+ this.myColumnIds = data.board.myColumnIds;
16
+ console.log(`Board initialized: ${this.boardId}, watching ${this.myColumnIds.length} column(s)`);
17
+ // Backfill: check for existing unassigned cards in my columns
18
+ await this.checkQueue(data);
19
+ return this.boardId;
20
+ }
21
+ catch (err) {
22
+ console.error('Board initialization failed:', err instanceof Error ? err.message : err);
23
+ return null;
24
+ }
25
+ }
26
+ async onBoardEvent(event) {
27
+ switch (event.event) {
28
+ case 'card:entered':
29
+ if (!event.columnId || !this.myColumnIds.includes(event.columnId))
30
+ return;
31
+ if (this.boardState.state === 'working')
32
+ return;
33
+ await this.tryClaimCard(event.cardId);
34
+ break;
35
+ case 'card:claimed':
36
+ case 'card:moved':
37
+ case 'card:commented':
38
+ // Info only, ignore
39
+ break;
40
+ }
41
+ }
42
+ async tryClaimCard(cardId) {
43
+ try {
44
+ const res = await this.api.post(`/api/agent/board/cards/${cardId}/claim`);
45
+ if (res.ok) {
46
+ this.boardState = { state: 'working', cardId };
47
+ console.log(`Claimed card: ${cardId}`);
48
+ await this.startTask(cardId);
49
+ }
50
+ else if (res.status === 409) {
51
+ console.log(`Card ${cardId} already claimed or agent busy, skipping`);
52
+ }
53
+ else {
54
+ const body = await res.text().catch(() => '');
55
+ console.error(`Claim card ${cardId} failed (HTTP ${res.status}): ${body}`);
56
+ }
57
+ }
58
+ catch (err) {
59
+ console.error('tryClaimCard error:', err instanceof Error ? err.message : err);
60
+ }
61
+ }
62
+ async startTask(cardId) {
63
+ try {
64
+ const data = await this.api.get(`/api/agent/board/cards/${cardId}`);
65
+ const card = data.card;
66
+ const message = [
67
+ `New task from board: ${card.title}`,
68
+ card.description ? `Description: ${card.description}` : null,
69
+ card.priority ? `Priority: ${card.priority}` : null,
70
+ '',
71
+ 'Instructions:',
72
+ '- Complete the task described above.',
73
+ '- Post progress and results as comments using: POST /api/agent/board/cards/' + cardId + '/comments',
74
+ '- When done, move the card forward using: POST /api/agent/board/cards/' + cardId + '/move',
75
+ ].filter(Boolean).join('\n');
76
+ await this.execOpenclaw(message);
77
+ }
78
+ catch (err) {
79
+ console.error(`startTask(${cardId}) error:`, err instanceof Error ? err.message : err);
80
+ }
81
+ }
82
+ completeTask(cardId) {
83
+ console.log(`Task completed: ${cardId}`);
84
+ this.boardState = { state: 'idle', cardId: null };
85
+ this.checkQueue().catch((err) => {
86
+ console.error('checkQueue after completeTask error:', err instanceof Error ? err.message : err);
87
+ });
88
+ }
89
+ async checkQueue(existingData) {
90
+ try {
91
+ const data = existingData ?? await this.api.get('/api/agent/board');
92
+ const candidates = [];
93
+ for (const col of data.board.columns) {
94
+ if (!this.myColumnIds.includes(col.id))
95
+ continue;
96
+ for (const card of col.cards) {
97
+ if (!card.assignedAgentId) {
98
+ candidates.push(card);
99
+ }
100
+ }
101
+ }
102
+ if (candidates.length === 0)
103
+ return;
104
+ // Sort by priority (high > medium > low) then by creation date (oldest first)
105
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
106
+ candidates.sort((a, b) => {
107
+ const pa = priorityOrder[a.priority ?? 'medium'] ?? 2;
108
+ const pb = priorityOrder[b.priority ?? 'medium'] ?? 2;
109
+ if (pa !== pb)
110
+ return pa - pb;
111
+ return (a.createdAt ?? '').localeCompare(b.createdAt ?? '');
112
+ });
113
+ console.log(`Queue: ${candidates.length} unassigned card(s) in my columns, trying first`);
114
+ await this.tryClaimCard(candidates[0].id);
115
+ }
116
+ catch (err) {
117
+ console.error('checkQueue error:', err instanceof Error ? err.message : err);
118
+ }
119
+ }
120
+ getBoardStatus() {
121
+ return this.boardState;
122
+ }
123
+ getBoardId() {
124
+ return this.boardId;
125
+ }
126
+ execOpenclaw(message) {
127
+ return new Promise((resolve) => {
128
+ const escaped = message.replace(/'/g, "'\\''");
129
+ exec(`openclaw system event --text '${escaped}' --mode now`, { timeout: OPENCLAW_TIMEOUT }, (error, stdout, stderr) => {
130
+ if (error) {
131
+ console.error('openclaw system event failed:', stderr.toString() || error.message);
132
+ }
133
+ else {
134
+ console.log('openclaw system event sent');
135
+ }
136
+ resolve();
137
+ });
138
+ });
139
+ }
140
+ }
141
+ //# sourceMappingURL=board-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board-handler.js","sourceRoot":"","sources":["../../src/handlers/board-handler.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,9 @@
1
- import type { HeartbeatPayload } from './types.js';
1
+ import type { BoardState, HeartbeatPayload } from './types.js';
2
2
  export interface HeartbeatOptions {
3
3
  centrifugoInternalUrl: string;
4
4
  centrifugoApiKey: string;
5
5
  agentId: string;
6
+ getBoardStatus?: () => BoardState;
6
7
  }
7
8
  declare function getMetrics(): HeartbeatPayload['metrics'];
8
9
  export declare function startHeartbeat(opts: HeartbeatOptions): NodeJS.Timeout;
package/dist/heartbeat.js CHANGED
@@ -21,6 +21,7 @@ async function publishHeartbeat(opts) {
21
21
  agentId: opts.agentId,
22
22
  ts: Date.now(),
23
23
  metrics: getMetrics(),
24
+ ...(opts.getBoardStatus ? { boardStatus: opts.getBoardStatus() } : {}),
24
25
  };
25
26
  const controller = new AbortController();
26
27
  const timeout = setTimeout(() => controller.abort(), 5000);
@@ -1 +1 @@
1
- {"version":3,"file":"heartbeat.js","sourceRoot":"","sources":["../src/heartbeat.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"heartbeat.js","sourceRoot":"","sources":["../src/heartbeat.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { createConnection } from './connection.js';
2
2
  import { startHeartbeat } from './heartbeat.js';
3
+ import { createAgentApi } from './api.js';
4
+ import { BoardHandler } from './handlers/board-handler.js';
3
5
  function requireEnv(name) {
4
6
  const value = process.env[name];
5
7
  if (!value) {
@@ -22,10 +24,43 @@ export function main() {
22
24
  console.log(`Backend URL: ${backendUrl} (JWT mode)`);
23
25
  }
24
26
  const { client } = createConnection({ url, token, agentId, backendUrl: backendUrl || undefined });
27
+ // Board handler (optional — requires BACKEND_URL and AGENT_TOKEN)
28
+ let boardHandler = null;
29
+ const apiBaseUrl = process.env.BACKEND_URL || backendUrl;
30
+ if (apiBaseUrl && token) {
31
+ const api = createAgentApi(apiBaseUrl, token);
32
+ boardHandler = new BoardHandler(api);
33
+ // Initialize board handler after connection is established
34
+ boardHandler.initialize().then((boardId) => {
35
+ if (!boardId) {
36
+ console.log('No board found for this agent, board handler dormant');
37
+ return;
38
+ }
39
+ const boardChannel = `board:${boardId}`;
40
+ const boardSub = client.newSubscription(boardChannel);
41
+ boardSub.on('publication', (ctx) => {
42
+ const event = ctx.data;
43
+ if (!event || !event.event)
44
+ return;
45
+ boardHandler.onBoardEvent(event).catch((err) => {
46
+ console.error('Board event handler error:', err instanceof Error ? err.message : err);
47
+ });
48
+ });
49
+ boardSub.subscribe();
50
+ console.log(`Subscribed to board channel: ${boardChannel}`);
51
+ }).catch((err) => {
52
+ console.error('Board handler init error:', err instanceof Error ? err.message : err);
53
+ });
54
+ }
25
55
  let heartbeatTimer;
26
56
  if (centrifugoInternalUrl && centrifugoApiKey) {
27
57
  console.log(`Heartbeat via Centrifugo internal API: ${centrifugoInternalUrl}`);
28
- heartbeatTimer = startHeartbeat({ centrifugoInternalUrl, centrifugoApiKey, agentId });
58
+ heartbeatTimer = startHeartbeat({
59
+ centrifugoInternalUrl,
60
+ centrifugoApiKey,
61
+ agentId,
62
+ ...(boardHandler ? { getBoardStatus: () => boardHandler.getBoardStatus() } : {}),
63
+ });
29
64
  }
30
65
  else {
31
66
  console.warn('CENTRIFUGO_INTERNAL_URL or CENTRIFUGO_API_KEY not set, heartbeat disabled');
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEhD,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,0CAA0C,IAAI,EAAE,CAAC,CAAC;QAChE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,IAAI;IAClB,MAAM,MAAM,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAC5C,0CAA0C;IAC1C,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,QAAQ,CAAC,GAAG,uBAAuB,CAAC;IAC7G,MAAM,KAAK,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC;IACxC,MAAM,OAAO,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IACvC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,EAAE,CAAC;IAC1D,MAAM,qBAAqB,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,EAAE,CAAC;IACxE,MAAM,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,EAAE,CAAC;IAE9D,OAAO,CAAC,GAAG,CAAC,wCAAwC,OAAO,EAAE,CAAC,CAAC;IAC/D,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,CAAC,GAAG,CAAC,gBAAgB,UAAU,aAAa,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,EAAE,MAAM,EAAE,GAAG,gBAAgB,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,IAAI,SAAS,EAAE,CAAC,CAAC;IAElG,IAAI,cAA0C,CAAC;IAC/C,IAAI,qBAAqB,IAAI,gBAAgB,EAAE,CAAC;QAC9C,OAAO,CAAC,GAAG,CAAC,0CAA0C,qBAAqB,EAAE,CAAC,CAAC;QAC/E,cAAc,GAAG,cAAc,CAAC,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAC,CAAC;IACxF,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,IAAI,CAAC,2EAA2E,CAAC,CAAC;IAC5F,CAAC;IAED,MAAM,QAAQ,GAAG,GAAG,EAAE;QACpB,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QAChC,IAAI,cAAc;YAAE,aAAa,CAAC,cAAc,CAAC,CAAC;QAClD,MAAM,CAAC,UAAU,EAAE,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAClC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -25,4 +25,55 @@ export interface HeartbeatPayload {
25
25
  mem: number;
26
26
  uptime: number;
27
27
  };
28
+ boardStatus?: BoardState;
29
+ }
30
+ export interface BoardState {
31
+ state: 'idle' | 'working';
32
+ cardId: string | null;
33
+ }
34
+ export interface BoardEvent {
35
+ event: 'card:entered' | 'card:claimed' | 'card:moved' | 'card:commented';
36
+ cardId: string;
37
+ columnId?: string;
38
+ columnName?: string;
39
+ title?: string;
40
+ description?: string;
41
+ priority?: string;
42
+ agentId?: string;
43
+ fromColumnId?: string;
44
+ toColumnId?: string;
45
+ authorType?: string;
46
+ authorId?: string;
47
+ content?: string;
48
+ }
49
+ export interface BoardInfo {
50
+ board: {
51
+ id: string;
52
+ columns: Array<{
53
+ id: string;
54
+ name: string;
55
+ type: string;
56
+ position: number;
57
+ cards: Array<{
58
+ id: string;
59
+ title: string;
60
+ description?: string;
61
+ priority?: string;
62
+ assignedAgentId?: string | null;
63
+ createdAt?: string;
64
+ }>;
65
+ }>;
66
+ myColumnIds: string[];
67
+ };
68
+ }
69
+ export interface CardDetail {
70
+ card: {
71
+ id: string;
72
+ title: string;
73
+ description?: string;
74
+ priority?: string;
75
+ columnId: string;
76
+ columnName?: string;
77
+ assignedAgentId?: string | null;
78
+ };
28
79
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw-cloud/agent-controller",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/api.ts ADDED
@@ -0,0 +1,40 @@
1
+ const TIMEOUT = 10_000;
2
+
3
+ export interface AgentApi {
4
+ get(path: string): Promise<unknown>;
5
+ post(path: string, body?: unknown): Promise<Response>;
6
+ }
7
+
8
+ export function createAgentApi(backendUrl: string, agentToken: string): AgentApi {
9
+ async function request(method: string, path: string, body?: unknown): Promise<Response> {
10
+ const controller = new AbortController();
11
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT);
12
+ try {
13
+ const res = await fetch(`${backendUrl}${path}`, {
14
+ method,
15
+ headers: {
16
+ 'Content-Type': 'application/json',
17
+ 'Authorization': `Bearer ${agentToken}`,
18
+ },
19
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
20
+ signal: controller.signal,
21
+ });
22
+ return res;
23
+ } finally {
24
+ clearTimeout(timeout);
25
+ }
26
+ }
27
+
28
+ return {
29
+ async get(path: string): Promise<unknown> {
30
+ const res = await request('GET', path);
31
+ if (!res.ok) {
32
+ throw new Error(`GET ${path} failed: HTTP ${res.status}`);
33
+ }
34
+ return res.json();
35
+ },
36
+ async post(path: string, body?: unknown): Promise<Response> {
37
+ return request('POST', path, body);
38
+ },
39
+ };
40
+ }
@@ -10,6 +10,7 @@ const UPLOAD_TIMEOUT = 120_000;
10
10
 
11
11
  export function handleBackup(command: AgentCommand): Promise<AgentResponse> {
12
12
  const uploadUrl = command.payload.uploadUrl as string;
13
+ const token = command.payload.token as string;
13
14
  if (!uploadUrl) {
14
15
  return Promise.resolve({
15
16
  id: command.id,
@@ -24,7 +25,7 @@ export function handleBackup(command: AgentCommand): Promise<AgentResponse> {
24
25
  const archivePath = path.join(BACKUP_DIR, filename);
25
26
 
26
27
  return createArchive(archivePath)
27
- .then(() => uploadArchive(archivePath, uploadUrl))
28
+ .then(() => uploadArchive(archivePath, uploadUrl, token))
28
29
  .then(async (size) => {
29
30
  await cleanup(archivePath);
30
31
  return {
@@ -61,28 +62,25 @@ function createArchive(archivePath: string): Promise<void> {
61
62
  });
62
63
  }
63
64
 
64
- async function uploadArchive(archivePath: string, uploadUrl: string): Promise<number> {
65
+ async function uploadArchive(archivePath: string, uploadUrl: string, token?: string): Promise<number> {
65
66
  const stat = await fs.stat(archivePath);
66
67
  const fileBuffer = await fs.readFile(archivePath);
67
68
 
68
- const boundary = `----backup-${Date.now()}`;
69
- const filename = path.basename(archivePath);
70
-
71
- const header = Buffer.from(
72
- `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${filename}"\r\nContent-Type: application/gzip\r\n\r\n`,
73
- );
74
- const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
75
- const body = Buffer.concat([header, fileBuffer, footer]);
69
+ const headers: Record<string, string> = { 'Content-Type': 'application/gzip' };
70
+ if (token) {
71
+ headers['Authorization'] = `Bearer ${token}`;
72
+ }
76
73
 
77
74
  const response = await fetch(uploadUrl, {
78
75
  method: 'POST',
79
- headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` },
80
- body,
76
+ headers,
77
+ body: fileBuffer,
81
78
  signal: AbortSignal.timeout(UPLOAD_TIMEOUT),
82
79
  });
83
80
 
84
81
  if (!response.ok) {
85
- throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
82
+ const text = await response.text().catch(() => '');
83
+ throw new Error(`Upload failed: ${response.status} ${response.statusText} ${text}`);
86
84
  }
87
85
 
88
86
  return stat.size;
@@ -0,0 +1,152 @@
1
+ import { exec } from 'node:child_process';
2
+ import type { AgentApi } from '../api.js';
3
+ import type { BoardState, BoardEvent, BoardInfo, CardDetail } from '../types.js';
4
+
5
+ const OPENCLAW_TIMEOUT = 30_000;
6
+
7
+ export class BoardHandler {
8
+ private boardState: BoardState = { state: 'idle', cardId: null };
9
+ private myColumnIds: string[] = [];
10
+ private boardId: string | null = null;
11
+ private api: AgentApi;
12
+
13
+ constructor(api: AgentApi) {
14
+ this.api = api;
15
+ }
16
+
17
+ async initialize(): Promise<string | null> {
18
+ try {
19
+ const data = await this.api.get('/api/agent/board') as BoardInfo;
20
+ this.boardId = data.board.id;
21
+ this.myColumnIds = data.board.myColumnIds;
22
+ console.log(`Board initialized: ${this.boardId}, watching ${this.myColumnIds.length} column(s)`);
23
+
24
+ // Backfill: check for existing unassigned cards in my columns
25
+ await this.checkQueue(data);
26
+ return this.boardId;
27
+ } catch (err) {
28
+ console.error('Board initialization failed:', err instanceof Error ? err.message : err);
29
+ return null;
30
+ }
31
+ }
32
+
33
+ async onBoardEvent(event: BoardEvent): Promise<void> {
34
+ switch (event.event) {
35
+ case 'card:entered':
36
+ if (!event.columnId || !this.myColumnIds.includes(event.columnId)) return;
37
+ if (this.boardState.state === 'working') return;
38
+ await this.tryClaimCard(event.cardId);
39
+ break;
40
+ case 'card:claimed':
41
+ case 'card:moved':
42
+ case 'card:commented':
43
+ // Info only, ignore
44
+ break;
45
+ }
46
+ }
47
+
48
+ async tryClaimCard(cardId: string): Promise<void> {
49
+ try {
50
+ const res = await this.api.post(`/api/agent/board/cards/${cardId}/claim`);
51
+ if (res.ok) {
52
+ this.boardState = { state: 'working', cardId };
53
+ console.log(`Claimed card: ${cardId}`);
54
+ await this.startTask(cardId);
55
+ } else if (res.status === 409) {
56
+ console.log(`Card ${cardId} already claimed or agent busy, skipping`);
57
+ } else {
58
+ const body = await res.text().catch(() => '');
59
+ console.error(`Claim card ${cardId} failed (HTTP ${res.status}): ${body}`);
60
+ }
61
+ } catch (err) {
62
+ console.error('tryClaimCard error:', err instanceof Error ? err.message : err);
63
+ }
64
+ }
65
+
66
+ async startTask(cardId: string): Promise<void> {
67
+ try {
68
+ const data = await this.api.get(`/api/agent/board/cards/${cardId}`) as CardDetail;
69
+ const card = data.card;
70
+ const message = [
71
+ `New task from board: ${card.title}`,
72
+ card.description ? `Description: ${card.description}` : null,
73
+ card.priority ? `Priority: ${card.priority}` : null,
74
+ '',
75
+ 'Instructions:',
76
+ '- Complete the task described above.',
77
+ '- Post progress and results as comments using: POST /api/agent/board/cards/' + cardId + '/comments',
78
+ '- When done, move the card forward using: POST /api/agent/board/cards/' + cardId + '/move',
79
+ ].filter(Boolean).join('\n');
80
+
81
+ await this.execOpenclaw(message);
82
+ } catch (err) {
83
+ console.error(`startTask(${cardId}) error:`, err instanceof Error ? err.message : err);
84
+ }
85
+ }
86
+
87
+ completeTask(cardId: string): void {
88
+ console.log(`Task completed: ${cardId}`);
89
+ this.boardState = { state: 'idle', cardId: null };
90
+ this.checkQueue().catch((err) => {
91
+ console.error('checkQueue after completeTask error:', err instanceof Error ? err.message : err);
92
+ });
93
+ }
94
+
95
+ async checkQueue(existingData?: BoardInfo): Promise<void> {
96
+ try {
97
+ const data = existingData ?? (await this.api.get('/api/agent/board') as BoardInfo);
98
+ const candidates: Array<{ id: string; priority?: string; createdAt?: string }> = [];
99
+
100
+ for (const col of data.board.columns) {
101
+ if (!this.myColumnIds.includes(col.id)) continue;
102
+ for (const card of col.cards) {
103
+ if (!card.assignedAgentId) {
104
+ candidates.push(card);
105
+ }
106
+ }
107
+ }
108
+
109
+ if (candidates.length === 0) return;
110
+
111
+ // Sort by priority (high > medium > low) then by creation date (oldest first)
112
+ const priorityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
113
+ candidates.sort((a, b) => {
114
+ const pa = priorityOrder[a.priority ?? 'medium'] ?? 2;
115
+ const pb = priorityOrder[b.priority ?? 'medium'] ?? 2;
116
+ if (pa !== pb) return pa - pb;
117
+ return (a.createdAt ?? '').localeCompare(b.createdAt ?? '');
118
+ });
119
+
120
+ console.log(`Queue: ${candidates.length} unassigned card(s) in my columns, trying first`);
121
+ await this.tryClaimCard(candidates[0].id);
122
+ } catch (err) {
123
+ console.error('checkQueue error:', err instanceof Error ? err.message : err);
124
+ }
125
+ }
126
+
127
+ getBoardStatus(): BoardState {
128
+ return this.boardState;
129
+ }
130
+
131
+ getBoardId(): string | null {
132
+ return this.boardId;
133
+ }
134
+
135
+ private execOpenclaw(message: string): Promise<void> {
136
+ return new Promise((resolve) => {
137
+ const escaped = message.replace(/'/g, "'\\''");
138
+ exec(
139
+ `openclaw system event --text '${escaped}' --mode now`,
140
+ { timeout: OPENCLAW_TIMEOUT },
141
+ (error, stdout, stderr) => {
142
+ if (error) {
143
+ console.error('openclaw system event failed:', stderr.toString() || error.message);
144
+ } else {
145
+ console.log('openclaw system event sent');
146
+ }
147
+ resolve();
148
+ },
149
+ );
150
+ });
151
+ }
152
+ }
package/src/heartbeat.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import os from 'node:os';
2
- import type { HeartbeatPayload } from './types.js';
2
+ import type { BoardState, HeartbeatPayload } from './types.js';
3
3
 
4
4
  const HEARTBEAT_INTERVAL = 30_000;
5
5
  const HEARTBEAT_CHANNEL = 'heartbeat:data';
@@ -8,6 +8,7 @@ export interface HeartbeatOptions {
8
8
  centrifugoInternalUrl: string;
9
9
  centrifugoApiKey: string;
10
10
  agentId: string;
11
+ getBoardStatus?: () => BoardState;
11
12
  }
12
13
 
13
14
  function getMetrics(): HeartbeatPayload['metrics'] {
@@ -33,6 +34,7 @@ async function publishHeartbeat(opts: HeartbeatOptions): Promise<void> {
33
34
  agentId: opts.agentId,
34
35
  ts: Date.now(),
35
36
  metrics: getMetrics(),
37
+ ...(opts.getBoardStatus ? { boardStatus: opts.getBoardStatus() } : {}),
36
38
  };
37
39
 
38
40
  const controller = new AbortController();
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { createConnection } from './connection.js';
2
2
  import { startHeartbeat } from './heartbeat.js';
3
+ import { createAgentApi } from './api.js';
4
+ import { BoardHandler } from './handlers/board-handler.js';
5
+ import type { BoardEvent } from './types.js';
3
6
 
4
7
  function requireEnv(name: string): string {
5
8
  const value = process.env[name];
@@ -27,10 +30,47 @@ export function main(): void {
27
30
 
28
31
  const { client } = createConnection({ url, token, agentId, backendUrl: backendUrl || undefined });
29
32
 
33
+ // Board handler (optional — requires BACKEND_URL and AGENT_TOKEN)
34
+ let boardHandler: BoardHandler | null = null;
35
+ const apiBaseUrl = process.env.BACKEND_URL || backendUrl;
36
+ if (apiBaseUrl && token) {
37
+ const api = createAgentApi(apiBaseUrl, token);
38
+ boardHandler = new BoardHandler(api);
39
+
40
+ // Initialize board handler after connection is established
41
+ boardHandler.initialize().then((boardId) => {
42
+ if (!boardId) {
43
+ console.log('No board found for this agent, board handler dormant');
44
+ return;
45
+ }
46
+
47
+ const boardChannel = `board:${boardId}`;
48
+ const boardSub = client.newSubscription(boardChannel);
49
+
50
+ boardSub.on('publication', (ctx) => {
51
+ const event = ctx.data as BoardEvent;
52
+ if (!event || !event.event) return;
53
+ boardHandler!.onBoardEvent(event).catch((err) => {
54
+ console.error('Board event handler error:', err instanceof Error ? err.message : err);
55
+ });
56
+ });
57
+
58
+ boardSub.subscribe();
59
+ console.log(`Subscribed to board channel: ${boardChannel}`);
60
+ }).catch((err) => {
61
+ console.error('Board handler init error:', err instanceof Error ? err.message : err);
62
+ });
63
+ }
64
+
30
65
  let heartbeatTimer: NodeJS.Timeout | undefined;
31
66
  if (centrifugoInternalUrl && centrifugoApiKey) {
32
67
  console.log(`Heartbeat via Centrifugo internal API: ${centrifugoInternalUrl}`);
33
- heartbeatTimer = startHeartbeat({ centrifugoInternalUrl, centrifugoApiKey, agentId });
68
+ heartbeatTimer = startHeartbeat({
69
+ centrifugoInternalUrl,
70
+ centrifugoApiKey,
71
+ agentId,
72
+ ...(boardHandler ? { getBoardStatus: () => boardHandler!.getBoardStatus() } : {}),
73
+ });
34
74
  } else {
35
75
  console.warn('CENTRIFUGO_INTERNAL_URL or CENTRIFUGO_API_KEY not set, heartbeat disabled');
36
76
  }
package/src/types.ts CHANGED
@@ -29,4 +29,59 @@ export interface HeartbeatPayload {
29
29
  mem: number;
30
30
  uptime: number;
31
31
  };
32
+ boardStatus?: BoardState;
33
+ }
34
+
35
+ export interface BoardState {
36
+ state: 'idle' | 'working';
37
+ cardId: string | null;
38
+ }
39
+
40
+ export interface BoardEvent {
41
+ event: 'card:entered' | 'card:claimed' | 'card:moved' | 'card:commented';
42
+ cardId: string;
43
+ columnId?: string;
44
+ columnName?: string;
45
+ title?: string;
46
+ description?: string;
47
+ priority?: string;
48
+ agentId?: string;
49
+ fromColumnId?: string;
50
+ toColumnId?: string;
51
+ authorType?: string;
52
+ authorId?: string;
53
+ content?: string;
54
+ }
55
+
56
+ export interface BoardInfo {
57
+ board: {
58
+ id: string;
59
+ columns: Array<{
60
+ id: string;
61
+ name: string;
62
+ type: string;
63
+ position: number;
64
+ cards: Array<{
65
+ id: string;
66
+ title: string;
67
+ description?: string;
68
+ priority?: string;
69
+ assignedAgentId?: string | null;
70
+ createdAt?: string;
71
+ }>;
72
+ }>;
73
+ myColumnIds: string[];
74
+ };
75
+ }
76
+
77
+ export interface CardDetail {
78
+ card: {
79
+ id: string;
80
+ title: string;
81
+ description?: string;
82
+ priority?: string;
83
+ columnId: string;
84
+ columnName?: string;
85
+ assignedAgentId?: string | null;
86
+ };
32
87
  }