@occum-net/occumclaw 0.5.0 → 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 +201 -1
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -327,12 +327,85 @@ function listAccountIds(cfg: any): string[] {
327
327
  return [];
328
328
  }
329
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
+
330
402
  // ─── Channel Plugin ─────────────────────────────────────────────────────────
331
403
 
332
404
  function createOccumChannelPlugin(logger: PluginLogger) {
333
- // Shared state: set by startAccount, read by resolveTarget/sendText.
405
+ // Shared state: set by startAccount, read by resolveTarget/sendText/directory.
334
406
  // All outbound delivery routes to the agent's control channel.
335
407
  let controlChannelId: string | null = null;
408
+ let cachedConfig: ToolConfig | null = null;
336
409
 
337
410
  return {
338
411
  id: "occum" as const,
@@ -353,6 +426,132 @@ function createOccumChannelPlugin(logger: PluginLogger) {
353
426
  resolveToolPolicy: resolveOccumToolPolicy,
354
427
  },
355
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
+
356
555
  config: {
357
556
  listAccountIds: (cfg: any) => listAccountIds(cfg),
358
557
  resolveAccount: (cfg: any, accountId?: string | null) => resolveAccount(cfg, accountId),
@@ -422,6 +621,7 @@ function createOccumChannelPlugin(logger: PluginLogger) {
422
621
 
423
622
  const channelRuntime = ctx.channelRuntime;
424
623
  const config: ToolConfig = { agentToken: account.agentToken, apiUrl: account.apiUrl };
624
+ cachedConfig = config;
425
625
 
426
626
  // Discover agent identity
427
627
  let agentId: string | null = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@occum-net/occumclaw",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },