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.
@@ -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
+ }