@occum-net/occumclaw 0.4.1 → 0.6.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.
Files changed (2) hide show
  1. package/index.ts +273 -47
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -8,12 +8,8 @@
8
8
  * occum.net.
9
9
  *
10
10
  * Configuration (set via `openclaw config set`):
11
- * channels.occum.accounts.default.agentToken — Agent bearer token (occ_...)
12
- * channels.occum.accounts.default.apiUrl — API base URL (default: https://api.occum.net)
13
- *
14
- * Legacy config path (auto-migrated):
15
- * plugins.entries.occumclaw.config.agentToken
16
- * plugins.entries.occumclaw.config.apiUrl
11
+ * plugins.entries.occumclaw.config.agentToken — Agent bearer token (occ_...)
12
+ * plugins.entries.occumclaw.config.apiUrl — API base URL (default: https://api.occum.net)
17
13
  */
18
14
 
19
15
  import { OccumWsClient, type OccumEvent, type AuthOkData } from "./ws-client";
@@ -69,12 +65,14 @@ interface MsgContext {
69
65
  BodyForAgent?: string;
70
66
  CommandBody?: string;
71
67
  From?: string;
72
- To?: string;
68
+ OriginatingChannel?: string;
69
+ OriginatingTo?: string;
70
+ Surface?: string;
73
71
  SessionKey?: string;
74
72
  AccountId?: string;
75
73
  Provider?: string;
76
74
  ChatType?: string;
77
- GroupId?: string;
75
+ GroupChannel?: string;
78
76
  SenderId?: string;
79
77
  SenderName?: string;
80
78
  SenderUsername?: string;
@@ -154,8 +152,7 @@ interface OpenClawPluginApi {
154
152
  // 4. Wildcard tool policy (groups.*.tools)
155
153
  // 5. undefined → fall back to global (tools.allow / tools.profile)
156
154
  //
157
- // Config path (either works; plugin-style avoids config validation issues):
158
- // channels.occum.accounts.<accountId>.groups.<channelId>.tools
155
+ // Config path:
159
156
  // plugins.entries.occumclaw.config.groups.<channelId>.tools
160
157
  //
161
158
  // Per-sender key format:
@@ -263,20 +260,16 @@ function resolveToolsBySender(
263
260
  /**
264
261
  * Resolve the effective tool policy for an inbound occum.net message.
265
262
  *
266
- * Looks up groups config from (in order):
267
- * 1. channels.occum.accounts.<accountId>.groups (channel-style, if OpenClaw validates it)
268
- * 2. plugins.entries.occumclaw.config.groups (plugin-style, always safe)
263
+ * Looks up groups config from:
264
+ * plugins.entries.occumclaw.config.groups
269
265
  *
270
266
  * Then resolves per-channel and per-sender overrides with wildcard fallback.
271
267
  */
272
268
  function resolveOccumToolPolicy(params: ChannelGroupContext): GroupToolPolicyConfig | undefined {
273
269
  const { cfg, groupId, accountId, senderId, senderName, senderUsername, senderE164 } = params;
274
270
 
275
- // Try channel-style config first, then fall back to plugin config
276
- const acctId = accountId ?? "default";
277
271
  const groups: Record<string, OccumGroupConfig> | undefined =
278
- cfg?.channels?.occum?.accounts?.[acctId]?.groups
279
- ?? cfg?.plugins?.entries?.occumclaw?.config?.groups;
272
+ cfg?.plugins?.entries?.occumclaw?.config?.groups;
280
273
 
281
274
  if (!groups || typeof groups !== "object") return undefined;
282
275
 
@@ -313,22 +306,8 @@ function resolveApiUrl(raw?: string): string {
313
306
  return (raw ?? "https://api.occum.net").replace(/\/+$/, "") + "/v1";
314
307
  }
315
308
 
316
- /**
317
- * Resolve account config from OpenClaw config.
318
- * Supports both channel-style config and legacy plugin config.
319
- */
320
- function resolveAccount(cfg: any, accountId?: string | null): OccumAccount {
321
- // Channel-style: channels.occum.accounts.<accountId>
322
- const acctId = accountId ?? "default";
323
- const channelAcct = cfg?.channels?.occum?.accounts?.[acctId];
324
- if (channelAcct?.agentToken) {
325
- return {
326
- agentToken: channelAcct.agentToken,
327
- apiUrl: resolveApiUrl(channelAcct.apiUrl),
328
- };
329
- }
330
-
331
- // Plugin config: plugins.entries.occumclaw.config
309
+ /** Resolve account config from plugins.entries.occumclaw.config. */
310
+ function resolveAccount(cfg: any, _accountId?: string | null): OccumAccount {
332
311
  const pluginCfg = cfg?.plugins?.entries?.occumclaw?.config;
333
312
  if (pluginCfg?.agentToken) {
334
313
  return {
@@ -341,12 +320,6 @@ function resolveAccount(cfg: any, accountId?: string | null): OccumAccount {
341
320
  }
342
321
 
343
322
  function listAccountIds(cfg: any): string[] {
344
- // Channel-style accounts
345
- const channelAccounts = cfg?.channels?.occum?.accounts;
346
- if (channelAccounts && typeof channelAccounts === "object") {
347
- return Object.keys(channelAccounts);
348
- }
349
- // Plugin config: single implicit account
350
323
  const pluginCfg = cfg?.plugins?.entries?.occumclaw?.config;
351
324
  if (pluginCfg?.agentToken) {
352
325
  return ["default"];
@@ -354,9 +327,86 @@ function listAccountIds(cfg: any): string[] {
354
327
  return [];
355
328
  }
356
329
 
330
+ // ─── Directory Helpers ───────────────────────────────────────────────────────
331
+
332
+ interface DirectoryEntry {
333
+ kind: "user" | "group" | "channel";
334
+ id: string;
335
+ name?: string;
336
+ handle?: string;
337
+ avatarUrl?: string;
338
+ rank?: number;
339
+ }
340
+
341
+ /** Case-insensitive substring filter on id, name, and handle. */
342
+ function filterEntries(entries: DirectoryEntry[], query?: string | null, limit?: number | null): DirectoryEntry[] {
343
+ if (query) {
344
+ const q = query.trim().toLowerCase();
345
+ entries = entries.filter(
346
+ (e) =>
347
+ e.id.toLowerCase().includes(q) ||
348
+ e.name?.toLowerCase().includes(q) ||
349
+ e.handle?.toLowerCase().includes(q),
350
+ );
351
+ }
352
+ if (limit && limit > 0) entries = entries.slice(0, limit);
353
+ return entries;
354
+ }
355
+
356
+ /** Convert an occum.net channel member to a directory entry. */
357
+ function memberToEntry(m: any): DirectoryEntry {
358
+ return {
359
+ kind: "user",
360
+ id: m.memberType === "agent" ? m.agentId : m.userId,
361
+ name: m.displayName ?? m.invitedName ?? undefined,
362
+ handle: m.memberType === "agent" ? m.agentId : undefined,
363
+ avatarUrl: m.avatarUrl ?? undefined,
364
+ };
365
+ }
366
+
367
+ /** Convert an occum.net channel to a directory group entry. */
368
+ function channelToEntry(ch: any): DirectoryEntry {
369
+ return {
370
+ kind: "group",
371
+ id: ch.id,
372
+ name: ch.name ?? undefined,
373
+ };
374
+ }
375
+
376
+ /**
377
+ * Build a ToolConfig from either the gateway closure or from the cfg param.
378
+ * The gateway sets cachedConfig/controlChannelId at startup; the CLI resolver
379
+ * only has cfg, so we fall back to resolveAccount + /agents/me.
380
+ */
381
+ async function resolveDirectoryContext(
382
+ cachedConfig: ToolConfig | null,
383
+ controlChannelId: string | null,
384
+ cfg?: any,
385
+ ): Promise<{ config: ToolConfig; controlChannelId: string } | null> {
386
+ if (cachedConfig && controlChannelId) {
387
+ return { config: cachedConfig, controlChannelId };
388
+ }
389
+ // Fallback: build config from cfg param (used by CLI commands)
390
+ const account = resolveAccount(cfg ?? {});
391
+ if (!account.agentToken) return null;
392
+ const config: ToolConfig = { agentToken: account.agentToken, apiUrl: account.apiUrl };
393
+ try {
394
+ const me = await apiRequest(config, "/agents/me");
395
+ if (!me.controlChannelId) return null;
396
+ return { config, controlChannelId: me.controlChannelId };
397
+ } catch {
398
+ return null;
399
+ }
400
+ }
401
+
357
402
  // ─── Channel Plugin ─────────────────────────────────────────────────────────
358
403
 
359
404
  function createOccumChannelPlugin(logger: PluginLogger) {
405
+ // Shared state: set by startAccount, read by resolveTarget/sendText/directory.
406
+ // All outbound delivery routes to the agent's control channel.
407
+ let controlChannelId: string | null = null;
408
+ let cachedConfig: ToolConfig | null = null;
409
+
360
410
  return {
361
411
  id: "occum" as const,
362
412
 
@@ -376,6 +426,132 @@ function createOccumChannelPlugin(logger: PluginLogger) {
376
426
  resolveToolPolicy: resolveOccumToolPolicy,
377
427
  },
378
428
 
429
+ directory: {
430
+ async self(params: { cfg?: any }) {
431
+ const ctx = await resolveDirectoryContext(cachedConfig, controlChannelId, params.cfg);
432
+ if (!ctx) return null;
433
+ try {
434
+ const me = await apiRequest(ctx.config, "/agents/me");
435
+ return { kind: "user" as const, id: me.id, name: me.name ?? undefined };
436
+ } catch {
437
+ return null;
438
+ }
439
+ },
440
+
441
+ async listPeers(params: { cfg?: any; query?: string | null; limit?: number | null; accountId?: string | null }) {
442
+ const ctx = await resolveDirectoryContext(cachedConfig, controlChannelId, params.cfg);
443
+ if (!ctx) return [];
444
+ try {
445
+ const members: any[] = await apiRequest(ctx.config, `/channels/${ctx.controlChannelId}/members`);
446
+ const entries = members.map(memberToEntry).filter((e) => !!e.id);
447
+ return filterEntries(entries, params.query, params.limit);
448
+ } catch {
449
+ return [];
450
+ }
451
+ },
452
+
453
+ async listPeersLive(params: { cfg?: any; query?: string | null; limit?: number | null; accountId?: string | null }) {
454
+ const ctx = await resolveDirectoryContext(cachedConfig, controlChannelId, params.cfg);
455
+ if (!ctx) return [];
456
+ try {
457
+ const members: any[] = await apiRequest(ctx.config, `/channels/${ctx.controlChannelId}/members`);
458
+ const entries = members.map(memberToEntry).filter((e) => !!e.id);
459
+ return filterEntries(entries, params.query, params.limit);
460
+ } catch {
461
+ return [];
462
+ }
463
+ },
464
+
465
+ async listGroups(params: { cfg?: any; query?: string | null; limit?: number | null; accountId?: string | null }) {
466
+ const ctx = await resolveDirectoryContext(cachedConfig, controlChannelId, params.cfg);
467
+ if (!ctx) return [];
468
+ try {
469
+ const channels: any[] = await apiRequest(ctx.config, "/channels");
470
+ const entries = channels.map(channelToEntry);
471
+ return filterEntries(entries, params.query, params.limit);
472
+ } catch {
473
+ return [];
474
+ }
475
+ },
476
+
477
+ async listGroupsLive(params: { cfg?: any; query?: string | null; limit?: number | null; accountId?: string | null }) {
478
+ const ctx = await resolveDirectoryContext(cachedConfig, controlChannelId, params.cfg);
479
+ if (!ctx) return [];
480
+ try {
481
+ const channels: any[] = await apiRequest(ctx.config, "/channels");
482
+ const entries = channels.map(channelToEntry);
483
+ return filterEntries(entries, params.query, params.limit);
484
+ } catch {
485
+ return [];
486
+ }
487
+ },
488
+
489
+ async listGroupMembers(params: { cfg?: any; groupId: string; limit?: number | null; accountId?: string | null }) {
490
+ const ctx = await resolveDirectoryContext(cachedConfig, controlChannelId, params.cfg);
491
+ if (!ctx) return [];
492
+ try {
493
+ const members: any[] = await apiRequest(ctx.config, `/channels/${params.groupId}/members`);
494
+ const entries = members.map(memberToEntry).filter((e) => !!e.id);
495
+ return params.limit && params.limit > 0 ? entries.slice(0, params.limit) : entries;
496
+ } catch {
497
+ return [];
498
+ }
499
+ },
500
+ },
501
+
502
+ resolver: {
503
+ async resolveTargets(params: {
504
+ cfg?: any;
505
+ accountId?: string | null;
506
+ inputs: string[];
507
+ kind: "user" | "group";
508
+ }) {
509
+ const ctx = await resolveDirectoryContext(cachedConfig, controlChannelId, params.cfg);
510
+ if (!ctx) {
511
+ return params.inputs.map((input) => ({ input, resolved: false, note: "Not configured" }));
512
+ }
513
+ if (params.kind === "group") {
514
+ try {
515
+ const channels: any[] = await apiRequest(ctx.config, "/channels");
516
+ return params.inputs.map((input) => {
517
+ const q = input.trim().toLowerCase();
518
+ const match = channels.find(
519
+ (ch) => ch.id === input || ch.name?.toLowerCase().includes(q),
520
+ );
521
+ return match
522
+ ? { input, resolved: true, id: match.id, name: match.name ?? undefined }
523
+ : { input, resolved: false };
524
+ });
525
+ } catch {
526
+ return params.inputs.map((input) => ({ input, resolved: false }));
527
+ }
528
+ }
529
+ // kind === "user"
530
+ try {
531
+ const members: any[] = await apiRequest(ctx.config, `/channels/${ctx.controlChannelId}/members`);
532
+ return params.inputs.map((input) => {
533
+ const q = input.trim().toLowerCase();
534
+ const match = members.find(
535
+ (m) =>
536
+ m.userId === input ||
537
+ m.agentId === input ||
538
+ m.displayName?.toLowerCase().includes(q) ||
539
+ m.invitedName?.toLowerCase().includes(q),
540
+ );
541
+ if (!match) return { input, resolved: false };
542
+ return {
543
+ input,
544
+ resolved: true,
545
+ id: match.memberType === "agent" ? match.agentId : match.userId,
546
+ name: match.displayName ?? match.invitedName ?? undefined,
547
+ };
548
+ });
549
+ } catch {
550
+ return params.inputs.map((input) => ({ input, resolved: false }));
551
+ }
552
+ },
553
+ },
554
+
379
555
  config: {
380
556
  listAccountIds: (cfg: any) => listAccountIds(cfg),
381
557
  resolveAccount: (cfg: any, accountId?: string | null) => resolveAccount(cfg, accountId),
@@ -386,6 +562,22 @@ function createOccumChannelPlugin(logger: PluginLogger) {
386
562
  outbound: {
387
563
  deliveryMode: "direct" as const,
388
564
 
565
+ resolveTarget(params: {
566
+ cfg?: any;
567
+ to?: string;
568
+ allowFrom?: string[];
569
+ accountId?: string | null;
570
+ mode?: string;
571
+ }): { ok: true; to: string } | { ok: false; error: Error } {
572
+ // All outbound delivery routes to the control channel.
573
+ // OpenClaw may pass a user ID (e.g. from "channel": "last" cron
574
+ // delivery) — we resolve it to the control channel regardless.
575
+ if (!controlChannelId) {
576
+ return { ok: false, error: new Error("Occum.net agent not connected yet (no control channel)") };
577
+ }
578
+ return { ok: true, to: controlChannelId };
579
+ },
580
+
389
581
  async sendText(ctx: {
390
582
  cfg: any;
391
583
  to: string;
@@ -395,8 +587,9 @@ function createOccumChannelPlugin(logger: PluginLogger) {
395
587
  const account = resolveAccount(ctx.cfg, ctx.accountId);
396
588
  const config: ToolConfig = { agentToken: account.agentToken, apiUrl: account.apiUrl };
397
589
 
398
- // `to` is the occum.net channel ID (UUIDv7 string)
399
- const channelId = ctx.to;
590
+ // Use control channel if `to` doesn't look like a channel ID
591
+ // (e.g. OpenClaw passed a user ID from "channel: last" resolution)
592
+ const channelId = controlChannelId ?? ctx.to;
400
593
 
401
594
  const event = await apiRequest(config, `/channels/${channelId}/messages`, {
402
595
  method: "POST",
@@ -428,14 +621,14 @@ function createOccumChannelPlugin(logger: PluginLogger) {
428
621
 
429
622
  const channelRuntime = ctx.channelRuntime;
430
623
  const config: ToolConfig = { agentToken: account.agentToken, apiUrl: account.apiUrl };
624
+ cachedConfig = config;
431
625
 
432
626
  // Discover agent identity
433
627
  let agentId: string | null = null;
434
- let controlChannelId: string | null = null;
435
628
  try {
436
629
  const me = await apiRequest(config, "/agents/me");
437
630
  agentId = me.id;
438
- controlChannelId = me.controlChannelId;
631
+ controlChannelId = me.controlChannelId ?? null;
439
632
  log?.info?.(`Agent identity: ${me.name} (${agentId}), control channel #${controlChannelId}`);
440
633
  } catch (err) {
441
634
  log?.warn?.(`Failed to discover agent identity: ${err}`);
@@ -545,12 +738,14 @@ function createOccumChannelPlugin(logger: PluginLogger) {
545
738
  From: event.senderType === "user"
546
739
  ? `user:${event.senderUserId}`
547
740
  : `agent:${event.senderAgentId}`,
548
- To: replyChannelId,
741
+ OriginatingChannel: "occum",
742
+ OriginatingTo: replyChannelId,
743
+ Surface: "occum",
549
744
  SessionKey: `occum-${ctx.accountId}-${event.channelId}`,
550
745
  AccountId: ctx.accountId,
551
746
  Provider: "occum",
552
747
  ChatType: "direct",
553
- GroupId: event.channelId,
748
+ GroupChannel: event.channelId,
554
749
  SenderId: event.senderType === "user"
555
750
  ? String(event.senderUserId)
556
751
  : String(event.senderAgentId),
@@ -582,15 +777,46 @@ function createOccumChannelPlugin(logger: PluginLogger) {
582
777
  ctx: msgCtx,
583
778
  cfg,
584
779
  dispatcherOptions: {
585
- deliver: async (payload: ReplyPayload) => {
780
+ deliver: async (payload: ReplyPayload, info: ReplyDispatchKindInfo) => {
586
781
  if (!payload.text) return;
782
+
783
+ // Reasoning/thinking blocks → work update
784
+ if (payload.isReasoning) {
785
+ log?.info?.(`[occum] Work update (thinking, ${payload.text.length} chars)`);
786
+ apiRequest(config, `/channels/${event.channelId}/work/${sessionId}/update`, {
787
+ method: "POST",
788
+ body: JSON.stringify({ updateType: "thinking", content: payload.text }),
789
+ }).catch((err) => log?.warn?.(`[occum] Failed to post thinking update: ${err}`));
790
+ return;
791
+ }
792
+
793
+ // Tool results and intermediate blocks → work updates
794
+ if (info.kind === "tool") {
795
+ log?.info?.(`[occum] Work update (tool, ${payload.text.length} chars)`);
796
+ apiRequest(config, `/channels/${event.channelId}/work/${sessionId}/update`, {
797
+ method: "POST",
798
+ body: JSON.stringify({ updateType: "tool_use", content: payload.text }),
799
+ }).catch((err) => log?.warn?.(`[occum] Failed to post tool update: ${err}`));
800
+ return;
801
+ }
802
+
803
+ if (info.kind === "block") {
804
+ log?.info?.(`[occum] Work update (text, ${payload.text.length} chars)`);
805
+ apiRequest(config, `/channels/${event.channelId}/work/${sessionId}/update`, {
806
+ method: "POST",
807
+ body: JSON.stringify({ updateType: "text", content: payload.text }),
808
+ }).catch((err) => log?.warn?.(`[occum] Failed to post text update: ${err}`));
809
+ return;
810
+ }
811
+
812
+ // Final reply → send as actual channel message
587
813
  replyText += payload.text;
588
814
  try {
589
815
  await apiRequest(config, `/channels/${event.channelId}/messages`, {
590
816
  method: "POST",
591
817
  body: JSON.stringify({ text: payload.text }),
592
818
  });
593
- log?.info?.(`[occum] Reply sent to #${event.channelId} (${payload.text.length} chars)`);
819
+ log?.info?.(`[occum] Final reply sent to #${event.channelId} (${payload.text.length} chars)`);
594
820
  } catch (err) {
595
821
  log?.error?.(`[occum] Failed to send reply: ${err}`);
596
822
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@occum-net/occumclaw",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },