@clawmem-ai/clawmem 0.1.10 → 0.1.12

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/src/service.ts CHANGED
@@ -12,6 +12,9 @@ import { buildAgentBootstrapRegistration, inferAgentIdFromTranscriptPath, normal
12
12
 
13
13
  type TurnPayload = { sessionId?: string; sessionKey?: string; agentId?: string; messages: unknown[] };
14
14
  type FinalizePayload = { sessionId?: string; sessionKey?: string; sessionFile?: string; agentId?: string; reason?: string; messages?: unknown[] };
15
+ type CollaborationPermission = "read" | "write" | "admin";
16
+ type CollaborationOrgRole = "member" | "admin";
17
+ type CollaborationTeamRole = "member" | "maintainer";
15
18
 
16
19
  class ClawMemService {
17
20
  private readonly config: ClawMemPluginConfig;
@@ -185,7 +188,7 @@ class ClawMemService {
185
188
 
186
189
  this.api.registerTool({
187
190
  name: "memory_labels",
188
- description: "List existing ClawMem schema labels so the agent can reuse current kinds and topics before adding new ones.",
191
+ description: "List existing ClawMem schema labels so the agent can reuse current kinds and topics first, then extend the schema deliberately when a new reusable label is justified.",
189
192
  required: true,
190
193
  parameters: {
191
194
  type: "object",
@@ -390,6 +393,891 @@ class ClawMemService {
390
393
  return toolText(`Marked memory [${forgotten.memoryId}] stale in ${resolved.route.repo}: ${forgotten.detail}`);
391
394
  },
392
395
  });
396
+ this.registerCollaborationTools();
397
+ }
398
+
399
+ private registerCollaborationTools(): void {
400
+ this.api.registerTool({
401
+ name: "collaboration_orgs",
402
+ description: "List organizations visible to the current ClawMem identity before creating or modifying collaboration boundaries.",
403
+ required: true,
404
+ parameters: {
405
+ type: "object",
406
+ additionalProperties: false,
407
+ properties: {
408
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
409
+ },
410
+ },
411
+ execute: async (_id: string, params: unknown) => {
412
+ const p = asRecord(params);
413
+ const agentId = this.resolveToolAgentId(p.agentId);
414
+ const resolved = await this.requireToolIdentity(agentId);
415
+ if ("error" in resolved) return toolText(resolved.error);
416
+ try {
417
+ const orgs = await resolved.client.listUserOrgs();
418
+ if (orgs.length === 0) return toolText(`No organizations are visible to agent "${agentId}".`);
419
+ return toolText([
420
+ `Visible organizations for agent "${agentId}":`,
421
+ ...orgs.map((org) => `- ${renderOrgLine(org)}`),
422
+ ].join("\n"));
423
+ } catch (error) {
424
+ return toolText(`Unable to list organizations for agent "${agentId}": ${String(error)}`);
425
+ }
426
+ },
427
+ });
428
+
429
+ this.api.registerTool({
430
+ name: "collaboration_org_create",
431
+ description: "Create a new organization for shared ClawMem collaboration. Requires confirmed=true after explicit user approval.",
432
+ required: true,
433
+ parameters: {
434
+ type: "object",
435
+ additionalProperties: false,
436
+ properties: {
437
+ login: { type: "string", minLength: 1, description: "Organization login / slug." },
438
+ name: { type: "string", minLength: 1, description: "Optional human-readable organization name." },
439
+ defaultPermission: {
440
+ type: "string",
441
+ enum: ["none", "read", "write", "admin"],
442
+ description: "Default repository permission for org members. Defaults to read.",
443
+ },
444
+ confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
445
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
446
+ },
447
+ required: ["login"],
448
+ },
449
+ execute: async (_id: string, params: unknown) => {
450
+ const p = asRecord(params);
451
+ const blocked = this.requireMutationConfirmation(p, "create an organization");
452
+ if (blocked) return toolText(blocked);
453
+ const login = typeof p.login === "string" ? p.login.trim() : "";
454
+ if (!login) return toolText("login is empty.");
455
+ const defaultPermission = this.resolveOrgDefaultPermission(p.defaultPermission, "read");
456
+ if ("error" in defaultPermission) return toolText(defaultPermission.error);
457
+ const agentId = this.resolveToolAgentId(p.agentId);
458
+ const resolved = await this.requireToolIdentity(agentId);
459
+ if ("error" in resolved) return toolText(resolved.error);
460
+ try {
461
+ const created = await resolved.client.createUserOrg({
462
+ login,
463
+ ...(typeof p.name === "string" && p.name.trim() ? { name: p.name.trim() } : {}),
464
+ ...(defaultPermission.permission ? { defaultRepositoryPermission: defaultPermission.permission } : {}),
465
+ });
466
+ return toolText(`Created organization ${renderOrgLine(created)}.`);
467
+ } catch (error) {
468
+ return toolText(`Unable to create organization "${login}": ${String(error)}`);
469
+ }
470
+ },
471
+ });
472
+
473
+ this.api.registerTool({
474
+ name: "collaboration_teams",
475
+ description: "List teams in an organization before granting repo access or managing membership.",
476
+ required: true,
477
+ parameters: {
478
+ type: "object",
479
+ additionalProperties: false,
480
+ properties: {
481
+ org: { type: "string", minLength: 1, description: "Organization login." },
482
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
483
+ },
484
+ required: ["org"],
485
+ },
486
+ execute: async (_id: string, params: unknown) => {
487
+ const p = asRecord(params);
488
+ const org = typeof p.org === "string" ? p.org.trim() : "";
489
+ if (!org) return toolText("org is empty.");
490
+ const agentId = this.resolveToolAgentId(p.agentId);
491
+ const resolved = await this.requireToolIdentity(agentId);
492
+ if ("error" in resolved) return toolText(resolved.error);
493
+ try {
494
+ const teams = await resolved.client.listOrgTeams(org);
495
+ if (teams.length === 0) return toolText(`No teams found in org "${org}".`);
496
+ return toolText([
497
+ `Teams in org "${org}":`,
498
+ ...teams.map((team) => `- ${renderTeamLine(team)}`),
499
+ ].join("\n"));
500
+ } catch (error) {
501
+ return toolText(`Unable to list teams for org "${org}": ${String(error)}`);
502
+ }
503
+ },
504
+ });
505
+
506
+ this.api.registerTool({
507
+ name: "collaboration_team_create",
508
+ description: "Create a team inside an organization. Requires confirmed=true after explicit user approval.",
509
+ required: true,
510
+ parameters: {
511
+ type: "object",
512
+ additionalProperties: false,
513
+ properties: {
514
+ org: { type: "string", minLength: 1, description: "Organization login." },
515
+ name: { type: "string", minLength: 1, description: "Team display name." },
516
+ description: { type: "string", minLength: 1, description: "Optional team description." },
517
+ privacy: { type: "string", enum: ["closed", "secret"], description: "Team privacy. Defaults to closed." },
518
+ confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
519
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
520
+ },
521
+ required: ["org", "name"],
522
+ },
523
+ execute: async (_id: string, params: unknown) => {
524
+ const p = asRecord(params);
525
+ const blocked = this.requireMutationConfirmation(p, "create a team");
526
+ if (blocked) return toolText(blocked);
527
+ const org = typeof p.org === "string" ? p.org.trim() : "";
528
+ const name = typeof p.name === "string" ? p.name.trim() : "";
529
+ if (!org) return toolText("org is empty.");
530
+ if (!name) return toolText("name is empty.");
531
+ const agentId = this.resolveToolAgentId(p.agentId);
532
+ const resolved = await this.requireToolIdentity(agentId);
533
+ if ("error" in resolved) return toolText(resolved.error);
534
+ try {
535
+ const team = await resolved.client.createOrgTeam(org, {
536
+ name,
537
+ ...(typeof p.description === "string" && p.description.trim() ? { description: p.description.trim() } : {}),
538
+ ...(p.privacy === "secret" ? { privacy: "secret" } : { privacy: "closed" }),
539
+ });
540
+ return toolText(`Created team in "${org}": ${renderTeamLine(team)}.`);
541
+ } catch (error) {
542
+ return toolText(`Unable to create team "${name}" in org "${org}": ${String(error)}`);
543
+ }
544
+ },
545
+ });
546
+
547
+ this.api.registerTool({
548
+ name: "collaboration_team_membership_set",
549
+ description: "Add or update a user's membership in an organization team. Requires confirmed=true after explicit user approval.",
550
+ required: true,
551
+ parameters: {
552
+ type: "object",
553
+ additionalProperties: false,
554
+ properties: {
555
+ org: { type: "string", minLength: 1, description: "Organization login." },
556
+ teamSlug: { type: "string", minLength: 1, description: "Team slug." },
557
+ username: { type: "string", minLength: 1, description: "Username to add or update." },
558
+ role: { type: "string", enum: ["member", "maintainer"], description: "Membership role. Defaults to member." },
559
+ confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
560
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
561
+ },
562
+ required: ["org", "teamSlug", "username"],
563
+ },
564
+ execute: async (_id: string, params: unknown) => {
565
+ const p = asRecord(params);
566
+ const blocked = this.requireMutationConfirmation(p, "change team membership");
567
+ if (blocked) return toolText(blocked);
568
+ const org = typeof p.org === "string" ? p.org.trim() : "";
569
+ const teamSlug = typeof p.teamSlug === "string" ? p.teamSlug.trim() : "";
570
+ const username = typeof p.username === "string" ? p.username.trim() : "";
571
+ if (!org || !teamSlug || !username) return toolText("org, teamSlug, and username are required.");
572
+ const role: CollaborationTeamRole = p.role === "maintainer" ? "maintainer" : "member";
573
+ const agentId = this.resolveToolAgentId(p.agentId);
574
+ const resolved = await this.requireToolIdentity(agentId);
575
+ if ("error" in resolved) return toolText(resolved.error);
576
+ try {
577
+ const membership = await resolved.client.setTeamMembership(org, teamSlug, username, role);
578
+ return toolText(`Set ${username} in ${org}/${teamSlug} to role=${membership.role || role}, state=${membership.state || "active"}.`);
579
+ } catch (error) {
580
+ return toolText(`Unable to set membership for ${username} in ${org}/${teamSlug}: ${String(error)}`);
581
+ }
582
+ },
583
+ });
584
+
585
+ this.api.registerTool({
586
+ name: "collaboration_team_membership_remove",
587
+ description: "Remove a user from an organization team. Requires confirmed=true after explicit user approval.",
588
+ required: true,
589
+ parameters: {
590
+ type: "object",
591
+ additionalProperties: false,
592
+ properties: {
593
+ org: { type: "string", minLength: 1, description: "Organization login." },
594
+ teamSlug: { type: "string", minLength: 1, description: "Team slug." },
595
+ username: { type: "string", minLength: 1, description: "Username to remove." },
596
+ confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
597
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
598
+ },
599
+ required: ["org", "teamSlug", "username"],
600
+ },
601
+ execute: async (_id: string, params: unknown) => {
602
+ const p = asRecord(params);
603
+ const blocked = this.requireMutationConfirmation(p, "remove a team membership");
604
+ if (blocked) return toolText(blocked);
605
+ const org = typeof p.org === "string" ? p.org.trim() : "";
606
+ const teamSlug = typeof p.teamSlug === "string" ? p.teamSlug.trim() : "";
607
+ const username = typeof p.username === "string" ? p.username.trim() : "";
608
+ if (!org || !teamSlug || !username) return toolText("org, teamSlug, and username are required.");
609
+ const agentId = this.resolveToolAgentId(p.agentId);
610
+ const resolved = await this.requireToolIdentity(agentId);
611
+ if ("error" in resolved) return toolText(resolved.error);
612
+ try {
613
+ await resolved.client.removeTeamMembership(org, teamSlug, username);
614
+ return toolText(`Removed ${username} from ${org}/${teamSlug}.`);
615
+ } catch (error) {
616
+ return toolText(`Unable to remove ${username} from ${org}/${teamSlug}: ${String(error)}`);
617
+ }
618
+ },
619
+ });
620
+
621
+ this.api.registerTool({
622
+ name: "collaboration_team_repos",
623
+ description: "List repositories currently granted to an organization team.",
624
+ required: true,
625
+ parameters: {
626
+ type: "object",
627
+ additionalProperties: false,
628
+ properties: {
629
+ org: { type: "string", minLength: 1, description: "Organization login." },
630
+ teamSlug: { type: "string", minLength: 1, description: "Team slug." },
631
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
632
+ },
633
+ required: ["org", "teamSlug"],
634
+ },
635
+ execute: async (_id: string, params: unknown) => {
636
+ const p = asRecord(params);
637
+ const org = typeof p.org === "string" ? p.org.trim() : "";
638
+ const teamSlug = typeof p.teamSlug === "string" ? p.teamSlug.trim() : "";
639
+ if (!org || !teamSlug) return toolText("org and teamSlug are required.");
640
+ const agentId = this.resolveToolAgentId(p.agentId);
641
+ const resolved = await this.requireToolIdentity(agentId);
642
+ if ("error" in resolved) return toolText(resolved.error);
643
+ try {
644
+ const repos = await resolved.client.listTeamRepos(org, teamSlug);
645
+ if (repos.length === 0) return toolText(`No repositories are granted to ${org}/${teamSlug}.`);
646
+ return toolText([
647
+ `Repositories granted to ${org}/${teamSlug}:`,
648
+ ...repos.map((repo) => `- ${renderRepoGrantLine(repo)}`),
649
+ ].join("\n"));
650
+ } catch (error) {
651
+ return toolText(`Unable to list repositories for ${org}/${teamSlug}: ${String(error)}`);
652
+ }
653
+ },
654
+ });
655
+
656
+ this.api.registerTool({
657
+ name: "collaboration_team_repo_set",
658
+ description: "Grant an organization team access to a repo. Requires confirmed=true after explicit user approval.",
659
+ required: true,
660
+ parameters: {
661
+ type: "object",
662
+ additionalProperties: false,
663
+ properties: {
664
+ org: { type: "string", minLength: 1, description: "Organization login." },
665
+ teamSlug: { type: "string", minLength: 1, description: "Team slug." },
666
+ repo: { type: "string", minLength: 3, description: "Optional target repo in owner/repo form. Defaults to the agent's defaultRepo." },
667
+ permission: { type: "string", enum: ["read", "write", "admin"], description: "Repo permission. Defaults to write." },
668
+ confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
669
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
670
+ },
671
+ required: ["org", "teamSlug"],
672
+ },
673
+ execute: async (_id: string, params: unknown) => {
674
+ const p = asRecord(params);
675
+ const blocked = this.requireMutationConfirmation(p, "grant team repo access");
676
+ if (blocked) return toolText(blocked);
677
+ const org = typeof p.org === "string" ? p.org.trim() : "";
678
+ const teamSlug = typeof p.teamSlug === "string" ? p.teamSlug.trim() : "";
679
+ if (!org || !teamSlug) return toolText("org and teamSlug are required.");
680
+ const permission = this.resolveCollaborationPermission(p.permission, "write");
681
+ if ("error" in permission) return toolText(permission.error);
682
+ const agentId = this.resolveToolAgentId(p.agentId);
683
+ const target = await this.requireCollaborationRepo(agentId, p.repo);
684
+ if ("error" in target) return toolText(target.error);
685
+ try {
686
+ await target.client.setTeamRepoAccess(org, teamSlug, target.owner, target.repo, permission.permission);
687
+ return toolText(`Granted ${org}/${teamSlug} ${permission.permission} access to ${target.fullName}.`);
688
+ } catch (error) {
689
+ return toolText(`Unable to grant ${org}/${teamSlug} access to ${target.fullName}: ${String(error)}`);
690
+ }
691
+ },
692
+ });
693
+
694
+ this.api.registerTool({
695
+ name: "collaboration_team_repo_remove",
696
+ description: "Remove an organization team's repo grant. Requires confirmed=true after explicit user approval.",
697
+ required: true,
698
+ parameters: {
699
+ type: "object",
700
+ additionalProperties: false,
701
+ properties: {
702
+ org: { type: "string", minLength: 1, description: "Organization login." },
703
+ teamSlug: { type: "string", minLength: 1, description: "Team slug." },
704
+ repo: { type: "string", minLength: 3, description: "Optional target repo in owner/repo form. Defaults to the agent's defaultRepo." },
705
+ confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
706
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
707
+ },
708
+ required: ["org", "teamSlug"],
709
+ },
710
+ execute: async (_id: string, params: unknown) => {
711
+ const p = asRecord(params);
712
+ const blocked = this.requireMutationConfirmation(p, "remove a team repo grant");
713
+ if (blocked) return toolText(blocked);
714
+ const org = typeof p.org === "string" ? p.org.trim() : "";
715
+ const teamSlug = typeof p.teamSlug === "string" ? p.teamSlug.trim() : "";
716
+ if (!org || !teamSlug) return toolText("org and teamSlug are required.");
717
+ const agentId = this.resolveToolAgentId(p.agentId);
718
+ const target = await this.requireCollaborationRepo(agentId, p.repo);
719
+ if ("error" in target) return toolText(target.error);
720
+ try {
721
+ await target.client.removeTeamRepoAccess(org, teamSlug, target.owner, target.repo);
722
+ return toolText(`Removed team grant ${org}/${teamSlug} from ${target.fullName}.`);
723
+ } catch (error) {
724
+ return toolText(`Unable to remove ${org}/${teamSlug} from ${target.fullName}: ${String(error)}`);
725
+ }
726
+ },
727
+ });
728
+
729
+ this.api.registerTool({
730
+ name: "collaboration_repo_collaborators",
731
+ description: "List direct collaborators on a repo before changing repository-level access.",
732
+ required: true,
733
+ parameters: {
734
+ type: "object",
735
+ additionalProperties: false,
736
+ properties: {
737
+ repo: { type: "string", minLength: 3, description: "Optional target repo in owner/repo form. Defaults to the agent's defaultRepo." },
738
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
739
+ },
740
+ },
741
+ execute: async (_id: string, params: unknown) => {
742
+ const p = asRecord(params);
743
+ const agentId = this.resolveToolAgentId(p.agentId);
744
+ const target = await this.requireCollaborationRepo(agentId, p.repo);
745
+ if ("error" in target) return toolText(target.error);
746
+ try {
747
+ const collaborators = await target.client.listRepoCollaborators(target.owner, target.repo);
748
+ if (collaborators.length === 0) return toolText(`No direct collaborators found on ${target.fullName}.`);
749
+ return toolText([
750
+ `Direct collaborators on ${target.fullName}:`,
751
+ ...collaborators.map((collaborator) => `- ${renderCollaboratorLine(collaborator)}`),
752
+ ].join("\n"));
753
+ } catch (error) {
754
+ return toolText(`Unable to list collaborators on ${target.fullName}: ${String(error)}`);
755
+ }
756
+ },
757
+ });
758
+
759
+ this.api.registerTool({
760
+ name: "collaboration_repo_invitations",
761
+ description: "List pending repository invitations on a repo before assuming a collaborator grant is active.",
762
+ required: true,
763
+ parameters: {
764
+ type: "object",
765
+ additionalProperties: false,
766
+ properties: {
767
+ repo: { type: "string", minLength: 3, description: "Optional target repo in owner/repo form. Defaults to the agent's defaultRepo." },
768
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
769
+ },
770
+ },
771
+ execute: async (_id: string, params: unknown) => {
772
+ const p = asRecord(params);
773
+ const agentId = this.resolveToolAgentId(p.agentId);
774
+ const target = await this.requireCollaborationRepo(agentId, p.repo);
775
+ if ("error" in target) return toolText(target.error);
776
+ try {
777
+ const invitations = await target.client.listRepoInvitations(target.owner, target.repo);
778
+ if (invitations.length === 0) return toolText(`No pending repository invitations found on ${target.fullName}.`);
779
+ return toolText([
780
+ `Pending repository invitations on ${target.fullName}:`,
781
+ ...invitations.map((invitation) => `- ${renderRepoInvitationLine(invitation)}`),
782
+ ].join("\n"));
783
+ } catch (error) {
784
+ return toolText(`Unable to list pending repository invitations on ${target.fullName}: ${String(error)}`);
785
+ }
786
+ },
787
+ });
788
+
789
+ this.api.registerTool({
790
+ name: "collaboration_repo_collaborator_set",
791
+ description: "Add or update a direct collaborator on a repo. Requires confirmed=true after explicit user approval.",
792
+ required: true,
793
+ parameters: {
794
+ type: "object",
795
+ additionalProperties: false,
796
+ properties: {
797
+ repo: { type: "string", minLength: 3, description: "Optional target repo in owner/repo form. Defaults to the agent's defaultRepo." },
798
+ username: { type: "string", minLength: 1, description: "Username to grant direct access." },
799
+ permission: { type: "string", enum: ["read", "write", "admin"], description: "Repo permission. Defaults to read." },
800
+ confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
801
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
802
+ },
803
+ required: ["username"],
804
+ },
805
+ execute: async (_id: string, params: unknown) => {
806
+ const p = asRecord(params);
807
+ const blocked = this.requireMutationConfirmation(p, "change a direct collaborator");
808
+ if (blocked) return toolText(blocked);
809
+ const username = typeof p.username === "string" ? p.username.trim() : "";
810
+ if (!username) return toolText("username is empty.");
811
+ const permission = this.resolveCollaborationPermission(p.permission, "read");
812
+ if ("error" in permission) return toolText(permission.error);
813
+ const agentId = this.resolveToolAgentId(p.agentId);
814
+ const target = await this.requireCollaborationRepo(agentId, p.repo);
815
+ if ("error" in target) return toolText(target.error);
816
+ try {
817
+ const invitation = await target.client.setRepoCollaborator(target.owner, target.repo, username, permission.permission);
818
+ if (invitation?.id) {
819
+ return toolText(`Created pending invitation ${invitation.id} for ${username} on ${target.fullName} with ${permission.permission} permission. The user must accept it before the repo appears in their accessible memory repos.`);
820
+ }
821
+ return toolText(`Updated direct collaborator ${username} on ${target.fullName} to ${permission.permission}.`);
822
+ } catch (error) {
823
+ return toolText(`Unable to grant ${username} access to ${target.fullName}: ${String(error)}`);
824
+ }
825
+ },
826
+ });
827
+
828
+ this.api.registerTool({
829
+ name: "collaboration_repo_collaborator_remove",
830
+ description: "Remove a direct collaborator from a repo. Requires confirmed=true after explicit user approval.",
831
+ required: true,
832
+ parameters: {
833
+ type: "object",
834
+ additionalProperties: false,
835
+ properties: {
836
+ repo: { type: "string", minLength: 3, description: "Optional target repo in owner/repo form. Defaults to the agent's defaultRepo." },
837
+ username: { type: "string", minLength: 1, description: "Username to remove." },
838
+ confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
839
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
840
+ },
841
+ required: ["username"],
842
+ },
843
+ execute: async (_id: string, params: unknown) => {
844
+ const p = asRecord(params);
845
+ const blocked = this.requireMutationConfirmation(p, "remove a direct collaborator");
846
+ if (blocked) return toolText(blocked);
847
+ const username = typeof p.username === "string" ? p.username.trim() : "";
848
+ if (!username) return toolText("username is empty.");
849
+ const agentId = this.resolveToolAgentId(p.agentId);
850
+ const target = await this.requireCollaborationRepo(agentId, p.repo);
851
+ if ("error" in target) return toolText(target.error);
852
+ try {
853
+ await target.client.removeRepoCollaborator(target.owner, target.repo, username);
854
+ return toolText(`Removed ${username} from ${target.fullName}.`);
855
+ } catch (error) {
856
+ return toolText(`Unable to remove ${username} from ${target.fullName}: ${String(error)}`);
857
+ }
858
+ },
859
+ });
860
+
861
+ this.api.registerTool({
862
+ name: "collaboration_user_repo_invitations",
863
+ description: "List pending repository invitations for the current ClawMem identity before concluding that no shared repo is available.",
864
+ required: true,
865
+ parameters: {
866
+ type: "object",
867
+ additionalProperties: false,
868
+ properties: {
869
+ repo: { type: "string", minLength: 3, description: "Optional owner/repo filter." },
870
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
871
+ },
872
+ },
873
+ execute: async (_id: string, params: unknown) => {
874
+ const p = asRecord(params);
875
+ const parsedRepo = this.resolveToolRepo(p.repo);
876
+ if (parsedRepo.error) return toolText(parsedRepo.error);
877
+ const agentId = this.resolveToolAgentId(p.agentId);
878
+ const resolved = await this.requireToolIdentity(agentId);
879
+ if ("error" in resolved) return toolText(resolved.error);
880
+ try {
881
+ const invitations = await resolved.client.listUserRepoInvitations();
882
+ const filtered = parsedRepo.repo
883
+ ? invitations.filter((invitation) => repoSummaryFullName(invitation.repository) === parsedRepo.repo)
884
+ : invitations;
885
+ if (filtered.length === 0) {
886
+ return toolText(parsedRepo.repo
887
+ ? `No pending repository invitations matched ${parsedRepo.repo} for agent "${agentId}".`
888
+ : `No pending repository invitations are visible to agent "${agentId}".`);
889
+ }
890
+ return toolText([
891
+ parsedRepo.repo
892
+ ? `Pending repository invitations for agent "${agentId}" on ${parsedRepo.repo}:`
893
+ : `Pending repository invitations for agent "${agentId}":`,
894
+ ...filtered.map((invitation) => `- ${renderRepoInvitationLine(invitation)}`),
895
+ ].join("\n"));
896
+ } catch (error) {
897
+ return toolText(`Unable to list pending repository invitations for agent "${agentId}": ${String(error)}`);
898
+ }
899
+ },
900
+ });
901
+
902
+ this.api.registerTool({
903
+ name: "collaboration_user_repo_invitation_accept",
904
+ description: "Accept a pending repository invitation for the current ClawMem identity. Requires confirmed=true after explicit user approval.",
905
+ required: true,
906
+ parameters: {
907
+ type: "object",
908
+ additionalProperties: false,
909
+ properties: {
910
+ invitationId: { type: "integer", minimum: 1, description: "Pending repository invitation id." },
911
+ confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
912
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
913
+ },
914
+ required: ["invitationId"],
915
+ },
916
+ execute: async (_id: string, params: unknown) => {
917
+ const p = asRecord(params);
918
+ const blocked = this.requireMutationConfirmation(p, "accept a repository invitation");
919
+ if (blocked) return toolText(blocked);
920
+ const invitationId = this.resolvePositiveInteger(p.invitationId, "invitationId");
921
+ if ("error" in invitationId) return toolText(invitationId.error);
922
+ const agentId = this.resolveToolAgentId(p.agentId);
923
+ const resolved = await this.requireToolIdentity(agentId);
924
+ if ("error" in resolved) return toolText(resolved.error);
925
+ try {
926
+ await resolved.client.acceptUserRepoInvitation(invitationId.value);
927
+ return toolText(`Accepted repository invitation ${invitationId.value} for agent "${agentId}". Re-run memory_repos if you want to confirm the shared repo is now visible.`);
928
+ } catch (error) {
929
+ return toolText(`Unable to accept repository invitation ${invitationId.value} for agent "${agentId}": ${String(error)}`);
930
+ }
931
+ },
932
+ });
933
+
934
+ this.api.registerTool({
935
+ name: "collaboration_user_repo_invitation_decline",
936
+ description: "Decline a pending repository invitation for the current ClawMem identity. Requires confirmed=true after explicit user approval.",
937
+ required: true,
938
+ parameters: {
939
+ type: "object",
940
+ additionalProperties: false,
941
+ properties: {
942
+ invitationId: { type: "integer", minimum: 1, description: "Pending repository invitation id." },
943
+ confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
944
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
945
+ },
946
+ required: ["invitationId"],
947
+ },
948
+ execute: async (_id: string, params: unknown) => {
949
+ const p = asRecord(params);
950
+ const blocked = this.requireMutationConfirmation(p, "decline a repository invitation");
951
+ if (blocked) return toolText(blocked);
952
+ const invitationId = this.resolvePositiveInteger(p.invitationId, "invitationId");
953
+ if ("error" in invitationId) return toolText(invitationId.error);
954
+ const agentId = this.resolveToolAgentId(p.agentId);
955
+ const resolved = await this.requireToolIdentity(agentId);
956
+ if ("error" in resolved) return toolText(resolved.error);
957
+ try {
958
+ await resolved.client.declineUserRepoInvitation(invitationId.value);
959
+ return toolText(`Declined repository invitation ${invitationId.value} for agent "${agentId}".`);
960
+ } catch (error) {
961
+ return toolText(`Unable to decline repository invitation ${invitationId.value} for agent "${agentId}": ${String(error)}`);
962
+ }
963
+ },
964
+ });
965
+
966
+ this.api.registerTool({
967
+ name: "collaboration_org_invitations",
968
+ description: "List pending organization invitations before issuing or debugging membership changes.",
969
+ required: true,
970
+ parameters: {
971
+ type: "object",
972
+ additionalProperties: false,
973
+ properties: {
974
+ org: { type: "string", minLength: 1, description: "Organization login." },
975
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
976
+ },
977
+ required: ["org"],
978
+ },
979
+ execute: async (_id: string, params: unknown) => {
980
+ const p = asRecord(params);
981
+ const org = typeof p.org === "string" ? p.org.trim() : "";
982
+ if (!org) return toolText("org is empty.");
983
+ const agentId = this.resolveToolAgentId(p.agentId);
984
+ const resolved = await this.requireToolIdentity(agentId);
985
+ if ("error" in resolved) return toolText(resolved.error);
986
+ try {
987
+ const invitations = await resolved.client.listOrgInvitations(org);
988
+ if (invitations.length === 0) return toolText(`No pending invitations found in org "${org}".`);
989
+ return toolText([
990
+ `Pending invitations in org "${org}":`,
991
+ ...invitations.map((invitation) => `- ${renderInvitationLine(invitation)}`),
992
+ ].join("\n"));
993
+ } catch (error) {
994
+ return toolText(`Unable to list invitations for org "${org}": ${String(error)}`);
995
+ }
996
+ },
997
+ });
998
+
999
+ this.api.registerTool({
1000
+ name: "collaboration_org_invitation_create",
1001
+ description: "Create an organization invitation, optionally pre-assigning team ids. Requires confirmed=true after explicit user approval.",
1002
+ required: true,
1003
+ parameters: {
1004
+ type: "object",
1005
+ additionalProperties: false,
1006
+ properties: {
1007
+ org: { type: "string", minLength: 1, description: "Organization login." },
1008
+ inviteeLogin: { type: "string", minLength: 1, description: "Username to invite." },
1009
+ role: { type: "string", enum: ["member", "admin"], description: "Org role for the invitation. Defaults to member." },
1010
+ teamIds: {
1011
+ type: "array",
1012
+ description: "Optional numeric team ids to pre-assign on acceptance.",
1013
+ items: { type: "integer", minimum: 1 },
1014
+ minItems: 1,
1015
+ maxItems: 20,
1016
+ },
1017
+ expiresInDays: { type: "integer", minimum: 1, maximum: 365, description: "Optional invitation expiry in days." },
1018
+ confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
1019
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
1020
+ },
1021
+ required: ["org", "inviteeLogin"],
1022
+ },
1023
+ execute: async (_id: string, params: unknown) => {
1024
+ const p = asRecord(params);
1025
+ const blocked = this.requireMutationConfirmation(p, "create an organization invitation");
1026
+ if (blocked) return toolText(blocked);
1027
+ const org = typeof p.org === "string" ? p.org.trim() : "";
1028
+ const inviteeLogin = typeof p.inviteeLogin === "string" ? p.inviteeLogin.trim() : "";
1029
+ if (!org || !inviteeLogin) return toolText("org and inviteeLogin are required.");
1030
+ const role: CollaborationOrgRole = p.role === "admin" ? "admin" : "member";
1031
+ const teamIds = Array.isArray(p.teamIds)
1032
+ ? p.teamIds.filter((value): value is number => typeof value === "number" && Number.isInteger(value) && value > 0)
1033
+ : undefined;
1034
+ if (Array.isArray(p.teamIds) && teamIds && teamIds.length !== p.teamIds.length) return toolText("teamIds must contain only positive integers.");
1035
+ const expiresInDays = typeof p.expiresInDays === "number" && Number.isInteger(p.expiresInDays) ? p.expiresInDays : undefined;
1036
+ const agentId = this.resolveToolAgentId(p.agentId);
1037
+ const resolved = await this.requireToolIdentity(agentId);
1038
+ if ("error" in resolved) return toolText(resolved.error);
1039
+ try {
1040
+ const invitation = await resolved.client.createOrgInvitation(org, {
1041
+ inviteeLogin,
1042
+ role,
1043
+ ...(teamIds && teamIds.length > 0 ? { teamIds } : {}),
1044
+ ...(expiresInDays ? { expiresInDays } : {}),
1045
+ });
1046
+ return toolText(`Created invitation in "${org}": ${renderInvitationLine(invitation)}.`);
1047
+ } catch (error) {
1048
+ return toolText(`Unable to create invitation for ${inviteeLogin} in org "${org}": ${String(error)}`);
1049
+ }
1050
+ },
1051
+ });
1052
+
1053
+ this.api.registerTool({
1054
+ name: "collaboration_user_org_invitations",
1055
+ description: "List pending organization invitations for the current ClawMem identity before concluding that no shared org access is available.",
1056
+ required: true,
1057
+ parameters: {
1058
+ type: "object",
1059
+ additionalProperties: false,
1060
+ properties: {
1061
+ org: { type: "string", minLength: 1, description: "Optional organization login filter." },
1062
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
1063
+ },
1064
+ },
1065
+ execute: async (_id: string, params: unknown) => {
1066
+ const p = asRecord(params);
1067
+ const orgFilter = typeof p.org === "string" && p.org.trim() ? p.org.trim() : undefined;
1068
+ const agentId = this.resolveToolAgentId(p.agentId);
1069
+ const resolved = await this.requireToolIdentity(agentId);
1070
+ if ("error" in resolved) return toolText(resolved.error);
1071
+ try {
1072
+ const invitations = await resolved.client.listUserOrgInvitations();
1073
+ const filtered = orgFilter
1074
+ ? invitations.filter((invitation) => invitation.organization?.login?.trim() === orgFilter)
1075
+ : invitations;
1076
+ if (filtered.length === 0) {
1077
+ return toolText(orgFilter
1078
+ ? `No pending organization invitations matched "${orgFilter}" for agent "${agentId}".`
1079
+ : `No pending organization invitations are visible to agent "${agentId}".`);
1080
+ }
1081
+ return toolText([
1082
+ orgFilter
1083
+ ? `Pending organization invitations for agent "${agentId}" in "${orgFilter}":`
1084
+ : `Pending organization invitations for agent "${agentId}":`,
1085
+ ...filtered.map((invitation) => `- ${renderUserOrganizationInvitationLine(invitation)}`),
1086
+ ].join("\n"));
1087
+ } catch (error) {
1088
+ return toolText(`Unable to list pending organization invitations for agent "${agentId}": ${String(error)}`);
1089
+ }
1090
+ },
1091
+ });
1092
+
1093
+ this.api.registerTool({
1094
+ name: "collaboration_user_org_invitation_accept",
1095
+ description: "Accept a pending organization invitation for the current ClawMem identity. Requires confirmed=true after explicit user approval.",
1096
+ required: true,
1097
+ parameters: {
1098
+ type: "object",
1099
+ additionalProperties: false,
1100
+ properties: {
1101
+ invitationId: { type: "integer", minimum: 1, description: "Pending organization invitation id." },
1102
+ confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
1103
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
1104
+ },
1105
+ required: ["invitationId"],
1106
+ },
1107
+ execute: async (_id: string, params: unknown) => {
1108
+ const p = asRecord(params);
1109
+ const blocked = this.requireMutationConfirmation(p, "accept an organization invitation");
1110
+ if (blocked) return toolText(blocked);
1111
+ const invitationId = this.resolvePositiveInteger(p.invitationId, "invitationId");
1112
+ if ("error" in invitationId) return toolText(invitationId.error);
1113
+ const agentId = this.resolveToolAgentId(p.agentId);
1114
+ const resolved = await this.requireToolIdentity(agentId);
1115
+ if ("error" in resolved) return toolText(resolved.error);
1116
+ try {
1117
+ await resolved.client.acceptUserOrgInvitation(invitationId.value);
1118
+ return toolText(`Accepted organization invitation ${invitationId.value} for agent "${agentId}".`);
1119
+ } catch (error) {
1120
+ return toolText(`Unable to accept organization invitation ${invitationId.value} for agent "${agentId}": ${String(error)}`);
1121
+ }
1122
+ },
1123
+ });
1124
+
1125
+ this.api.registerTool({
1126
+ name: "collaboration_user_org_invitation_decline",
1127
+ description: "Decline a pending organization invitation for the current ClawMem identity. Requires confirmed=true after explicit user approval.",
1128
+ required: true,
1129
+ parameters: {
1130
+ type: "object",
1131
+ additionalProperties: false,
1132
+ properties: {
1133
+ invitationId: { type: "integer", minimum: 1, description: "Pending organization invitation id." },
1134
+ confirmed: { type: "boolean", description: "Must be true after the user approves the exact write action." },
1135
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
1136
+ },
1137
+ required: ["invitationId"],
1138
+ },
1139
+ execute: async (_id: string, params: unknown) => {
1140
+ const p = asRecord(params);
1141
+ const blocked = this.requireMutationConfirmation(p, "decline an organization invitation");
1142
+ if (blocked) return toolText(blocked);
1143
+ const invitationId = this.resolvePositiveInteger(p.invitationId, "invitationId");
1144
+ if ("error" in invitationId) return toolText(invitationId.error);
1145
+ const agentId = this.resolveToolAgentId(p.agentId);
1146
+ const resolved = await this.requireToolIdentity(agentId);
1147
+ if ("error" in resolved) return toolText(resolved.error);
1148
+ try {
1149
+ await resolved.client.declineUserOrgInvitation(invitationId.value);
1150
+ return toolText(`Declined organization invitation ${invitationId.value} for agent "${agentId}".`);
1151
+ } catch (error) {
1152
+ return toolText(`Unable to decline organization invitation ${invitationId.value} for agent "${agentId}": ${String(error)}`);
1153
+ }
1154
+ },
1155
+ });
1156
+
1157
+ this.api.registerTool({
1158
+ name: "collaboration_outside_collaborators",
1159
+ description: "List outside collaborators in an organization to inspect non-member repo access.",
1160
+ required: true,
1161
+ parameters: {
1162
+ type: "object",
1163
+ additionalProperties: false,
1164
+ properties: {
1165
+ org: { type: "string", minLength: 1, description: "Organization login." },
1166
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
1167
+ },
1168
+ required: ["org"],
1169
+ },
1170
+ execute: async (_id: string, params: unknown) => {
1171
+ const p = asRecord(params);
1172
+ const org = typeof p.org === "string" ? p.org.trim() : "";
1173
+ if (!org) return toolText("org is empty.");
1174
+ const agentId = this.resolveToolAgentId(p.agentId);
1175
+ const resolved = await this.requireToolIdentity(agentId);
1176
+ if ("error" in resolved) return toolText(resolved.error);
1177
+ try {
1178
+ const users = await resolved.client.listOrgOutsideCollaborators(org);
1179
+ if (users.length === 0) return toolText(`No outside collaborators found in org "${org}".`);
1180
+ return toolText([
1181
+ `Outside collaborators in org "${org}":`,
1182
+ ...users.map((user) => `- ${renderCollaboratorLine(user)}`),
1183
+ ].join("\n"));
1184
+ } catch (error) {
1185
+ return toolText(`Unable to list outside collaborators for org "${org}": ${String(error)}`);
1186
+ }
1187
+ },
1188
+ });
1189
+
1190
+ this.api.registerTool({
1191
+ name: "collaboration_repo_access_inspect",
1192
+ description: "Inspect repo access paths by summarizing direct collaborators, team grants, and org-level context.",
1193
+ required: true,
1194
+ parameters: {
1195
+ type: "object",
1196
+ additionalProperties: false,
1197
+ properties: {
1198
+ repo: { type: "string", minLength: 3, description: "Optional target repo in owner/repo form. Defaults to the agent's defaultRepo." },
1199
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
1200
+ },
1201
+ },
1202
+ execute: async (_id: string, params: unknown) => {
1203
+ const p = asRecord(params);
1204
+ const agentId = this.resolveToolAgentId(p.agentId);
1205
+ const target = await this.requireCollaborationRepo(agentId, p.repo);
1206
+ if ("error" in target) return toolText(target.error);
1207
+
1208
+ try {
1209
+ const lines = [`Repo access inspection for ${target.fullName}:`];
1210
+ const notes: string[] = [];
1211
+ let orgName: string | undefined;
1212
+
1213
+ try {
1214
+ const repo = await target.client.getRepo(target.owner, target.repo);
1215
+ lines.push(`- Visibility: ${repo.private ? "private" : "shared/public"}`);
1216
+ if (repo.description?.trim()) lines.push(`- Description: ${repo.description.trim()}`);
1217
+ orgName = repo.owner?.login?.trim() || target.owner;
1218
+ } catch (error) {
1219
+ notes.push(`Repo metadata unavailable: ${String(error)}`);
1220
+ orgName = target.owner;
1221
+ }
1222
+
1223
+ try {
1224
+ const org = await target.client.getOrg(orgName);
1225
+ lines.push(`- Org default repository permission: ${org.default_repository_permission?.trim() || "unknown"}`);
1226
+ } catch (error) {
1227
+ notes.push(`Org metadata unavailable for "${orgName}": ${String(error)}`);
1228
+ }
1229
+
1230
+ try {
1231
+ const collaborators = await target.client.listRepoCollaborators(target.owner, target.repo);
1232
+ lines.push("");
1233
+ lines.push("Direct collaborators:");
1234
+ if (collaborators.length === 0) lines.push("- None visible");
1235
+ else lines.push(...collaborators.map((collaborator) => `- ${renderCollaboratorLine(collaborator)}`));
1236
+ } catch (error) {
1237
+ notes.push(`Direct collaborator lookup failed: ${String(error)}`);
1238
+ }
1239
+
1240
+ try {
1241
+ const invitations = await target.client.listRepoInvitations(target.owner, target.repo);
1242
+ lines.push("");
1243
+ lines.push("Pending repository invitations:");
1244
+ if (invitations.length === 0) lines.push("- None visible");
1245
+ else lines.push(...invitations.map((invitation) => `- ${renderRepoInvitationLine(invitation)}`));
1246
+ } catch (error) {
1247
+ notes.push(`Repo invitation lookup failed: ${String(error)}`);
1248
+ }
1249
+
1250
+ try {
1251
+ const teams = await target.client.listRepoTeams(target.owner, target.repo);
1252
+ lines.push("");
1253
+ lines.push("Teams with repo access:");
1254
+ if (teams.length === 0) lines.push("- None visible");
1255
+ else lines.push(...teams.map((team) => `- ${renderTeamLine(team)}`));
1256
+ } catch (error) {
1257
+ notes.push(`Repo team grant lookup failed: ${String(error)}`);
1258
+ }
1259
+
1260
+ try {
1261
+ const outside = await target.client.listOrgOutsideCollaborators(orgName);
1262
+ lines.push("");
1263
+ lines.push(`Outside collaborators in owner org "${orgName}":`);
1264
+ if (outside.length === 0) lines.push("- None visible");
1265
+ else lines.push(...outside.map((user) => `- ${renderCollaboratorLine(user)}`));
1266
+ } catch (error) {
1267
+ notes.push(`Outside collaborator lookup failed: ${String(error)}`);
1268
+ }
1269
+
1270
+ if (notes.length > 0) {
1271
+ lines.push("");
1272
+ lines.push("Notes:");
1273
+ lines.push(...notes.map((note) => `- ${note}`));
1274
+ }
1275
+ return toolText(lines.join("\n"));
1276
+ } catch (error) {
1277
+ return toolText(`Unable to inspect access for ${target.fullName}: ${String(error)}`);
1278
+ }
1279
+ },
1280
+ });
393
1281
  }
394
1282
 
395
1283
  private async handleBeforeAgentStart(prompt: unknown, agentId?: string): Promise<{ prependContext: string } | void> {
@@ -735,6 +1623,56 @@ class ClawMemService {
735
1623
  }
736
1624
  return services;
737
1625
  }
1626
+ private async requireCollaborationRepo(
1627
+ agentId: string,
1628
+ repo: unknown,
1629
+ ): Promise<{ route: ClawMemResolvedRoute; client: GitHubIssueClient; owner: string; repo: string; fullName: string } | { error: string }> {
1630
+ const parsed = this.resolveToolRepo(repo);
1631
+ if (parsed.error) return { error: parsed.error };
1632
+ const resolved = await this.requireToolIdentity(agentId);
1633
+ if ("error" in resolved) return resolved;
1634
+ const fullName = parsed.repo ?? resolved.route.defaultRepo;
1635
+ if (!fullName) {
1636
+ return {
1637
+ error: `No target repo selected for agent "${agentId}". Provide repo explicitly or configure agents.${agentId}.defaultRepo.`,
1638
+ };
1639
+ }
1640
+ const [owner, repoName] = fullName.split("/");
1641
+ if (!owner || !repoName) return { error: `Invalid repo "${fullName}". Expected owner/repo.` };
1642
+ return { ...resolved, owner, repo: repoName, fullName };
1643
+ }
1644
+ private requireMutationConfirmation(params: Record<string, unknown>, action: string): string | null {
1645
+ if (params.confirmed === true) return null;
1646
+ return `Refusing to ${action} without explicit confirmation. Inspect current state first, then retry with confirmed=true only after the user approves the exact change.`;
1647
+ }
1648
+ private resolveCollaborationPermission(
1649
+ value: unknown,
1650
+ fallback: CollaborationPermission,
1651
+ ): { permission: CollaborationPermission } | { error: string } {
1652
+ if (value === undefined || value === null || value === "") return { permission: fallback };
1653
+ if (typeof value !== "string") return { error: "permission must be one of read, write, or admin." };
1654
+ const normalized = normalizePermissionAlias(value);
1655
+ if (normalized === "read" || normalized === "write" || normalized === "admin") return { permission: normalized };
1656
+ return { error: `Unsupported permission "${value}". Use read, write, or admin.` };
1657
+ }
1658
+ private resolveOrgDefaultPermission(
1659
+ value: unknown,
1660
+ fallback: "none" | CollaborationPermission,
1661
+ ): { permission: "none" | CollaborationPermission } | { error: string } {
1662
+ if (value === undefined || value === null || value === "") return { permission: fallback };
1663
+ if (typeof value !== "string") return { error: "defaultPermission must be one of none, read, write, or admin." };
1664
+ const normalized = normalizePermissionAlias(value);
1665
+ if (normalized === "none" || normalized === "read" || normalized === "write" || normalized === "admin") {
1666
+ return { permission: normalized };
1667
+ }
1668
+ return { error: `Unsupported defaultPermission "${value}". Use none, read, write, or admin.` };
1669
+ }
1670
+ private resolvePositiveInteger(value: unknown, field: string): { value: number } | { error: string } {
1671
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
1672
+ return { error: `${field} must be a positive integer.` };
1673
+ }
1674
+ return { value };
1675
+ }
738
1676
  /**
739
1677
  * After finalization, check if the repo still has an empty/default description.
740
1678
  * If so, use the conversation summary to suggest a meaningful name and update
@@ -786,4 +1724,108 @@ function renderMemoryBlock(memory: { memoryId: string; issueNumber?: number; tit
786
1724
  return lines.join("\n");
787
1725
  }
788
1726
 
1727
+ function renderOrgLine(org: { login?: string; name?: string; default_repository_permission?: string; description?: string }): string {
1728
+ const login = org.login?.trim() || "unknown-org";
1729
+ const name = org.name?.trim() ? ` (${org.name.trim()})` : "";
1730
+ const permission = org.default_repository_permission?.trim() ? ` [default:${normalizePermissionAlias(org.default_repository_permission) || org.default_repository_permission.trim()}]` : "";
1731
+ const description = org.description?.trim() ? ` - ${org.description.trim()}` : "";
1732
+ return `${login}${name}${permission}${description}`;
1733
+ }
1734
+
1735
+ function renderTeamLine(team: { slug?: string; name?: string; description?: string; privacy?: string; permission?: string; role_name?: string; permissions?: Record<string, boolean | undefined> }): string {
1736
+ const slug = team.slug?.trim() || team.name?.trim() || "unknown-team";
1737
+ const name = team.name?.trim() && team.name?.trim() !== slug ? ` (${team.name.trim()})` : "";
1738
+ const privacy = team.privacy?.trim() ? ` [${team.privacy.trim()}]` : "";
1739
+ const permission = canonicalPermission(team.permissions, team.permission || team.role_name);
1740
+ const permissionText = permission !== "unknown" ? ` [perm:${permission}]` : "";
1741
+ const description = team.description?.trim() ? ` - ${team.description.trim()}` : "";
1742
+ return `${slug}${name}${privacy}${permissionText}${description}`;
1743
+ }
1744
+
1745
+ function repoSummaryFullName(repo?: { full_name?: string; owner?: { login?: string }; name?: string }): string | undefined {
1746
+ const fullName = repo?.full_name?.trim();
1747
+ if (fullName) return fullName;
1748
+ const owner = repo?.owner?.login?.trim();
1749
+ const name = repo?.name?.trim();
1750
+ if (owner && name) return `${owner}/${name}`;
1751
+ return name || undefined;
1752
+ }
1753
+
1754
+ function renderRepoGrantLine(repo: { full_name?: string; name?: string; permissions?: Record<string, boolean | undefined>; role_name?: string; description?: string }): string {
1755
+ const fullName = repoSummaryFullName(repo) || "unknown-repo";
1756
+ const permission = canonicalPermission(repo.permissions, repo.role_name);
1757
+ const permissionText = permission !== "unknown" ? ` [${permission}]` : "";
1758
+ const description = repo.description?.trim() ? ` - ${repo.description.trim()}` : "";
1759
+ return `${fullName}${permissionText}${description}`;
1760
+ }
1761
+
1762
+ function renderCollaboratorLine(user: { login?: string; name?: string; permissions?: Record<string, boolean | undefined>; role_name?: string }): string {
1763
+ const login = user.login?.trim() || user.name?.trim() || "unknown-user";
1764
+ const name = user.name?.trim() && user.name?.trim() !== login ? ` (${user.name.trim()})` : "";
1765
+ const permission = canonicalPermission(user.permissions, user.role_name);
1766
+ const permissionText = permission !== "unknown" ? ` [${permission}]` : "";
1767
+ return `${login}${name}${permissionText}`;
1768
+ }
1769
+
1770
+ function renderRepoInvitationLine(invitation: { id?: number; created_at?: string; permissions?: string; repository?: { full_name?: string; owner?: { login?: string }; name?: string }; invitee?: { login?: string }; inviter?: { login?: string } }): string {
1771
+ const repo = repoSummaryFullName(invitation.repository) || "unknown-repo";
1772
+ const permission = normalizePermissionAlias(invitation.permissions) || invitation.permissions?.trim() || "read";
1773
+ const idText = typeof invitation.id === "number" ? ` id:${invitation.id}` : "";
1774
+ const created = invitation.created_at?.trim() ? ` created:${invitation.created_at.trim()}` : "";
1775
+ const invitee = invitation.invitee?.login?.trim() ? ` invitee:${invitation.invitee.login.trim()}` : "";
1776
+ const inviter = invitation.inviter?.login?.trim() ? ` inviter:${invitation.inviter.login.trim()}` : "";
1777
+ return `${repo} [perm:${permission}${idText}${created}${invitee}${inviter}]`;
1778
+ }
1779
+
1780
+ function renderInvitationLine(invitation: { id?: number; role?: string; created_at?: string; expires_at?: string | null; email?: string; login?: string; organization?: { login?: string }; invitee?: { login?: string }; team_ids?: number[]; teams?: Array<{ name?: string; slug?: string }> }): string {
1781
+ const target = invitation.invitee?.login?.trim() || invitation.login?.trim() || invitation.email?.trim() || "unknown-invitee";
1782
+ const role = invitation.role?.trim() || "member";
1783
+ const created = invitation.created_at?.trim() ? ` created:${invitation.created_at.trim()}` : "";
1784
+ const expires = typeof invitation.expires_at === "string" && invitation.expires_at.trim() ? ` expires:${invitation.expires_at.trim()}` : "";
1785
+ const teams = Array.isArray(invitation.teams)
1786
+ ? invitation.teams.map((team) => team.slug?.trim() || team.name?.trim() || "").filter(Boolean)
1787
+ : Array.isArray(invitation.team_ids)
1788
+ ? invitation.team_ids.filter((teamId): teamId is number => typeof teamId === "number" && Number.isInteger(teamId) && teamId > 0).map(String)
1789
+ : [];
1790
+ const teamsText = teams.length > 0 ? ` teams:${teams.join(",")}` : "";
1791
+ const idText = typeof invitation.id === "number" ? ` id:${invitation.id}` : "";
1792
+ const orgText = invitation.organization?.login?.trim() ? ` org:${invitation.organization.login.trim()}` : "";
1793
+ return `${target} [role:${role}${idText}${created}${expires}${teamsText}${orgText}]`;
1794
+ }
1795
+
1796
+ function renderUserOrganizationInvitationLine(invitation: { id?: number; role?: string; created_at?: string; expires_at?: string | null; organization?: { login?: string }; inviter?: { login?: string }; team_ids?: number[] }): string {
1797
+ const org = invitation.organization?.login?.trim() || "unknown-org";
1798
+ const role = invitation.role?.trim() || "member";
1799
+ const idText = typeof invitation.id === "number" ? ` id:${invitation.id}` : "";
1800
+ const created = invitation.created_at?.trim() ? ` created:${invitation.created_at.trim()}` : "";
1801
+ const expires = typeof invitation.expires_at === "string" && invitation.expires_at.trim() ? ` expires:${invitation.expires_at.trim()}` : "";
1802
+ const teamIds = Array.isArray(invitation.team_ids)
1803
+ ? invitation.team_ids.filter((teamId): teamId is number => typeof teamId === "number" && Number.isInteger(teamId) && teamId > 0).map(String)
1804
+ : [];
1805
+ const teamsText = teamIds.length > 0 ? ` teamIds:${teamIds.join(",")}` : "";
1806
+ const inviter = invitation.inviter?.login?.trim() ? ` inviter:${invitation.inviter.login.trim()}` : "";
1807
+ return `${org} [role:${role}${idText}${created}${expires}${teamsText}${inviter}]`;
1808
+ }
1809
+
1810
+ function canonicalPermission(permissions?: Record<string, boolean | undefined>, explicit?: string): string {
1811
+ const direct = normalizePermissionAlias(explicit);
1812
+ if (direct) return direct;
1813
+ if (!permissions) return "unknown";
1814
+ if (permissions.admin === true) return "admin";
1815
+ if (permissions.maintain === true || permissions.push === true || permissions.write === true) return "write";
1816
+ if (permissions.triage === true || permissions.pull === true || permissions.read === true) return "read";
1817
+ return "unknown";
1818
+ }
1819
+
1820
+ function normalizePermissionAlias(value: unknown): "none" | CollaborationPermission | undefined {
1821
+ if (typeof value !== "string") return undefined;
1822
+ const normalized = value.trim().toLowerCase();
1823
+ if (!normalized) return undefined;
1824
+ if (normalized === "none") return "none";
1825
+ if (normalized === "read" || normalized === "pull" || normalized === "triage") return "read";
1826
+ if (normalized === "write" || normalized === "push" || normalized === "maintain") return "write";
1827
+ if (normalized === "admin") return "admin";
1828
+ return undefined;
1829
+ }
1830
+
789
1831
  export function createClawMemPlugin(api: OpenClawPluginApi): void { new ClawMemService(api).register(); }