@clawmem-ai/clawmem 0.1.9 → 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.
package/src/service.ts CHANGED
@@ -1,17 +1,20 @@
1
1
  // Thin orchestrator: wires conversation mirroring, memory store, and plugin lifecycle.
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
3
- import { isAgentConfigured, resolveAgentRoute, resolvePluginConfig } from "./config.js";
3
+ import { hasDefaultRepo, isAgentConfigured, resolveAgentRoute, resolvePluginConfig } from "./config.js";
4
4
  import { ConversationMirror } from "./conversation.js";
5
5
  import { GitHubIssueClient } from "./github-client.js";
6
6
  import { KeyedAsyncQueue } from "./keyed-async-queue.js";
7
7
  import { MemoryStore } from "./memory.js";
8
8
  import { loadState, resolveStatePath, saveState } from "./state.js";
9
9
  import { readTranscriptSnapshot } from "./transcript.js";
10
- import type { ClawMemPluginConfig, PluginState, SessionMirrorState, TranscriptSnapshot } from "./types.js";
11
- import { inferAgentIdFromTranscriptPath, normalizeAgentId, sessionScopeKey } from "./utils.js";
10
+ import type { BootstrapIdentityResponse, ClawMemPluginConfig, ClawMemResolvedRoute, PluginState, SessionMirrorState, TranscriptSnapshot } from "./types.js";
11
+ import { buildAgentBootstrapRegistration, inferAgentIdFromTranscriptPath, normalizeAgentId, sessionScopeKey } from "./utils.js";
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;
@@ -46,7 +49,8 @@ class ClawMemService {
46
49
  void this.track(this.handleTranscript(u.sessionFile)).catch((e) => this.warn("transcript update", e));
47
50
  });
48
51
  const configuredCount = Object.keys(this.config.agents).filter((agentId) => {
49
- return isAgentConfigured(resolveAgentRoute(this.config, agentId));
52
+ const route = resolveAgentRoute(this.config, agentId);
53
+ return isAgentConfigured(route) && hasDefaultRepo(route);
50
54
  }).length;
