@clawmem-ai/clawmem 0.1.16 → 0.1.17

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.
@@ -52,6 +52,12 @@ type RepositoryInvitationResponse = {
52
52
  inviter?: { login?: string; name?: string };
53
53
  };
54
54
  type TeamMembershipResponse = { state?: string; role?: string };
55
+ type OrganizationMembershipResponse = {
56
+ state?: string;
57
+ role?: string;
58
+ organization?: OrgResponse;
59
+ user?: CollaboratorResponse;
60
+ };
55
61
  type InvitationResponse = {
56
62
  id?: number;
57
63
  role?: string;
@@ -145,9 +151,30 @@ export class GitHubIssueClient {
145
151
  async getOrg(org: string): Promise<OrgResponse> {
146
152
  return this.req<OrgResponse>(`orgs/${encodeURIComponent(org)}`, { method: "GET" });
147
153
  }
154
+ async listOrgMembers(org: string, role?: "admin"): Promise<CollaboratorResponse[]> {
155
+ const q = new URLSearchParams();
156
+ if (role) q.set("role", role);
157
+ const suffix = q.toString();
158
+ return this.req<CollaboratorResponse[]>(`orgs/${encodeURIComponent(org)}/members${suffix ? `?${suffix}` : ""}`, { method: "GET" });
159
+ }
160
+ async getOrgMembership(org: string, username: string): Promise<OrganizationMembershipResponse> {
161
+ return this.req<OrganizationMembershipResponse>(
162
+ `orgs/${encodeURIComponent(org)}/memberships/${encodeURIComponent(username)}`,
163
+ { method: "GET" },
164
+ );
165
+ }
166
+ async removeOrgMember(org: string, username: string): Promise<void> {
167
+ await this.req(`orgs/${encodeURIComponent(org)}/members/${encodeURIComponent(username)}`, { method: "DELETE" });
168
+ }
169
+ async removeOrgMembership(org: string, username: string): Promise<void> {
170
+ await this.req(`orgs/${encodeURIComponent(org)}/memberships/${encodeURIComponent(username)}`, { method: "DELETE" });
171
+ }
148
172
  async listOrgTeams(org: string): Promise<TeamResponse[]> {
149
173
  return this.req<TeamResponse[]>(`orgs/${encodeURIComponent(org)}/teams`, { method: "GET" });
150
174
  }
175
+ async getTeam(org: string, teamSlug: string): Promise<TeamResponse> {
176
+ return this.req<TeamResponse>(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}`, { method: "GET" });
177
+ }
151
178
  async createOrgTeam(org: string, params: { name: string; description?: string; privacy?: "closed" | "secret" }): Promise<TeamResponse> {
152
179
  return this.req<TeamResponse>(`orgs/${encodeURIComponent(org)}/teams`, {
153
180
  method: "POST",
@@ -158,6 +185,29 @@ export class GitHubIssueClient {
158
185
  }),
159
186
  });
160
187
  }
188
+ async updateTeam(
189
+ org: string,
190
+ teamSlug: string,
191
+ params: { name?: string; description?: string; privacy?: "closed" | "secret" },
192
+ ): Promise<TeamResponse> {
193
+ return this.req<TeamResponse>(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}`, {
194
+ method: "PATCH",
195
+ body: JSON.stringify({
196
+ ...(params.name ? { name: params.name } : {}),
197
+ ...(params.description ? { description: params.description } : {}),
198
+ ...(params.privacy ? { privacy: params.privacy } : {}),
199
+ }),
200
+ });
201
+ }
202
+ async deleteTeam(org: string, teamSlug: string): Promise<void> {
203
+ await this.req(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}`, { method: "DELETE" });
204
+ }
205
+ async listTeamMembers(org: string, teamSlug: string): Promise<CollaboratorResponse[]> {
206
+ return this.req<CollaboratorResponse[]>(
207
+ `orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/members`,
208
+ { method: "GET" },
209
+ );
210
+ }
161
211
  async setTeamMembership(org: string, teamSlug: string, username: string, role: "member" | "maintainer"): Promise<TeamMembershipResponse> {
162
212
  return this.req<TeamMembershipResponse>(
163
213
  `orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/memberships/${encodeURIComponent(username)}`,
@@ -238,6 +288,9 @@ export class GitHubIssueClient {
238
288
  }),
239
289
  });
240
290
  }
291
+ async revokeOrgInvitation(org: string, invitationId: number): Promise<void> {
292
+ await this.req(`orgs/${encodeURIComponent(org)}/invitations/${invitationId}`, { method: "DELETE" });
293
+ }
241
294
  async listOrgOutsideCollaborators(org: string): Promise<CollaboratorResponse[]> {
242
295
  return this.req<CollaboratorResponse[]>(`orgs/${encodeURIComponent(org)}/outside_collaborators`, { method: "GET" });
243
296
  }
@@ -250,6 +303,12 @@ export class GitHubIssueClient {
250
303
  async declineUserOrgInvitation(invitationId: number): Promise<void> {
251
304
  await this.req(`user/organization_invitations/${invitationId}`, { method: "DELETE" });
252
305
  }
306
+ async transferRepo(owner: string, repo: string, newOwner: string): Promise<RepoResponse> {
307
+ return this.req<RepoResponse>(`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/transfer`, {
308
+ method: "POST",
309
+ body: JSON.stringify({ new_owner: newOwner }),
310
+ });
311
+ }
253
312
  async ensureLabels(labels: string[]): Promise<void> {
254
313
  for (const label of labels) {
255
314
  if (!label.trim()) continue;
@@ -1,5 +1,6 @@
1
- import { MemoryStore, mergeMemoryCandidates, scoreMemoryMatch } from "./memory.js";
1
+ import { MemoryStore, mergeMemoryCandidates } from "./memory.js";
2
2
  import type { ParsedMemoryIssue } from "./types.js";
3
+ import { sha256 } from "./utils.js";
3
4
  import { stringifyFlatYaml } from "./yaml.js";
4
5
 
5
6
  function memory(overrides: Partial<ParsedMemoryIssue> = {}): ParsedMemoryIssue {
@@ -41,18 +42,6 @@ function assert(condition: unknown, message: string): void {
41
42
  if (!condition) throw new Error(message);
42
43
  }
43
44
 
44
- function testConfig(): never {
45
- return {
46
- memoryRecallLimit: 5,
47
- memoryAutoRecallLimit: 3,
48
- turnCommentDelayMs: 1000,
49
- digestWaitTimeoutMs: 30000,
50
- summaryWaitTimeoutMs: 120000,
51
- memoryExtractWaitTimeoutMs: 45000,
52
- memoryReconcileWaitTimeoutMs: 45000,
53
- } as never;
54
- }
55
-
56
45
  async function testBackendSearchBuildsSingleCleanedQuery(): Promise<void> {
57
46
  const queries: string[] = [];
58
47
  const client = {
@@ -62,7 +51,7 @@ async function testBackendSearchBuildsSingleCleanedQuery(): Promise<void> {
62
51
  return [] as IssueRecord[];
63
52
  },
64
53
  };
65
- const store = new MemoryStore(client as never, {} as never, testConfig());
54
+ const store = new MemoryStore(client as never);
66
55
  await store.search([
67
56
  "<clawmem-context>",
68
57
  "- [11] Previous memory that should be stripped",
@@ -111,7 +100,7 @@ async function testBackendSearchPreferredForRecall(): Promise<void> {
111
100
  return searched;
112
101
  },
113
102
  };
114
- const store = new MemoryStore(client as never, {} as never, testConfig());
103
+ const store = new MemoryStore(client as never);
115
104
  const found = await store.search("redis rate limiting", 1);
116
105
 
117
106
  assert(queries.length === 1, "expected backend search to be called once");
@@ -135,7 +124,7 @@ async function testBackendSearchReturnsEmptyWithoutLexicalFallback(): Promise<vo
135
124
  listIssues: async () => issues,
136
125
  searchIssues: async () => [] as IssueRecord[],
137
126
  };
138
- const store = new MemoryStore(client as never, {} as never, testConfig());
127
+ const store = new MemoryStore(client as never);
139
128
  const found = await store.search("redis rate limiting", 5);
140
129
 
141
130
  assert(found.length === 0, "expected backend-only recall to return no results when the backend finds nothing");
@@ -146,7 +135,7 @@ async function testBackendSearchPropagatesErrors(): Promise<void> {
146
135
  repo: () => "owner/main-memory",
147
136
  searchIssues: async () => { throw new Error("search unavailable"); },
148
137
  };
149
- const store = new MemoryStore(client as never, {} as never, testConfig());
138
+ const store = new MemoryStore(client as never);
150
139
  let message = "";
151
140
  try {
152
141
  await store.search("redis rate limiting", 5);
@@ -157,25 +146,6 @@ async function testBackendSearchPropagatesErrors(): Promise<void> {
157
146
  assert(message.includes("search unavailable"), "expected backend failures to propagate instead of falling back locally");
158
147
  }
159
148
 
160
- function testCjkScoring(): void {
161
- const billing = memory({
162
- issueNumber: 3,
163
- title: "Memory: 账单修复流程",
164
- detail: "遇到账单不一致时,先核对 invoice_id,再补发 webhook。",
165
- topics: ["账单", "支付"],
166
- });
167
- const unrelated = memory({
168
- issueNumber: 4,
169
- title: "Memory: 部署备注",
170
- detail: "发布前需要确认灰度流量比例。",
171
- topics: ["部署"],
172
- });
173
- const billingScore = scoreMemoryMatch(billing, "账单 webhook");
174
- const unrelatedScore = scoreMemoryMatch(unrelated, "账单 webhook");
175
- assert(billingScore > unrelatedScore, "expected Chinese query scoring to prefer the billing memory");
176
- assert(billingScore > 0, "expected Chinese query to produce a positive match score");
177
- }
178
-
179
149
  function testMergeMemoryCandidates(): void {
180
150
  const merged = mergeMemoryCandidates(
181
151
  [
@@ -207,6 +177,8 @@ async function testStructuredStoreAndSchema(): Promise<void> {
207
177
  const ensured: string[][] = [];
208
178
  const labels: LabelRecord[] = [{ name: "kind:lesson" }, { name: "topic:redis" }];
209
179
  const client = {
180
+ repo: () => "owner/main-memory",
181
+ searchIssues: async () => [] as IssueRecord[],
210
182
  listIssues: async () => [] as IssueRecord[],
211
183
  listLabels: async () => labels,
212
184
  ensureLabels: async (next: string[]) => { ensured.push(next); },
@@ -215,7 +187,7 @@ async function testStructuredStoreAndSchema(): Promise<void> {
215
187
  return { number: 99, title: payload.title };
216
188
  },
217
189
  };
218
- const store = new MemoryStore(client as never, {} as never, testConfig());
190
+ const store = new MemoryStore(client as never);
219
191
  const result = await store.store({ detail: "Redis Lua scripts are required for atomic rate limiting.", kind: "Lesson", topics: ["Redis Ops", "rate_limit"] });
220
192
  const schema = await store.listSchema();
221
193
 
@@ -236,6 +208,47 @@ async function testStructuredStoreAndSchema(): Promise<void> {
236
208
  assert(schema.topics.includes("redis"), "expected schema to expose existing topic labels");
237
209
  }
238
210
 
211
+ async function testListSchemaPrefersLabelsWithoutIssueScan(): Promise<void> {
212
+ const client = {
213
+ listLabels: async () => [{ name: "kind:lesson" }, { name: "topic:redis" }, { name: "topic:rate-limit" }],
214
+ listIssues: async () => { throw new Error("listSchema should not scan issues when label schema is available"); },
215
+ };
216
+ const store = new MemoryStore(client as never);
217
+ const schema = await store.listSchema();
218
+
219
+ assert(JSON.stringify(schema.kinds) === JSON.stringify(["lesson"]), "expected schema kinds to come from labels");
220
+ assert(JSON.stringify(schema.topics) === JSON.stringify(["rate-limit", "redis"]), "expected schema topics to come from labels");
221
+ }
222
+
223
+ async function testStoreDeduplicatesViaHashSearch(): Promise<void> {
224
+ const detail = "Redis Lua scripts are required for atomic rate limiting.";
225
+ const hash = sha256(detail);
226
+ const existing = memory({
227
+ issueNumber: 77,
228
+ detail,
229
+ memoryHash: hash,
230
+ kind: "lesson",
231
+ topics: ["redis-ops"],
232
+ });
233
+ const queries: string[] = [];
234
+ const client = {
235
+ repo: () => "owner/main-memory",
236
+ searchIssues: async (query: string) => {
237
+ queries.push(query);
238
+ return [issueFromMemory(existing)];
239
+ },
240
+ listIssues: async () => { throw new Error("store should not scan all active memories"); },
241
+ ensureLabels: async () => {},
242
+ createIssue: async () => { throw new Error("store should not create a duplicate issue"); },
243
+ };
244
+ const store = new MemoryStore(client as never);
245
+ const result = await store.store({ detail, kind: "lesson", topics: ["redis_ops"] });
246
+
247
+ assert(result.created === false, "expected hash search to reuse an existing exact duplicate");
248
+ assert(result.memory.issueNumber === 77, "expected hash search to return the existing memory");
249
+ assert(queries.length === 1 && queries[0]?.includes(hash), "expected store to query by memory hash");
250
+ }
251
+
239
252
  async function testStoreKeepsFullAutoTitleAndSupportsExplicitTitle(): Promise<void> {
240
253
  const created: Array<{ title: string; body: string; labels: string[] }> = [];
241
254
  const client = {
@@ -247,7 +260,7 @@ async function testStoreKeepsFullAutoTitleAndSupportsExplicitTitle(): Promise<vo
247
260
  return { number: created.length + 100, title: payload.title };
248
261
  },
249
262
  };
250
- const store = new MemoryStore(client as never, {} as never, testConfig());
263
+ const store = new MemoryStore(client as never);
251
264
  const longDetail = "Tech Decision #001: Frontend = React Native, Backend = FastAPI, Database = PostgreSQL, and analytics events must stay append-only for auditability.";
252
265
  const auto = await store.store({ detail: longDetail });
253
266
  const explicit = await store.store({ title: "Architecture Decision #001", detail: "Use React Native + FastAPI for the first mobile stack." });
@@ -284,6 +297,11 @@ async function testGetAndListMemories(): Promise<void> {
284
297
  })),
285
298
  ];
286
299
  const client = {
300
+ getIssue: async (number: number) => {
301
+ const issue = issues.find((entry) => entry.number === number);
302
+ if (!issue) throw new Error("issue missing");
303
+ return issue;
304
+ },
287
305
  listIssues: async (params?: { labels?: string[]; state?: "open" | "closed" | "all" }) => {
288
306
  const labels = params?.labels ?? [];
289
307
  const state = params?.state ?? "open";
@@ -295,7 +313,7 @@ async function testGetAndListMemories(): Promise<void> {
295
313
  });
296
314
  },
297
315
  };
298
- const store = new MemoryStore(client as never, {} as never, testConfig());
316
+ const store = new MemoryStore(client as never);
299
317
  const exact = await store.get("4");
300
318
  const activeFacts = await store.listMemories({ status: "active", kind: "core-fact", limit: 10 });
301
319
  const sports = await store.listMemories({ status: "all", topic: "sports", limit: 10 });
@@ -317,6 +335,11 @@ async function testLegacyMemoriesWithoutSessionOrDate(): Promise<void> {
317
335
  ];
318
336
  const client = {
319
337
  repo: () => "owner/main-memory",
338
+ getIssue: async (number: number) => {
339
+ const issue = issues.find((entry) => entry.number === number);
340
+ if (!issue) throw new Error("issue missing");
341
+ return issue;
342
+ },
320
343
  listIssues: async (params?: { labels?: string[]; state?: "open" | "closed" | "all" }) => {
321
344
  const labels = params?.labels ?? [];
322
345
  const state = params?.state ?? "open";
@@ -329,7 +352,7 @@ async function testLegacyMemoriesWithoutSessionOrDate(): Promise<void> {
329
352
  },
330
353
  searchIssues: async () => issues,
331
354
  };
332
- const store = new MemoryStore(client as never, {} as never, testConfig());
355
+ const store = new MemoryStore(client as never);
333
356
  const exact = await store.get("4");
334
357
  const recalled = await store.search("F1 Dota 2", 5);
335
358
 
@@ -352,6 +375,11 @@ async function testUpdateMemoryInPlace(): Promise<void> {
352
375
  const updatedIssues: Array<{ number: number; title?: string; body?: string }> = [];
353
376
  const syncedLabels: Array<{ number: number; labels: string[] }> = [];
354
377
  const client = {
378
+ getIssue: async (number: number) => {
379
+ const issue = issues.find((entry) => entry.number === number);
380
+ if (!issue) throw new Error("issue missing");
381
+ return issue;
382
+ },
355
383
  listIssues: async (params?: { labels?: string[]; state?: "open" | "closed" | "all" }) => {
356
384
  const labels = params?.labels ?? [];
357
385
  const state = params?.state ?? "open";
@@ -378,7 +406,7 @@ async function testUpdateMemoryInPlace(): Promise<void> {
378
406
  issue.labels = labels;
379
407
  },
380
408
  };
381
- const store = new MemoryStore(client as never, {} as never, testConfig());
409
+ const store = new MemoryStore(client as never);
382
410
  const updated = await store.update("4", {
383
411
  detail: "xiangz likes F1, watches Dota 2 as a viewer, and recently follows tennis.",
384
412
  topics: ["preferences", "sports"],
@@ -407,6 +435,11 @@ async function testUpdateSupportsExplicitRetitle(): Promise<void> {
407
435
  ];
408
436
  const updatedIssues: Array<{ number: number; title?: string; body?: string }> = [];
409
437
  const client = {
438
+ getIssue: async (number: number) => {
439
+ const issue = issues.find((entry) => entry.number === number);
440
+ if (!issue) throw new Error("issue missing");
441
+ return issue;
442
+ },
410
443
  listIssues: async (params?: { labels?: string[]; state?: "open" | "closed" | "all" }) => {
411
444
  const labels = params?.labels ?? [];
412
445
  const state = params?.state ?? "open";
@@ -428,13 +461,58 @@ async function testUpdateSupportsExplicitRetitle(): Promise<void> {
428
461
  },
429
462
  syncManagedLabels: async () => {},
430
463
  };
431
- const store = new MemoryStore(client as never, {} as never, testConfig());
464
+ const store = new MemoryStore(client as never);
432
465
  const updated = await store.update("20", { title: "Billing Audit Convention" });
433
466
 
434
467
  assert(updated?.title === "Memory: Billing Audit Convention", "expected memory_update to support explicit retitle");
435
468
  assert(updatedIssues[0]?.title === "Memory: Billing Audit Convention", "expected issue title patch to use the explicit retitle");
436
469
  }
437
470
 
471
+ async function testUpdateUsesHashSearchForDuplicateCheck(): Promise<void> {
472
+ const currentDetail = "We use append-only audit events for billing changes.";
473
+ const conflictingDetail = "Billing events must stay append-only for auditability.";
474
+ const current = issueFromMemory(memory({
475
+ issueNumber: 20,
476
+ title: "Memory: billing convention",
477
+ detail: currentDetail,
478
+ memoryHash: sha256(currentDetail),
479
+ kind: "convention",
480
+ }));
481
+ const conflicting = issueFromMemory(memory({
482
+ issueNumber: 21,
483
+ title: "Memory: audit rule",
484
+ detail: conflictingDetail,
485
+ memoryHash: sha256(conflictingDetail),
486
+ kind: "convention",
487
+ }));
488
+ const queries: string[] = [];
489
+ const client = {
490
+ repo: () => "owner/main-memory",
491
+ getIssue: async (number: number) => {
492
+ if (number === 20) return current;
493
+ throw new Error("issue missing");
494
+ },
495
+ searchIssues: async (query: string) => {
496
+ queries.push(query);
497
+ return [conflicting];
498
+ },
499
+ listIssues: async () => { throw new Error("update should not scan all active memories when direct lookup/search are available"); },
500
+ ensureLabels: async () => {},
501
+ updateIssue: async () => { throw new Error("duplicate update should fail before mutating"); },
502
+ syncManagedLabels: async () => {},
503
+ };
504
+ const store = new MemoryStore(client as never);
505
+ let message = "";
506
+ try {
507
+ await store.update("20", { detail: conflictingDetail });
508
+ } catch (error) {
509
+ message = String(error);
510
+ }
511
+
512
+ assert(message.includes("[21]"), "expected duplicate detection to reference the conflicting memory");
513
+ assert(queries.length === 1 && queries[0]?.includes(sha256(conflictingDetail)), "expected update duplicate checks to search by memory hash");
514
+ }
515
+
438
516
  async function testForgetClosesMemoryIssue(): Promise<void> {
439
517
  const issues: IssueRecord[] = [
440
518
  issueFromMemory(memory({
@@ -448,6 +526,11 @@ async function testForgetClosesMemoryIssue(): Promise<void> {
448
526
  const syncedLabels: Array<{ number: number; labels: string[] }> = [];
449
527
  const updatedIssues: Array<{ number: number; state?: "open" | "closed" }> = [];
450
528
  const client = {
529
+ getIssue: async (number: number) => {
530
+ const issue = issues.find((entry) => entry.number === number);
531
+ if (!issue) throw new Error("issue missing");
532
+ return issue;
533
+ },
451
534
  listIssues: async (params?: { labels?: string[]; state?: "open" | "closed" | "all" }) => {
452
535
  const labels = params?.labels ?? [];
453
536
  const state = params?.state ?? "open";
@@ -472,7 +555,7 @@ async function testForgetClosesMemoryIssue(): Promise<void> {
472
555
  return issue;
473
556
  },
474
557
  };
475
- const store = new MemoryStore(client as never, {} as never, testConfig());
558
+ const store = new MemoryStore(client as never);
476
559
  const forgotten = await store.forget("12");
477
560
 
478
561
  assert(forgotten?.status === "stale", "expected forgotten memory to be returned as stale");
@@ -485,15 +568,17 @@ async function main(): Promise<void> {
485
568
  await testBackendSearchPreferredForRecall();
486
569
  await testBackendSearchReturnsEmptyWithoutLexicalFallback();
487
570
  await testBackendSearchPropagatesErrors();
488
- testCjkScoring();
489
- testMergeMemoryCandidates();
490
- await testStructuredStoreAndSchema();
571
+ testMergeMemoryCandidates();
572
+ await testStructuredStoreAndSchema();
573
+ await testListSchemaPrefersLabelsWithoutIssueScan();
574
+ await testStoreDeduplicatesViaHashSearch();
491
575
  await testStoreKeepsFullAutoTitleAndSupportsExplicitTitle();
492
576
  await testGetAndListMemories();
493
577
  await testLegacyMemoriesWithoutSessionOrDate();
494
578
  await testUpdateMemoryInPlace();
495
579
  await testUpdateSupportsExplicitRetitle();
496
580
  await testForgetClosesMemoryIssue();
581
+ await testUpdateUsesHashSearchForDuplicateCheck();
497
582
  console.log("memory tests passed");
498
583
  }
499
584