@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/README.md +37 -189
- package/openclaw.plugin.json +20 -89
- package/package.json +2 -1
- package/skills/clawmem/SKILL.md +78 -0
- package/skills/clawmem/references/collaboration.md +223 -0
- package/skills/clawmem/references/communication.md +82 -0
- package/skills/clawmem/references/manual-ops.md +205 -0
- package/skills/clawmem/references/repair.md +127 -0
- package/skills/clawmem/references/schema.md +63 -0
- package/skills/clawmem/scripts/clawmem_exports.py +54 -0
- package/src/config.test.ts +82 -0
- package/src/config.ts +23 -6
- package/src/github-client.ts +207 -1
- package/src/memory.test.ts +2 -4
- package/src/memory.ts +11 -15
- package/src/service.ts +1249 -56
- package/src/types.ts +9 -2
- package/src/utils.ts +13 -0
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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 } : {}) }
|
|
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
|
-
|
|
270
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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;
|
|
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
|
|
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";
|
|
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(); }
|