@clawmem-ai/clawmem 0.1.18 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +28 -9
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +4 -0
  4. package/dist/src/collaboration.d.ts +49 -0
  5. package/dist/src/collaboration.js +69 -0
  6. package/dist/src/config.d.ts +21 -0
  7. package/dist/src/config.js +119 -0
  8. package/dist/src/conversation.d.ts +30 -0
  9. package/dist/src/conversation.js +323 -0
  10. package/dist/src/github-client.d.ts +269 -0
  11. package/dist/src/github-client.js +350 -0
  12. package/dist/src/keyed-async-queue.d.ts +12 -0
  13. package/dist/src/keyed-async-queue.js +23 -0
  14. package/dist/src/memory.d.ts +29 -0
  15. package/dist/src/memory.js +451 -0
  16. package/dist/src/recall-sanitize.d.ts +1 -0
  17. package/dist/src/recall-sanitize.js +149 -0
  18. package/dist/src/runtime-env.d.ts +2 -0
  19. package/dist/src/runtime-env.js +12 -0
  20. package/dist/src/service.d.ts +18 -0
  21. package/dist/src/service.js +3645 -0
  22. package/dist/src/state.d.ts +4 -0
  23. package/dist/src/state.js +182 -0
  24. package/dist/src/transcript.d.ts +3 -0
  25. package/dist/src/transcript.js +164 -0
  26. package/dist/src/types.d.ts +130 -0
  27. package/dist/src/types.js +1 -0
  28. package/dist/src/utils.d.ts +26 -0
  29. package/dist/src/utils.js +62 -0
  30. package/dist/src/yaml.d.ts +2 -0
  31. package/dist/src/yaml.js +81 -0
  32. package/openclaw.plugin.json +14 -1
  33. package/package.json +21 -7
  34. package/skills/clawmem/SKILL.md +26 -5
  35. package/skills/clawmem/references/collaboration.md +13 -5
  36. package/skills/clawmem/references/review.md +77 -0
  37. package/skills/clawmem/references/schema.md +44 -1
  38. package/index.ts +0 -6
  39. package/src/collaboration.test.ts +0 -71
  40. package/src/collaboration.ts +0 -109
  41. package/src/config.test.ts +0 -83
  42. package/src/config.ts +0 -117
  43. package/src/conversation.test.ts +0 -120
  44. package/src/conversation.ts +0 -304
  45. package/src/github-client.test.ts +0 -101
  46. package/src/github-client.ts +0 -363
  47. package/src/keyed-async-queue.ts +0 -26
  48. package/src/memory.test.ts +0 -588
  49. package/src/memory.ts +0 -444
  50. package/src/recall-sanitize.ts +0 -143
  51. package/src/runtime-env.ts +0 -12
  52. package/src/service.test.ts +0 -337
  53. package/src/service.ts +0 -2786
  54. package/src/state.test.ts +0 -119
  55. package/src/state.ts +0 -206
  56. package/src/transcript.ts +0 -186
  57. package/src/types.ts +0 -86
  58. package/src/utils.ts +0 -74
  59. package/src/yaml.ts +0 -88
  60. package/tsconfig.json +0 -15