51
55
  this.api.logger.info?.(
52
56
  configuredCount > 0
@@ -64,6 +68,86 @@ class ClawMemService {
64
68
  }
65
69
 
66
70
  private registerTools(): void {
71
+ this.api.registerTool({
72
+ name: "memory_repos",
73
+ description: "List the memory repos the current ClawMem agent identity can access so the agent can choose the right space before retrieving or storing memory.",
74
+ required: true,
75
+ parameters: {
76
+ type: "object",
77
+ additionalProperties: false,
78
+ properties: {
79
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
80
+ },
81
+ },
82
+ execute: async (_id: string, params: unknown) => {
83
+ const p = asRecord(params);
84
+ const agentId = this.resolveToolAgentId(p.agentId);
85
+ const resolved = await this.requireToolIdentity(agentId);
86
+ if ("error" in resolved) return toolText(resolved.error);
87
+ const repos = await resolved.client.listUserRepos();
88
+ if (repos.length === 0) return toolText(`Agent "${agentId}" has no accessible ClawMem repos yet.`);
89
+ const lines = [
90
+ `Accessible ClawMem repos for agent "${agentId}":`,
91
+ ...repos
92
+ .map((repo) => {
93
+ const fullName = repo.full_name?.trim() || repo.name?.trim() || "unknown";
94
+ const flags = [
95
+ resolved.route.defaultRepo === fullName ? "default" : "",
96
+ repo.private ? "private" : "shared",
97
+ ].filter(Boolean).join(", ");
98
+ const description = repo.description?.trim() ? ` - ${repo.description.trim()}` : "";
99
+ return `- ${fullName}${flags ? ` [${flags}]` : ""}${description}`;
100
+ }),
101
+ ];
102
+ return toolText(lines.join("\n"));
103
+ },
104
+ });
105
+
106
+ this.api.registerTool({
107
+ name: "memory_repo_create",
108
+ description: "Create a new ClawMem repo under the current agent identity when the agent decides a new memory space is needed.",
109
+ required: true,
110
+ parameters: {
111
+ type: "object",
112
+ additionalProperties: false,
113
+ properties: {
114
+ name: { type: "string", minLength: 1, description: "Repository name only, without owner prefix." },
115
+ description: { type: "string", minLength: 1, description: "Optional repo description." },
116
+ private: { type: "boolean", description: "Whether the new repo should be private. Defaults to true." },
117
+ setDefault: { type: "boolean", description: "Whether to make the new repo this agent's default memory repo." },
118
+ agentId: { type: "string", minLength: 1, description: "Optional agent identity override. Defaults to the current agent when available." },
119
+ },
120
+ required: ["name"],
121
+ },
122
+ execute: async (_id: string, params: unknown) => {
123
+ const p = asRecord(params);
124
+ const name = typeof p.name === "string" ? p.name.trim() : "";
125
+ if (!name) return toolText("name is empty.");
126
+ const agentId = this.resolveToolAgentId(p.agentId);
127
+ const resolved = await this.requireToolIdentity(agentId);
128
+ if ("error" in resolved) return toolText(resolved.error);
129
+ const created = await resolved.client.createUserRepo({
130
+ name,
131
+ ...(typeof p.description === "string" && p.description.trim() ? { description: p.description.trim() } : {}),
132
+ ...(typeof p.private === "boolean" ? { private: p.private } : {}),
133
+ });
134
+ const fullName = created.full_name?.trim() || created.name?.trim() || name;
135
+ let defaultNote = "";
136
+ const shouldSetDefault = p.setDefault === true || !resolved.route.defaultRepo;
137
+ if (shouldSetDefault && fullName.includes("/")) {
138
+ await this.persistAgentConfig(agentId, {
139
+ baseUrl: resolved.route.baseUrl,
140
+ authScheme: resolved.route.authScheme,
141
+ token: resolved.route.token!,
142
+ defaultRepo: fullName,
143
+ });
144
+ this.config.agents[agentId] = { ...(this.config.agents[agentId] ?? {}), defaultRepo: fullName };
145
+ defaultNote = resolved.route.defaultRepo ? "\nSet as default repo for this agent." : "\nSet as the first default repo for this agent.";
146
+ }
147
+ return toolText(`Created memory repo ${fullName}.${defaultNote}`);
148
+ },
149
+ });
150
+
67
151
  this.api.registerTool({
68
152
  name: "memory_list",
69
153
  description: "List ClawMem memories by status or schema so the agent can inspect the current memory index before deduping or saving.",
@@ -76,25 +160,26 @@ class ClawMemService {
76
160
  kind: { type: "string", minLength: 1, description: "Optional kind filter, for example core-fact, lesson, or task." },
77
161
  topic: { type: "string", minLength: 1, description: "Optional topic filter." },
78
162
  limit: { type: "integer", minimum: 1, maximum: 200, description: "Maximum number of memories to return." },
163
+ repo: { type: "string", minLength: 3, description: "Optional memory repo override in owner/repo form. Defaults to the agent's defaultRepo." },
79
164
  agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
80
165
  },
81
166
  },
82
167
  execute: async (_id: string, params: unknown) => {
83
168
  const p = asRecord(params);
84
169
  const agentId = this.resolveToolAgentId(p.agentId);
85
- if (!(await this.ensureConfigured(agentId))) return toolText(`ClawMem route for agent "${agentId}" is not configured.`);
86
- const { mem } = this.getServices(agentId);
170
+ const resolved = await this.requireToolRoute(agentId, p.repo);
171
+ if ("error" in resolved) return toolText(resolved.error);
87
172
  const status = p.status === "stale" || p.status === "all" ? p.status : "active";
88
173
  const limit = typeof p.limit === "number" && Number.isFinite(p.limit) ? Math.floor(p.limit) : 20;
89
174
  const kind = typeof p.kind === "string" && p.kind.trim() ? p.kind.trim() : undefined;
90
175
  const topic = typeof p.topic === "string" && p.topic.trim() ? p.topic.trim() : undefined;
91
- const memories = await mem.listMemories({ status, kind, topic, limit });
176
+ const memories = await resolved.mem.listMemories({ status, kind, topic, limit });
92
177
  if (memories.length === 0) {
93
178
  const filters = [status !== "active" ? `status=${status}` : "", kind ? `kind=${kind}` : "", topic ? `topic=${topic}` : ""].filter(Boolean).join(", ");
94
- return toolText(`No memories matched${filters ? ` (${filters})` : ""}.`);
179
+ return toolText(`No memories matched in ${resolved.route.repo}${filters ? ` (${filters})` : ""}.`);
95
180
  }
96
181
  const lines = [
97
- `Found ${memories.length} ${status === "all" ? "" : `${status} `}memor${memories.length === 1 ? "y" : "ies"}:`,
182
+ `Found ${memories.length} ${status === "all" ? "" : `${status} `}memor${memories.length === 1 ? "y" : "ies"} in ${resolved.route.repo}:`,
98
183
  ...memories.map((memory) => `- ${renderMemoryLine(memory)}`),
99
184
  ];
100
185
  return toolText(lines.join("\n"));
@@ -103,12 +188,13 @@ class ClawMemService {
103
188
 
104
189
  this.api.registerTool({
105
190
  name: "memory_labels",
106
- 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.",
107
192
  required: true,
108
193
  parameters: {
109
194
  type: "object",
110
195
  additionalProperties: false,
111
196
  properties: {
197
+ repo: { type: "string", minLength: 3, description: "Optional memory repo override in owner/repo form. Defaults to the agent's defaultRepo." },
112
198
  agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
113
199
  limitTopics: { type: "integer", minimum: 1, maximum: 200, description: "Maximum number of topic labels to display." },
114
200
  },
@@ -116,16 +202,16 @@ class ClawMemService {
116
202
  execute: async (_id: string, params: unknown) => {
117
203
  const p = asRecord(params);
118
204
  const agentId = this.resolveToolAgentId(p.agentId);
119
- if (!(await this.ensureConfigured(agentId))) return toolText(`ClawMem route for agent "${agentId}" is not configured.`);
120
- const { mem } = this.getServices(agentId);
121
- const schema = await mem.listSchema();
205
+ const resolved = await this.requireToolRoute(agentId, p.repo);
206
+ if ("error" in resolved) return toolText(resolved.error);
207
+ const schema = await resolved.mem.listSchema();
122
208
  const rawLimit = typeof p.limitTopics === "number" && Number.isFinite(p.limitTopics) ? Math.floor(p.limitTopics) : 50;
123
209
  const limitTopics = Math.min(200, Math.max(1, rawLimit));
124
210
  const kinds = schema.kinds.length > 0 ? schema.kinds.map((kind) => `- kind:${kind}`).join("\n") : "- None";
125
211
  const topics = schema.topics.length > 0 ? schema.topics.slice(0, limitTopics).map((topic) => `- topic:${topic}`).join("\n") : "- None";
126
212
  const extra = schema.topics.length > limitTopics ? `\n- ...and ${schema.topics.length - limitTopics} more topics` : "";
127
213
  return toolText([
128
- "Current ClawMem schema labels:",
214
+ `Current ClawMem schema labels in ${resolved.route.repo}:`,
129
215
  "",
130
216
  "Kinds:",
131
217
  kinds,
@@ -146,6 +232,7 @@ class ClawMemService {
146
232
  properties: {
147
233
  query: { type: "string", minLength: 1, description: "What to recall from memory." },
148
234
  limit: { type: "integer", minimum: 1, maximum: 20, description: "Maximum number of memories to return." },
235
+ repo: { type: "string", minLength: 3, description: "Optional memory repo override in owner/repo form. Defaults to the agent's defaultRepo." },
149
236
  agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
150
237
  },
151
238
  required: ["query"],
@@ -155,14 +242,14 @@ class ClawMemService {
155
242
  const query = typeof p.query === "string" ? p.query.trim() : "";
156
243
  if (!query) return toolText("Query is empty.");
157
244
  const agentId = this.resolveToolAgentId(p.agentId);
158
- if (!(await this.ensureConfigured(agentId))) return toolText(`ClawMem route for agent "${agentId}" is not configured.`);
159
- const { mem } = this.getServices(agentId);
245
+ const resolved = await this.requireToolRoute(agentId, p.repo);
246
+ if ("error" in resolved) return toolText(resolved.error);
160
247
  const rawLimit = typeof p.limit === "number" && Number.isFinite(p.limit) ? Math.floor(p.limit) : this.config.memoryRecallLimit;
161
248
  const limit = Math.min(20, Math.max(1, rawLimit));
162
- const memories = await mem.search(query, limit);
163
- if (memories.length === 0) return toolText(`No active memories matched "${query}".`);
249
+ const memories = await resolved.mem.search(query, limit);
250
+ if (memories.length === 0) return toolText(`No active memories matched "${query}" in ${resolved.route.repo}.`);
164
251
  const text = [
165
- `Found ${memories.length} active memor${memories.length === 1 ? "y" : "ies"} for "${query}":`,
252
+ `Found ${memories.length} active memor${memories.length === 1 ? "y" : "ies"} for "${query}" in ${resolved.route.repo}:`,
166
253
  ...memories.map((memory) => `- ${renderMemoryLine(memory)}`),
167
254
  ].join("\n");
168
255
  return toolText(text);
@@ -179,6 +266,7 @@ class ClawMemService {
179
266
  properties: {
180
267
  memoryId: { type: "string", minLength: 1, description: "The memory id or issue number to retrieve." },
181
268
  status: { type: "string", enum: ["active", "stale", "all"], description: "Which status bucket to search. Defaults to all." },
269
+ repo: { type: "string", minLength: 3, description: "Optional memory repo override in owner/repo form. Defaults to the agent's defaultRepo." },
182
270
  agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
183
271
  },
184
272
  required: ["memoryId"],
@@ -188,12 +276,12 @@ class ClawMemService {
188
276
  const memoryId = typeof p.memoryId === "string" ? p.memoryId.trim() : "";
189
277
  if (!memoryId) return toolText("memoryId is empty.");
190
278
  const agentId = this.resolveToolAgentId(p.agentId);
191
- if (!(await this.ensureConfigured(agentId))) return toolText(`ClawMem route for agent "${agentId}" is not configured.`);
192
- const { mem } = this.getServices(agentId);
279
+ const resolved = await this.requireToolRoute(agentId, p.repo);
280
+ if ("error" in resolved) return toolText(resolved.error);
193
281
  const status = p.status === "active" || p.status === "stale" ? p.status : "all";
194
- const memory = await mem.get(memoryId, status);
195
- if (!memory) return toolText(`No ${status === "all" ? "" : `${status} `}memory matched id "${memoryId}".`);
196
- return toolText(renderMemoryBlock(memory));
282
+ const memory = await resolved.mem.get(memoryId, status);
283
+ if (!memory) return toolText(`No ${status === "all" ? "" : `${status} `}memory matched id "${memoryId}" in ${resolved.route.repo}.`);
284
+ return toolText(`Repo: ${resolved.route.repo}\n${renderMemoryBlock(memory)}`);
197
285
  },
198
286
  });
199
287
 
@@ -214,7 +302,7 @@ class ClawMemService {
214
302
  minItems: 1,
215
303
  maxItems: 10,
216
304
  },
217
- sessionId: { type: "string", minLength: 1, description: "Optional source session id label. Defaults to manual." },
305
+ repo: { type: "string", minLength: 3, description: "Optional memory repo override in owner/repo form. Defaults to the agent's defaultRepo." },
218
306
  agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
219
307
  },
220
308
  required: ["detail"],
@@ -224,14 +312,13 @@ class ClawMemService {
224
312
  const detail = typeof p.detail === "string" ? p.detail.trim() : "";
225
313
  if (!detail) return toolText("Detail is empty.");
226
314
  const agentId = this.resolveToolAgentId(p.agentId);
227
- if (!(await this.ensureConfigured(agentId))) return toolText(`ClawMem route for agent "${agentId}" is not configured.`);
228
- const { mem } = this.getServices(agentId);
229
- const sessionId = typeof p.sessionId === "string" && p.sessionId.trim() ? p.sessionId.trim() : "manual";
315
+ const resolved = await this.requireToolRoute(agentId, p.repo);
316
+ if ("error" in resolved) return toolText(resolved.error);
230
317
  const kind = typeof p.kind === "string" && p.kind.trim() ? p.kind.trim() : undefined;
231
318
  const topics = Array.isArray(p.topics) ? p.topics.filter((topic): topic is string => typeof topic === "string" && topic.trim().length > 0) : undefined;
232
- const result = await mem.store({ detail, ...(kind ? { kind } : {}), ...(topics && topics.length > 0 ? { topics } : {}) }, sessionId);
233
- if (!result.created) return toolText(`Memory already exists.\n${renderMemoryBlock(result.memory)}`);
234
- return toolText(`Stored memory.\n${renderMemoryBlock(result.memory)}`);
319
+ const result = await resolved.mem.store({ detail, ...(kind ? { kind } : {}), ...(topics && topics.length > 0 ? { topics } : {}) });
320
+ if (!result.created) return toolText(`Memory already exists in ${resolved.route.repo}.\n${renderMemoryBlock(result.memory)}`);
321
+ return toolText(`Stored memory in ${resolved.route.repo}.\n${renderMemoryBlock(result.memory)}`);
235
322
  },
236
323
  });
237
324
 
@@ -253,6 +340,7 @@ class ClawMemService {
253
340
  minItems: 1,
254
341
  maxItems: 10,
255
342
  },
343
+ repo: { type: "string", minLength: 3, description: "Optional memory repo override in owner/repo form. Defaults to the agent's defaultRepo." },
256
344
  agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
257
345
  },
258
346
  required: ["memoryId"],
@@ -266,16 +354,16 @@ class ClawMemService {
266
354
  const topics = Array.isArray(p.topics) ? p.topics.filter((topic): topic is string => typeof topic === "string" && topic.trim().length > 0) : undefined;
267
355
  if (!detail && kind === undefined && topics === undefined) return toolText("Provide at least one of detail, kind, or topics.");
268
356
  const agentId = this.resolveToolAgentId(p.agentId);
269
- if (!(await this.ensureConfigured(agentId))) return toolText(`ClawMem route for agent "${agentId}" is not configured.`);
270
- const { mem } = this.getServices(agentId);
357
+ const resolved = await this.requireToolRoute(agentId, p.repo);
358
+ if ("error" in resolved) return toolText(resolved.error);
271
359
  let updated;
272
360
  try {
273
- updated = await mem.update(memoryId, { ...(detail ? { detail } : {}), ...(kind !== undefined ? { kind } : {}), ...(topics !== undefined ? { topics } : {}) });
361
+ updated = await resolved.mem.update(memoryId, { ...(detail ? { detail } : {}), ...(kind !== undefined ? { kind } : {}), ...(topics !== undefined ? { topics } : {}) });
274
362
  } catch (error) {
275
363
  return toolText(`Unable to update memory "${memoryId}": ${String(error)}`);
276
364
  }
277
- if (!updated) return toolText(`No memory matched id "${memoryId}".`);
278
- return toolText(`Updated memory.\n${renderMemoryBlock(updated)}`);
365
+ if (!updated) return toolText(`No memory matched id "${memoryId}" in ${resolved.route.repo}.`);
366
+ return toolText(`Updated memory in ${resolved.route.repo}.\n${renderMemoryBlock(updated)}`);
279
367
  },
280
368
  });
281
369
 
@@ -288,6 +376,7 @@ class ClawMemService {
288
376
  additionalProperties: false,
289
377
  properties: {
290
378
  memoryId: { type: "string", minLength: 1, description: "The memory id or issue number to mark stale." },
379
+ repo: { type: "string", minLength: 3, description: "Optional memory repo override in owner/repo form. Defaults to the agent's defaultRepo." },
291
380
  agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
292
381
  },
293
382
  required: ["memoryId"],
@@ -297,18 +386,903 @@ class ClawMemService {
297
386
  const memoryId = typeof p.memoryId === "string" ? p.memoryId.trim() : "";
298
387
  if (!memoryId) return toolText("memoryId is empty.");
299
388
  const agentId = this.resolveToolAgentId(p.agentId);
300
- if (!(await this.ensureConfigured(agentId))) return toolText(`ClawMem route for agent "${agentId}" is not configured.`);
301
- const { mem } = this.getServices(agentId);
302
- const forgotten = await mem.forget(memoryId);
303
- if (!forgotten) return toolText(`No active memory matched id "${memoryId}".`);
304
- return toolText(`Marked memory [${forgotten.memoryId}] stale: ${forgotten.detail}`);
389
+ const resolved = await this.requireToolRoute(agentId, p.repo);
390
+ if ("error" in resolved) return toolText(resolved.error);
391
+ const forgotten = await resolved.mem.forget(memoryId);
392
+ if (!forgotten) return toolText(`No active memory matched id "${memoryId}" in ${resolved.route.repo}.`);
393
+ return toolText(`Marked memory [${forgotten.memoryId}] stale in ${resolved.route.repo}: ${forgotten.detail}`);
394
+ },
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
+ }
305
1279
  },
306
1280
  });
307
1281
  }
308
1282
 
309
1283
  private async handleBeforeAgentStart(prompt: unknown, agentId?: string): Promise<{ prependContext: string } | void> {
310
1284
  const routeAgentId = normalizeAgentId(agentId);
311
- if (!(await this.ensureConfigured(routeAgentId))) return;
1285
+ if (!(await this.ensureDefaultRepoConfigured(routeAgentId))) return;
312
1286
  await this.runRequestMaintenance(routeAgentId);
313
1287
  if (typeof prompt !== "string" || prompt.trim().length < 5) return;
314
1288
  try {
@@ -333,7 +1307,7 @@ class ClawMemService {
333
1307
  }
334
1308
  const { conv } = this.getServices(agentId);
335
1309
  if (!conv.shouldMirror(snap.sessionId, snap.messages)) return;
336
- if (!(await this.ensureConfigured(agentId))) return;
1310
+ if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
337
1311
  await this.enqueueSession(sessionScopeKey(snap.sessionId, agentId), async () => {
338
1312
  const s = this.getOrCreate(snap.sessionId!, agentId);
339
1313
  s.sessionFile = sessionFile;
@@ -359,7 +1333,7 @@ class ClawMemService {
359
1333
  private async syncTurn(p: TurnPayload): Promise<void> {
360
1334
  if (!p.sessionId) return;
361
1335
  const agentId = normalizeAgentId(p.agentId);
362
- if (!(await this.ensureConfigured(agentId))) return;
1336
+ if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
363
1337
  const { conv } = this.getServices(agentId);
364
1338
  const s = this.getOrCreate(p.sessionId, agentId);
365
1339
  s.sessionKey = p.sessionKey ?? s.sessionKey; s.agentId = agentId; s.updatedAt = new Date().toISOString();
@@ -383,7 +1357,7 @@ class ClawMemService {
383
1357
  private async finalize(p: FinalizePayload): Promise<void> {
384
1358
  if (!p.sessionId) return;
385
1359
  const agentId = normalizeAgentId(p.agentId);
386
- if (!(await this.ensureConfigured(agentId))) return;
1360
+ if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
387
1361
  const { conv } = this.getServices(agentId);
388
1362
  const s = this.getOrCreate(p.sessionId, agentId);
389
1363
  if (s.finalizedAt) return;
@@ -456,7 +1430,7 @@ class ClawMemService {
456
1430
  })();
457
1431
  return this.loadPromise;
458
1432
  }
459
- private async ensureConfigured(agentId?: string): Promise<boolean> {
1433
+ private async ensureIdentityConfigured(agentId?: string): Promise<boolean> {
460
1434
  const id = normalizeAgentId(agentId);
461
1435
  if (isAgentConfigured(resolveAgentRoute(this.config, id))) return true;
462
1436
  const pending = this.configPromises.get(id);
@@ -465,19 +1439,50 @@ class ClawMemService {
465
1439
  this.configPromises.set(id, p);
466
1440
  try { return await p; } finally { if (this.configPromises.get(id) === p) this.configPromises.delete(id); }
467
1441
  }
1442
+ private async ensureDefaultRepoConfigured(agentId?: string): Promise<boolean> {
1443
+ const id = normalizeAgentId(agentId);
1444
+ if (!(await this.ensureIdentityConfigured(id))) return false;
1445
+ return hasDefaultRepo(resolveAgentRoute(this.config, id));
1446
+ }
468
1447
  private async bootstrap(agentId: string): Promise<boolean> {
469
1448
  const route = resolveAgentRoute(this.config, agentId);
470
1449
  if (!route.baseUrl) { this.api.logger.warn(`clawmem: cannot provision Git credentials for ${agentId} without a baseUrl`); return false; }
471
1450
  try {
472
1451
  const client = new GitHubIssueClient(route, this.api.logger);
473
- const locale = Intl?.DateTimeFormat?.()?.resolvedOptions?.()?.locale ?? "";
474
- const sess = await client.createAnonymousSession(locale);
475
- await this.persistAgentConfig(agentId, { baseUrl: route.baseUrl, authScheme: "token", token: sess.token, repo: sess.repo_full_name });
476
- this.config.agents[agentId] = { ...(this.config.agents[agentId] ?? {}), baseUrl: route.baseUrl, authScheme: "token", token: sess.token, repo: sess.repo_full_name };
477
- this.api.logger.info?.(`clawmem: provisioned Git credentials for agent ${agentId} -> ${sess.repo_full_name} via ${route.baseUrl}`);
1452
+ const bootstrap = await this.provisionAgentIdentity(client, agentId);
1453
+ await this.persistAgentConfig(agentId, {
1454
+ baseUrl: route.baseUrl,
1455
+ authScheme: "token",
1456
+ token: bootstrap.identity.token,
1457
+ defaultRepo: bootstrap.identity.repo_full_name,
1458
+ });
1459
+ this.config.agents[agentId] = {
1460
+ ...(this.config.agents[agentId] ?? {}),
1461
+ baseUrl: route.baseUrl,
1462
+ authScheme: "token",
1463
+ token: bootstrap.identity.token,
1464
+ defaultRepo: bootstrap.identity.repo_full_name,
1465
+ };
1466
+ this.api.logger.info?.(
1467
+ `clawmem: provisioned Git credentials for agent ${agentId} with default repo ${bootstrap.identity.repo_full_name} via ${route.baseUrl} (${bootstrap.method})`,
1468
+ );
478
1469
  return true;
479
1470
  } catch (error) { this.api.logger.warn(`clawmem: failed to provision Git credentials for agent ${agentId} via ${route.baseUrl}: ${String(error)}`); return false; }
480
1471
  }
1472
+ private async provisionAgentIdentity(client: GitHubIssueClient, agentId: string): Promise<{ identity: BootstrapIdentityResponse; method: string }> {
1473
+ const registration = buildAgentBootstrapRegistration(agentId);
1474
+ try {
1475
+ const identity = await client.registerAgent(registration.prefixLogin, registration.defaultRepoName);
1476
+ return { identity, method: "/api/v3/agents" };
1477
+ } catch (error) {
1478
+ if (!shouldFallbackToAnonymousBootstrap(error)) throw error;
1479
+ this.api.logger.warn?.(`clawmem: /api/v3/agents is unavailable for agent ${agentId}; falling back to deprecated anonymous bootstrap`);
1480
+ }
1481
+
1482
+ const locale = Intl?.DateTimeFormat?.()?.resolvedOptions?.()?.locale ?? "";
1483
+ const identity = await client.createAnonymousSession(locale);
1484
+ return { identity, method: "/api/v3/anonymous/session" };
1485
+ }
481
1486
  private warnIfInactiveMemorySlot(): void {
482
1487
  try {
483
1488
  const root = this.api.runtime.config.loadConfig();
@@ -499,7 +1504,7 @@ class ClawMemService {
499
1504
  this.api.logger.warn(`clawmem: memory slot check failed: ${String(error)}`);
500
1505
  }
501
1506
  }
502
- private async persistAgentConfig(agentId: string, values: { baseUrl: string; authScheme: "token" | "bearer"; token: string; repo: string }): Promise<void> {
1507
+ private async persistAgentConfig(agentId: string, values: { baseUrl: string; authScheme: "token" | "bearer"; token: string; defaultRepo: string }): Promise<void> {
503
1508
  const root = this.api.runtime.config.loadConfig();
504
1509
  const plugins = root.plugins;
505
1510
  const entries = plugins?.entries && typeof plugins.entries === "object" && !Array.isArray(plugins.entries) ? (plugins.entries as Record<string, unknown>) : {};
@@ -577,9 +1582,12 @@ class ClawMemService {
577
1582
  if (changed) await this.persistState();
578
1583
  }
579
1584
 
580
- private getServices(agentId?: string): { conv: ConversationMirror; mem: MemoryStore } {
581
- const client = new GitHubIssueClient(resolveAgentRoute(this.config, agentId), this.api.logger);
1585
+ private getServices(agentId?: string, repo?: string): { route: ClawMemResolvedRoute; conv: ConversationMirror; mem: MemoryStore; client: GitHubIssueClient } {
1586
+ const route = resolveAgentRoute(this.config, agentId, repo);
1587
+ const client = new GitHubIssueClient(route, this.api.logger);
582
1588
  return {
1589
+ route,
1590
+ client,
583
1591
  conv: new ConversationMirror(client, this.api, this.config),
584
1592
  mem: new MemoryStore(client, this.api, this.config),
585
1593
  };
@@ -587,6 +1595,84 @@ class ClawMemService {
587
1595
  private resolveToolAgentId(agentId: unknown): string {
588
1596
  return normalizeAgentId(typeof agentId === "string" && agentId.trim() ? agentId : process.env.OPENCLAW_AGENT_ID);
589
1597
  }
1598
+ private resolveToolRepo(repo: unknown): { repo?: string; error?: string } {
1599
+ if (repo === undefined || repo === null || repo === "") return {};
1600
+ if (typeof repo !== "string") return { error: "repo must be a string like owner/repo." };
1601
+ const trimmed = repo.trim().replace(/^\/+|\/+$/g, "");
1602
+ if (!/^[^/\s]+\/[^/\s]+$/.test(trimmed)) return { error: `Invalid repo "${repo}". Expected owner/repo.` };
1603
+ return { repo: trimmed };
1604
+ }
1605
+ private async requireToolIdentity(agentId: string): Promise<{ route: ClawMemResolvedRoute; client: GitHubIssueClient } | { error: string }> {
1606
+ if (!(await this.ensureIdentityConfigured(agentId))) {
1607
+ return { error: `ClawMem identity for agent "${agentId}" is not configured.` };
1608
+ }
1609
+ const { route, client } = this.getServices(agentId);
1610
+ return { route, client };
1611
+ }
1612
+ private async requireToolRoute(agentId: string, repo: unknown): Promise<{ route: ClawMemResolvedRoute; conv: ConversationMirror; mem: MemoryStore; client: GitHubIssueClient } | { error: string }> {
1613
+ const parsed = this.resolveToolRepo(repo);
1614
+ if (parsed.error) return { error: parsed.error };
1615
+ if (!(await this.ensureIdentityConfigured(agentId))) {
1616
+ return { error: `ClawMem identity for agent "${agentId}" is not configured.` };
1617
+ }
1618
+ const services = this.getServices(agentId, parsed.repo);
1619
+ if (!services.route.repo) {
1620
+ return {
1621
+ error: `No memory repo selected for agent "${agentId}". Provide repo explicitly or configure agents.${agentId}.defaultRepo.`,
1622
+ };
1623
+ }
1624
+ return services;
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
+ }
590
1676
  /**
591
1677
  * After finalization, check if the repo still has an empty/default description.
592
1678
  * If so, use the conversation summary to suggest a meaningful name and update
@@ -613,6 +1699,10 @@ class ClawMemService {
613
1699
  }
614
1700
 
615
1701
  function asRecord(v: unknown): Record<string, unknown> { return v && typeof v === "object" ? (v as Record<string, unknown>) : {}; }
1702
+ function shouldFallbackToAnonymousBootstrap(error: unknown): boolean {
1703
+ const msg = String(error);
1704
+ return /^Error:\s*HTTP (404|405|501):/i.test(msg) || /^HTTP (404|405|501):/i.test(msg);
1705
+ }
616
1706
  function toolText(text: string): { content: Array<{ type: "text"; text: string }> } {
617
1707
  return { content: [{ type: "text", text }] };
618
1708
  }
@@ -620,7 +1710,7 @@ function renderMemoryLine(memory: { memoryId: string; title?: string; detail: st
620
1710
  const schema = [memory.kind ? `kind:${memory.kind}` : "", ...(memory.topics ?? []).map((topic) => `topic:${topic}`)].filter(Boolean).join(", ");
621
1711
  return `[${memory.memoryId}] ${memory.title || "Memory"}${schema ? ` (${schema})` : ""}${memory.status === "stale" ? " [stale]" : ""}: ${memory.detail}`;
622
1712
  }
623
- function renderMemoryBlock(memory: { memoryId: string; issueNumber?: number; title?: string; detail: string; kind?: string; topics?: string[]; status: "active" | "stale"; sessionId?: string; date?: string }): string {
1713
+ function renderMemoryBlock(memory: { memoryId: string; issueNumber?: number; title?: string; detail: string; kind?: string; topics?: string[]; status: "active" | "stale"; date?: string }): string {
624
1714
  const lines = [
625
1715
  `Memory ID: ${memory.memoryId}`,
626
1716
  ...(typeof memory.issueNumber === "number" ? [`Issue Number: ${memory.issueNumber}`] : []),
@@ -628,11 +1718,114 @@ function renderMemoryBlock(memory: { memoryId: string; issueNumber?: number; tit
628
1718
  `Title: ${memory.title || "Memory"}`,
629
1719
  ...(memory.kind ? [`Kind: ${memory.kind}`] : []),
630
1720
  ...(memory.topics && memory.topics.length > 0 ? [`Topics: ${memory.topics.join(", ")}`] : []),
631
- ...(memory.sessionId ? [`Session: ${memory.sessionId}`] : []),
632
1721
  ...(memory.date ? [`Date: ${memory.date}`] : []),
633
1722
  `Detail: ${memory.detail}`,
634
1723
  ];
635
1724
  return lines.join("\n");
636
1725
  }
637
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
+
638
1831
  export function createClawMemPlugin(api: OpenClawPluginApi): void { new ClawMemService(api).register(); }