@hyperspell/openclaw-hyperspell 0.9.0 → 0.11.0
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/client.ts +166 -60
- package/commands/slash.ts +94 -15
- package/config.test.ts +202 -0
- package/config.ts +212 -0
- package/graph/ops.ts +34 -28
- package/graph/tools.ts +1 -1
- package/hooks/auto-context.ts +178 -11
- package/hooks/auto-trace.ts +16 -2
- package/hooks/memory-sync.ts +5 -3
- package/index.ts +12 -6
- package/lib/sender.test.ts +234 -0
- package/lib/sender.ts +234 -0
- package/lib/voice-id.ts +39 -0
- package/openclaw.plugin.json +37 -0
- package/package.json +3 -2
- package/sync/markdown.ts +4 -1
- package/tools/remember.ts +113 -41
- package/tools/search.ts +114 -81
- package/types/openclaw.d.ts +16 -0
package/config.test.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import { test } from "node:test"
|
|
3
|
+
import { parseConfig } from "./config.ts"
|
|
4
|
+
|
|
5
|
+
const base = {
|
|
6
|
+
apiKey: "test-key",
|
|
7
|
+
userId: "u1",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
test("parseConfig — no multiUser returns no scoping", () => {
|
|
11
|
+
const cfg = parseConfig(base)
|
|
12
|
+
assert.equal(cfg.multiUser, undefined)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test("parseConfig — multiUser without scoping is valid", () => {
|
|
16
|
+
const cfg = parseConfig({
|
|
17
|
+
...base,
|
|
18
|
+
multiUser: {
|
|
19
|
+
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
20
|
+
sharedUserId: "shared",
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
assert.equal(cfg.multiUser?.scoping, undefined)
|
|
24
|
+
assert.equal(cfg.multiUser?.sharedUserId, "shared")
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test("parseConfig — scoping disabled returns no scoping", () => {
|
|
28
|
+
const cfg = parseConfig({
|
|
29
|
+
...base,
|
|
30
|
+
multiUser: {
|
|
31
|
+
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
32
|
+
scoping: {
|
|
33
|
+
enabled: false,
|
|
34
|
+
defaultScope: "private",
|
|
35
|
+
scopes: ["private"],
|
|
36
|
+
roles: {},
|
|
37
|
+
users: {},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
assert.equal(cfg.multiUser?.scoping, undefined)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test("parseConfig — empty scopes array throws", () => {
|
|
45
|
+
assert.throws(
|
|
46
|
+
() =>
|
|
47
|
+
parseConfig({
|
|
48
|
+
...base,
|
|
49
|
+
multiUser: {
|
|
50
|
+
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
51
|
+
scoping: {
|
|
52
|
+
enabled: true,
|
|
53
|
+
defaultScope: "private",
|
|
54
|
+
scopes: [],
|
|
55
|
+
roles: {},
|
|
56
|
+
users: {},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
/scoping\.scopes/,
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test("parseConfig — defaultScope not in scopes throws", () => {
|
|
65
|
+
assert.throws(
|
|
66
|
+
() =>
|
|
67
|
+
parseConfig({
|
|
68
|
+
...base,
|
|
69
|
+
multiUser: {
|
|
70
|
+
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
71
|
+
scoping: {
|
|
72
|
+
enabled: true,
|
|
73
|
+
defaultScope: "nonexistent",
|
|
74
|
+
scopes: ["private", "family"],
|
|
75
|
+
roles: {},
|
|
76
|
+
users: {},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
/defaultScope/,
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test("parseConfig — role canRead with unknown scope throws", () => {
|
|
85
|
+
assert.throws(
|
|
86
|
+
() =>
|
|
87
|
+
parseConfig({
|
|
88
|
+
...base,
|
|
89
|
+
multiUser: {
|
|
90
|
+
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
91
|
+
scoping: {
|
|
92
|
+
enabled: true,
|
|
93
|
+
defaultScope: "private",
|
|
94
|
+
scopes: ["private", "family"],
|
|
95
|
+
roles: {
|
|
96
|
+
weird: { canRead: ["private", "ghost"], defaultWriteScope: "private" },
|
|
97
|
+
},
|
|
98
|
+
users: {},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
/canRead.*ghost/,
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test("parseConfig — role defaultWriteScope not in scopes throws", () => {
|
|
107
|
+
assert.throws(
|
|
108
|
+
() =>
|
|
109
|
+
parseConfig({
|
|
110
|
+
...base,
|
|
111
|
+
multiUser: {
|
|
112
|
+
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
113
|
+
scoping: {
|
|
114
|
+
enabled: true,
|
|
115
|
+
defaultScope: "private",
|
|
116
|
+
scopes: ["private", "family"],
|
|
117
|
+
roles: {
|
|
118
|
+
weird: { canRead: ["family"], defaultWriteScope: "nonexistent" },
|
|
119
|
+
},
|
|
120
|
+
users: {},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
/defaultWriteScope/,
|
|
125
|
+
)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test("parseConfig — canWriteScopes with unknown scope throws", () => {
|
|
129
|
+
assert.throws(
|
|
130
|
+
() =>
|
|
131
|
+
parseConfig({
|
|
132
|
+
...base,
|
|
133
|
+
multiUser: {
|
|
134
|
+
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
135
|
+
scoping: {
|
|
136
|
+
enabled: true,
|
|
137
|
+
defaultScope: "private",
|
|
138
|
+
scopes: ["private", "family"],
|
|
139
|
+
roles: {
|
|
140
|
+
weird: {
|
|
141
|
+
canRead: ["*"],
|
|
142
|
+
defaultWriteScope: "private",
|
|
143
|
+
canWriteScopes: ["ghost"],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
users: {},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
/canWriteScopes.*ghost/,
|
|
151
|
+
)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test("parseConfig — user role keying into unknown role throws", () => {
|
|
155
|
+
assert.throws(
|
|
156
|
+
() =>
|
|
157
|
+
parseConfig({
|
|
158
|
+
...base,
|
|
159
|
+
multiUser: {
|
|
160
|
+
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
161
|
+
scoping: {
|
|
162
|
+
enabled: true,
|
|
163
|
+
defaultScope: "private",
|
|
164
|
+
scopes: ["private", "family"],
|
|
165
|
+
roles: {
|
|
166
|
+
parent: { canRead: ["*"], defaultWriteScope: "private" },
|
|
167
|
+
},
|
|
168
|
+
users: { u1: { role: "nonexistent" } },
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
/does not key into scoping\.roles/,
|
|
173
|
+
)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test("parseConfig — valid scoping parses", () => {
|
|
177
|
+
const cfg = parseConfig({
|
|
178
|
+
...base,
|
|
179
|
+
multiUser: {
|
|
180
|
+
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
181
|
+
scoping: {
|
|
182
|
+
enabled: true,
|
|
183
|
+
defaultScope: "private",
|
|
184
|
+
scopes: ["private", "family", "parent_only"],
|
|
185
|
+
roles: {
|
|
186
|
+
parent: { canRead: ["*"], defaultWriteScope: "private" },
|
|
187
|
+
kid: { canRead: ["family"], defaultWriteScope: "family" },
|
|
188
|
+
},
|
|
189
|
+
users: {
|
|
190
|
+
u1: { role: "parent" },
|
|
191
|
+
},
|
|
192
|
+
collections: { family: "household" },
|
|
193
|
+
voiceId: { enabled: true, confidenceThreshold: 0.8 },
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
assert.equal(cfg.multiUser?.scoping?.enabled, true)
|
|
198
|
+
assert.deepEqual(cfg.multiUser?.scoping?.scopes, ["private", "family", "parent_only"])
|
|
199
|
+
assert.equal(cfg.multiUser?.scoping?.roles.parent?.defaultWriteScope, "private")
|
|
200
|
+
assert.equal(cfg.multiUser?.scoping?.collections?.family, "household")
|
|
201
|
+
assert.equal(cfg.multiUser?.scoping?.voiceId?.confidenceThreshold, 0.8)
|
|
202
|
+
})
|
package/config.ts
CHANGED
|
@@ -25,6 +25,55 @@ export type AutoTraceConfig = {
|
|
|
25
25
|
metadata?: Record<string, string | number | boolean>;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
export type UserProfile = {
|
|
29
|
+
userId: string;
|
|
30
|
+
name: string;
|
|
31
|
+
context?: string;
|
|
32
|
+
role?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type ScopeName = string;
|
|
36
|
+
export type CanReadScope = ScopeName | "*" | "self";
|
|
37
|
+
|
|
38
|
+
export type Role = {
|
|
39
|
+
canRead: CanReadScope[];
|
|
40
|
+
defaultWriteScope: ScopeName;
|
|
41
|
+
canWriteScopes?: ScopeName[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type VoiceIdConfig = {
|
|
45
|
+
enabled: boolean;
|
|
46
|
+
adapter?: string;
|
|
47
|
+
confidenceThreshold?: number;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type ScopingConfig = {
|
|
51
|
+
enabled: boolean;
|
|
52
|
+
defaultScope: ScopeName;
|
|
53
|
+
scopes: ScopeName[];
|
|
54
|
+
roles: Record<string, Role>;
|
|
55
|
+
users: Record<string, { role: string }>;
|
|
56
|
+
collections?: Record<ScopeName, string>;
|
|
57
|
+
voiceId?: VoiceIdConfig;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type MultiUserConfig = {
|
|
61
|
+
senderMap: Record<string, UserProfile>;
|
|
62
|
+
sharedUserId: string;
|
|
63
|
+
includeSharedInSearch: boolean;
|
|
64
|
+
scoping?: ScopingConfig;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Convert user-facing scope names (which may contain hyphens) to SDK-safe
|
|
69
|
+
* metadata values (alphanumeric + underscore only). Must be applied at every
|
|
70
|
+
* boundary where scopes cross into Hyperspell metadata — writes and reads —
|
|
71
|
+
* or filters will silently miss.
|
|
72
|
+
*/
|
|
73
|
+
export function normalizeScope(scope: ScopeName): string {
|
|
74
|
+
return scope.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
75
|
+
}
|
|
76
|
+
|
|
28
77
|
export type HyperspellConfig = {
|
|
29
78
|
apiKey: string;
|
|
30
79
|
userId?: string;
|
|
@@ -38,6 +87,7 @@ export type HyperspellConfig = {
|
|
|
38
87
|
relevanceThreshold: number;
|
|
39
88
|
debug: boolean;
|
|
40
89
|
knowledgeGraph: KnowledgeGraphConfig;
|
|
90
|
+
multiUser?: MultiUserConfig;
|
|
41
91
|
};
|
|
42
92
|
|
|
43
93
|
const ALLOWED_KEYS = [
|
|
@@ -53,6 +103,7 @@ const ALLOWED_KEYS = [
|
|
|
53
103
|
"relevanceThreshold",
|
|
54
104
|
"debug",
|
|
55
105
|
"knowledgeGraph",
|
|
106
|
+
"multiUser",
|
|
56
107
|
];
|
|
57
108
|
|
|
58
109
|
const VALID_SOURCES: HyperspellSource[] = [
|
|
@@ -135,6 +186,166 @@ function parseSources(raw: string | string[] | undefined): HyperspellSource[] {
|
|
|
135
186
|
return sources;
|
|
136
187
|
}
|
|
137
188
|
|
|
189
|
+
function parseScoping(raw: unknown): ScopingConfig | undefined {
|
|
190
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
191
|
+
const sc = raw as Record<string, unknown>;
|
|
192
|
+
|
|
193
|
+
if (sc.enabled !== true) return undefined;
|
|
194
|
+
|
|
195
|
+
const scopes = Array.isArray(sc.scopes)
|
|
196
|
+
? (sc.scopes.filter((s) => typeof s === "string") as ScopeName[])
|
|
197
|
+
: [];
|
|
198
|
+
if (scopes.length === 0) {
|
|
199
|
+
throw new Error("scoping.scopes must be a non-empty array of scope names");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const defaultScope =
|
|
203
|
+
typeof sc.defaultScope === "string" ? sc.defaultScope : "private";
|
|
204
|
+
if (!scopes.includes(defaultScope)) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`scoping.defaultScope "${defaultScope}" must be one of scopes: ${scopes.join(", ")}`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const rolesRaw =
|
|
211
|
+
sc.roles && typeof sc.roles === "object"
|
|
212
|
+
? (sc.roles as Record<string, unknown>)
|
|
213
|
+
: {};
|
|
214
|
+
const roles: Record<string, Role> = {};
|
|
215
|
+
for (const [roleName, rRaw] of Object.entries(rolesRaw)) {
|
|
216
|
+
if (!rRaw || typeof rRaw !== "object") continue;
|
|
217
|
+
const r = rRaw as Record<string, unknown>;
|
|
218
|
+
|
|
219
|
+
const canRead = Array.isArray(r.canRead)
|
|
220
|
+
? (r.canRead.filter((s) => typeof s === "string") as CanReadScope[])
|
|
221
|
+
: [];
|
|
222
|
+
for (const s of canRead) {
|
|
223
|
+
if (s === "*" || s === "self") continue;
|
|
224
|
+
if (!scopes.includes(s)) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`scoping.roles.${roleName}.canRead contains unknown scope "${s}"`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const defaultWriteScope =
|
|
232
|
+
typeof r.defaultWriteScope === "string"
|
|
233
|
+
? r.defaultWriteScope
|
|
234
|
+
: defaultScope;
|
|
235
|
+
if (!scopes.includes(defaultWriteScope)) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`scoping.roles.${roleName}.defaultWriteScope "${defaultWriteScope}" must be one of scopes: ${scopes.join(", ")}`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let canWriteScopes: ScopeName[] | undefined;
|
|
242
|
+
if (Array.isArray(r.canWriteScopes)) {
|
|
243
|
+
canWriteScopes = r.canWriteScopes.filter(
|
|
244
|
+
(s): s is ScopeName => typeof s === "string",
|
|
245
|
+
);
|
|
246
|
+
for (const s of canWriteScopes) {
|
|
247
|
+
if (!scopes.includes(s)) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`scoping.roles.${roleName}.canWriteScopes contains unknown scope "${s}"`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
roles[roleName] = { canRead, defaultWriteScope, canWriteScopes };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const usersRaw =
|
|
259
|
+
sc.users && typeof sc.users === "object"
|
|
260
|
+
? (sc.users as Record<string, unknown>)
|
|
261
|
+
: {};
|
|
262
|
+
const users: Record<string, { role: string }> = {};
|
|
263
|
+
for (const [uid, uRaw] of Object.entries(usersRaw)) {
|
|
264
|
+
if (!uRaw || typeof uRaw !== "object") continue;
|
|
265
|
+
const u = uRaw as Record<string, unknown>;
|
|
266
|
+
if (typeof u.role !== "string") continue;
|
|
267
|
+
if (!roles[u.role]) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`scoping.users.${uid}.role "${u.role}" does not key into scoping.roles`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
users[uid] = { role: u.role };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const collectionsRaw =
|
|
276
|
+
sc.collections && typeof sc.collections === "object"
|
|
277
|
+
? (sc.collections as Record<string, unknown>)
|
|
278
|
+
: undefined;
|
|
279
|
+
let collections: Record<ScopeName, string> | undefined;
|
|
280
|
+
if (collectionsRaw) {
|
|
281
|
+
collections = {};
|
|
282
|
+
for (const [scopeName, coll] of Object.entries(collectionsRaw)) {
|
|
283
|
+
if (typeof coll === "string" && scopes.includes(scopeName)) {
|
|
284
|
+
collections[scopeName] = coll;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let voiceId: VoiceIdConfig | undefined;
|
|
290
|
+
if (sc.voiceId && typeof sc.voiceId === "object") {
|
|
291
|
+
const v = sc.voiceId as Record<string, unknown>;
|
|
292
|
+
voiceId = {
|
|
293
|
+
enabled: v.enabled === true,
|
|
294
|
+
adapter: typeof v.adapter === "string" ? v.adapter : undefined,
|
|
295
|
+
confidenceThreshold:
|
|
296
|
+
typeof v.confidenceThreshold === "number"
|
|
297
|
+
? v.confidenceThreshold
|
|
298
|
+
: undefined,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
enabled: true,
|
|
304
|
+
defaultScope,
|
|
305
|
+
scopes,
|
|
306
|
+
roles,
|
|
307
|
+
users,
|
|
308
|
+
collections,
|
|
309
|
+
voiceId,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function parseMultiUser(raw: unknown): MultiUserConfig | undefined {
|
|
314
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
315
|
+
const mu = raw as Record<string, unknown>;
|
|
316
|
+
|
|
317
|
+
const senderMap: Record<string, UserProfile> = {};
|
|
318
|
+
const rawMap = mu.senderMap as Record<string, unknown> | undefined;
|
|
319
|
+
if (rawMap && typeof rawMap === "object") {
|
|
320
|
+
for (const [handle, profile] of Object.entries(rawMap)) {
|
|
321
|
+
if (profile && typeof profile === "object") {
|
|
322
|
+
const p = profile as Record<string, unknown>;
|
|
323
|
+
if (typeof p.userId === "string" && typeof p.name === "string") {
|
|
324
|
+
senderMap[handle] = {
|
|
325
|
+
userId: p.userId,
|
|
326
|
+
name: p.name,
|
|
327
|
+
context: typeof p.context === "string" ? p.context : undefined,
|
|
328
|
+
role: typeof p.role === "string" ? p.role : undefined,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (Object.keys(senderMap).length === 0) return undefined;
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
senderMap,
|
|
339
|
+
sharedUserId:
|
|
340
|
+
typeof mu.sharedUserId === "string" ? mu.sharedUserId : "shared",
|
|
341
|
+
includeSharedInSearch:
|
|
342
|
+
typeof mu.includeSharedInSearch === "boolean"
|
|
343
|
+
? mu.includeSharedInSearch
|
|
344
|
+
: true,
|
|
345
|
+
scoping: parseScoping(mu.scoping),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
138
349
|
export function parseConfig(raw: unknown): HyperspellConfig {
|
|
139
350
|
const cfg =
|
|
140
351
|
raw && typeof raw === "object" && !Array.isArray(raw)
|
|
@@ -184,6 +395,7 @@ export function parseConfig(raw: unknown): HyperspellConfig {
|
|
|
184
395
|
scanIntervalMinutes: (kgRaw.scanIntervalMinutes as number) ?? 60,
|
|
185
396
|
batchSize: (kgRaw.batchSize as number) ?? 20,
|
|
186
397
|
},
|
|
398
|
+
multiUser: parseMultiUser(cfg.multiUser),
|
|
187
399
|
};
|
|
188
400
|
}
|
|
189
401
|
|
package/graph/ops.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs"
|
|
2
2
|
import * as path from "node:path"
|
|
3
3
|
import type { HyperspellClient } from "../client.ts"
|
|
4
|
+
import type { HyperspellConfig } from "../config.ts"
|
|
5
|
+
import { getAllUserIds } from "../lib/sender.ts"
|
|
4
6
|
import { log } from "../logger.ts"
|
|
5
7
|
import { NetworkStateManager } from "./state.ts"
|
|
6
8
|
|
|
@@ -93,38 +95,42 @@ export async function scanMemories(
|
|
|
93
95
|
client: HyperspellClient,
|
|
94
96
|
stateManager: NetworkStateManager,
|
|
95
97
|
batchSize: number,
|
|
98
|
+
cfg?: HyperspellConfig,
|
|
96
99
|
): Promise<ScannedMemory[]> {
|
|
97
100
|
const unprocessed: ScannedMemory[] = []
|
|
101
|
+
const userIds = cfg ? getAllUserIds(cfg) : [undefined]
|
|
102
|
+
|
|
103
|
+
outer: for (const userId of userIds) {
|
|
104
|
+
for await (const mem of client.listMemories({ userId })) {
|
|
105
|
+
if (stateManager.isProcessed(mem.resourceId)) continue
|
|
106
|
+
if (mem.metadata?.graph_entity === "true" || mem.metadata?.graph_entity === true) continue
|
|
107
|
+
if ((mem.metadata?.status as string) !== "completed") continue
|
|
108
|
+
|
|
109
|
+
let summary = ""
|
|
110
|
+
try {
|
|
111
|
+
const full = await client.getMemory(mem.resourceId, mem.source as import("../config.ts").HyperspellSource, { userId })
|
|
112
|
+
const data = full.data as unknown[] | undefined
|
|
113
|
+
|
|
114
|
+
const participants = full.participants as Array<{ name?: string; email?: string }> | undefined
|
|
115
|
+
const participantLine = participants?.length
|
|
116
|
+
? `Participants: ${participants.map((p) => `${p.name || ""} <${p.email || ""}>`).join(", ")}`
|
|
117
|
+
: ""
|
|
118
|
+
|
|
119
|
+
const dataSummary = data ? summarizeMemoryData(data, mem.source) : ""
|
|
120
|
+
summary = [participantLine, dataSummary].filter(Boolean).join("\n\n")
|
|
121
|
+
} catch {
|
|
122
|
+
summary = "(content unavailable)"
|
|
123
|
+
}
|
|
98
124
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
const full = await client.getMemory(mem.resourceId, mem.source)
|
|
107
|
-
const data = full.data as unknown[] | undefined
|
|
108
|
-
|
|
109
|
-
const participants = full.participants as Array<{ name?: string; email?: string }> | undefined
|
|
110
|
-
const participantLine = participants?.length
|
|
111
|
-
? `Participants: ${participants.map((p) => `${p.name || ""} <${p.email || ""}>`).join(", ")}`
|
|
112
|
-
: ""
|
|
113
|
-
|
|
114
|
-
const dataSummary = data ? summarizeMemoryData(data, mem.source) : ""
|
|
115
|
-
summary = [participantLine, dataSummary].filter(Boolean).join("\n\n")
|
|
116
|
-
} catch {
|
|
117
|
-
summary = "(content unavailable)"
|
|
118
|
-
}
|
|
125
|
+
unprocessed.push({
|
|
126
|
+
resourceId: mem.resourceId,
|
|
127
|
+
source: mem.source,
|
|
128
|
+
title: mem.title,
|
|
129
|
+
summary: summary.slice(0, 1000),
|
|
130
|
+
})
|
|
119
131
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
source: mem.source,
|
|
123
|
-
title: mem.title,
|
|
124
|
-
summary: summary.slice(0, 1000),
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
if (unprocessed.length >= batchSize) break
|
|
132
|
+
if (unprocessed.length >= batchSize) break outer
|
|
133
|
+
}
|
|
128
134
|
}
|
|
129
135
|
|
|
130
136
|
return unprocessed
|
package/graph/tools.ts
CHANGED
|
@@ -32,7 +32,7 @@ export function registerNetworkTools(
|
|
|
32
32
|
async execute(_toolCallId: string, params: { batchSize?: number }) {
|
|
33
33
|
const batchSize = params.batchSize ?? cfg.knowledgeGraph.batchSize
|
|
34
34
|
try {
|
|
35
|
-
const memories = await scanMemories(client, stateManager, batchSize)
|
|
35
|
+
const memories = await scanMemories(client, stateManager, batchSize, cfg)
|
|
36
36
|
const text = formatScanResults(memories, stateManager.getProcessedCount(), stateManager.getLastScanAt())
|
|
37
37
|
return {
|
|
38
38
|
content: [{ type: "text" as const, text }],
|