@hyperspell/openclaw-hyperspell 0.8.1 → 0.10.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 +271 -59
- package/commands/setup.ts +1 -0
- package/commands/slash.ts +15 -7
- package/config.ts +55 -0
- package/graph/ops.ts +34 -28
- package/graph/tools.ts +1 -1
- package/hooks/auto-context.ts +163 -10
- package/hooks/auto-trace.ts +12 -2
- package/hooks/emotional-state.ts +97 -0
- package/hooks/memory-sync.ts +5 -3
- package/index.ts +19 -6
- package/lib/sender.ts +76 -0
- package/openclaw.plugin.json +17 -0
- package/package.json +1 -1
- package/sync/markdown.ts +4 -1
- package/tools/remember.ts +56 -48
- package/tools/search.ts +86 -82
- package/types/openclaw.d.ts +16 -0
package/client.ts
CHANGED
|
@@ -37,6 +37,25 @@ export type Connection = {
|
|
|
37
37
|
provider: HyperspellSource;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
export type EmotionalStateResponse = {
|
|
41
|
+
resourceId: string
|
|
42
|
+
summary: string
|
|
43
|
+
extractedAt: string
|
|
44
|
+
sessionId: string | null
|
|
45
|
+
relationshipId: string | null
|
|
46
|
+
status: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type EmotionalStateLatest = {
|
|
50
|
+
resourceId: string
|
|
51
|
+
summary: string
|
|
52
|
+
extractedAt: string
|
|
53
|
+
sessionId: string | null
|
|
54
|
+
relationshipId: string | null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const API_BASE_URL = "https://api.hyperspell.com"
|
|
58
|
+
|
|
40
59
|
export class HyperspellClient {
|
|
41
60
|
private client: Hyperspell;
|
|
42
61
|
private config: HyperspellConfig;
|
|
@@ -52,29 +71,63 @@ export class HyperspellClient {
|
|
|
52
71
|
);
|
|
53
72
|
}
|
|
54
73
|
|
|
74
|
+
private rawHeaders(): Record<string, string> {
|
|
75
|
+
const headers: Record<string, string> = {
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
78
|
+
};
|
|
79
|
+
if (this.config.userId) {
|
|
80
|
+
headers["X-As-User"] = this.config.userId;
|
|
81
|
+
}
|
|
82
|
+
return headers;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private requestOptions(userId?: string) {
|
|
86
|
+
if (!userId) return undefined;
|
|
87
|
+
return { headers: { "X-As-User": userId } };
|
|
88
|
+
}
|
|
89
|
+
|
|
55
90
|
async search(
|
|
56
91
|
query: string,
|
|
57
|
-
options?: {
|
|
92
|
+
options?: {
|
|
93
|
+
limit?: number;
|
|
94
|
+
sources?: HyperspellSource[];
|
|
95
|
+
after?: string;
|
|
96
|
+
before?: string;
|
|
97
|
+
userId?: string;
|
|
98
|
+
},
|
|
58
99
|
): Promise<SearchResult[]> {
|
|
59
100
|
const limit = options?.limit ?? this.config.maxResults;
|
|
60
101
|
const sources =
|
|
61
102
|
options?.sources ??
|
|
62
103
|
(this.config.sources.length > 0 ? this.config.sources : undefined);
|
|
63
104
|
|
|
64
|
-
log.debugRequest("memories.search", {
|
|
65
|
-
|
|
66
|
-
const response = await this.client.memories.search({
|
|
105
|
+
log.debugRequest("memories.search", {
|
|
67
106
|
query,
|
|
107
|
+
limit,
|
|
68
108
|
sources,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
...(options?.before ? { before: options.before } : {}),
|
|
73
|
-
},
|
|
109
|
+
after: options?.after,
|
|
110
|
+
before: options?.before,
|
|
111
|
+
userId: options?.userId,
|
|
74
112
|
});
|
|
75
113
|
|
|
114
|
+
const response = await this.client.memories.search(
|
|
115
|
+
{
|
|
116
|
+
query,
|
|
117
|
+
sources,
|
|
118
|
+
options: {
|
|
119
|
+
max_results: limit,
|
|
120
|
+
...(options?.after ? { after: options.after } : {}),
|
|
121
|
+
...(options?.before ? { before: options.before } : {}),
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
this.requestOptions(options?.userId),
|
|
125
|
+
);
|
|
126
|
+
|
|
76
127
|
const results: SearchResult[] = response.documents.map((doc) => {
|
|
77
|
-
const raw = doc as typeof doc & {
|
|
128
|
+
const raw = doc as typeof doc & {
|
|
129
|
+
highlights?: Array<{ id: string; score: number; text: string }>;
|
|
130
|
+
};
|
|
78
131
|
return {
|
|
79
132
|
resourceId: doc.resource_id,
|
|
80
133
|
title: doc.title ?? null,
|
|
@@ -82,7 +135,11 @@ export class HyperspellClient {
|
|
|
82
135
|
score: doc.score ?? null,
|
|
83
136
|
url: (doc.metadata?.url as string | null) ?? null,
|
|
84
137
|
createdAt: (doc.metadata?.created_at as string | null) ?? null,
|
|
85
|
-
highlights: (raw.highlights ?? []).map((h) => ({
|
|
138
|
+
highlights: (raw.highlights ?? []).map((h) => ({
|
|
139
|
+
id: h.id,
|
|
140
|
+
score: h.score,
|
|
141
|
+
text: h.text,
|
|
142
|
+
})),
|
|
86
143
|
};
|
|
87
144
|
});
|
|
88
145
|
|
|
@@ -92,25 +149,41 @@ export class HyperspellClient {
|
|
|
92
149
|
|
|
93
150
|
async searchRaw(
|
|
94
151
|
query: string,
|
|
95
|
-
options?: {
|
|
152
|
+
options?: {
|
|
153
|
+
limit?: number;
|
|
154
|
+
sources?: HyperspellSource[];
|
|
155
|
+
after?: string;
|
|
156
|
+
before?: string;
|
|
157
|
+
userId?: string;
|
|
158
|
+
},
|
|
96
159
|
): Promise<Record<string, unknown>> {
|
|
97
160
|
const limit = options?.limit ?? this.config.maxResults;
|
|
98
161
|
const sources =
|
|
99
162
|
options?.sources ??
|
|
100
163
|
(this.config.sources.length > 0 ? this.config.sources : undefined);
|
|
101
164
|
|
|
102
|
-
log.debugRequest("memories.search (raw)", {
|
|
103
|
-
|
|
104
|
-
const response = await this.client.memories.search({
|
|
165
|
+
log.debugRequest("memories.search (raw)", {
|
|
105
166
|
query,
|
|
167
|
+
limit,
|
|
106
168
|
sources,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
...(options?.before ? { before: options.before } : {}),
|
|
111
|
-
},
|
|
169
|
+
after: options?.after,
|
|
170
|
+
before: options?.before,
|
|
171
|
+
userId: options?.userId,
|
|
112
172
|
});
|
|
113
173
|
|
|
174
|
+
const response = await this.client.memories.search(
|
|
175
|
+
{
|
|
176
|
+
query,
|
|
177
|
+
sources,
|
|
178
|
+
options: {
|
|
179
|
+
max_results: limit,
|
|
180
|
+
...(options?.after ? { after: options.after } : {}),
|
|
181
|
+
...(options?.before ? { before: options.before } : {}),
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
this.requestOptions(options?.userId),
|
|
185
|
+
);
|
|
186
|
+
|
|
114
187
|
log.debugResponse("memories.search (raw)", {
|
|
115
188
|
count: response.documents.length,
|
|
116
189
|
});
|
|
@@ -120,7 +193,11 @@ export class HyperspellClient {
|
|
|
120
193
|
|
|
121
194
|
async searchWithAnswer(
|
|
122
195
|
query: string,
|
|
123
|
-
options?: {
|
|
196
|
+
options?: {
|
|
197
|
+
limit?: number;
|
|
198
|
+
sources?: HyperspellSource[];
|
|
199
|
+
userId?: string;
|
|
200
|
+
},
|
|
124
201
|
): Promise<SearchWithAnswerResult> {
|
|
125
202
|
const limit = options?.limit ?? this.config.maxResults;
|
|
126
203
|
const sources =
|
|
@@ -131,16 +208,20 @@ export class HyperspellClient {
|
|
|
131
208
|
query,
|
|
132
209
|
limit,
|
|
133
210
|
sources,
|
|
211
|
+
userId: options?.userId,
|
|
134
212
|
});
|
|
135
213
|
|
|
136
|
-
const response = await this.client.memories.search(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
214
|
+
const response = await this.client.memories.search(
|
|
215
|
+
{
|
|
216
|
+
query,
|
|
217
|
+
sources,
|
|
218
|
+
answer: true,
|
|
219
|
+
options: {
|
|
220
|
+
max_results: limit,
|
|
221
|
+
},
|
|
142
222
|
},
|
|
143
|
-
|
|
223
|
+
this.requestOptions(options?.userId),
|
|
224
|
+
);
|
|
144
225
|
|
|
145
226
|
const documents: SearchResult[] = response.documents.map((doc) => ({
|
|
146
227
|
resourceId: doc.resource_id,
|
|
@@ -171,6 +252,7 @@ export class HyperspellClient {
|
|
|
171
252
|
collection?: string;
|
|
172
253
|
date?: string;
|
|
173
254
|
metadata?: Record<string, string | number | boolean>;
|
|
255
|
+
userId?: string;
|
|
174
256
|
},
|
|
175
257
|
): Promise<{ resourceId: string }> {
|
|
176
258
|
log.debugRequest("memories.add", {
|
|
@@ -179,19 +261,23 @@ export class HyperspellClient {
|
|
|
179
261
|
resourceId: options?.resourceId,
|
|
180
262
|
collection: options?.collection,
|
|
181
263
|
date: options?.date,
|
|
264
|
+
userId: options?.userId,
|
|
182
265
|
});
|
|
183
266
|
|
|
184
|
-
const result = await this.client.memories.add(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
267
|
+
const result = await this.client.memories.add(
|
|
268
|
+
{
|
|
269
|
+
text,
|
|
270
|
+
title: options?.title,
|
|
271
|
+
resource_id: options?.resourceId,
|
|
272
|
+
collection: options?.collection,
|
|
273
|
+
date: options?.date,
|
|
274
|
+
metadata: {
|
|
275
|
+
...options?.metadata,
|
|
276
|
+
openclaw_source: "command",
|
|
277
|
+
},
|
|
193
278
|
},
|
|
194
|
-
|
|
279
|
+
this.requestOptions(options?.userId),
|
|
280
|
+
);
|
|
195
281
|
|
|
196
282
|
log.debugResponse("memories.add", { resourceId: result.resource_id });
|
|
197
283
|
return { resourceId: result.resource_id };
|
|
@@ -215,10 +301,15 @@ export class HyperspellClient {
|
|
|
215
301
|
|
|
216
302
|
async getConnectUrl(
|
|
217
303
|
integrationId: string,
|
|
304
|
+
options?: { userId?: string },
|
|
218
305
|
): Promise<{ url: string; expiresAt: string }> {
|
|
219
306
|
log.debugRequest("integrations.connect", { integrationId });
|
|
220
307
|
|
|
221
|
-
const response = await this.client.integrations.connect(
|
|
308
|
+
const response = await this.client.integrations.connect(
|
|
309
|
+
integrationId,
|
|
310
|
+
undefined,
|
|
311
|
+
this.requestOptions(options?.userId),
|
|
312
|
+
);
|
|
222
313
|
|
|
223
314
|
log.debugResponse("integrations.connect", { url: response.url });
|
|
224
315
|
return {
|
|
@@ -231,6 +322,7 @@ export class HyperspellClient {
|
|
|
231
322
|
source?: HyperspellSource;
|
|
232
323
|
collection?: string;
|
|
233
324
|
pageSize?: number;
|
|
325
|
+
userId?: string;
|
|
234
326
|
}): AsyncGenerator<{
|
|
235
327
|
resourceId: string;
|
|
236
328
|
source: HyperspellSource;
|
|
@@ -240,6 +332,7 @@ export class HyperspellClient {
|
|
|
240
332
|
log.debugRequest("memories.list", {
|
|
241
333
|
source: options?.source,
|
|
242
334
|
collection: options?.collection,
|
|
335
|
+
userId: options?.userId,
|
|
243
336
|
});
|
|
244
337
|
|
|
245
338
|
const params: Record<string, unknown> = {
|
|
@@ -248,7 +341,10 @@ export class HyperspellClient {
|
|
|
248
341
|
if (options?.source) params.source = options.source;
|
|
249
342
|
if (options?.collection) params.collection = options.collection;
|
|
250
343
|
|
|
251
|
-
for await (const memory of this.client.memories.list(
|
|
344
|
+
for await (const memory of this.client.memories.list(
|
|
345
|
+
params as any,
|
|
346
|
+
this.requestOptions(options?.userId),
|
|
347
|
+
)) {
|
|
252
348
|
yield {
|
|
253
349
|
resourceId: memory.resource_id,
|
|
254
350
|
source: memory.source as HyperspellSource,
|
|
@@ -261,10 +357,15 @@ export class HyperspellClient {
|
|
|
261
357
|
async getMemory(
|
|
262
358
|
resourceId: string,
|
|
263
359
|
source: HyperspellSource,
|
|
360
|
+
options?: { userId?: string },
|
|
264
361
|
): Promise<Record<string, unknown>> {
|
|
265
362
|
log.debugRequest("memories.get", { resourceId, source });
|
|
266
363
|
|
|
267
|
-
const response = await this.client.memories.get(
|
|
364
|
+
const response = await this.client.memories.get(
|
|
365
|
+
resourceId,
|
|
366
|
+
{ source },
|
|
367
|
+
this.requestOptions(options?.userId),
|
|
368
|
+
);
|
|
268
369
|
const raw = response as unknown as Record<string, unknown>;
|
|
269
370
|
|
|
270
371
|
log.debugResponse("memories.get", { resourceId, hasData: "data" in raw });
|
|
@@ -278,30 +379,35 @@ export class HyperspellClient {
|
|
|
278
379
|
title?: string;
|
|
279
380
|
extract?: Array<"procedure" | "memory" | "mood">;
|
|
280
381
|
metadata?: Record<string, string | number | boolean>;
|
|
382
|
+
userId?: string;
|
|
281
383
|
},
|
|
282
384
|
): Promise<{ resourceId: string; status: string }> {
|
|
283
385
|
log.debugRequest("sessions.add", {
|
|
284
386
|
historyLength: history.length,
|
|
285
387
|
sessionId: options?.sessionId,
|
|
286
388
|
extract: options?.extract,
|
|
389
|
+
userId: options?.userId,
|
|
287
390
|
});
|
|
288
391
|
|
|
289
|
-
const result = await this.client.sessions.add(
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
"procedure"
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
392
|
+
const result = await this.client.sessions.add(
|
|
393
|
+
{
|
|
394
|
+
history,
|
|
395
|
+
session_id: options?.sessionId,
|
|
396
|
+
title: options?.title,
|
|
397
|
+
format: "openclaw",
|
|
398
|
+
// Cast: SDK 0.35 typing accepts only ["procedure" | "memory"], but the
|
|
399
|
+
// backend's mood extractor (hyperspell/hyperspell#581) accepts "mood".
|
|
400
|
+
// Remove this cast once the OpenAPI spec is updated.
|
|
401
|
+
extract: (options?.extract ?? ["procedure"]) as Array<
|
|
402
|
+
"procedure" | "memory"
|
|
403
|
+
>,
|
|
404
|
+
metadata: {
|
|
405
|
+
...options?.metadata,
|
|
406
|
+
openclaw_source: "agent_end",
|
|
407
|
+
},
|
|
303
408
|
},
|
|
304
|
-
|
|
409
|
+
this.requestOptions(options?.userId),
|
|
410
|
+
);
|
|
305
411
|
|
|
306
412
|
log.debugResponse("sessions.add", {
|
|
307
413
|
resourceId: result.resource_id,
|
|
@@ -310,10 +416,14 @@ export class HyperspellClient {
|
|
|
310
416
|
return { resourceId: result.resource_id, status: result.status };
|
|
311
417
|
}
|
|
312
418
|
|
|
313
|
-
async listConnections(
|
|
314
|
-
|
|
419
|
+
async listConnections(options?: {
|
|
420
|
+
userId?: string;
|
|
421
|
+
}): Promise<Connection[]> {
|
|
422
|
+
log.debugRequest("connections.list", { userId: options?.userId });
|
|
315
423
|
|
|
316
|
-
const response = await this.client.connections.list(
|
|
424
|
+
const response = await this.client.connections.list(
|
|
425
|
+
this.requestOptions(options?.userId),
|
|
426
|
+
);
|
|
317
427
|
|
|
318
428
|
const connections: Connection[] = response.connections.map((conn) => ({
|
|
319
429
|
id: conn.id,
|
|
@@ -325,4 +435,106 @@ export class HyperspellClient {
|
|
|
325
435
|
log.debugResponse("connections.list", { count: connections.length });
|
|
326
436
|
return connections;
|
|
327
437
|
}
|
|
438
|
+
|
|
439
|
+
// -- Emotional State (raw fetch -- not in public SDK) -----------------------
|
|
440
|
+
|
|
441
|
+
async storeEmotionalState(
|
|
442
|
+
conversation: string,
|
|
443
|
+
options?: {
|
|
444
|
+
sessionId?: string;
|
|
445
|
+
relationshipId?: string;
|
|
446
|
+
metadata?: Record<string, string | number | boolean>;
|
|
447
|
+
},
|
|
448
|
+
): Promise<EmotionalStateResponse> {
|
|
449
|
+
log.debugRequest("emotional-state.store", {
|
|
450
|
+
conversationLength: conversation.length,
|
|
451
|
+
relationshipId: options?.relationshipId,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const body: Record<string, unknown> = { conversation };
|
|
455
|
+
if (options?.sessionId) body.session_id = options.sessionId;
|
|
456
|
+
if (options?.relationshipId) body.relationship_id = options.relationshipId;
|
|
457
|
+
if (options?.metadata) body.metadata = options.metadata;
|
|
458
|
+
|
|
459
|
+
const res = await fetch(`${API_BASE_URL}/emotional-state`, {
|
|
460
|
+
method: "POST",
|
|
461
|
+
headers: this.rawHeaders(),
|
|
462
|
+
body: JSON.stringify(body),
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
if (!res.ok) {
|
|
466
|
+
const text = await res.text().catch(() => "");
|
|
467
|
+
throw new Error(`POST /emotional-state failed (${res.status}): ${text}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const data = await res.json();
|
|
471
|
+
const result: EmotionalStateResponse = {
|
|
472
|
+
resourceId: data.resource_id,
|
|
473
|
+
summary: data.summary,
|
|
474
|
+
extractedAt: data.extracted_at,
|
|
475
|
+
sessionId: data.session_id ?? null,
|
|
476
|
+
relationshipId: data.relationship_id ?? null,
|
|
477
|
+
status: data.status,
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
log.debugResponse("emotional-state.store", { resourceId: result.resourceId });
|
|
481
|
+
return result;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async getEmotionalState(relationshipId?: string): Promise<EmotionalStateLatest | null> {
|
|
485
|
+
log.debugRequest("emotional-state.get", { relationshipId });
|
|
486
|
+
|
|
487
|
+
const url = new URL(`${API_BASE_URL}/emotional-state`);
|
|
488
|
+
if (relationshipId) url.searchParams.set("relationship_id", relationshipId);
|
|
489
|
+
|
|
490
|
+
const res = await fetch(url.toString(), {
|
|
491
|
+
method: "GET",
|
|
492
|
+
headers: this.rawHeaders(),
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
if (!res.ok) {
|
|
496
|
+
const text = await res.text().catch(() => "");
|
|
497
|
+
throw new Error(`GET /emotional-state failed (${res.status}): ${text}`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const data = await res.json();
|
|
501
|
+
if (data === null) {
|
|
502
|
+
log.debugResponse("emotional-state.get", { found: false });
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const result: EmotionalStateLatest = {
|
|
507
|
+
resourceId: data.resource_id,
|
|
508
|
+
summary: data.summary,
|
|
509
|
+
extractedAt: data.extracted_at,
|
|
510
|
+
sessionId: data.session_id ?? null,
|
|
511
|
+
relationshipId: data.relationship_id ?? null,
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
log.debugResponse("emotional-state.get", { found: true, resourceId: result.resourceId });
|
|
515
|
+
return result;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async deleteEmotionalState(relationshipId?: string): Promise<{ deletedCount: number }> {
|
|
519
|
+
log.debugRequest("emotional-state.delete", { relationshipId });
|
|
520
|
+
|
|
521
|
+
const url = new URL(`${API_BASE_URL}/emotional-state`);
|
|
522
|
+
if (relationshipId) url.searchParams.set("relationship_id", relationshipId);
|
|
523
|
+
|
|
524
|
+
const res = await fetch(url.toString(), {
|
|
525
|
+
method: "DELETE",
|
|
526
|
+
headers: this.rawHeaders(),
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
if (!res.ok) {
|
|
530
|
+
const text = await res.text().catch(() => "");
|
|
531
|
+
throw new Error(`DELETE /emotional-state failed (${res.status}): ${text}`);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const data = await res.json();
|
|
535
|
+
const result = { deletedCount: data.deleted_count };
|
|
536
|
+
|
|
537
|
+
log.debugResponse("emotional-state.delete", result);
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
328
540
|
}
|
package/commands/setup.ts
CHANGED
package/commands/slash.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
|
2
2
|
import type { HyperspellClient } from "../client.ts"
|
|
3
3
|
import type { HyperspellConfig } from "../config.ts"
|
|
4
4
|
import { getWorkspaceDir } from "../config.ts"
|
|
5
|
+
import { resolveUser } from "../lib/sender.ts"
|
|
5
6
|
import { log } from "../logger.ts"
|
|
6
7
|
import { syncAllMemoryFiles } from "../sync/markdown.ts"
|
|
7
8
|
|
|
@@ -18,7 +19,7 @@ function formatScore(score: number | null): string {
|
|
|
18
19
|
export function registerCommands(
|
|
19
20
|
api: OpenClawPluginApi,
|
|
20
21
|
client: HyperspellClient,
|
|
21
|
-
|
|
22
|
+
cfg: HyperspellConfig,
|
|
22
23
|
): void {
|
|
23
24
|
// /getcontext <query> - Search memories and show summaries
|
|
24
25
|
api.registerCommand({
|
|
@@ -26,16 +27,18 @@ export function registerCommands(
|
|
|
26
27
|
description: "Search your memories for relevant context",
|
|
27
28
|
acceptsArgs: true,
|
|
28
29
|
requireAuth: true,
|
|
29
|
-
handler: async (ctx: { args?: string }) => {
|
|
30
|
+
handler: async (ctx: { args?: string; senderId?: string; channel?: string }) => {
|
|
30
31
|
const query = ctx.args?.trim()
|
|
31
32
|
if (!query) {
|
|
32
33
|
return { text: "Usage: /getcontext <search query>" }
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
const resolved = resolveUser(ctx as Record<string, unknown>, cfg)
|
|
37
|
+
const userId = resolved?.userId
|
|
38
|
+
log.debug(`/getcontext command: "${query}" userId=${userId}`)
|
|
36
39
|
|
|
37
40
|
try {
|
|
38
|
-
const results = await client.search(query, { limit: 5 })
|
|
41
|
+
const results = await client.search(query, { limit: 5, userId })
|
|
39
42
|
|
|
40
43
|
if (results.length === 0) {
|
|
41
44
|
return { text: `No memories found for: "${query}"` }
|
|
@@ -63,17 +66,20 @@ export function registerCommands(
|
|
|
63
66
|
description: "Save something to memory",
|
|
64
67
|
acceptsArgs: true,
|
|
65
68
|
requireAuth: true,
|
|
66
|
-
handler: async (ctx: { args?: string }) => {
|
|
69
|
+
handler: async (ctx: { args?: string; senderId?: string; channel?: string }) => {
|
|
67
70
|
const text = ctx.args?.trim()
|
|
68
71
|
if (!text) {
|
|
69
72
|
return { text: "Usage: /remember <text to remember>" }
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
|
|
75
|
+
const resolved = resolveUser(ctx as Record<string, unknown>, cfg)
|
|
76
|
+
const userId = resolved?.userId
|
|
77
|
+
log.debug(`/remember command: "${truncate(text, 50)}" userId=${userId}`)
|
|
73
78
|
|
|
74
79
|
try {
|
|
75
80
|
await client.addMemory(text, {
|
|
76
81
|
metadata: { source: "openclaw_command" },
|
|
82
|
+
userId,
|
|
77
83
|
})
|
|
78
84
|
|
|
79
85
|
const preview = truncate(text, 60)
|
|
@@ -96,7 +102,9 @@ export function registerCommands(
|
|
|
96
102
|
|
|
97
103
|
try {
|
|
98
104
|
const workspaceDir = getWorkspaceDir()
|
|
99
|
-
const result = await syncAllMemoryFiles(client, workspaceDir
|
|
105
|
+
const result = await syncAllMemoryFiles(client, workspaceDir, {
|
|
106
|
+
userId: cfg.multiUser?.sharedUserId,
|
|
107
|
+
})
|
|
100
108
|
|
|
101
109
|
if (result.synced === 0 && result.failed === 0) {
|
|
102
110
|
return { text: "No memory files found in memory/ directory." }
|
package/config.ts
CHANGED
|
@@ -25,17 +25,32 @@ 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
|
+
};
|
|
33
|
+
|
|
34
|
+
export type MultiUserConfig = {
|
|
35
|
+
senderMap: Record<string, UserProfile>;
|
|
36
|
+
sharedUserId: string;
|
|
37
|
+
includeSharedInSearch: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
28
40
|
export type HyperspellConfig = {
|
|
29
41
|
apiKey: string;
|
|
30
42
|
userId?: string;
|
|
31
43
|
autoContext: boolean;
|
|
32
44
|
autoTrace: AutoTraceConfig;
|
|
45
|
+
emotionalContext: boolean;
|
|
46
|
+
relationshipId?: string;
|
|
33
47
|
syncMemories: boolean;
|
|
34
48
|
sources: HyperspellSource[];
|
|
35
49
|
maxResults: number;
|
|
36
50
|
relevanceThreshold: number;
|
|
37
51
|
debug: boolean;
|
|
38
52
|
knowledgeGraph: KnowledgeGraphConfig;
|
|
53
|
+
multiUser?: MultiUserConfig;
|
|
39
54
|
};
|
|
40
55
|
|
|
41
56
|
const ALLOWED_KEYS = [
|
|
@@ -43,12 +58,15 @@ const ALLOWED_KEYS = [
|
|
|
43
58
|
"userId",
|
|
44
59
|
"autoContext",
|
|
45
60
|
"autoTrace",
|
|
61
|
+
"emotionalContext",
|
|
62
|
+
"relationshipId",
|
|
46
63
|
"syncMemories",
|
|
47
64
|
"sources",
|
|
48
65
|
"maxResults",
|
|
49
66
|
"relevanceThreshold",
|
|
50
67
|
"debug",
|
|
51
68
|
"knowledgeGraph",
|
|
69
|
+
"multiUser",
|
|
52
70
|
];
|
|
53
71
|
|
|
54
72
|
const VALID_SOURCES: HyperspellSource[] = [
|
|
@@ -131,6 +149,40 @@ function parseSources(raw: string | string[] | undefined): HyperspellSource[] {
|
|
|
131
149
|
return sources;
|
|
132
150
|
}
|
|
133
151
|
|
|
152
|
+
function parseMultiUser(raw: unknown): MultiUserConfig | undefined {
|
|
153
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
154
|
+
const mu = raw as Record<string, unknown>;
|
|
155
|
+
|
|
156
|
+
const senderMap: Record<string, UserProfile> = {};
|
|
157
|
+
const rawMap = mu.senderMap as Record<string, unknown> | undefined;
|
|
158
|
+
if (rawMap && typeof rawMap === "object") {
|
|
159
|
+
for (const [handle, profile] of Object.entries(rawMap)) {
|
|
160
|
+
if (profile && typeof profile === "object") {
|
|
161
|
+
const p = profile as Record<string, unknown>;
|
|
162
|
+
if (typeof p.userId === "string" && typeof p.name === "string") {
|
|
163
|
+
senderMap[handle] = {
|
|
164
|
+
userId: p.userId,
|
|
165
|
+
name: p.name,
|
|
166
|
+
context: typeof p.context === "string" ? p.context : undefined,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (Object.keys(senderMap).length === 0) return undefined;
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
senderMap,
|
|
177
|
+
sharedUserId:
|
|
178
|
+
typeof mu.sharedUserId === "string" ? mu.sharedUserId : "shared",
|
|
179
|
+
includeSharedInSearch:
|
|
180
|
+
typeof mu.includeSharedInSearch === "boolean"
|
|
181
|
+
? mu.includeSharedInSearch
|
|
182
|
+
: true,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
134
186
|
export function parseConfig(raw: unknown): HyperspellConfig {
|
|
135
187
|
const cfg =
|
|
136
188
|
raw && typeof raw === "object" && !Array.isArray(raw)
|
|
@@ -168,6 +220,8 @@ export function parseConfig(raw: unknown): HyperspellConfig {
|
|
|
168
220
|
| Record<string, string | number | boolean>
|
|
169
221
|
| undefined,
|
|
170
222
|
},
|
|
223
|
+
emotionalContext: (cfg.emotionalContext as boolean) ?? false,
|
|
224
|
+
relationshipId: cfg.relationshipId as string | undefined,
|
|
171
225
|
syncMemories: (cfg.syncMemories as boolean) ?? false,
|
|
172
226
|
sources: parseSources(cfg.sources as string | string[] | undefined),
|
|
173
227
|
maxResults: (cfg.maxResults as number) ?? 10,
|
|
@@ -178,6 +232,7 @@ export function parseConfig(raw: unknown): HyperspellConfig {
|
|
|
178
232
|
scanIntervalMinutes: (kgRaw.scanIntervalMinutes as number) ?? 60,
|
|
179
233
|
batchSize: (kgRaw.batchSize as number) ?? 20,
|
|
180
234
|
},
|
|
235
|
+
multiUser: parseMultiUser(cfg.multiUser),
|
|
181
236
|
};
|
|
182
237
|
}
|
|
183
238
|
|