@clawmem-ai/clawmem 0.1.10 → 0.1.11

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,205 @@
1
+ # ClawMem Manual Operations And Troubleshooting
2
+
3
+ Use this reference only when:
4
+ - the user explicitly wants raw GitHub-style repo or issue operations
5
+ - you are debugging backend state or labels
6
+ - the plugin memory tools are unavailable
7
+
8
+ ClawMem runs on a GitHub-compatible backend, so repo, issue, label, invitation, and team operations follow GitHub-shaped APIs. That is why `gh` and `curl` are valid fallback tools here.
9
+
10
+ ## Contents
11
+
12
+ - Route resolution
13
+ - Preflight
14
+ - Save a memory manually
15
+ - Search memories manually
16
+ - Mark memory as stale manually
17
+ - Link related memories manually
18
+ - `git push` to ClawMem
19
+ - Known pitfalls
20
+ - Autonomy
21
+
22
+ If the plugin tools are available, prefer:
23
+ - `memory_repos` to inspect available repos
24
+ - `memory_list` to inspect the current active-memory index
25
+ - `memory_get` to verify one exact memory
26
+ - `memory_labels` to inspect current schema
27
+ - `memory_repo_create` to create a new memory repo
28
+ - `memory_store` to save
29
+ - `memory_update` to evolve one canonical memory in place
30
+ - `memory_recall` to search
31
+ - `memory_forget` to retire stale memories
32
+
33
+ ## Route resolution
34
+
35
+ ClawMem is routed per agent identity, not through one global repo or token.
36
+
37
+ Resolve the current route with the bundled helper:
38
+
39
+ ```sh
40
+ eval "$(python3 scripts/clawmem_exports.py)"
41
+ ```
42
+
43
+ That exports:
44
+ - `CLAWMEM_AGENT_ID`
45
+ - `CLAWMEM_BASE_URL`
46
+ - `CLAWMEM_HOST`
47
+ - `CLAWMEM_DEFAULT_REPO`
48
+ - `CLAWMEM_REPO`
49
+ - `CLAWMEM_TOKEN`
50
+
51
+ Rules:
52
+ - Never store tokens in files or chat
53
+ - `CLAWMEM_DEFAULT_REPO` is the fallback memory space for automatic flows
54
+ - `CLAWMEM_REPO` is the repo chosen for the current operation
55
+ - If `CLAWMEM_TOKEN` is empty, this agent identity has not been provisioned yet
56
+
57
+ ## Preflight
58
+
59
+ ```sh
60
+ eval "$(python3 scripts/clawmem_exports.py)"
61
+
62
+ test -n "$CLAWMEM_REPO" || { echo "ClawMem repo missing for agent $CLAWMEM_AGENT_ID"; exit 1; }
63
+ test -n "$CLAWMEM_TOKEN" || { echo "ClawMem token missing for agent $CLAWMEM_AGENT_ID"; exit 1; }
64
+ case "$CLAWMEM_REPO" in
65
+ */*) ;;
66
+ *) echo "Invalid CLAWMEM_REPO='$CLAWMEM_REPO' (expected owner/repo)"; exit 1 ;;
67
+ esac
68
+ ```
69
+
70
+ For ClawMem, always pass `--repo "$CLAWMEM_REPO"` to `gh` or use `$CLAWMEM_BASE_URL/repos/$CLAWMEM_REPO/...` with `curl` explicitly.
71
+
72
+ Do not export `GH_HOST` or `GH_ENTERPRISE_TOKEN` globally for unrelated github.com work. Use per-command env prefixes if you need isolation.
73
+
74
+ ## Save a memory manually
75
+
76
+ Use the tool path first. If raw issue control is required:
77
+
78
+ ### With `gh`
79
+
80
+ ```sh
81
+ for lbl in "type:memory" "kind:core-fact" "kind:convention" "kind:lesson" "kind:skill" "kind:task"; do
82
+ GH_HOST="$CLAWMEM_HOST" GH_ENTERPRISE_TOKEN="$CLAWMEM_TOKEN" \
83
+ gh label create "$lbl" --repo "$CLAWMEM_REPO" --color "5319e7" 2>/dev/null || true
84
+ done
85
+
86
+ GH_HOST="$CLAWMEM_HOST" GH_ENTERPRISE_TOKEN="$CLAWMEM_TOKEN" \
87
+ gh issue create --repo "$CLAWMEM_REPO" \
88
+ --title "Memory: <concise title>" \
89
+ --body "<durable detail in plain language>" \
90
+ --label "type:memory" \
91
+ --label "kind:lesson"
92
+ ```
93
+
94
+ ### With `curl`
95
+
96
+ ```sh
97
+ for lbl in "type:memory" "kind:core-fact" "kind:convention" "kind:lesson" "kind:skill" "kind:task"; do
98
+ curl -sf -X POST -H "Authorization: token $CLAWMEM_TOKEN" \
99
+ -H "Content-Type: application/json" \
100
+ "$CLAWMEM_BASE_URL/repos/$CLAWMEM_REPO/labels" \
101
+ -d "{\"name\":\"$lbl\",\"color\":\"5319e7\"}" >/dev/null 2>&1 || true
102
+ done
103
+
104
+ curl -sf -X POST -H "Authorization: token $CLAWMEM_TOKEN" \
105
+ -H "Content-Type: application/json" \
106
+ "$CLAWMEM_BASE_URL/repos/$CLAWMEM_REPO/issues" \
107
+ -d "{
108
+ \"title\": \"Memory: <concise title>\",
109
+ \"body\": \"<durable detail in plain language>\",
110
+ \"labels\": [\"type:memory\", \"kind:lesson\"]
111
+ }" | jq '{number, title, url: .html_url}'
112
+ ```
113
+
114
+ ## Search memories manually
115
+
116
+ ### With `gh`
117
+
118
+ ```sh
119
+ GH_HOST="$CLAWMEM_HOST" GH_ENTERPRISE_TOKEN="$CLAWMEM_TOKEN" \
120
+ gh issue list --repo "$CLAWMEM_REPO" \
121
+ --state open \
122
+ --label "type:memory" \
123
+ --search "<keywords>" \
124
+ --limit 100 \
125
+ --json number,title,body,labels,updatedAt
126
+ ```
127
+
128
+ ### With `curl`
129
+
130
+ ```sh
131
+ curl -sf -H "Authorization: token $CLAWMEM_TOKEN" \
132
+ "$CLAWMEM_BASE_URL/repos/$CLAWMEM_REPO/issues?state=open&labels=type:memory&per_page=100&type=issues" | \
133
+ jq --arg q "<keywords>" '
134
+ ($q | ascii_downcase) as $needle
135
+ | map(select(
136
+ ((.title // "") + "\n" + (.body // "")) | ascii_downcase | contains($needle)
137
+ ))
138
+ | map({number, title, body, labels: [.labels[]?.name], updatedAt: .updated_at})
139
+ '
140
+ ```
141
+
142
+ ## Mark memory as stale manually
143
+
144
+ If this is still the same canonical fact or task, prefer `memory_update` instead of retiring the old node.
145
+
146
+ ### With `gh`
147
+
148
+ ```sh
149
+ GH_HOST="$CLAWMEM_HOST" GH_ENTERPRISE_TOKEN="$CLAWMEM_TOKEN" \
150
+ gh issue close <number> --repo "$CLAWMEM_REPO"
151
+ ```
152
+
153
+ ### With `curl`
154
+
155
+ ```sh
156
+ curl -sf -X PATCH -H "Authorization: token $CLAWMEM_TOKEN" \
157
+ -H "Content-Type: application/json" \
158
+ "$CLAWMEM_BASE_URL/repos/$CLAWMEM_REPO/issues/<number>" \
159
+ -d '{"state": "closed"}'
160
+ ```
161
+
162
+ If a new memory replaces an old one, save the new memory first and mention the old `#ID` in the replacement body so the supersession is explicit.
163
+
164
+ ## Link related memories manually
165
+
166
+ When one memory depends on, refines, or supersedes another, mention `#<id>` in the body so the graph keeps an explicit edge.
167
+
168
+ Prefer doing this when you create a curated raw memory, or when you are already rewriting the full issue body intentionally.
169
+
170
+ If you patch an existing plugin-managed memory body by hand, preserve the existing structured body and add the `#<id>` relation into the durable detail instead of overwriting metadata fields blindly.
171
+
172
+ ## `git push` to ClawMem
173
+
174
+ `GH_HOST` and `GH_ENTERPRISE_TOKEN` affect `gh`, not `git push`. If you need to push code to a ClawMem git service repo:
175
+
176
+ ```sh
177
+ echo "$CLAWMEM_TOKEN" | gh auth login -h "$CLAWMEM_HOST" --with-token
178
+ ```
179
+
180
+ After that, `git push` to `https://git.clawmem.ai/...` works normally.
181
+
182
+ ## Known pitfalls
183
+
184
+ | Problem | Fix |
185
+ |---|---|
186
+ | Labels do not update reliably via `PATCH` on some backends | Use `PUT /repos/{owner}/{repo}/issues/{n}/labels` when you need exact label replacement |
187
+ | `openclaw config get` returns redacted token values | Read the config file path via `openclaw config file`, then inspect the JSON directly |
188
+ | Conversation mirror returns `404` | The cached conversation issue was deleted; the plugin recreates it on the next session |
189
+ | New session gets `401 Unauthorized` | Re-read the current agent route. If this is first use, trigger one real turn so the plugin can finish provisioning |
190
+ | Agent uses the wrong memory repo | Resolve `config.agents.<agentId>` for the current agent; do not read only top-level legacy repo settings |
191
+ | `gh` is not the official GitHub CLI | Run `gh --version`; if it is the npm `gh` package instead of the official CLI, use `curl` or replace the CLI install |
192
+
193
+ ## Autonomy
194
+
195
+ Without confirmation:
196
+ - creating or updating memory nodes
197
+ - adding comments
198
+ - reusing or creating labels
199
+ - closing stale memory nodes
200
+ - creating new memory repos when a new space is clearly needed
201
+
202
+ Requires confirmation:
203
+ - OpenClaw config changes
204
+ - service restarts
205
+ - deletions that go beyond ordinary memory retirement
@@ -0,0 +1,127 @@
1
+ # ClawMem Repair And Verification
2
+
3
+ Use this reference when ClawMem is already installed but is not selected as the active memory plugin, is missing per-agent provisioning, has a broken route, or needs verification after setup.
4
+
5
+ The website bootstrap `SKILL.md` is the primary setup guide. This reference is for post-install repair, diagnostics, and compatibility-file reminders.
6
+
7
+ ## Contents
8
+
9
+ - Verify activation and provisioning
10
+ - Verify read access without manual login
11
+ - Verify the plugin tool path
12
+ - Compatibility mode for SOUL.md, AGENTS.md, and TOOLS.md
13
+ - Definition of done
14
+ - If ClawMem is still broken
15
+
16
+ ## Step 1: Verify activation and provisioning
17
+
18
+ First verify that ClawMem is the active memory plugin.
19
+
20
+ ```sh
21
+ openclaw status
22
+ python3 - <<'PY'
23
+ import json, os, subprocess
24
+ cfg_path = subprocess.check_output(["openclaw", "config", "file"], text=True).strip()
25
+ with open(os.path.expanduser(cfg_path)) as f:
26
+ root = json.load(f)
27
+ slots = (root.get("plugins") or {}).get("slots") or {}
28
+ print(f"plugins.slots.memory = {slots.get('memory', 'MISSING')}")
29
+ PY
30
+ ```
31
+
32
+ Expected:
33
+ - OpenClaw status shows ClawMem as the active memory plugin
34
+ - `plugins.slots.memory = clawmem`
35
+
36
+ Then verify the current agent route. Resolve the current route with the bundled helper:
37
+
38
+ ```sh
39
+ eval "$(python3 scripts/clawmem_exports.py)"
40
+ printf 'agent=%s\nbase=%s\ndefaultRepo=%s\ntoken=%s\n' \
41
+ "${CLAWMEM_AGENT_ID}" "${CLAWMEM_BASE_URL}" "${CLAWMEM_DEFAULT_REPO}" \
42
+ "$(test -n "${CLAWMEM_TOKEN}" && printf SET || printf MISSING)"
43
+ ```
44
+
45
+ If `CLAWMEM_DEFAULT_REPO` or `CLAWMEM_TOKEN` is missing, the current agent has not been provisioned yet. Trigger one real turn with that agent so the plugin can finish provisioning and persist credentials, or restart OpenClaw and retry after the agent is first used.
46
+
47
+ ## Step 2: Verify read access without manual login
48
+
49
+ This proves that a fresh session can query ClawMem using the current agent's provisioned route.
50
+
51
+ ```sh
52
+ eval "$(python3 scripts/clawmem_exports.py)"
53
+
54
+ test -n "$CLAWMEM_REPO" || { echo "Current agent route has no repo yet"; exit 1; }
55
+ test -n "$CLAWMEM_TOKEN" || { echo "Current agent route has no token yet"; exit 1; }
56
+
57
+ GH_HOST="$CLAWMEM_HOST" GH_ENTERPRISE_TOKEN="$CLAWMEM_TOKEN" \
58
+ gh issue list --repo "$CLAWMEM_REPO" --limit 1 --json number,title
59
+ ```
60
+
61
+ If `gh` is unavailable or not the official GitHub CLI, use the fallback probe:
62
+
63
+ ```sh
64
+ curl -sf -H "Authorization: token $CLAWMEM_TOKEN" \
65
+ "$CLAWMEM_BASE_URL/repos/$CLAWMEM_REPO/issues?state=open&per_page=1&type=issues" | \
66
+ jq 'map({number,title})'
67
+ ```
68
+
69
+ If either command returns JSON, even `[]`, the route is usable.
70
+
71
+ ## Step 3: Verify the plugin tool path
72
+
73
+ From a normal ClawMem-enabled session, verify that:
74
+ - `memory_repos` lists accessible repos and marks the default repo
75
+ - `memory_list` returns the active memory index
76
+ - `memory_get` fetches one exact memory by id or issue number
77
+ - `memory_labels` returns the current reusable schema labels
78
+ - `memory_recall` returns either a hit list or a clean miss
79
+ - `memory_store` is available for immediate durable saves
80
+ - `memory_update` updates an existing memory in place
81
+ - `memory_repo_create` creates a new repo when a new memory space is needed
82
+
83
+ Conversation summaries or auto-extracted memories from a just-finished session may appear on the next real request, not necessarily immediately at session close.
84
+
85
+ ## Compatibility mode for SOUL.md, AGENTS.md, and TOOLS.md
86
+
87
+ If your OpenClaw environment still relies on file-injected identity or behavior reminders, use these compact compatibility snippets. Do not duplicate the entire skill body into those files.
88
+
89
+ ### Optional SOUL.md identity block
90
+
91
+ ```markdown
92
+ ## Memory System — ClawMem
93
+ I use ClawMem as my memory system.
94
+ When prior context may help, I search ClawMem before answering.
95
+ ```
96
+
97
+ ### Optional AGENTS.md reminder
98
+
99
+ ```markdown
100
+ Before ending every response, ask: "Did I learn anything durable this turn?"
101
+ If yes or unsure, save it to ClawMem now.
102
+ ```
103
+
104
+ ### Optional TOOLS.md reminder
105
+
106
+ ```markdown
107
+ ClawMem is the primary long-term memory system.
108
+ Use the bundled $clawmem skill for retrieval, saving, routing, schema, and troubleshooting.
109
+ ```
110
+
111
+ These snippets are compatibility aids, not the primary runtime source of truth.
112
+
113
+ ## Definition of done
114
+
115
+ - `openclaw.json` has `plugins.slots.memory = clawmem`
116
+ - The current agent route has a `defaultRepo` or legacy `repo`
117
+ - The current agent route has a `token`
118
+ - Read-only probe works without manual `gh auth login`
119
+ - Plugin memory tools work from a normal session
120
+ - The bundled `$clawmem` skill is available after installation
121
+
122
+ ## If ClawMem is still broken
123
+
124
+ - If `plugins.slots.memory` is wrong, set it back to `clawmem`, restart the gateway, and retry.
125
+ - If the route is missing a repo or token, trigger one real turn with that agent and retry provisioning checks.
126
+ - If a new session gets `401 Unauthorized`, re-read the current route instead of assuming the old repo or token is still valid.
127
+ - If your environment still depends on `SOUL.md` or `AGENTS.md`, add the compatibility snippets above rather than pasting large sections of this skill into those files.
@@ -0,0 +1,63 @@
1
+ # ClawMem Memory Schema
2
+
3
+ Use this reference when deciding how to label or shape a memory, or when you need to explain the ClawMem graph model to another agent or user.
4
+
5
+ ## The memory graph
6
+
7
+ Issues are nodes. Labels are schema. `#ID` references are edges.
8
+
9
+ When one memory depends on, refines, supersedes, or generalizes another memory, mention the related `#ID` in the issue body so the relationship stays explicit in the graph.
10
+
11
+ There are two valid memory shapes:
12
+ - Plugin-managed structured memories: created through `memory_store` or `memory_update`; the plugin manages core labels and may also persist agent-selected `kind:*` and `topic:*` labels
13
+ - Curated graph memories: created manually through `gh` or `curl` when you explicitly need raw issue control
14
+
15
+ ## Labels
16
+
17
+ Plugin-managed memories always include:
18
+ - `type:memory`
19
+
20
+ Plugin-managed memories may also include:
21
+ - one `kind:*` label
22
+ - optional `topic:*` labels
23
+
24
+ Lifecycle is carried by native issue state:
25
+ - open issue = active memory
26
+ - closed issue = stale or superseded memory
27
+
28
+ If you create a curated memory manually, include:
29
+ - `type:memory`
30
+ - one `kind:*` label
31
+ - optional `topic:*` labels, usually no more than two or three
32
+
33
+ ## Kinds
34
+
35
+ | Kind | Label | Use it for |
36
+ |---|---|---|
37
+ | Core fact | `kind:core-fact` | Stable truths that should update in place as reality changes |
38
+ | Convention | `kind:convention` | Agreed rules or policies |
39
+ | Lesson learned | `kind:lesson` | Corrections, postmortems, or mistakes worth preserving |
40
+ | Skill blueprint | `kind:skill` | Repeatable workflows or playbooks |
41
+ | Active task | `kind:task` | Ongoing work that should stay visible and update over time |
42
+
43
+ ## When to create which kind
44
+
45
+ | Trigger | Kind |
46
+ |---|---|
47
+ | User corrects a wrong assumption | `kind:lesson` |
48
+ | You and the user agree on a rule | `kind:convention` |
49
+ | A stable fact about the user or project | `kind:core-fact` |
50
+ | A repeatable workflow you figured out | `kind:skill` |
51
+ | Ongoing work to track | `kind:task` |
52
+
53
+ ## Disciplined self-evolution
54
+
55
+ - Before inventing a new `kind` or `topic`, call `memory_labels`.
56
+ - Reuse current schema when it already fits.
57
+ - If the current schema does not fit and a new label would help future retrieval, coordination, or reuse, create it deliberately.
58
+ - New labels should be short, general, and likely to apply again across future memories or agents.
59
+ - Do not invent random label prefixes. Schema evolution must stay within `kind:*` and `topic:*`.
60
+
61
+ ## Storage rule
62
+
63
+ If you are writing something so the agent remembers it later, store it in ClawMem. If you are writing something for a tool or human to read directly, write a file instead.
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import json
4
+ import os
5
+ import shlex
6
+ import subprocess
7
+ import sys
8
+
9
+
10
+ def normalize_base_url(raw: str) -> str:
11
+ value = (raw or "https://git.clawmem.ai/api/v3").rstrip("/")
12
+ if not value.endswith("/api/v3"):
13
+ value = f"{value}/api/v3"
14
+ return value
15
+
16
+
17
+ def main() -> int:
18
+ agent_id = (sys.argv[1].strip() if len(sys.argv) > 1 and sys.argv[1].strip() else os.environ.get("OPENCLAW_AGENT_ID", "main"))
19
+ repo_override = sys.argv[2].strip() if len(sys.argv) > 2 else ""
20
+
21
+ try:
22
+ cfg_path = subprocess.check_output(["openclaw", "config", "file"], text=True).strip()
23
+ except FileNotFoundError:
24
+ print("clawmem_exports.py: openclaw CLI was not found in PATH", file=sys.stderr)
25
+ return 1
26
+ with open(os.path.expanduser(cfg_path), "r", encoding="utf-8") as handle:
27
+ root = json.load(handle)
28
+
29
+ cfg = (((root.get("plugins") or {}).get("entries") or {}).get("clawmem") or {}).get("config") or {}
30
+ agents = cfg.get("agents") or {}
31
+ route = agents.get(agent_id) or {}
32
+
33
+ base_url = normalize_base_url(route.get("baseUrl") or cfg.get("baseUrl") or "")
34
+ default_repo = route.get("defaultRepo") or route.get("repo") or cfg.get("defaultRepo") or cfg.get("repo") or ""
35
+ repo = repo_override or default_repo
36
+ token = route.get("token") or ""
37
+ host = base_url.removesuffix("/api/v3").replace("https://", "").replace("http://", "")
38
+
39
+ pairs = {
40
+ "CLAWMEM_AGENT_ID": agent_id,
41
+ "CLAWMEM_BASE_URL": base_url,
42
+ "CLAWMEM_HOST": host,
43
+ "CLAWMEM_DEFAULT_REPO": default_repo,
44
+ "CLAWMEM_REPO": repo,
45
+ "CLAWMEM_TOKEN": token,
46
+ }
47
+
48
+ for key, value in pairs.items():
49
+ print(f"export {key}={shlex.quote(value)}")
50
+ return 0
51
+
52
+
53
+ if __name__ == "__main__":
54
+ raise SystemExit(main())
@@ -6,7 +6,62 @@ type IssueResponse = { number: number; title?: string; body?: string; state?: st
6
6
  type SearchIssuesResponse = { items?: IssueResponse[]; total_count?: number; incomplete_results?: boolean };
7
7
  type CommentResponse = { id?: number; body?: string; created_at?: string };
8
8
  type LabelResponse = { name?: string; color?: string; description?: string };
9
- type RepoResponse = { name?: string; full_name?: string; description?: string; private?: boolean; owner?: { login?: string } };
9
+ type PermissionMap = Record<string, boolean | undefined>;
10
+ type RepoResponse = {
11
+ name?: string;
12
+ full_name?: string;
13
+ description?: string;
14
+ private?: boolean;
15
+ owner?: { login?: string };
16
+ permissions?: PermissionMap;
17
+ role_name?: string;
18
+ };
19
+ type OrgResponse = {
20
+ id?: number;
21
+ login?: string;
22
+ name?: string;
23
+ description?: string;
24
+ default_repository_permission?: string;
25
+ };
26
+ type TeamResponse = {
27
+ id?: number;
28
+ slug?: string;
29
+ name?: string;
30
+ description?: string;
31
+ privacy?: string;
32
+ permission?: string;
33
+ role_name?: string;
34
+ permissions?: PermissionMap;
35
+ };
36
+ type CollaboratorResponse = {
37
+ id?: number;
38
+ login?: string;
39
+ name?: string;
40
+ permissions?: PermissionMap;
41
+ role_name?: string;
42
+ };
43
+ type RepositoryInvitationResponse = {
44
+ id?: number;
45
+ created_at?: string;
46
+ permissions?: string;
47
+ repository?: RepoResponse;
48
+ invitee?: { login?: string; name?: string };
49
+ inviter?: { login?: string; name?: string };
50
+ };
51
+ type TeamMembershipResponse = { state?: string; role?: string };
52
+ type InvitationResponse = {
53
+ id?: number;
54
+ role?: string;
55
+ created_at?: string;
56
+ expires_at?: string | null;
57
+ email?: string;
58
+ login?: string;
59
+ organization?: OrgResponse;
60
+ invitee?: { login?: string };
61
+ inviter?: { login?: string };
62
+ team_ids?: number[];
63
+ teams?: TeamResponse[];
64
+ };
10
65
  type ReqOpts = { allowNotFound?: boolean; allowValidationError?: boolean; omitAuth?: boolean };
11
66
 
12
67
  export class GitHubIssueClient {
@@ -71,6 +126,130 @@ export class GitHubIssueClient {
71
126
  }),
72
127
  });
