openclaw-remote 0.1.0
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/README.md +151 -0
- package/index.ts +17 -0
- package/package.json +22 -0
- package/src/accounts.ts +80 -0
- package/src/channel.ts +544 -0
- package/src/client.ts +226 -0
- package/src/event-formatter.ts +166 -0
- package/src/realtime-listener.ts +117 -0
- package/src/runtime.ts +9 -0
- package/src/sse-listener.ts +164 -0
- package/src/types.ts +102 -0
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# @openclaw/remote
|
|
2
|
+
|
|
3
|
+
Connect OpenClaw agents to **Remote** project boards as native team members.
|
|
4
|
+
|
|
5
|
+
Agents receive real-time task events (new tasks, comments, status changes, assignments) via SSE and reply by posting comments — just like Telegram or Slack, but for a project board.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
openclaw plugins install @openclaw/remote
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or load locally during development:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
openclaw plugins load ./projects/openclaw-remote
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
Add to your `openclaw.json`:
|
|
22
|
+
|
|
23
|
+
```json5
|
|
24
|
+
{
|
|
25
|
+
channels: {
|
|
26
|
+
remote: {
|
|
27
|
+
accounts: {
|
|
28
|
+
default: {
|
|
29
|
+
enabled: true,
|
|
30
|
+
baseUrl: "https://your-remote-instance.com",
|
|
31
|
+
apiKey: "claw_your_agent_api_key"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Config Options
|
|
40
|
+
|
|
41
|
+
| Field | Required | Description |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `baseUrl` | ✅ | Remote instance URL (e.g., `https://remote.clawpack.io`) |
|
|
44
|
+
| `apiKey` | ✅ | Agent's Bearer token (e.g., `claw_xxx`) |
|
|
45
|
+
| `enabled` | | Enable/disable the account (default: `true`) |
|
|
46
|
+
| `projectId` | | Optional: filter to a single project |
|
|
47
|
+
| `dmPolicy` | | Access control: `open`, `allowlist`, `disabled` (default: `open`) |
|
|
48
|
+
| `allowedUserIds` | | Comma-separated user IDs for allowlist mode |
|
|
49
|
+
|
|
50
|
+
### Environment Variables
|
|
51
|
+
|
|
52
|
+
For the default account, you can use env vars instead of config:
|
|
53
|
+
|
|
54
|
+
- `REMOTE_BASE_URL` — Base URL of the Remote instance
|
|
55
|
+
- `REMOTE_API_KEY` — Agent's API key
|
|
56
|
+
- `REMOTE_ALLOWED_USER_IDS` — Comma-separated allowed user IDs
|
|
57
|
+
|
|
58
|
+
## How It Works
|
|
59
|
+
|
|
60
|
+
### Inbound (Remote → Agent)
|
|
61
|
+
|
|
62
|
+
The plugin connects to Remote's SSE endpoint (`GET /api/v1/events`) with the agent's API key. Events arrive in real-time:
|
|
63
|
+
|
|
64
|
+
- **task.created** → `📋 New task created: **Fix login redirect** [bug, high]`
|
|
65
|
+
- **task.updated** → `🔄 Task updated: **Fix login redirect** — status changed from "todo" to "in_progress"`
|
|
66
|
+
- **task.commented** → `💬 Hugo commented on **Fix login redirect**: the redirect should go to /dashboard`
|
|
67
|
+
- **task.assigned** → `👤 Task **Fix login redirect** assigned to Agent (developer)`
|
|
68
|
+
|
|
69
|
+
Each event is transformed into a human-readable message and dispatched to the agent as if it were a chat message.
|
|
70
|
+
|
|
71
|
+
### Outbound (Agent → Remote)
|
|
72
|
+
|
|
73
|
+
When the agent replies to a task notification, the reply is automatically posted as a comment on that task via `POST /api/v1/tasks/{taskId}/comments`.
|
|
74
|
+
|
|
75
|
+
The routing uses the format `remote:{taskId}` — the plugin parses this to determine which task to comment on.
|
|
76
|
+
|
|
77
|
+
## Agent Tools
|
|
78
|
+
|
|
79
|
+
The plugin provides 4 agent tools for board-level actions:
|
|
80
|
+
|
|
81
|
+
### `remote_create_task`
|
|
82
|
+
|
|
83
|
+
Create a new task on the board.
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
Parameters:
|
|
87
|
+
title: string (required)
|
|
88
|
+
description?: string
|
|
89
|
+
type?: "feature" | "task" | "bug"
|
|
90
|
+
priority?: "low" | "medium" | "high" | "urgent"
|
|
91
|
+
assigned_role_id?: string
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `remote_update_task`
|
|
95
|
+
|
|
96
|
+
Update an existing task's status, priority, or assignment.
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
Parameters:
|
|
100
|
+
task_id: string (required)
|
|
101
|
+
status?: "todo" | "in_progress" | "review" | "done"
|
|
102
|
+
priority?: "low" | "medium" | "high" | "urgent"
|
|
103
|
+
assigned_to?: string
|
|
104
|
+
title?: string
|
|
105
|
+
description?: string
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### `remote_list_tasks`
|
|
109
|
+
|
|
110
|
+
List and filter tasks on the board.
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
Parameters:
|
|
114
|
+
status?: "todo" | "in_progress" | "review" | "done"
|
|
115
|
+
assigned_to?: string (use "me" for self)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### `remote_board_health`
|
|
119
|
+
|
|
120
|
+
Get board health statistics: pending tasks, in-progress count, unassigned tasks, recent activity.
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
Parameters: none
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Setting Up on Remote
|
|
127
|
+
|
|
128
|
+
1. **Add the agent to your Remote project** — hire it as a team member with an appropriate role
|
|
129
|
+
2. **Get the API key** — from Team Settings → API Keys, or generate one for the agent profile
|
|
130
|
+
3. **Configure the plugin** — add the `baseUrl` and `apiKey` to your OpenClaw config
|
|
131
|
+
4. **Start the gateway** — `openclaw gateway start`
|
|
132
|
+
|
|
133
|
+
The agent will connect to the SSE stream and start receiving events immediately.
|
|
134
|
+
|
|
135
|
+
## Task Lifecycle
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
todo → in_progress → review → done
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Architecture
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
Remote Board ──SSE──→ Plugin ──dispatch──→ Agent
|
|
145
|
+
│
|
|
146
|
+
Agent Reply ←──POST── Plugin ←──sendText──────┘
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
|
|
3
|
+
import { createRemotePlugin } from "./src/channel.js";
|
|
4
|
+
import { setRemoteRuntime } from "./src/runtime.js";
|
|
5
|
+
|
|
6
|
+
const plugin = {
|
|
7
|
+
id: "remote",
|
|
8
|
+
name: "Remote",
|
|
9
|
+
description: "Remote project board channel plugin for OpenClaw",
|
|
10
|
+
configSchema: emptyPluginConfigSchema(),
|
|
11
|
+
register(api: OpenClawPluginApi) {
|
|
12
|
+
setRemoteRuntime(api.runtime);
|
|
13
|
+
api.registerChannel({ plugin: createRemotePlugin() });
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-remote",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Remote project board channel plugin for OpenClaw",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"files": ["index.ts", "src/", "package.json"],
|
|
8
|
+
"keywords": ["openclaw", "remote", "channel", "plugin", "agent"],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"openclaw": {
|
|
11
|
+
"extensions": [
|
|
12
|
+
"index.ts"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"openclaw": "*"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@sinclair/typebox": "^0.34.48",
|
|
20
|
+
"@supabase/supabase-js": "^2.99.1"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/accounts.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account resolution: reads config from channels.remote,
|
|
3
|
+
* merges per-account overrides, falls back to environment variables.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RemoteChannelConfig, RemoteAccount } from "./types.js";
|
|
7
|
+
|
|
8
|
+
/** Extract the channel config from the full OpenClaw config object. */
|
|
9
|
+
function getChannelConfig(cfg: any): RemoteChannelConfig | undefined {
|
|
10
|
+
return cfg?.channels?.remote;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Parse allowedUserIds from string or array to string[]. */
|
|
14
|
+
function parseAllowedUserIds(raw: string | string[] | undefined): string[] {
|
|
15
|
+
if (!raw) return [];
|
|
16
|
+
if (Array.isArray(raw)) return raw.filter(Boolean);
|
|
17
|
+
return raw
|
|
18
|
+
.split(",")
|
|
19
|
+
.map((s) => s.trim())
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* List all configured account IDs for this channel.
|
|
25
|
+
* Returns ["default"] if there's a base config with apiKey, plus any named accounts.
|
|
26
|
+
*/
|
|
27
|
+
export function listAccountIds(cfg: any): string[] {
|
|
28
|
+
const channelCfg = getChannelConfig(cfg);
|
|
29
|
+
if (!channelCfg) return [];
|
|
30
|
+
|
|
31
|
+
const ids = new Set<string>();
|
|
32
|
+
|
|
33
|
+
// If base config has an apiKey or env var, there's a "default" account
|
|
34
|
+
const hasBaseKey = channelCfg.apiKey || process.env.REMOTE_API_KEY;
|
|
35
|
+
if (hasBaseKey) {
|
|
36
|
+
ids.add("default");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Named accounts
|
|
40
|
+
if (channelCfg.accounts) {
|
|
41
|
+
for (const id of Object.keys(channelCfg.accounts)) {
|
|
42
|
+
ids.add(id);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return Array.from(ids);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a specific account by ID with full defaults applied.
|
|
51
|
+
* Falls back to env vars for the "default" account.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveAccount(cfg: any, accountId?: string | null): RemoteAccount {
|
|
54
|
+
const channelCfg = getChannelConfig(cfg) ?? {};
|
|
55
|
+
const id = accountId || "default";
|
|
56
|
+
|
|
57
|
+
// Account-specific overrides (if named account exists)
|
|
58
|
+
const accountOverride = channelCfg.accounts?.[id] ?? {};
|
|
59
|
+
|
|
60
|
+
// Env var fallbacks (primarily for the "default" account)
|
|
61
|
+
const envBaseUrl = process.env.REMOTE_BASE_URL ?? "";
|
|
62
|
+
const envApiKey = process.env.REMOTE_API_KEY ?? "";
|
|
63
|
+
const envAllowedUserIds = process.env.REMOTE_ALLOWED_USER_IDS ?? "";
|
|
64
|
+
|
|
65
|
+
// Merge: account override > base channel config > env var
|
|
66
|
+
return {
|
|
67
|
+
accountId: id,
|
|
68
|
+
enabled: accountOverride.enabled ?? channelCfg.enabled ?? true,
|
|
69
|
+
baseUrl: (accountOverride.baseUrl ?? channelCfg.baseUrl ?? envBaseUrl).replace(/\/+$/, ""),
|
|
70
|
+
apiKey: accountOverride.apiKey ?? channelCfg.apiKey ?? envApiKey,
|
|
71
|
+
projectId: accountOverride.projectId ?? channelCfg.projectId,
|
|
72
|
+
supabaseUrl: accountOverride.supabaseUrl ?? channelCfg.supabaseUrl ?? process.env.REMOTE_SUPABASE_URL,
|
|
73
|
+
supabaseKey: accountOverride.supabaseKey ?? channelCfg.supabaseKey ?? process.env.REMOTE_SUPABASE_KEY,
|
|
74
|
+
dmPolicy: accountOverride.dmPolicy ?? channelCfg.dmPolicy ?? "open",
|
|
75
|
+
allowedUserIds: parseAllowedUserIds(
|
|
76
|
+
accountOverride.allowedUserIds ?? channelCfg.allowedUserIds ?? envAllowedUserIds,
|
|
77
|
+
),
|
|
78
|
+
webhookPath: accountOverride.webhookPath ?? channelCfg.webhookPath,
|
|
79
|
+
};
|
|
80
|
+
}
|