agentspd 1.0.0 → 1.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 +77 -294
- package/dist/api.d.ts +66 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +147 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +114 -32
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +58 -41
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +2 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +111 -19
- package/dist/commands/openclaw.d.ts +3 -0
- package/dist/commands/openclaw.d.ts.map +1 -0
- package/dist/commands/openclaw.js +1033 -0
- package/dist/commands/policies.d.ts.map +1 -1
- package/dist/commands/policies.js +137 -41
- package/dist/commands/threats.d.ts.map +1 -1
- package/dist/commands/threats.js +75 -29
- package/dist/commands/webhooks.d.ts.map +1 -1
- package/dist/commands/webhooks.js +24 -21
- package/dist/commands/workspaces.d.ts +5 -0
- package/dist/commands/workspaces.d.ts.map +1 -0
- package/dist/commands/workspaces.js +514 -0
- package/dist/config.d.ts +6 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +34 -23
- package/dist/index.js +178 -155
- package/dist/openclaw-api.d.ts +81 -0
- package/dist/openclaw-api.d.ts.map +1 -0
- package/dist/openclaw-api.js +262 -0
- package/package.json +6 -6
|
@@ -0,0 +1,1033 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { api } from "../api.js";
|
|
6
|
+
import { getConfig, isAuthenticated, setConfig } from "../config.js";
|
|
7
|
+
import { invokeHook, listSessions, testConnection, } from "../openclaw-api.js";
|
|
8
|
+
import * as output from "../output.js";
|
|
9
|
+
const DEFAULT_GATEWAY_URL = process.env.OPENCLAW_GATEWAY_URL || "http://127.0.0.1:18789";
|
|
10
|
+
const OPENCLAW_CHANNELS = [
|
|
11
|
+
"last",
|
|
12
|
+
"whatsapp",
|
|
13
|
+
"telegram",
|
|
14
|
+
"discord",
|
|
15
|
+
"slack",
|
|
16
|
+
"signal",
|
|
17
|
+
"imessage",
|
|
18
|
+
"msteams",
|
|
19
|
+
];
|
|
20
|
+
// ── Default policy for openclaw init ─────────────────────────────────────────
|
|
21
|
+
const DEFAULT_OPENCLAW_POLICY = `version: "1.0"
|
|
22
|
+
name: "openclaw-agent-policy"
|
|
23
|
+
description: "Security policy for OpenClaw AI assistant — allows code, web, and file operations with guardrails"
|
|
24
|
+
|
|
25
|
+
settings:
|
|
26
|
+
defaultAction: deny
|
|
27
|
+
requireIdentity: true
|
|
28
|
+
log_all_actions: true
|
|
29
|
+
|
|
30
|
+
tools:
|
|
31
|
+
- pattern: "read_file"
|
|
32
|
+
action: allow
|
|
33
|
+
constraints: ["path must not contain .env or credentials"]
|
|
34
|
+
- pattern: "write_file"
|
|
35
|
+
action: allow
|
|
36
|
+
constraints: ["path must not contain /etc or /sys"]
|
|
37
|
+
- pattern: "list_directory"
|
|
38
|
+
action: allow
|
|
39
|
+
- pattern: "search_files"
|
|
40
|
+
action: allow
|
|
41
|
+
- pattern: "web_search"
|
|
42
|
+
action: allow
|
|
43
|
+
- pattern: "web_fetch"
|
|
44
|
+
action: allow
|
|
45
|
+
constraints: ["url must not contain internal or localhost"]
|
|
46
|
+
- pattern: "execute_command"
|
|
47
|
+
action: allow
|
|
48
|
+
constraints:
|
|
49
|
+
- "command must not contain rm -rf or sudo"
|
|
50
|
+
- "command must not contain curl.*| bash"
|
|
51
|
+
|
|
52
|
+
blocked_patterns:
|
|
53
|
+
- credential_exfiltration
|
|
54
|
+
- prompt_injection
|
|
55
|
+
- encoding_evasion
|
|
56
|
+
- base64_decode_secrets
|
|
57
|
+
|
|
58
|
+
rate_limits:
|
|
59
|
+
max_tool_calls_per_minute: 60
|
|
60
|
+
max_file_writes_per_minute: 20
|
|
61
|
+
`;
|
|
62
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
63
|
+
function getGatewayConfig() {
|
|
64
|
+
const config = getConfig();
|
|
65
|
+
return {
|
|
66
|
+
url: config.openclawGatewayUrl || DEFAULT_GATEWAY_URL,
|
|
67
|
+
token: config.openclawToken,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function requireGatewayConfig() {
|
|
71
|
+
const { url, token } = getGatewayConfig();
|
|
72
|
+
if (!token) {
|
|
73
|
+
output.error("OpenClaw Gateway not configured. Run `agentspd openclaw connect` first.");
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
return { url, token };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Auto-join an agent to a workspace via invite+join.
|
|
80
|
+
* If `agent` is provided, use it directly. Otherwise, discover the matching
|
|
81
|
+
* agent from the session name pattern.
|
|
82
|
+
*/
|
|
83
|
+
async function autoJoinAgent(session, workspaceId, agent) {
|
|
84
|
+
let matchingAgent = agent;
|
|
85
|
+
if (!matchingAgent) {
|
|
86
|
+
const agentsResult = await api.listAgents({ limit: 200 });
|
|
87
|
+
matchingAgent = (agentsResult.data?.items || []).find((a) => a.name === `openclaw-${session.key}` ||
|
|
88
|
+
a.name === `openclaw-${session.agentId || session.key}`);
|
|
89
|
+
}
|
|
90
|
+
if (!matchingAgent)
|
|
91
|
+
return false;
|
|
92
|
+
const inviteResult = await api.workspaceInvite(workspaceId, {
|
|
93
|
+
agentName: matchingAgent.name,
|
|
94
|
+
});
|
|
95
|
+
if (!inviteResult.data?.inviteToken)
|
|
96
|
+
return false;
|
|
97
|
+
const joinResult = await api.workspaceJoin(workspaceId, {
|
|
98
|
+
agentId: matchingAgent.id,
|
|
99
|
+
inviteToken: inviteResult.data.inviteToken,
|
|
100
|
+
});
|
|
101
|
+
return !joinResult.error;
|
|
102
|
+
}
|
|
103
|
+
// ── Command factory ──────────────────────────────────────────────────────────
|
|
104
|
+
export function createOpenClawCommand() {
|
|
105
|
+
const openclaw = new Command("openclaw")
|
|
106
|
+
.alias("oc")
|
|
107
|
+
.description("Manage OpenClaw Gateway integration");
|
|
108
|
+
// ── openclaw init ───────────────────────────────────────────────────────
|
|
109
|
+
openclaw
|
|
110
|
+
.command("init")
|
|
111
|
+
.description("Full bootstrap: connect, create policy, register agents, sync workspaces — in one command")
|
|
112
|
+
.option("-u, --url <url>", "Gateway URL", DEFAULT_GATEWAY_URL)
|
|
113
|
+
.option("-t, --token <token>", "Gateway auth token")
|
|
114
|
+
.option("-p, --policy-name <name>", "Policy name", "openclaw-agent-policy")
|
|
115
|
+
.option("-e, --environment <env>", "Agent environment", "production")
|
|
116
|
+
.option("--json", "Output as JSON")
|
|
117
|
+
.action(async (options) => {
|
|
118
|
+
// Summary counters (used across steps and in final output)
|
|
119
|
+
let orgName = "N/A";
|
|
120
|
+
let sessions = [];
|
|
121
|
+
let registered = 0;
|
|
122
|
+
let skipped = 0;
|
|
123
|
+
let wsCreated = 0;
|
|
124
|
+
let wsBridged = 0;
|
|
125
|
+
let wsJoined = 0;
|
|
126
|
+
let policyId;
|
|
127
|
+
const gatewayUrl = options.url.replace(/\/+$/, "");
|
|
128
|
+
// ── Step 1: Verify authentication ───────────────────────────
|
|
129
|
+
if (!isAuthenticated()) {
|
|
130
|
+
output.error('Not authenticated. Run `agentspd login -k "<your-api-key>"` first.');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const spinner1 = ora("Verifying authentication...").start();
|
|
134
|
+
const meResult = await api.me();
|
|
135
|
+
if (meResult.error) {
|
|
136
|
+
spinner1.fail("Authentication check failed");
|
|
137
|
+
output.error(meResult.error.message);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
orgName = meResult.data?.org?.name || "N/A";
|
|
141
|
+
const authLabel = meResult.data?.user?.name
|
|
142
|
+
? `${meResult.data.user.name} (${orgName})`
|
|
143
|
+
: orgName;
|
|
144
|
+
spinner1.succeed(`Authenticated as ${authLabel}`);
|
|
145
|
+
// ── Step 2: Connect to OpenClaw Gateway (idempotent) ────────
|
|
146
|
+
const token = options.token ||
|
|
147
|
+
process.env.OPENCLAW_GATEWAY_TOKEN ||
|
|
148
|
+
getConfig().openclawToken;
|
|
149
|
+
if (!token) {
|
|
150
|
+
output.error("No Gateway token found. Provide --token, set OPENCLAW_GATEWAY_TOKEN, or run `agentspd openclaw connect` first.");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Check if already connected with valid creds
|
|
154
|
+
const existing = getGatewayConfig();
|
|
155
|
+
let needConnect = true;
|
|
156
|
+
if (existing.token && existing.url === gatewayUrl) {
|
|
157
|
+
try {
|
|
158
|
+
const test = await testConnection(gatewayUrl, existing.token);
|
|
159
|
+
if (test.connected) {
|
|
160
|
+
needConnect = false;
|
|
161
|
+
sessions = test.sessions || [];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Will reconnect below
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (needConnect) {
|
|
169
|
+
const spinner2 = ora("Connecting to OpenClaw Gateway...").start();
|
|
170
|
+
const status = await testConnection(gatewayUrl, token);
|
|
171
|
+
if (!status.connected) {
|
|
172
|
+
spinner2.fail("Cannot connect to OpenClaw Gateway");
|
|
173
|
+
output.error(status.error || "Unknown error");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
setConfig("openclawGatewayUrl", gatewayUrl);
|
|
177
|
+
setConfig("openclawToken", token);
|
|
178
|
+
sessions = status.sessions || [];
|
|
179
|
+
spinner2.succeed(`Connected to Gateway (${status.latencyMs}ms, ${sessions.length} sessions)`);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
output.success(`Gateway already connected (${sessions.length} sessions)`);
|
|
183
|
+
}
|
|
184
|
+
// ── Step 3: Create default security policy (idempotent) ─────
|
|
185
|
+
const policyName = options.policyName;
|
|
186
|
+
const spinner3 = ora(`Creating security policy "${policyName}"...`).start();
|
|
187
|
+
try {
|
|
188
|
+
policyId = await api.resolvePolicy(policyName);
|
|
189
|
+
spinner3.succeed(`Policy "${policyName}" already exists — reusing`);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// Policy not found — create it
|
|
193
|
+
const policyResult = await api.createPolicy({
|
|
194
|
+
name: policyName,
|
|
195
|
+
description: "Security policy for OpenClaw AI assistant — deny-by-default with guardrails",
|
|
196
|
+
content: DEFAULT_OPENCLAW_POLICY,
|
|
197
|
+
});
|
|
198
|
+
if (policyResult.error) {
|
|
199
|
+
spinner3.fail("Failed to create policy");
|
|
200
|
+
output.error(policyResult.error.message);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
policyId = policyResult.data?.id;
|
|
204
|
+
if (policyId) {
|
|
205
|
+
await api.activatePolicy(policyId);
|
|
206
|
+
}
|
|
207
|
+
spinner3.succeed(`Policy "${policyName}" created and activated`);
|
|
208
|
+
}
|
|
209
|
+
// ── Step 4: Register agents (idempotent) ────────────────────
|
|
210
|
+
if (sessions.length === 0) {
|
|
211
|
+
output.info("No active sessions on Gateway — agent registration skipped");
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const existingAgents = await api.listAgents({ limit: 200 });
|
|
215
|
+
const existingNames = new Set((existingAgents.data?.items || []).map((a) => a.name));
|
|
216
|
+
for (const session of sessions) {
|
|
217
|
+
const name = `openclaw-${session.agentId || session.key || "agent"}`;
|
|
218
|
+
if (existingNames.has(name)) {
|
|
219
|
+
skipped++;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const regSpinner = ora(`Registering "${name}"...`).start();
|
|
223
|
+
const result = await api.createAgent({
|
|
224
|
+
name,
|
|
225
|
+
description: `OpenClaw agent (session: ${session.key})`,
|
|
226
|
+
environment: options.environment,
|
|
227
|
+
policyId,
|
|
228
|
+
});
|
|
229
|
+
if (result.error) {
|
|
230
|
+
regSpinner.fail(`Failed: "${name}"`);
|
|
231
|
+
output.error(result.error.message);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
regSpinner.succeed(`Registered "${name}"`);
|
|
235
|
+
registered++;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (registered > 0 || skipped > 0) {
|
|
239
|
+
output.success(`Agents: ${registered} registered, ${skipped} already existed`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// ── Step 5: Sync workspaces (idempotent) ────────────────────
|
|
243
|
+
if (sessions.length > 0) {
|
|
244
|
+
const spinner5 = ora("Bridging sessions to Emotos workspaces...").start();
|
|
245
|
+
const wsResult = await api.listWorkspaces({ status: "active" });
|
|
246
|
+
const existingWorkspaces = wsResult.data?.items || [];
|
|
247
|
+
const bridgedKeys = new Set(existingWorkspaces
|
|
248
|
+
.filter((w) => (w.purpose || "").includes("[oc-session:"))
|
|
249
|
+
.map((w) => {
|
|
250
|
+
const match = (w.purpose || "").match(/\[oc-session:([^\]]+)\]/);
|
|
251
|
+
return match ? match[1] : "";
|
|
252
|
+
})
|
|
253
|
+
.filter(Boolean));
|
|
254
|
+
const unbridged = sessions.filter((s) => !bridgedKeys.has(s.key));
|
|
255
|
+
// Create workspaces for unbridged sessions
|
|
256
|
+
for (const session of unbridged) {
|
|
257
|
+
const displayName = session.displayName ||
|
|
258
|
+
session.key.split(":").slice(-2).join(":");
|
|
259
|
+
const wsName = `oc:${displayName}`;
|
|
260
|
+
const wsCreateResult = await api.createWorkspace({
|
|
261
|
+
name: wsName,
|
|
262
|
+
purpose: `Bridged from OpenClaw [oc-session:${session.key}] (${session.channel || "webchat"})`,
|
|
263
|
+
mode: "hybrid",
|
|
264
|
+
maxParticipants: 20,
|
|
265
|
+
});
|
|
266
|
+
if (wsCreateResult.data?.id) {
|
|
267
|
+
await autoJoinAgent(session, wsCreateResult.data.id);
|
|
268
|
+
wsCreated++;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Auto-join agents to existing workspaces missing them
|
|
272
|
+
const allAgents = (await api.listAgents({ limit: 200 })).data?.items || [];
|
|
273
|
+
for (const session of sessions.filter((s) => bridgedKeys.has(s.key))) {
|
|
274
|
+
const ws = existingWorkspaces.find((w) => (w.purpose || "").includes(`[oc-session:${session.key}]`));
|
|
275
|
+
if (!ws)
|
|
276
|
+
continue;
|
|
277
|
+
const participants = ws.participants || [];
|
|
278
|
+
const namePatterns = [
|
|
279
|
+
`openclaw-${session.key}`,
|
|
280
|
+
`openclaw-${session.agentId || session.key}`,
|
|
281
|
+
];
|
|
282
|
+
if (participants.some((p) => namePatterns.includes(p.agentName)))
|
|
283
|
+
continue;
|
|
284
|
+
const matchingAgent = allAgents.find((a) => namePatterns.includes(a.name));
|
|
285
|
+
if (!matchingAgent)
|
|
286
|
+
continue;
|
|
287
|
+
const wsId = ws.id;
|
|
288
|
+
if (wsId &&
|
|
289
|
+
(await autoJoinAgent(session, wsId, matchingAgent))) {
|
|
290
|
+
wsJoined++;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
wsBridged = bridgedKeys.size;
|
|
294
|
+
spinner5.succeed(`Workspaces: ${wsCreated} created, ${wsBridged} already bridged` +
|
|
295
|
+
(wsJoined > 0 ? `, ${wsJoined} agents joined` : ""));
|
|
296
|
+
}
|
|
297
|
+
// ── Step 6: Summary ─────────────────────────────────────────
|
|
298
|
+
console.log();
|
|
299
|
+
if (options.json) {
|
|
300
|
+
output.printJson({
|
|
301
|
+
authenticated: true,
|
|
302
|
+
org: orgName,
|
|
303
|
+
gatewayUrl,
|
|
304
|
+
gatewayConnected: true,
|
|
305
|
+
policy: policyName,
|
|
306
|
+
policyId,
|
|
307
|
+
sessions: sessions.length,
|
|
308
|
+
agentsRegistered: registered,
|
|
309
|
+
agentsSkipped: skipped,
|
|
310
|
+
workspacesCreated: wsCreated,
|
|
311
|
+
workspacesBridged: wsBridged,
|
|
312
|
+
workspacesJoined: wsJoined,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
output.heading("OpenClaw Init Complete");
|
|
317
|
+
output.printKeyValue([
|
|
318
|
+
["Organization", orgName],
|
|
319
|
+
["Gateway", gatewayUrl],
|
|
320
|
+
[
|
|
321
|
+
"Policy",
|
|
322
|
+
`${policyName} (${policyId?.slice(0, 8) || "N/A"}...)`,
|
|
323
|
+
],
|
|
324
|
+
["Sessions", String(sessions.length)],
|
|
325
|
+
["Agents Registered", String(registered)],
|
|
326
|
+
["Agents Skipped", String(skipped)],
|
|
327
|
+
[
|
|
328
|
+
"Workspaces Synced",
|
|
329
|
+
String(wsCreated + wsBridged),
|
|
330
|
+
],
|
|
331
|
+
]);
|
|
332
|
+
console.log();
|
|
333
|
+
output.success("Bootstrap complete. Threat monitoring is active.");
|
|
334
|
+
console.log();
|
|
335
|
+
output.info("Next commands:");
|
|
336
|
+
console.log(` ${output.highlight("agentspd openclaw status")} — Integration health check`);
|
|
337
|
+
console.log(` ${output.highlight("agentspd threats list")} — View detected threats`);
|
|
338
|
+
console.log(` ${output.highlight("agentspd audit events")} — Review audit trail`);
|
|
339
|
+
console.log(` ${output.highlight("agentspd openclaw webhook")} — Set up threat alerting`);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
// ── openclaw connect ─────────────────────────────────────────────────────
|
|
343
|
+
openclaw
|
|
344
|
+
.command("connect")
|
|
345
|
+
.description("Configure and test connection to an OpenClaw Gateway")
|
|
346
|
+
.option("-u, --url <url>", "Gateway URL", DEFAULT_GATEWAY_URL)
|
|
347
|
+
.option("-t, --token <token>", "Gateway auth token")
|
|
348
|
+
.option("--json", "Output as JSON")
|
|
349
|
+
.action(async (options) => {
|
|
350
|
+
let gatewayUrl = options.url;
|
|
351
|
+
let token = options.token;
|
|
352
|
+
// Interactive prompts if not provided via flags
|
|
353
|
+
if (!token) {
|
|
354
|
+
const answers = await inquirer.prompt([
|
|
355
|
+
{
|
|
356
|
+
type: "input",
|
|
357
|
+
name: "url",
|
|
358
|
+
message: "OpenClaw Gateway URL:",
|
|
359
|
+
default: gatewayUrl,
|
|
360
|
+
validate: (input) => {
|
|
361
|
+
try {
|
|
362
|
+
new URL(input);
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return "Please enter a valid URL (e.g. http://127.0.0.1:18789)";
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
type: "password",
|
|
372
|
+
name: "token",
|
|
373
|
+
message: "Gateway auth token (from OPENCLAW_GATEWAY_TOKEN):",
|
|
374
|
+
validate: (input) => input.length > 0 || "Token is required",
|
|
375
|
+
},
|
|
376
|
+
]);
|
|
377
|
+
gatewayUrl = answers.url;
|
|
378
|
+
token = answers.token;
|
|
379
|
+
}
|
|
380
|
+
// Strip trailing slash
|
|
381
|
+
gatewayUrl = gatewayUrl.replace(/\/+$/, "");
|
|
382
|
+
const spinner = ora("Testing connection to OpenClaw Gateway...").start();
|
|
383
|
+
const status = await testConnection(gatewayUrl, token);
|
|
384
|
+
if (!status.connected) {
|
|
385
|
+
spinner.fail("Cannot connect to OpenClaw Gateway");
|
|
386
|
+
output.error(status.error || "Unknown error");
|
|
387
|
+
console.log();
|
|
388
|
+
output.info("Make sure your OpenClaw Gateway is running:");
|
|
389
|
+
console.log(` ${output.highlight("openclaw gateway run")}`);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
// Save config
|
|
393
|
+
setConfig("openclawGatewayUrl", gatewayUrl);
|
|
394
|
+
setConfig("openclawToken", token);
|
|
395
|
+
if (options.json) {
|
|
396
|
+
spinner.stop();
|
|
397
|
+
output.printJson({
|
|
398
|
+
connected: true,
|
|
399
|
+
gatewayUrl,
|
|
400
|
+
latencyMs: status.latencyMs,
|
|
401
|
+
sessions: status.sessions?.length ?? "unknown",
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
spinner.succeed("Connected to OpenClaw Gateway");
|
|
406
|
+
console.log();
|
|
407
|
+
output.printKeyValue([
|
|
408
|
+
["Gateway URL", gatewayUrl],
|
|
409
|
+
["Latency", `${status.latencyMs}ms`],
|
|
410
|
+
["Sessions", String(status.sessions?.length ?? "N/A")],
|
|
411
|
+
]);
|
|
412
|
+
console.log();
|
|
413
|
+
output.success("Configuration saved. Run `agentspd openclaw status` to check health.");
|
|
414
|
+
console.log();
|
|
415
|
+
output.info("Next steps:");
|
|
416
|
+
console.log(` ${output.highlight("agentspd openclaw register")} — Register OpenClaw agents in Emotos`);
|
|
417
|
+
console.log(` ${output.highlight("agentspd openclaw webhook")} — Set up threat alerting`);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
// ── openclaw register ────────────────────────────────────────────────────
|
|
421
|
+
openclaw
|
|
422
|
+
.command("register")
|
|
423
|
+
.description("Register OpenClaw agents as Emotos agents")
|
|
424
|
+
.option("-p, --policy <name>", "Policy to assign (name or UUID)")
|
|
425
|
+
.option("-e, --environment <env>", "Environment", "production")
|
|
426
|
+
.option("--all", "Register all discovered agents without prompting")
|
|
427
|
+
.option("--json", "Output as JSON")
|
|
428
|
+
.action(async (options) => {
|
|
429
|
+
const { url: gatewayUrl, token } = requireGatewayConfig();
|
|
430
|
+
// Step 1: List OpenClaw sessions/agents
|
|
431
|
+
const spinner = ora("Discovering OpenClaw agents...").start();
|
|
432
|
+
let sessions;
|
|
433
|
+
try {
|
|
434
|
+
sessions = await listSessions(gatewayUrl, token);
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
spinner.fail("Failed to list OpenClaw sessions");
|
|
438
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (!sessions || sessions.length === 0) {
|
|
442
|
+
spinner.warn("No active sessions found on OpenClaw Gateway");
|
|
443
|
+
output.info("Start an OpenClaw agent session first, then try again.");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
spinner.succeed(`Found ${sessions.length} OpenClaw session(s)`);
|
|
447
|
+
// Step 2: Resolve policy if specified
|
|
448
|
+
let policyId;
|
|
449
|
+
if (options.policy) {
|
|
450
|
+
try {
|
|
451
|
+
policyId = await api.resolvePolicy(options.policy);
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Step 3: Select which sessions to register
|
|
459
|
+
let selectedSessions = sessions;
|
|
460
|
+
if (!options.all && sessions.length > 1) {
|
|
461
|
+
const answers = await inquirer.prompt([
|
|
462
|
+
{
|
|
463
|
+
type: "checkbox",
|
|
464
|
+
name: "selected",
|
|
465
|
+
message: "Select agents to register:",
|
|
466
|
+
choices: sessions.map((s) => ({
|
|
467
|
+
name: `${s.agentId || s.key}${s.summary ? ` — ${s.summary}` : ""}`,
|
|
468
|
+
value: s,
|
|
469
|
+
checked: true,
|
|
470
|
+
})),
|
|
471
|
+
validate: (input) => input.length > 0 || "Select at least one agent",
|
|
472
|
+
},
|
|
473
|
+
]);
|
|
474
|
+
selectedSessions = answers.selected;
|
|
475
|
+
}
|
|
476
|
+
// Step 4: Register each in Emotos
|
|
477
|
+
const results = [];
|
|
478
|
+
for (const session of selectedSessions) {
|
|
479
|
+
const name = session.agentId || session.key || "openclaw-agent";
|
|
480
|
+
const regSpinner = ora(`Registering "${name}"...`).start();
|
|
481
|
+
const result = await api.createAgent({
|
|
482
|
+
name: `openclaw-${name}`,
|
|
483
|
+
description: `OpenClaw agent (session: ${session.key})`,
|
|
484
|
+
environment: options.environment,
|
|
485
|
+
policyId,
|
|
486
|
+
});
|
|
487
|
+
if (result.error) {
|
|
488
|
+
regSpinner.fail(`Failed to register "${name}"`);
|
|
489
|
+
results.push({ session: name, error: result.error.message });
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
regSpinner.succeed(`Registered "${name}"`);
|
|
493
|
+
results.push({ session: name, agentId: result.data?.id });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
// Step 5: Output results
|
|
497
|
+
console.log();
|
|
498
|
+
if (options.json) {
|
|
499
|
+
output.printJson(results);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
output.heading("Registration Results");
|
|
503
|
+
output.printTable(["OpenClaw Session", "Emotos Agent ID", "Status"], results.map((r) => [
|
|
504
|
+
r.session,
|
|
505
|
+
r.agentId || "—",
|
|
506
|
+
r.error ? chalk.red(r.error) : chalk.green("Registered"),
|
|
507
|
+
]));
|
|
508
|
+
const successCount = results.filter((r) => r.agentId).length;
|
|
509
|
+
if (successCount > 0) {
|
|
510
|
+
console.log();
|
|
511
|
+
output.info("Issue JWT tokens for proxy connections:");
|
|
512
|
+
for (const r of results) {
|
|
513
|
+
if (r.agentId) {
|
|
514
|
+
console.log(` ${output.highlight(`agentspd agents token ${r.agentId}`)}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
// ── openclaw sync ────────────────────────────────────────────────────────
|
|
521
|
+
openclaw
|
|
522
|
+
.command("sync")
|
|
523
|
+
.description("Show drift between OpenClaw and Emotos agent registrations")
|
|
524
|
+
.option("--reconcile", "Automatically register new agents")
|
|
525
|
+
.option("--json", "Output as JSON")
|
|
526
|
+
.action(async (options) => {
|
|
527
|
+
const { url: gatewayUrl, token } = requireGatewayConfig();
|
|
528
|
+
const spinner = ora("Syncing agent registrations...").start();
|
|
529
|
+
// Fetch both sides in parallel
|
|
530
|
+
let ocSessions;
|
|
531
|
+
try {
|
|
532
|
+
ocSessions = await listSessions(gatewayUrl, token);
|
|
533
|
+
}
|
|
534
|
+
catch (err) {
|
|
535
|
+
spinner.fail("Failed to list OpenClaw sessions");
|
|
536
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const emotosResult = await api.listAgents({ limit: 200 });
|
|
540
|
+
if (emotosResult.error) {
|
|
541
|
+
spinner.fail("Failed to list Emotos agents");
|
|
542
|
+
output.error(emotosResult.error.message);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
spinner.succeed("Fetched agent data from both platforms");
|
|
546
|
+
const emotosAgents = emotosResult.data?.items || [];
|
|
547
|
+
const ocNames = new Set(ocSessions.map((s) => `openclaw-${s.agentId || s.key}`));
|
|
548
|
+
const emotosNames = new Set(emotosAgents.map((a) => a.name));
|
|
549
|
+
// Compute drift
|
|
550
|
+
const unregistered = [...ocNames].filter((n) => !emotosNames.has(n));
|
|
551
|
+
const stale = emotosAgents.filter((a) => a.name.startsWith("openclaw-") && !ocNames.has(a.name));
|
|
552
|
+
if (options.json) {
|
|
553
|
+
output.printJson({
|
|
554
|
+
openclawSessions: ocSessions.length,
|
|
555
|
+
emotosAgents: emotosAgents.filter((a) => a.name.startsWith("openclaw-")).length,
|
|
556
|
+
unregistered,
|
|
557
|
+
stale: stale.map((a) => ({ id: a.id, name: a.name })),
|
|
558
|
+
});
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
console.log();
|
|
562
|
+
output.printKeyValue([
|
|
563
|
+
["OpenClaw sessions", String(ocSessions.length)],
|
|
564
|
+
[
|
|
565
|
+
"Emotos agents (openclaw-*)",
|
|
566
|
+
String(emotosAgents.filter((a) => a.name.startsWith("openclaw-")).length),
|
|
567
|
+
],
|
|
568
|
+
]);
|
|
569
|
+
console.log();
|
|
570
|
+
if (unregistered.length === 0 && stale.length === 0) {
|
|
571
|
+
output.success("All agents are in sync");
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (unregistered.length > 0) {
|
|
575
|
+
output.heading("Unregistered (in OpenClaw, not in Emotos)");
|
|
576
|
+
for (const name of unregistered) {
|
|
577
|
+
console.log(` ${chalk.yellow("+")} ${name}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (stale.length > 0) {
|
|
581
|
+
output.heading("Stale (in Emotos, not in OpenClaw)");
|
|
582
|
+
for (const agent of stale) {
|
|
583
|
+
console.log(` ${chalk.red("−")} ${agent.name} (${agent.id})`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (options.reconcile && unregistered.length > 0) {
|
|
587
|
+
console.log();
|
|
588
|
+
const regSpinner = ora(`Registering ${unregistered.length} new agent(s)...`).start();
|
|
589
|
+
let registered = 0;
|
|
590
|
+
for (const name of unregistered) {
|
|
591
|
+
const result = await api.createAgent({
|
|
592
|
+
name,
|
|
593
|
+
description: "Auto-registered from OpenClaw sync",
|
|
594
|
+
environment: "production",
|
|
595
|
+
});
|
|
596
|
+
if (!result.error)
|
|
597
|
+
registered++;
|
|
598
|
+
}
|
|
599
|
+
regSpinner.succeed(`Registered ${registered}/${unregistered.length} agent(s)`);
|
|
600
|
+
}
|
|
601
|
+
else if (unregistered.length > 0) {
|
|
602
|
+
console.log();
|
|
603
|
+
output.info("Run with --reconcile to auto-register new agents:");
|
|
604
|
+
console.log(` ${output.highlight("agentspd openclaw sync --reconcile")}`);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
// ── openclaw webhook ─────────────────────────────────────────────────────
|
|
608
|
+
openclaw
|
|
609
|
+
.command("webhook")
|
|
610
|
+
.description("Set up threat alerting through OpenClaw messaging channels")
|
|
611
|
+
.option("-c, --channel <channel>", "Delivery channel (whatsapp, telegram, discord, slack, signal, imessage, msteams)")
|
|
612
|
+
.option("--events <events>", "Comma-separated events (default: threat.detected,threat.blocked)")
|
|
613
|
+
.option("--json", "Output as JSON")
|
|
614
|
+
.action(async (options) => {
|
|
615
|
+
const { url: gatewayUrl, token } = requireGatewayConfig();
|
|
616
|
+
let channel = options.channel;
|
|
617
|
+
const events = options.events
|
|
618
|
+
? options.events.split(",").map((e) => e.trim())
|
|
619
|
+
: ["threat.detected", "threat.blocked"];
|
|
620
|
+
if (!channel) {
|
|
621
|
+
const answers = await inquirer.prompt([
|
|
622
|
+
{
|
|
623
|
+
type: "list",
|
|
624
|
+
name: "channel",
|
|
625
|
+
message: "Which messaging channel should receive threat alerts?",
|
|
626
|
+
choices: OPENCLAW_CHANNELS.map((c) => ({
|
|
627
|
+
name: c === "last" ? "last (most recent channel)" : c,
|
|
628
|
+
value: c,
|
|
629
|
+
})),
|
|
630
|
+
default: "last",
|
|
631
|
+
},
|
|
632
|
+
]);
|
|
633
|
+
channel = answers.channel;
|
|
634
|
+
}
|
|
635
|
+
// Verify OpenClaw Gateway can deliver messages
|
|
636
|
+
const testSpinner = ora("Verifying OpenClaw message delivery...").start();
|
|
637
|
+
try {
|
|
638
|
+
const hookResult = await invokeHook(gatewayUrl, token, {
|
|
639
|
+
message: "[AgentsPD] Webhook bridge test — this confirms threat alerts will be delivered here.",
|
|
640
|
+
channel,
|
|
641
|
+
});
|
|
642
|
+
if (!hookResult.ok) {
|
|
643
|
+
testSpinner.fail("OpenClaw message delivery failed");
|
|
644
|
+
output.error(hookResult.error || "Unknown error");
|
|
645
|
+
output.info("Ensure the Gateway is running and the channel is configured.");
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
testSpinner.succeed("Test message delivered via OpenClaw");
|
|
649
|
+
}
|
|
650
|
+
catch (err) {
|
|
651
|
+
testSpinner.fail("Failed to deliver test message");
|
|
652
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
// Use the tools/invoke endpoint as the webhook target
|
|
656
|
+
const hooksUrl = `${gatewayUrl}/tools/invoke`;
|
|
657
|
+
// Register the Emotos webhook
|
|
658
|
+
const spinner = ora("Creating Emotos webhook...").start();
|
|
659
|
+
const result = await api.createWebhook({
|
|
660
|
+
url: hooksUrl,
|
|
661
|
+
events,
|
|
662
|
+
});
|
|
663
|
+
if (result.error) {
|
|
664
|
+
spinner.fail("Failed to create webhook");
|
|
665
|
+
output.error(result.error.message);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (options.json) {
|
|
669
|
+
spinner.stop();
|
|
670
|
+
output.printJson({
|
|
671
|
+
webhookId: result.data?.id,
|
|
672
|
+
url: hooksUrl,
|
|
673
|
+
events,
|
|
674
|
+
channel,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
spinner.succeed("Webhook bridge created");
|
|
679
|
+
console.log();
|
|
680
|
+
output.printKeyValue([
|
|
681
|
+
["Webhook ID", result.data?.id || "N/A"],
|
|
682
|
+
["Target", hooksUrl],
|
|
683
|
+
["Events", events.join(", ")],
|
|
684
|
+
["Channel", channel],
|
|
685
|
+
]);
|
|
686
|
+
console.log();
|
|
687
|
+
output.success(`Threat alerts will now be delivered to your ${channel === "last" ? "most recent" : channel} channel via OpenClaw.`);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
// ── openclaw status ──────────────────────────────────────────────────────
|
|
691
|
+
openclaw
|
|
692
|
+
.command("status")
|
|
693
|
+
.description("Show OpenClaw integration health")
|
|
694
|
+
.option("--json", "Output as JSON")
|
|
695
|
+
.action(async (options) => {
|
|
696
|
+
const config = getConfig();
|
|
697
|
+
const gatewayUrl = config.openclawGatewayUrl;
|
|
698
|
+
const token = config.openclawToken;
|
|
699
|
+
const configured = !!(gatewayUrl && token);
|
|
700
|
+
// Connectivity
|
|
701
|
+
let connectionStatus;
|
|
702
|
+
if (configured) {
|
|
703
|
+
const spinner = ora("Checking OpenClaw Gateway...").start();
|
|
704
|
+
connectionStatus = await testConnection(gatewayUrl, token);
|
|
705
|
+
if (connectionStatus.connected) {
|
|
706
|
+
spinner.succeed("OpenClaw Gateway is online");
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
spinner.fail("OpenClaw Gateway is unreachable");
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// Emotos agents with openclaw- prefix
|
|
713
|
+
const agentsResult = await api.listAgents({ limit: 200 });
|
|
714
|
+
const openclawAgents = (agentsResult.data?.items || []).filter((a) => a.name.startsWith("openclaw-"));
|
|
715
|
+
// Webhooks pointing to OpenClaw
|
|
716
|
+
const webhooksResult = await api.listWebhooks();
|
|
717
|
+
const gwUrl = gatewayUrl || "";
|
|
718
|
+
const openclawWebhooks = (webhooksResult.data?.webhooks || []).filter((w) => w.url.includes("/hooks/") ||
|
|
719
|
+
(gwUrl && w.url.startsWith(gwUrl)));
|
|
720
|
+
if (options.json) {
|
|
721
|
+
output.printJson({
|
|
722
|
+
configured,
|
|
723
|
+
gatewayUrl: gatewayUrl || null,
|
|
724
|
+
connected: connectionStatus?.connected ?? false,
|
|
725
|
+
latencyMs: connectionStatus?.latencyMs ?? null,
|
|
726
|
+
error: connectionStatus?.error ?? null,
|
|
727
|
+
registeredAgents: openclawAgents.length,
|
|
728
|
+
activeWebhooks: openclawWebhooks.length,
|
|
729
|
+
});
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
output.heading("OpenClaw Integration Status");
|
|
733
|
+
output.printKeyValue([
|
|
734
|
+
["Configured", configured ? chalk.green("Yes") : chalk.red("No")],
|
|
735
|
+
["Gateway URL", gatewayUrl || chalk.gray("not set")],
|
|
736
|
+
[
|
|
737
|
+
"Connected",
|
|
738
|
+
connectionStatus?.connected
|
|
739
|
+
? chalk.green("Online")
|
|
740
|
+
: chalk.red("Offline"),
|
|
741
|
+
],
|
|
742
|
+
[
|
|
743
|
+
"Latency",
|
|
744
|
+
connectionStatus ? `${connectionStatus.latencyMs}ms` : "N/A",
|
|
745
|
+
],
|
|
746
|
+
[
|
|
747
|
+
"Gateway Sessions",
|
|
748
|
+
connectionStatus?.sessions
|
|
749
|
+
? String(connectionStatus.sessions.length)
|
|
750
|
+
: "N/A",
|
|
751
|
+
],
|
|
752
|
+
]);
|
|
753
|
+
console.log();
|
|
754
|
+
output.printKeyValue([
|
|
755
|
+
["Registered Agents", String(openclawAgents.length)],
|
|
756
|
+
["Active Webhooks", String(openclawWebhooks.length)],
|
|
757
|
+
]);
|
|
758
|
+
if (!configured) {
|
|
759
|
+
console.log();
|
|
760
|
+
output.info("Get started with:");
|
|
761
|
+
console.log(` ${output.highlight("agentspd openclaw connect")}`);
|
|
762
|
+
}
|
|
763
|
+
else if (openclawAgents.length === 0) {
|
|
764
|
+
console.log();
|
|
765
|
+
output.info("No agents registered yet. Run:");
|
|
766
|
+
console.log(` ${output.highlight("agentspd openclaw register")}`);
|
|
767
|
+
}
|
|
768
|
+
if (openclawAgents.length > 0) {
|
|
769
|
+
console.log();
|
|
770
|
+
output.heading("Registered Agents");
|
|
771
|
+
output.printTable(["Name", "Status", "Environment", "Reputation"], openclawAgents.map((a) => [
|
|
772
|
+
a.name,
|
|
773
|
+
output.formatStatus(a.status),
|
|
774
|
+
output.formatEnvironment(a.environment),
|
|
775
|
+
output.formatReputation(a.reputationScore),
|
|
776
|
+
]));
|
|
777
|
+
}
|
|
778
|
+
if (openclawWebhooks.length > 0) {
|
|
779
|
+
console.log();
|
|
780
|
+
output.heading("Webhook Bridges");
|
|
781
|
+
output.printWebhookTable(openclawWebhooks);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
// ── openclaw sync-sessions ──────────────────────────────────────────────
|
|
785
|
+
openclaw
|
|
786
|
+
.command("sync-sessions")
|
|
787
|
+
.description("Bridge OpenClaw sessions into Emotos workspaces for cross-org collaboration")
|
|
788
|
+
.option("--auto-create", "Automatically create workspaces for new sessions")
|
|
789
|
+
.option("-m, --mode <mode>", "Workspace communication mode (live, mailbox, hybrid)", "hybrid")
|
|
790
|
+
.option("--json", "Output as JSON")
|
|
791
|
+
.action(async (options) => {
|
|
792
|
+
const { url: gatewayUrl, token } = requireGatewayConfig();
|
|
793
|
+
// Step 1: Fetch OpenClaw sessions
|
|
794
|
+
const spinner = ora("Fetching OpenClaw sessions...").start();
|
|
795
|
+
let sessions;
|
|
796
|
+
try {
|
|
797
|
+
sessions = await listSessions(gatewayUrl, token);
|
|
798
|
+
}
|
|
799
|
+
catch (err) {
|
|
800
|
+
spinner.fail("Failed to list OpenClaw sessions");
|
|
801
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (!sessions || sessions.length === 0) {
|
|
805
|
+
spinner.warn("No active sessions on OpenClaw Gateway");
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
spinner.succeed(`Found ${sessions.length} OpenClaw session(s)`);
|
|
809
|
+
// Step 2: Fetch existing Emotos workspaces to detect what's already bridged.
|
|
810
|
+
// We tag bridged workspaces by including the session key in the purpose
|
|
811
|
+
// field, prefixed with [oc-session:...].
|
|
812
|
+
const wsResult = await api.listWorkspaces({ status: "active" });
|
|
813
|
+
const existingWorkspaces = wsResult.data?.items || [];
|
|
814
|
+
const bridgedKeys = new Set(existingWorkspaces
|
|
815
|
+
.filter((w) => {
|
|
816
|
+
const purpose = w.purpose || "";
|
|
817
|
+
return purpose.includes("[oc-session:");
|
|
818
|
+
})
|
|
819
|
+
.map((w) => {
|
|
820
|
+
const purpose = w.purpose || "";
|
|
821
|
+
const match = purpose.match(/\[oc-session:([^\]]+)\]/);
|
|
822
|
+
return match ? match[1] : "";
|
|
823
|
+
})
|
|
824
|
+
.filter(Boolean));
|
|
825
|
+
// Step 3: Determine which sessions need workspaces
|
|
826
|
+
const unbridged = sessions.filter((s) => !bridgedKeys.has(s.key));
|
|
827
|
+
const alreadyBridged = sessions.filter((s) => bridgedKeys.has(s.key));
|
|
828
|
+
// Step 3b: Auto-join agents to existing workspaces that are missing the matching agent
|
|
829
|
+
let joined = 0;
|
|
830
|
+
const agentsResult = await api.listAgents({ limit: 200 });
|
|
831
|
+
const allAgents = agentsResult.data?.items || [];
|
|
832
|
+
for (const session of alreadyBridged) {
|
|
833
|
+
const ws = existingWorkspaces.find((w) => {
|
|
834
|
+
const purpose = w.purpose || "";
|
|
835
|
+
return purpose.includes(`[oc-session:${session.key}]`);
|
|
836
|
+
});
|
|
837
|
+
if (!ws)
|
|
838
|
+
continue;
|
|
839
|
+
// Check if any participant already matches the session name pattern
|
|
840
|
+
const participants = ws.participants || [];
|
|
841
|
+
const namePatterns = [
|
|
842
|
+
`openclaw-${session.key}`,
|
|
843
|
+
`openclaw-${session.agentId || session.key}`,
|
|
844
|
+
];
|
|
845
|
+
const hasMatchingParticipant = participants.some((p) => namePatterns.includes(p.agentName));
|
|
846
|
+
if (hasMatchingParticipant)
|
|
847
|
+
continue;
|
|
848
|
+
// Find any agent with the matching name to join
|
|
849
|
+
const matchingAgent = allAgents.find((a) => namePatterns.includes(a.name));
|
|
850
|
+
if (!matchingAgent)
|
|
851
|
+
continue;
|
|
852
|
+
const wsId = ws.id;
|
|
853
|
+
if (wsId && (await autoJoinAgent(session, wsId, matchingAgent))) {
|
|
854
|
+
joined++;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (unbridged.length === 0) {
|
|
858
|
+
const msg = joined > 0
|
|
859
|
+
? `All ${sessions.length} session(s) already have Emotos workspaces (${joined} agent(s) joined)`
|
|
860
|
+
: `All ${sessions.length} session(s) already have Emotos workspaces`;
|
|
861
|
+
output.success(msg);
|
|
862
|
+
if (options.json) {
|
|
863
|
+
output.printJson({
|
|
864
|
+
total: sessions.length,
|
|
865
|
+
bridged: alreadyBridged.length,
|
|
866
|
+
created: 0,
|
|
867
|
+
joined,
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
// Step 4: Prompt or auto-create
|
|
873
|
+
let toCreate = unbridged;
|
|
874
|
+
if (!options.autoCreate && unbridged.length > 1) {
|
|
875
|
+
const answers = await inquirer.prompt([
|
|
876
|
+
{
|
|
877
|
+
type: "checkbox",
|
|
878
|
+
name: "selected",
|
|
879
|
+
message: "Select sessions to create Emotos workspaces for:",
|
|
880
|
+
choices: unbridged.map((s) => ({
|
|
881
|
+
name: `${s.displayName || s.key}${s.channel ? ` (${s.channel})` : ""}`,
|
|
882
|
+
value: s,
|
|
883
|
+
checked: true,
|
|
884
|
+
})),
|
|
885
|
+
validate: (input) => input.length > 0 || "Select at least one session",
|
|
886
|
+
},
|
|
887
|
+
]);
|
|
888
|
+
toCreate = answers.selected;
|
|
889
|
+
}
|
|
890
|
+
// Step 5: Create workspaces
|
|
891
|
+
const results = [];
|
|
892
|
+
for (const session of toCreate) {
|
|
893
|
+
const displayName = session.displayName || session.key.split(":").slice(-2).join(":");
|
|
894
|
+
const wsName = `oc:${displayName}`;
|
|
895
|
+
const wsSpinner = ora(`Creating workspace for "${displayName}"...`).start();
|
|
896
|
+
// Tag the workspace purpose with the session key for future syncs
|
|
897
|
+
const wsCreateResult = await api.createWorkspace({
|
|
898
|
+
name: wsName,
|
|
899
|
+
purpose: `Bridged from OpenClaw [oc-session:${session.key}] (${session.channel || "webchat"})`,
|
|
900
|
+
mode: options.mode,
|
|
901
|
+
maxParticipants: 20,
|
|
902
|
+
});
|
|
903
|
+
if (wsCreateResult.error) {
|
|
904
|
+
wsSpinner.fail(`Failed: "${displayName}"`);
|
|
905
|
+
results.push({
|
|
906
|
+
sessionKey: session.key,
|
|
907
|
+
displayName,
|
|
908
|
+
error: wsCreateResult.error.message,
|
|
909
|
+
});
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
const workspaceId = wsCreateResult.data?.id;
|
|
913
|
+
// Auto-join any registered openclaw agents that match this session
|
|
914
|
+
if (workspaceId) {
|
|
915
|
+
await autoJoinAgent(session, workspaceId);
|
|
916
|
+
}
|
|
917
|
+
wsSpinner.succeed(`Created workspace for "${displayName}"`);
|
|
918
|
+
results.push({
|
|
919
|
+
sessionKey: session.key,
|
|
920
|
+
displayName,
|
|
921
|
+
workspaceId,
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
// Step 6: Output
|
|
925
|
+
console.log();
|
|
926
|
+
if (options.json) {
|
|
927
|
+
output.printJson({
|
|
928
|
+
total: sessions.length,
|
|
929
|
+
bridged: alreadyBridged.length,
|
|
930
|
+
created: results.filter((r) => r.workspaceId).length,
|
|
931
|
+
results,
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
output.heading("Session → Workspace Bridge Results");
|
|
936
|
+
output.printTable(["Session", "Workspace ID", "Status"], results.map((r) => [
|
|
937
|
+
r.displayName,
|
|
938
|
+
r.workspaceId?.slice(0, 8) + "..." || "—",
|
|
939
|
+
r.error ? chalk.red(r.error) : chalk.green("Created"),
|
|
940
|
+
]));
|
|
941
|
+
const created = results.filter((r) => r.workspaceId);
|
|
942
|
+
if (created.length > 0) {
|
|
943
|
+
console.log();
|
|
944
|
+
output.info("Invite agents from any org to these workspaces:");
|
|
945
|
+
for (const r of created) {
|
|
946
|
+
console.log(` ${output.highlight(`agentspd workspace invite ${r.workspaceId} --agent-name "partner-agent"`)}`);
|
|
947
|
+
}
|
|
948
|
+
console.log();
|
|
949
|
+
output.info("Or invite directly into an OpenClaw session:");
|
|
950
|
+
console.log(` ${output.highlight('agentspd openclaw invite-to-session <session-key> --agent-name "agent"')}`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
// ── openclaw invite-to-session ──────────────────────────────────────────
|
|
955
|
+
openclaw
|
|
956
|
+
.command("invite-to-session <sessionKey>")
|
|
957
|
+
.description("Invite an external Emotos agent into an OpenClaw session's workspace")
|
|
958
|
+
.requiredOption("--agent-name <name>", "Name of the agent to invite")
|
|
959
|
+
.option("--email <address>", "Send invite link via email")
|
|
960
|
+
.option("--expires <duration>", "Invite token expiry (e.g. 1h, 24h)", "24h")
|
|
961
|
+
.option("--max-uses <count>", "Max joins allowed (0 = unlimited)", "0")
|
|
962
|
+
.option("--json", "Output as JSON")
|
|
963
|
+
.action(async (sessionKey, options) => {
|
|
964
|
+
// Find the Emotos workspace that's bridged to this session
|
|
965
|
+
const spinner = ora("Looking up workspace for session...").start();
|
|
966
|
+
const wsResult = await api.listWorkspaces();
|
|
967
|
+
const workspaces = wsResult.data?.items || [];
|
|
968
|
+
const workspace = workspaces.find((w) => {
|
|
969
|
+
const purpose = w.purpose || "";
|
|
970
|
+
return purpose.includes(`[oc-session:${sessionKey}]`);
|
|
971
|
+
});
|
|
972
|
+
if (!workspace) {
|
|
973
|
+
spinner.fail("No Emotos workspace found for this session");
|
|
974
|
+
output.info("Bridge the session first with:");
|
|
975
|
+
console.log(` ${output.highlight("agentspd openclaw sync-sessions --auto-create")}`);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
spinner.succeed(`Found workspace: ${workspace.name}`);
|
|
979
|
+
// Generate invite
|
|
980
|
+
const invSpinner = ora("Generating invite...").start();
|
|
981
|
+
const maxUses = parseInt(options.maxUses, 10) || 0;
|
|
982
|
+
const invResult = await api.workspaceInvite(workspace.id, {
|
|
983
|
+
agentName: options.agentName,
|
|
984
|
+
expires: options.expires,
|
|
985
|
+
email: options.email,
|
|
986
|
+
maxUses,
|
|
987
|
+
});
|
|
988
|
+
if (invResult.error) {
|
|
989
|
+
invSpinner.fail("Failed to generate invite");
|
|
990
|
+
output.error(invResult.error.message);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (options.json) {
|
|
994
|
+
invSpinner.stop();
|
|
995
|
+
output.printJson({
|
|
996
|
+
workspaceId: workspace.id,
|
|
997
|
+
workspaceName: workspace.name,
|
|
998
|
+
sessionKey,
|
|
999
|
+
...invResult.data,
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
invSpinner.succeed("Invite generated!");
|
|
1004
|
+
const config = getConfig();
|
|
1005
|
+
const appUrl = config.appUrl;
|
|
1006
|
+
const joinUrl = `${appUrl}/workspaces/${workspace.id}/join?token=${encodeURIComponent(invResult.data?.inviteToken || "")}`;
|
|
1007
|
+
console.log();
|
|
1008
|
+
output.printKeyValue([
|
|
1009
|
+
["Workspace", workspace.name],
|
|
1010
|
+
["Session", sessionKey],
|
|
1011
|
+
["Invite Token", invResult.data?.inviteToken || "N/A"],
|
|
1012
|
+
["Join URL", joinUrl],
|
|
1013
|
+
[
|
|
1014
|
+
"Max Uses",
|
|
1015
|
+
invResult.data?.maxUses === "unlimited" ||
|
|
1016
|
+
invResult.data?.maxUses === 0
|
|
1017
|
+
? "Unlimited"
|
|
1018
|
+
: String(invResult.data?.maxUses),
|
|
1019
|
+
],
|
|
1020
|
+
]);
|
|
1021
|
+
console.log();
|
|
1022
|
+
output.info("Share this link with agents from any organization (OpenClaw or not):");
|
|
1023
|
+
console.log(` ${output.highlight(joinUrl)}`);
|
|
1024
|
+
console.log();
|
|
1025
|
+
output.info("Or join via CLI:");
|
|
1026
|
+
console.log(` ${output.highlight(`agentspd workspace join ${workspace.id} --invite-token "${invResult.data?.inviteToken}" --agent-id <agent>`)}`);
|
|
1027
|
+
if (options.email) {
|
|
1028
|
+
output.success(`Invite also sent to ${options.email}`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
return openclaw;
|
|
1033
|
+
}
|