@@ -0,0 +1,350 @@
1
+ // GitHub Issues API client for clawmem. No label caching — idempotent create-if-absent.
2
+ import { resolveLabelColor, labelDescription, extractLabelNames, isManagedLabel } from "./config.js";
3
+ export class GitHubIssueClient {
4
+ config;
5
+ log;
6
+ constructor(config, log) {
7
+ this.config = config;
8
+ this.log = log;
9
+ }
10
+ repo() {
11
+ return this.config.repo?.trim() || undefined;
12
+ }
13
+ defaultRepo() {
14
+ return this.config.defaultRepo?.trim() || undefined;
15
+ }
16
+ async createIssue(params) {
17
+ return this.req(this.repoPath("issues"), {
18
+ method: "POST",
19
+ body: JSON.stringify({
20
+ title: params.title,
21
+ ...(params.body !== undefined ? { body: params.body } : {}),
22
+ ...(params.labels && params.labels.length > 0 ? { labels: params.labels } : {}),
23
+ ...(params.assignees && params.assignees.length > 0 ? { assignees: params.assignees } : {}),
24
+ ...(params.assignee ? { assignee: params.assignee } : {}),
25
+ ...(params.state ? { state: params.state } : {}),
26
+ ...(params.stateReason ? { state_reason: params.stateReason } : {}),
27
+ }),
28
+ });
29
+ }
30
+ async updateIssue(n, params) {
31
+ return this.req(this.repoPath(`issues/${n}`), {
32
+ method: "PATCH",
33
+ body: JSON.stringify({
34
+ ...(params.title !== undefined ? { title: params.title } : {}),
35
+ ...(params.body !== undefined ? { body: params.body } : {}),
36
+ ...(params.state !== undefined ? { state: params.state } : {}),
37
+ ...(params.stateReason !== undefined ? { state_reason: params.stateReason } : {}),
38
+ ...(params.labels !== undefined ? { labels: params.labels } : {}),
39
+ ...(params.assignees !== undefined ? { assignees: params.assignees } : {}),
40
+ ...(params.locked !== undefined ? { locked: params.locked } : {}),
41
+ }),
42
+ });
43
+ }
44
+ async getIssue(n) {
45
+ return this.req(this.repoPath(`issues/${n}`), { method: "GET" });
46
+ }
47
+ async createComment(issueNumber, body, params) {
48
+ return this.req(this.repoPath(`issues/${issueNumber}/comments`), {
49
+ method: "POST",
50
+ body: JSON.stringify({
51
+ body,
52
+ ...(typeof params?.inReplyTo === "number" ? { in_reply_to: params.inReplyTo } : {}),
53
+ }),
54
+ });
55
+ }
56
+ async listComments(issueNumber, params) {
57
+ const q = new URLSearchParams();
58
+ q.set("page", String(params?.page ?? 1));
59
+ q.set("per_page", String(params?.perPage ?? 100));
60
+ if (params?.sort)
61
+ q.set("sort", params.sort);
62
+ if (params?.direction)
63
+ q.set("direction", params.direction);
64
+ if (params?.since)
65
+ q.set("since", params.since);
66
+ if (params?.threaded)
67
+ q.set("threaded", "true");
68
+ return this.req(`${this.repoPath(`issues/${issueNumber}/comments`)}?${q}`, { method: "GET" });
69
+ }
70
+ async listIssues(params) {
71
+ const q = new URLSearchParams();
72
+ q.set("state", params.state ?? "open");
73
+ q.set("page", String(params.page ?? 1));
74
+ q.set("per_page", String(params.perPage ?? 100));
75
+ if (params.labels?.length)
76
+ q.set("labels", params.labels.join(","));
77
+ if (params.assignee)
78
+ q.set("assignee", params.assignee);
79
+ if (params.creator)
80
+ q.set("creator", params.creator);
81
+ if (params.mentioned)
82
+ q.set("mentioned", params.mentioned);
83
+ if (params.sort)
84
+ q.set("sort", params.sort);
85
+ if (params.direction)
86
+ q.set("direction", params.direction);
87
+ if (params.since)
88
+ q.set("since", params.since);
89
+ return this.req(`${this.repoPath("issues")}?${q}`, { method: "GET" });
90
+ }
91
+ async searchIssues(query, params) {
92
+ const q = new URLSearchParams();
93
+ q.set("q", query);
94
+ q.set("page", String(params?.page ?? 1));
95
+ q.set("per_page", String(params?.perPage ?? 100));
96
+ const res = await this.req(`search/issues?${q}`, { method: "GET" });
97
+ return Array.isArray(res?.items) ? res.items : [];
98
+ }
99
+ async listLabels(params) {
100
+ const q = new URLSearchParams();
101
+ q.set("page", String(params?.page ?? 1));
102
+ q.set("per_page", String(params?.perPage ?? 100));
103
+ return this.req(`${this.repoPath("labels")}?${q}`, { method: "GET" });
104
+ }
105
+ async listUserRepos() {
106
+ return this.req("user/repos", { method: "GET" });
107
+ }
108
+ async createUserRepo(params) {
109
+ return this.req("user/repos", {
110
+ method: "POST",
111
+ body: JSON.stringify({
112
+ name: params.name,
113
+ ...(params.description ? { description: params.description } : {}),
114
+ private: params.private ?? true,
115
+ auto_init: params.autoInit ?? false,
116
+ }),
117
+ });
118
+ }
119
+ async createOrgRepo(org, params) {
120
+ return this.req(`orgs/${encodeURIComponent(org)}/repos`, {
121
+ method: "POST",
122
+ body: JSON.stringify({
123
+ name: params.name,
124
+ ...(params.description ? { description: params.description } : {}),
125
+ private: params.private ?? true,
126
+ auto_init: params.autoInit ?? false,
127
+ ...(params.hasIssues !== undefined ? { has_issues: params.hasIssues } : {}),
128
+ ...(params.hasWiki !== undefined ? { has_wiki: params.hasWiki } : {}),
129
+ }),
130
+ });
131
+ }
132
+ async listUserOrgs() {
133
+ return this.req("user/orgs", { method: "GET" });
134
+ }
135
+ async getCurrentUser() {
136
+ return this.req("user", { method: "GET" });
137
+ }
138
+ async createUserOrg(params) {
139
+ return this.req("user/orgs", {
140
+ method: "POST",
141
+ body: JSON.stringify({
142
+ login: params.login,
143
+ ...(params.name ? { name: params.name } : {}),
144
+ ...(params.defaultRepositoryPermission ? { default_repository_permission: params.defaultRepositoryPermission } : {}),
145
+ }),
146
+ });
147
+ }
148
+ async getOrg(org) {
149
+ return this.req(`orgs/${encodeURIComponent(org)}`, { method: "GET" });
150
+ }
151
+ async listOrgMembers(org, role) {
152
+ const q = new URLSearchParams();
153
+ if (role)
154
+ q.set("role", role);
155
+ const suffix = q.toString();
156
+ return this.req(`orgs/${encodeURIComponent(org)}/members${suffix ? `?${suffix}` : ""}`, { method: "GET" });
157
+ }
158
+ async getOrgMembership(org, username) {
159
+ return this.req(`orgs/${encodeURIComponent(org)}/memberships/${encodeURIComponent(username)}`, { method: "GET" });
160
+ }
161
+ async removeOrgMember(org, username) {
162
+ await this.req(`orgs/${encodeURIComponent(org)}/members/${encodeURIComponent(username)}`, { method: "DELETE" });
163
+ }
164
+ async removeOrgMembership(org, username) {
165
+ await this.req(`orgs/${encodeURIComponent(org)}/memberships/${encodeURIComponent(username)}`, { method: "DELETE" });
166
+ }
167
+ async listOrgTeams(org) {
168
+ return this.req(`orgs/${encodeURIComponent(org)}/teams`, { method: "GET" });
169
+ }
170
+ async getTeam(org, teamSlug) {
171
+ return this.req(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}`, { method: "GET" });
172
+ }
173
+ async createOrgTeam(org, params) {
174
+ return this.req(`orgs/${encodeURIComponent(org)}/teams`, {
175
+ method: "POST",
176
+ body: JSON.stringify({
177
+ name: params.name,
178
+ ...(params.description ? { description: params.description } : {}),
179
+ privacy: params.privacy ?? "closed",
180
+ }),
181
+ });
182
+ }
183
+ async updateTeam(org, teamSlug, params) {
184
+ return this.req(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}`, {
185
+ method: "PATCH",
186
+ body: JSON.stringify({
187
+ ...(params.name ? { name: params.name } : {}),
188
+ ...(params.description ? { description: params.description } : {}),
189
+ ...(params.privacy ? { privacy: params.privacy } : {}),
190
+ }),
191
+ });
192
+ }
193
+ async deleteTeam(org, teamSlug) {
194
+ await this.req(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}`, { method: "DELETE" });
195
+ }
196
+ async listTeamMembers(org, teamSlug) {
197
+ return this.req(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/members`, { method: "GET" });
198
+ }
199
+ async setTeamMembership(org, teamSlug, username, role) {
200
+ return this.req(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/memberships/${encodeURIComponent(username)}`, { method: "PUT", body: JSON.stringify({ role }) });
201
+ }
202
+ async removeTeamMembership(org, teamSlug, username) {
203
+ await this.req(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/memberships/${encodeURIComponent(username)}`, { method: "DELETE" });
204
+ }
205
+ async listTeamRepos(org, teamSlug) {
206
+ return this.req(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/repos`, { method: "GET" });
207
+ }
208
+ async setTeamRepoAccess(org, teamSlug, owner, repo, permission) {
209
+ await this.req(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, { method: "PUT", body: JSON.stringify({ permission }) });
210
+ }
211
+ async removeTeamRepoAccess(org, teamSlug, owner, repo) {
212
+ await this.req(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, { method: "DELETE" });
213
+ }
214
+ async listRepoCollaborators(owner, repo) {
215
+ return this.req(`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators`, { method: "GET" });
216
+ }
217
+ async listRepoInvitations(owner, repo) {
218
+ return this.req(`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/invitations`, { method: "GET" });
219
+ }
220
+ async setRepoCollaborator(owner, repo, username, permission) {
221
+ return this.req(`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`, { method: "PUT", body: JSON.stringify({ permission }) });
222
+ }
223
+ async removeRepoCollaborator(owner, repo, username) {
224
+ await this.req(`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`, { method: "DELETE" });
225
+ }
226
+ async getRepo(owner, repo) {
227
+ return this.req(`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, { method: "GET" });
228
+ }
229
+ async listUserRepoInvitations() {
230
+ return this.req("user/repository_invitations", { method: "GET" });
231
+ }
232
+ async acceptUserRepoInvitation(invitationId) {
233
+ await this.req(`user/repository_invitations/${invitationId}`, { method: "PATCH" });
234
+ }
235
+ async declineUserRepoInvitation(invitationId) {
236
+ await this.req(`user/repository_invitations/${invitationId}`, { method: "DELETE" });
237
+ }
238
+ async listOrgInvitations(org) {
239
+ return this.req(`orgs/${encodeURIComponent(org)}/invitations`, { method: "GET" });
240
+ }
241
+ async createOrgInvitation(org, params) {
242
+ return this.req(`orgs/${encodeURIComponent(org)}/invitations`, {
243
+ method: "POST",
244
+ body: JSON.stringify({
245
+ invitee_login: params.inviteeLogin,
246
+ role: params.role ?? "member",
247
+ ...(params.teamIds && params.teamIds.length > 0 ? { team_ids: params.teamIds } : {}),
248
+ ...(typeof params.expiresInDays === "number" ? { expires_in_days: params.expiresInDays } : {}),
249
+ }),
250
+ });
251
+ }
252
+ async revokeOrgInvitation(org, invitationId) {
253
+ await this.req(`orgs/${encodeURIComponent(org)}/invitations/${invitationId}`, { method: "DELETE" });
254
+ }
255
+ async listOrgOutsideCollaborators(org) {
256
+ return this.req(`orgs/${encodeURIComponent(org)}/outside_collaborators`, { method: "GET" });
257
+ }
258
+ async listUserOrgInvitations() {
259
+ return this.req("user/organization_invitations", { method: "GET" });
260
+ }
261
+ async acceptUserOrgInvitation(invitationId) {
262
+ await this.req(`user/organization_invitations/${invitationId}`, { method: "PATCH" });
263
+ }
264
+ async declineUserOrgInvitation(invitationId) {
265
+ await this.req(`user/organization_invitations/${invitationId}`, { method: "DELETE" });
266
+ }
267
+ async transferRepo(owner, repo, newOwner, newRepoName) {
268
+ return this.req(`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/transfer`, {
269
+ method: "POST",
270
+ body: JSON.stringify({
271
+ new_owner: newOwner,
272
+ ...(newRepoName ? { new_repo_name: newRepoName } : {}),
273
+ }),
274
+ });
275
+ }
276
+ async renameRepo(owner, repo, newName) {
277
+ return this.req(`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, {
278
+ method: "PATCH",
279
+ body: JSON.stringify({ name: newName }),
280
+ });
281
+ }
282
+ async ensureLabels(labels) {
283
+ for (const label of labels) {
284
+ if (!label.trim())
285
+ continue;
286
+ await this.req(this.repoPath("labels"), { method: "POST",
287
+ body: JSON.stringify({ name: label, color: resolveLabelColor(label), description: labelDescription(label) }) }, { allowValidationError: true });
288
+ }
289
+ }
290
+ async syncManagedLabels(issueNumber, desired) {
291
+ const issue = await this.getIssue(issueNumber);
292
+ const unmanaged = extractLabelNames(issue.labels).filter((l) => !isManagedLabel(l));
293
+ await this.updateIssue(issueNumber, { labels: [...new Set([...unmanaged, ...desired])] });
294
+ }
295
+ async getRepoInfo() {
296
+ return this.req(this.repoPath("").replace(/\/$/, ""), { method: "GET" });
297
+ }
298
+ async updateRepoDescription(description) {
299
+ await this.req(this.repoPath("").replace(/\/$/, ""), { method: "PATCH", body: JSON.stringify({ description }) });
300
+ }
301
+ async registerAgent(prefixLogin, defaultRepoName) {
302
+ return this.req("agents", {
303
+ method: "POST",
304
+ body: JSON.stringify({
305
+ prefix_login: prefixLogin,
306
+ default_repo_name: defaultRepoName,
307
+ }),
308
+ }, { omitAuth: true });
309
+ }
310
+ async createAnonymousSession(locale) {
311
+ const body = locale ? JSON.stringify({ locale }) : undefined;
312
+ return this.req("anonymous/session", { method: "POST", ...(body ? { body } : {}) }, { omitAuth: true });
313
+ }
314
+ repoPath(suffix) {
315
+ if (!this.config.repo)
316
+ throw new Error("clawmem repository is not configured");
317
+ return `repos/${this.config.repo}/${suffix}`;
318
+ }
319
+ async req(pathname, init, opts = {}) {
320
+ if (!this.config.baseUrl)
321
+ throw new Error("clawmem baseUrl is not configured");
322
+ if (!opts.omitAuth && !this.config.token)
323
+ throw new Error("clawmem token is not configured");
324
+ const base = this.config.baseUrl.replace(/\/+$/, "");
325
+ const headers = { Accept: "application/vnd.github+json", "Content-Type": "application/json" };
326
+ if (!opts.omitAuth)
327
+ headers.Authorization = this.config.authScheme === "bearer" ? `Bearer ${this.config.token}` : `token ${this.config.token}`;
328
+ const res = await fetch(new URL(pathname, `${base}/`), { ...init, headers: { ...headers, ...(init.headers ?? {}) } });
329
+ if (res.status === 404 && opts.allowNotFound)
330
+ return undefined;
331
+ if (res.status === 422 && opts.allowValidationError)
332
+ return undefined;
333
+ if (!res.ok) {
334
+ const d = await res.text();
335
+ throw new Error(`HTTP ${res.status}: ${d || res.statusText}`);
336
+ }
337
+ if (res.status === 204)
338
+ return undefined;
339
+ const text = await res.text();
340
+ if (!text.trim())
341
+ return undefined;
342
+ try {
343
+ return JSON.parse(text);
344
+ }
345
+ catch (e) {
346
+ this.log.warn?.(`clawmem: failed to parse API response: ${String(e)}`);
347
+ return undefined;
348
+ }
349
+ }
350
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * A keyed async queue that serializes async tasks per key.
3
+ * Inlined from openclaw/plugin-sdk/keyed-async-queue to avoid
4
+ * dependency on platform internals that may not exist in older versions.
5
+ *
6
+ * Matches the behaviour of the upstream implementation: a failed task
7
+ * does not block subsequent tasks on the same key.
8
+ */
9
+ export declare class KeyedAsyncQueue {
10
+ private readonly tails;
11
+ enqueue<T>(key: string, task: () => Promise<T>): Promise<T>;
12
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A keyed async queue that serializes async tasks per key.
3
+ * Inlined from openclaw/plugin-sdk/keyed-async-queue to avoid
4
+ * dependency on platform internals that may not exist in older versions.
5
+ *
6
+ * Matches the behaviour of the upstream implementation: a failed task
7
+ * does not block subsequent tasks on the same key.
8
+ */
9
+ export class KeyedAsyncQueue {
10
+ tails = new Map();
11
+ enqueue(key, task) {
12
+ const current = (this.tails.get(key) ?? Promise.resolve())
13
+ .catch(() => void 0)
14
+ .then(task);
15
+ const tail = current.then(() => void 0, () => void 0);
16
+ this.tails.set(key, tail);
17
+ tail.finally(() => {
18
+ if (this.tails.get(key) === tail)
19
+ this.tails.delete(key);
20
+ });
21
+ return current;
22
+ }
23
+ }
@@ -0,0 +1,29 @@
1
+ import type { GitHubIssueClient } from "./github-client.js";
2
+ import type { MemoryCandidate, MemoryDraft, MemoryListOptions, MemorySchema, ParsedMemoryIssue } from "./types.js";
3
+ export declare class MemoryStore {
4
+ private readonly client;
5
+ constructor(client: GitHubIssueClient);
6
+ search(query: string, limit: number): Promise<ParsedMemoryIssue[]>;
7
+ listSchema(): Promise<MemorySchema>;
8
+ get(memoryId: string, status?: "active" | "stale" | "all"): Promise<ParsedMemoryIssue | null>;
9
+ listMemories(options?: MemoryListOptions): Promise<ParsedMemoryIssue[]>;
10
+ store(draft: MemoryDraft): Promise<{
11
+ created: boolean;
12
+ memory: ParsedMemoryIssue;
13
+ }>;
14
+ update(memoryId: string, patch: {
15
+ title?: string;
16
+ detail?: string;
17
+ kind?: string;
18
+ topics?: string[];
19
+ }): Promise<ParsedMemoryIssue | null>;
20
+ forget(memoryId: string): Promise<ParsedMemoryIssue | null>;
21
+ private searchViaBackend;
22
+ private findActiveByHash;
23
+ private findByRef;
24
+ private findActiveByRef;
25
+ private parseIssue;
26
+ private mergeSchema;
27
+ }
28
+ export declare function parseCandidates(raw: string): MemoryCandidate[];
29
+ export declare function mergeMemoryCandidates(base: MemoryCandidate[], next: MemoryCandidate[]): MemoryCandidate[];