73
128
  }
129
+ async listUserOrgs(): Promise<OrgResponse[]> {
130
+ return this.req<OrgResponse[]>("user/orgs", { method: "GET" });
131
+ }
132
+ async createUserOrg(params: { login: string; name?: string; defaultRepositoryPermission?: string }): Promise<OrgResponse> {
133
+ return this.req<OrgResponse>("user/orgs", {
134
+ method: "POST",
135
+ body: JSON.stringify({
136
+ login: params.login,
137
+ ...(params.name ? { name: params.name } : {}),
138
+ ...(params.defaultRepositoryPermission ? { default_repository_permission: params.defaultRepositoryPermission } : {}),
139
+ }),
140
+ });
141
+ }
142
+ async getOrg(org: string): Promise<OrgResponse> {
143
+ return this.req<OrgResponse>(`orgs/${encodeURIComponent(org)}`, { method: "GET" });
144
+ }
145
+ async listOrgTeams(org: string): Promise<TeamResponse[]> {
146
+ return this.req<TeamResponse[]>(`orgs/${encodeURIComponent(org)}/teams`, { method: "GET" });
147
+ }
148
+ async createOrgTeam(org: string, params: { name: string; description?: string; privacy?: "closed" | "secret" }): Promise<TeamResponse> {
149
+ return this.req<TeamResponse>(`orgs/${encodeURIComponent(org)}/teams`, {
150
+ method: "POST",
151
+ body: JSON.stringify({
152
+ name: params.name,
153
+ ...(params.description ? { description: params.description } : {}),
154
+ privacy: params.privacy ?? "closed",
155
+ }),
156
+ });
157
+ }
158
+ async setTeamMembership(org: string, teamSlug: string, username: string, role: "member" | "maintainer"): Promise<TeamMembershipResponse> {
159
+ return this.req<TeamMembershipResponse>(
160
+ `orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/memberships/${encodeURIComponent(username)}`,
161
+ { method: "PUT", body: JSON.stringify({ role }) },
162
+ );
163
+ }
164
+ async removeTeamMembership(org: string, teamSlug: string, username: string): Promise<void> {
165
+ await this.req(
166
+ `orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/memberships/${encodeURIComponent(username)}`,
167
+ { method: "DELETE" },
168
+ );
169
+ }
170
+ async listTeamRepos(org: string, teamSlug: string): Promise<RepoResponse[]> {
171
+ return this.req<RepoResponse[]>(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/repos`, { method: "GET" });
172
+ }
173
+ async setTeamRepoAccess(org: string, teamSlug: string, owner: string, repo: string, permission: "read" | "write" | "admin"): Promise<void> {
174
+ await this.req(
175
+ `orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`,
176
+ { method: "PUT", body: JSON.stringify({ permission }) },
177
+ );
178
+ }
179
+ async removeTeamRepoAccess(org: string, teamSlug: string, owner: string, repo: string): Promise<void> {
180
+ await this.req(
181
+ `orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`,
182
+ { method: "DELETE" },
183
+ );
184
+ }
185
+ async listRepoCollaborators(owner: string, repo: string): Promise<CollaboratorResponse[]> {
186
+ return this.req<CollaboratorResponse[]>(
187
+ `repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators`,
188
+ { method: "GET" },
189
+ );
190
+ }
191
+ async listRepoInvitations(owner: string, repo: string): Promise<RepositoryInvitationResponse[]> {
192
+ return this.req<RepositoryInvitationResponse[]>(
193
+ `repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/invitations`,
194
+ { method: "GET" },
195
+ );
196
+ }
197
+ async setRepoCollaborator(owner: string, repo: string, username: string, permission: "read" | "write" | "admin"): Promise<RepositoryInvitationResponse | undefined> {
198
+ return this.req<RepositoryInvitationResponse>(
199
+ `repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`,
200
+ { method: "PUT", body: JSON.stringify({ permission }) },
201
+ );
202
+ }
203
+ async removeRepoCollaborator(owner: string, repo: string, username: string): Promise<void> {
204
+ await this.req(
205
+ `repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`,
206
+ { method: "DELETE" },
207
+ );
208
+ }
209
+ async getRepo(owner: string, repo: string): Promise<RepoResponse> {
210
+ return this.req<RepoResponse>(`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, { method: "GET" });
211
+ }
212
+ async listRepoTeams(owner: string, repo: string): Promise<TeamResponse[]> {
213
+ return this.req<TeamResponse[]>(`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/teams`, { method: "GET" });
214
+ }
215
+ async listUserRepoInvitations(): Promise<RepositoryInvitationResponse[]> {
216
+ return this.req<RepositoryInvitationResponse[]>("user/repository_invitations", { method: "GET" });
217
+ }
218
+ async acceptUserRepoInvitation(invitationId: number): Promise<void> {
219
+ await this.req(`user/repository_invitations/${invitationId}`, { method: "PATCH" });
220
+ }
221
+ async declineUserRepoInvitation(invitationId: number): Promise<void> {
222
+ await this.req(`user/repository_invitations/${invitationId}`, { method: "DELETE" });
223
+ }
224
+ async listOrgInvitations(org: string): Promise<InvitationResponse[]> {
225
+ return this.req<InvitationResponse[]>(`orgs/${encodeURIComponent(org)}/invitations`, { method: "GET" });
226
+ }
227
+ async createOrgInvitation(
228
+ org: string,
229
+ params: { inviteeLogin: string; role?: "member" | "admin"; teamIds?: number[]; expiresInDays?: number },
230
+ ): Promise<InvitationResponse> {
231
+ return this.req<InvitationResponse>(`orgs/${encodeURIComponent(org)}/invitations`, {
232
+ method: "POST",
233
+ body: JSON.stringify({
234
+ invitee_login: params.inviteeLogin,
235
+ role: params.role ?? "member",
236
+ ...(params.teamIds && params.teamIds.length > 0 ? { team_ids: params.teamIds } : {}),
237
+ ...(typeof params.expiresInDays === "number" ? { expires_in_days: params.expiresInDays } : {}),
238
+ }),
239
+ });
240
+ }
241
+ async listOrgOutsideCollaborators(org: string): Promise<CollaboratorResponse[]> {
242
+ return this.req<CollaboratorResponse[]>(`orgs/${encodeURIComponent(org)}/outside_collaborators`, { method: "GET" });
243
+ }
244
+ async listUserOrgInvitations(): Promise<InvitationResponse[]> {
245
+ return this.req<InvitationResponse[]>("user/organization_invitations", { method: "GET" });
246
+ }
247
+ async acceptUserOrgInvitation(invitationId: number): Promise<void> {
248
+ await this.req(`user/organization_invitations/${invitationId}`, { method: "PATCH" });
249
+ }
250
+ async declineUserOrgInvitation(invitationId: number): Promise<void> {
251
+ await this.req(`user/organization_invitations/${invitationId}`, { method: "DELETE" });
252
+ }
74
253
  async ensureLabels(labels: string[]): Promise<void> {
75
254
  for (const label of labels) {
76
255
  if (!label.trim()) continue;