@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/client.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import Hyperspell from "hyperspell";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
HyperspellConfig,
|
|
4
|
+
HyperspellSource,
|
|
5
|
+
ScopeName,
|
|
6
|
+
} from "./config.ts";
|
|
7
|
+
import { normalizeScope } from "./config.ts";
|
|
3
8
|
import { log } from "./logger.ts";
|
|
4
9
|
|
|
5
10
|
export type Highlight = {
|
|
@@ -82,29 +87,55 @@ export class HyperspellClient {
|
|
|
82
87
|
return headers;
|
|
83
88
|
}
|
|
84
89
|
|
|
90
|
+
private requestOptions(userId?: string) {
|
|
91
|
+
if (!userId) return undefined;
|
|
92
|
+
return { headers: { "X-As-User": userId } };
|
|
93
|
+
}
|
|
94
|
+
|
|
85
95
|
async search(
|
|
86
96
|
query: string,
|
|
87
|
-
options?: {
|
|
97
|
+
options?: {
|
|
98
|
+
limit?: number;
|
|
99
|
+
sources?: HyperspellSource[];
|
|
100
|
+
after?: string;
|
|
101
|
+
before?: string;
|
|
102
|
+
userId?: string;
|
|
103
|
+
filter?: Record<string, unknown>;
|
|
104
|
+
},
|
|
88
105
|
): Promise<SearchResult[]> {
|
|
89
106
|
const limit = options?.limit ?? this.config.maxResults;
|
|
90
107
|
const sources =
|
|
91
108
|
options?.sources ??
|
|
92
109
|
(this.config.sources.length > 0 ? this.config.sources : undefined);
|
|
93
110
|
|
|
94
|
-
log.debugRequest("memories.search", {
|
|
95
|
-
|
|
96
|
-
const response = await this.client.memories.search({
|
|
111
|
+
log.debugRequest("memories.search", {
|
|
97
112
|
query,
|
|
113
|
+
limit,
|
|
98
114
|
sources,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
},
|
|
115
|
+
after: options?.after,
|
|
116
|
+
before: options?.before,
|
|
117
|
+
userId: options?.userId,
|
|
118
|
+
filter: options?.filter,
|
|
104
119
|
});
|
|
105
120
|
|
|
121
|
+
const response = await this.client.memories.search(
|
|
122
|
+
{
|
|
123
|
+
query,
|
|
124
|
+
sources,
|
|
125
|
+
options: {
|
|
126
|
+
max_results: limit,
|
|
127
|
+
...(options?.after ? { after: options.after } : {}),
|
|
128
|
+
...(options?.before ? { before: options.before } : {}),
|
|
129
|
+
...(options?.filter ? { filter: options.filter } : {}),
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
this.requestOptions(options?.userId),
|
|
133
|
+
);
|
|
134
|
+
|
|
106
135
|
const results: SearchResult[] = response.documents.map((doc) => {
|
|
107
|
-
const raw = doc as typeof doc & {
|
|
136
|
+
const raw = doc as typeof doc & {
|
|
137
|
+
highlights?: Array<{ id: string; score: number; text: string }>;
|
|
138
|
+
};
|
|
108
139
|
return {
|
|
109
140
|
resourceId: doc.resource_id,
|
|
110
141
|
title: doc.title ?? null,
|
|
@@ -112,7 +143,11 @@ export class HyperspellClient {
|
|
|
112
143
|
score: doc.score ?? null,
|
|
113
144
|
url: (doc.metadata?.url as string | null) ?? null,
|
|
114
145
|
createdAt: (doc.metadata?.created_at as string | null) ?? null,
|
|
115
|
-
highlights: (raw.highlights ?? []).map((h) => ({
|
|
146
|
+
highlights: (raw.highlights ?? []).map((h) => ({
|
|
147
|
+
id: h.id,
|
|
148
|
+
score: h.score,
|
|
149
|
+
text: h.text,
|
|
150
|
+
})),
|
|
116
151
|
};
|
|
117
152
|
});
|
|
118
153
|
|
|
@@ -122,25 +157,44 @@ export class HyperspellClient {
|
|
|
122
157
|
|
|
123
158
|
async searchRaw(
|
|
124
159
|
query: string,
|
|
125
|
-
options?: {
|
|
160
|
+
options?: {
|
|
161
|
+
limit?: number;
|
|
162
|
+
sources?: HyperspellSource[];
|
|
163
|
+
after?: string;
|
|
164
|
+
before?: string;
|
|
165
|
+
userId?: string;
|
|
166
|
+
filter?: Record<string, unknown>;
|
|
167
|
+
},
|
|
126
168
|
): Promise<Record<string, unknown>> {
|
|
127
169
|
const limit = options?.limit ?? this.config.maxResults;
|
|
128
170
|
const sources =
|
|
129
171
|
options?.sources ??
|
|
130
172
|
(this.config.sources.length > 0 ? this.config.sources : undefined);
|
|
131
173
|
|
|
132
|
-
log.debugRequest("memories.search (raw)", {
|
|
133
|
-
|
|
134
|
-
const response = await this.client.memories.search({
|
|
174
|
+
log.debugRequest("memories.search (raw)", {
|
|
135
175
|
query,
|
|
176
|
+
limit,
|
|
136
177
|
sources,
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
},
|
|
178
|
+
after: options?.after,
|
|
179
|
+
before: options?.before,
|
|
180
|
+
userId: options?.userId,
|
|
181
|
+
filter: options?.filter,
|
|
142
182
|
});
|
|
143
183
|
|
|
184
|
+
const response = await this.client.memories.search(
|
|
185
|
+
{
|
|
186
|
+
query,
|
|
187
|
+
sources,
|
|
188
|
+
options: {
|
|
189
|
+
max_results: limit,
|
|
190
|
+
...(options?.after ? { after: options.after } : {}),
|
|
191
|
+
...(options?.before ? { before: options.before } : {}),
|
|
192
|
+
...(options?.filter ? { filter: options.filter } : {}),
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
this.requestOptions(options?.userId),
|
|
196
|
+
);
|
|
197
|
+
|
|
144
198
|
log.debugResponse("memories.search (raw)", {
|
|
145
199
|
count: response.documents.length,
|
|
146
200
|
});
|
|
@@ -150,7 +204,12 @@ export class HyperspellClient {
|
|
|
150
204
|
|
|
151
205
|
async searchWithAnswer(
|
|
152
206
|
query: string,
|
|
153
|
-
options?: {
|
|
207
|
+
options?: {
|
|
208
|
+
limit?: number;
|
|
209
|
+
sources?: HyperspellSource[];
|
|
210
|
+
userId?: string;
|
|
211
|
+
filter?: Record<string, unknown>;
|
|
212
|
+
},
|
|
154
213
|
): Promise<SearchWithAnswerResult> {
|
|
155
214
|
const limit = options?.limit ?? this.config.maxResults;
|
|
156
215
|
const sources =
|
|
@@ -161,16 +220,22 @@ export class HyperspellClient {
|
|
|
161
220
|
query,
|
|
162
221
|
limit,
|
|
163
222
|
sources,
|
|
223
|
+
userId: options?.userId,
|
|
224
|
+
filter: options?.filter,
|
|
164
225
|
});
|
|
165
226
|
|
|
166
|
-
const response = await this.client.memories.search(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
227
|
+
const response = await this.client.memories.search(
|
|
228
|
+
{
|
|
229
|
+
query,
|
|
230
|
+
sources,
|
|
231
|
+
answer: true,
|
|
232
|
+
options: {
|
|
233
|
+
max_results: limit,
|
|
234
|
+
...(options?.filter ? { filter: options.filter } : {}),
|
|
235
|
+
},
|
|
172
236
|
},
|
|
173
|
-
|
|
237
|
+
this.requestOptions(options?.userId),
|
|
238
|
+
);
|
|
174
239
|
|
|
175
240
|
const documents: SearchResult[] = response.documents.map((doc) => ({
|
|
176
241
|
resourceId: doc.resource_id,
|
|
@@ -201,6 +266,8 @@ export class HyperspellClient {
|
|
|
201
266
|
collection?: string;
|
|
202
267
|
date?: string;
|
|
203
268
|
metadata?: Record<string, string | number | boolean>;
|
|
269
|
+
userId?: string;
|
|
270
|
+
scope?: ScopeName;
|
|
204
271
|
},
|
|
205
272
|
): Promise<{ resourceId: string }> {
|
|
206
273
|
log.debugRequest("memories.add", {
|
|
@@ -209,19 +276,28 @@ export class HyperspellClient {
|
|
|
209
276
|
resourceId: options?.resourceId,
|
|
210
277
|
collection: options?.collection,
|
|
211
278
|
date: options?.date,
|
|
279
|
+
userId: options?.userId,
|
|
280
|
+
scope: options?.scope,
|
|
212
281
|
});
|
|
213
282
|
|
|
214
|
-
const result = await this.client.memories.add(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
283
|
+
const result = await this.client.memories.add(
|
|
284
|
+
{
|
|
285
|
+
text,
|
|
286
|
+
title: options?.title,
|
|
287
|
+
resource_id: options?.resourceId,
|
|
288
|
+
collection: options?.collection,
|
|
289
|
+
date: options?.date,
|
|
290
|
+
metadata: {
|
|
291
|
+
openclaw_source: "command",
|
|
292
|
+
...options?.metadata,
|
|
293
|
+
...(options?.userId ? { openclaw_user: options.userId } : {}),
|
|
294
|
+
...(options?.scope
|
|
295
|
+
? { openclaw_scope: normalizeScope(options.scope) }
|
|
296
|
+
: {}),
|
|
297
|
+
},
|
|
223
298
|
},
|
|
224
|
-
|
|
299
|
+
this.requestOptions(options?.userId),
|
|
300
|
+
);
|
|
225
301
|
|
|
226
302
|
log.debugResponse("memories.add", { resourceId: result.resource_id });
|
|
227
303
|
return { resourceId: result.resource_id };
|
|
@@ -245,10 +321,15 @@ export class HyperspellClient {
|
|
|
245
321
|
|
|
246
322
|
async getConnectUrl(
|
|
247
323
|
integrationId: string,
|
|
324
|
+
options?: { userId?: string },
|
|
248
325
|
): Promise<{ url: string; expiresAt: string }> {
|
|
249
326
|
log.debugRequest("integrations.connect", { integrationId });
|
|
250
327
|
|
|
251
|
-
const response = await this.client.integrations.connect(
|
|
328
|
+
const response = await this.client.integrations.connect(
|
|
329
|
+
integrationId,
|
|
330
|
+
undefined,
|
|
331
|
+
this.requestOptions(options?.userId),
|
|
332
|
+
);
|
|
252
333
|
|
|
253
334
|
log.debugResponse("integrations.connect", { url: response.url });
|
|
254
335
|
return {
|
|
@@ -261,6 +342,7 @@ export class HyperspellClient {
|
|
|
261
342
|
source?: HyperspellSource;
|
|
262
343
|
collection?: string;
|
|
263
344
|
pageSize?: number;
|
|
345
|
+
userId?: string;
|
|
264
346
|
}): AsyncGenerator<{
|
|
265
347
|
resourceId: string;
|
|
266
348
|
source: HyperspellSource;
|
|
@@ -270,6 +352,7 @@ export class HyperspellClient {
|
|
|
270
352
|
log.debugRequest("memories.list", {
|
|
271
353
|
source: options?.source,
|
|
272
354
|
collection: options?.collection,
|
|
355
|
+
userId: options?.userId,
|
|
273
356
|
});
|
|
274
357
|
|
|
275
358
|
const params: Record<string, unknown> = {
|
|
@@ -278,7 +361,10 @@ export class HyperspellClient {
|
|
|
278
361
|
if (options?.source) params.source = options.source;
|
|
279
362
|
if (options?.collection) params.collection = options.collection;
|
|
280
363
|
|
|
281
|
-
for await (const memory of this.client.memories.list(
|
|
364
|
+
for await (const memory of this.client.memories.list(
|
|
365
|
+
params as any,
|
|
366
|
+
this.requestOptions(options?.userId),
|
|
367
|
+
)) {
|
|
282
368
|
yield {
|
|
283
369
|
resourceId: memory.resource_id,
|
|
284
370
|
source: memory.source as HyperspellSource,
|
|
@@ -291,10 +377,15 @@ export class HyperspellClient {
|
|
|
291
377
|
async getMemory(
|
|
292
378
|
resourceId: string,
|
|
293
379
|
source: HyperspellSource,
|
|
380
|
+
options?: { userId?: string },
|
|
294
381
|
): Promise<Record<string, unknown>> {
|
|
295
382
|
log.debugRequest("memories.get", { resourceId, source });
|
|
296
383
|
|
|
297
|
-
const response = await this.client.memories.get(
|
|
384
|
+
const response = await this.client.memories.get(
|
|
385
|
+
resourceId,
|
|
386
|
+
{ source },
|
|
387
|
+
this.requestOptions(options?.userId),
|
|
388
|
+
);
|
|
298
389
|
const raw = response as unknown as Record<string, unknown>;
|
|
299
390
|
|
|
300
391
|
log.debugResponse("memories.get", { resourceId, hasData: "data" in raw });
|
|
@@ -308,30 +399,41 @@ export class HyperspellClient {
|
|
|
308
399
|
title?: string;
|
|
309
400
|
extract?: Array<"procedure" | "memory" | "mood">;
|
|
310
401
|
metadata?: Record<string, string | number | boolean>;
|
|
402
|
+
userId?: string;
|
|
403
|
+
scope?: ScopeName;
|
|
311
404
|
},
|
|
312
405
|
): Promise<{ resourceId: string; status: string }> {
|
|
313
406
|
log.debugRequest("sessions.add", {
|
|
314
407
|
historyLength: history.length,
|
|
315
408
|
sessionId: options?.sessionId,
|
|
316
409
|
extract: options?.extract,
|
|
410
|
+
userId: options?.userId,
|
|
411
|
+
scope: options?.scope,
|
|
317
412
|
});
|
|
318
413
|
|
|
319
|
-
const result = await this.client.sessions.add(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
"procedure"
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
414
|
+
const result = await this.client.sessions.add(
|
|
415
|
+
{
|
|
416
|
+
history,
|
|
417
|
+
session_id: options?.sessionId,
|
|
418
|
+
title: options?.title,
|
|
419
|
+
format: "openclaw",
|
|
420
|
+
// Cast: SDK 0.35 typing accepts only ["procedure" | "memory"], but the
|
|
421
|
+
// backend's mood extractor (hyperspell/hyperspell#581) accepts "mood".
|
|
422
|
+
// Remove this cast once the OpenAPI spec is updated.
|
|
423
|
+
extract: (options?.extract ?? ["procedure"]) as Array<
|
|
424
|
+
"procedure" | "memory"
|
|
425
|
+
>,
|
|
426
|
+
metadata: {
|
|
427
|
+
openclaw_source: "agent_end",
|
|
428
|
+
...options?.metadata,
|
|
429
|
+
...(options?.userId ? { openclaw_user: options.userId } : {}),
|
|
430
|
+
...(options?.scope
|
|
431
|
+
? { openclaw_scope: normalizeScope(options.scope) }
|
|
432
|
+
: {}),
|
|
433
|
+
},
|
|
333
434
|
},
|
|
334
|
-
|
|
435
|
+
this.requestOptions(options?.userId),
|
|
436
|
+
);
|
|
335
437
|
|
|
336
438
|
log.debugResponse("sessions.add", {
|
|
337
439
|
resourceId: result.resource_id,
|
|
@@ -340,10 +442,14 @@ export class HyperspellClient {
|
|
|
340
442
|
return { resourceId: result.resource_id, status: result.status };
|
|
341
443
|
}
|
|
342
444
|
|
|
343
|
-
async listConnections(
|
|
344
|
-
|
|
445
|
+
async listConnections(options?: {
|
|
446
|
+
userId?: string;
|
|
447
|
+
}): Promise<Connection[]> {
|
|
448
|
+
log.debugRequest("connections.list", { userId: options?.userId });
|
|
345
449
|
|
|
346
|
-
const response = await this.client.connections.list(
|
|
450
|
+
const response = await this.client.connections.list(
|
|
451
|
+
this.requestOptions(options?.userId),
|
|
452
|
+
);
|
|
347
453
|
|
|
348
454
|
const connections: Connection[] = response.connections.map((conn) => ({
|
|
349
455
|
id: conn.id,
|
package/commands/slash.ts
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
2
2
|
import type { HyperspellClient } from "../client.ts"
|
|
3
|
-
import type { HyperspellConfig } from "../config.ts"
|
|
3
|
+
import type { CanReadScope, HyperspellConfig } from "../config.ts"
|
|
4
4
|
import { getWorkspaceDir } from "../config.ts"
|
|
5
|
+
import {
|
|
6
|
+
buildScopeFilter,
|
|
7
|
+
getCanReadScopes,
|
|
8
|
+
getDefaultWriteScope,
|
|
9
|
+
resolveRole,
|
|
10
|
+
resolveUser,
|
|
11
|
+
routeWrite,
|
|
12
|
+
} from "../lib/sender.ts"
|
|
5
13
|
import { log } from "../logger.ts"
|
|
6
14
|
import { syncAllMemoryFiles } from "../sync/markdown.ts"
|
|
7
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Strip a `#scope-name` prefix from free text. Returns the scope and the
|
|
18
|
+
* remainder. Used by /remember and /getcontext to let users narrow or route
|
|
19
|
+
* via a single keystroke.
|
|
20
|
+
*/
|
|
21
|
+
function parseScopePrefix(text: string): { scope?: string; rest: string } {
|
|
22
|
+
const m = text.match(/^#(\S+)\s+(.*)$/)
|
|
23
|
+
if (!m) return { rest: text }
|
|
24
|
+
return { scope: m[1], rest: m[2] }
|
|
25
|
+
}
|
|
26
|
+
|
|
8
27
|
function truncate(text: string, maxLength: number): string {
|
|
9
28
|
if (text.length <= maxLength) return text
|
|
10
29
|
return `${text.slice(0, maxLength)}…`
|
|
@@ -18,7 +37,7 @@ function formatScore(score: number | null): string {
|
|
|
18
37
|
export function registerCommands(
|
|
19
38
|
api: OpenClawPluginApi,
|
|
20
39
|
client: HyperspellClient,
|
|
21
|
-
|
|
40
|
+
cfg: HyperspellConfig,
|
|
22
41
|
): void {
|
|
23
42
|
// /getcontext <query> - Search memories and show summaries
|
|
24
43
|
api.registerCommand({
|
|
@@ -26,16 +45,34 @@ export function registerCommands(
|
|
|
26
45
|
description: "Search your memories for relevant context",
|
|
27
46
|
acceptsArgs: true,
|
|
28
47
|
requireAuth: true,
|
|
29
|
-
handler: async (ctx: { args?: string }) => {
|
|
30
|
-
const
|
|
31
|
-
if (!
|
|
32
|
-
return { text: "Usage: /getcontext <search query>" }
|
|
48
|
+
handler: async (ctx: { args?: string; senderId?: string; channel?: string }) => {
|
|
49
|
+
const rawArgs = ctx.args?.trim()
|
|
50
|
+
if (!rawArgs) {
|
|
51
|
+
return { text: "Usage: /getcontext [#scope] <search query>" }
|
|
52
|
+
}
|
|
53
|
+
const { scope: requestedScope, rest: query } = parseScopePrefix(rawArgs)
|
|
54
|
+
|
|
55
|
+
const resolved = resolveUser(ctx as Record<string, unknown>, cfg)
|
|
56
|
+
const userId = resolved?.userId
|
|
57
|
+
|
|
58
|
+
// Build scope filter: intersect requested scope (if any) with caller's canRead.
|
|
59
|
+
let filter: Record<string, unknown> | undefined
|
|
60
|
+
if (cfg.multiUser?.scoping) {
|
|
61
|
+
const canRead = getCanReadScopes(resolved, cfg)
|
|
62
|
+
const allowed: CanReadScope[] = requestedScope
|
|
63
|
+
? canRead.includes("*") || canRead.includes(requestedScope)
|
|
64
|
+
? [requestedScope]
|
|
65
|
+
: []
|
|
66
|
+
: canRead
|
|
67
|
+
filter = buildScopeFilter(allowed, resolved?.userId ?? "")
|
|
33
68
|
}
|
|
34
69
|
|
|
35
|
-
log.debug(
|
|
70
|
+
log.debug(
|
|
71
|
+
`/getcontext command: "${query}" userId=${userId} scope=${requestedScope ?? "any"}`,
|
|
72
|
+
)
|
|
36
73
|
|
|
37
74
|
try {
|
|
38
|
-
const results = await client.search(query, { limit: 5 })
|
|
75
|
+
const results = await client.search(query, { limit: 5, userId, filter })
|
|
39
76
|
|
|
40
77
|
if (results.length === 0) {
|
|
41
78
|
return { text: `No memories found for: "${query}"` }
|
|
@@ -63,21 +100,61 @@ export function registerCommands(
|
|
|
63
100
|
description: "Save something to memory",
|
|
64
101
|
acceptsArgs: true,
|
|
65
102
|
requireAuth: true,
|
|
66
|
-
handler: async (ctx: { args?: string }) => {
|
|
67
|
-
const
|
|
68
|
-
if (!
|
|
69
|
-
return { text: "Usage: /remember <text to remember>" }
|
|
103
|
+
handler: async (ctx: { args?: string; senderId?: string; channel?: string }) => {
|
|
104
|
+
const rawArgs = ctx.args?.trim()
|
|
105
|
+
if (!rawArgs) {
|
|
106
|
+
return { text: "Usage: /remember [#scope] <text to remember>" }
|
|
107
|
+
}
|
|
108
|
+
const { scope: requestedScope, rest: text } = parseScopePrefix(rawArgs)
|
|
109
|
+
|
|
110
|
+
const resolved = resolveUser(ctx as Record<string, unknown>, cfg)
|
|
111
|
+
const scopingEnabled = !!cfg.multiUser?.scoping
|
|
112
|
+
const availableScopes = cfg.multiUser?.scoping?.scopes ?? []
|
|
113
|
+
|
|
114
|
+
// Scope resolution: prefix > role default > global default > "private"
|
|
115
|
+
const scope = scopingEnabled
|
|
116
|
+
? (requestedScope ?? getDefaultWriteScope(resolved, cfg))
|
|
117
|
+
: "private"
|
|
118
|
+
|
|
119
|
+
// Validate scope is in declared vocabulary
|
|
120
|
+
if (
|
|
121
|
+
scopingEnabled &&
|
|
122
|
+
requestedScope &&
|
|
123
|
+
availableScopes.length > 0 &&
|
|
124
|
+
!availableScopes.includes(scope)
|
|
125
|
+
) {
|
|
126
|
+
return {
|
|
127
|
+
text: `Unknown scope "${scope}". Available: ${availableScopes.join(", ")}.`,
|
|
128
|
+
}
|
|
70
129
|
}
|
|
71
130
|
|
|
72
|
-
|
|
131
|
+
// canWriteScopes enforcement
|
|
132
|
+
if (scopingEnabled) {
|
|
133
|
+
const role = resolveRole(resolved, cfg)
|
|
134
|
+
if (role?.canWriteScopes && !role.canWriteScopes.includes(scope)) {
|
|
135
|
+
return { text: `You cannot write to scope "${scope}".` }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const { userId, collection } = scopingEnabled
|
|
140
|
+
? routeWrite(resolved, scope, cfg)
|
|
141
|
+
: { userId: resolved?.userId, collection: undefined }
|
|
142
|
+
|
|
143
|
+
log.debug(
|
|
144
|
+
`/remember command: "${truncate(text, 50)}" userId=${userId} scope=${scope}`,
|
|
145
|
+
)
|
|
73
146
|
|
|
74
147
|
try {
|
|
75
148
|
await client.addMemory(text, {
|
|
76
149
|
metadata: { source: "openclaw_command" },
|
|
150
|
+
collection,
|
|
151
|
+
userId,
|
|
152
|
+
scope: scopingEnabled ? scope : undefined,
|
|
77
153
|
})
|
|
78
154
|
|
|
79
155
|
const preview = truncate(text, 60)
|
|
80
|
-
|
|
156
|
+
const scopeHint = scopingEnabled ? ` [${scope}]` : ""
|
|
157
|
+
return { text: `Remembered${scopeHint}: "${preview}"` }
|
|
81
158
|
} catch (err) {
|
|
82
159
|
log.error("/remember failed", err)
|
|
83
160
|
return { text: "Failed to save memory. Check logs for details." }
|
|
@@ -96,7 +173,9 @@ export function registerCommands(
|
|
|
96
173
|
|
|
97
174
|
try {
|
|
98
175
|
const workspaceDir = getWorkspaceDir()
|
|
99
|
-
const result = await syncAllMemoryFiles(client, workspaceDir
|
|
176
|
+
const result = await syncAllMemoryFiles(client, workspaceDir, {
|
|
177
|
+
userId: cfg.multiUser?.sharedUserId,
|
|
178
|
+
})
|
|
100
179
|
|
|
101
180
|
if (result.synced === 0 && result.failed === 0) {
|
|
102
181
|
return { text: "No memory files found in memory/ directory." }
|