@hyperspell/openclaw-hyperspell 0.9.0 → 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 CHANGED
@@ -82,29 +82,52 @@ export class HyperspellClient {
82
82
  return headers;
83
83
  }
84
84
 
85
+ private requestOptions(userId?: string) {
86
+ if (!userId) return undefined;
87
+ return { headers: { "X-As-User": userId } };
88
+ }
89
+
85
90
  async search(
86
91
  query: string,
87
- options?: { limit?: number; sources?: HyperspellSource[]; after?: string; before?: string },
92
+ options?: {
93
+ limit?: number;
94
+ sources?: HyperspellSource[];
95
+ after?: string;
96
+ before?: string;
97
+ userId?: string;
98
+ },
88
99
  ): Promise<SearchResult[]> {
89
100
  const limit = options?.limit ?? this.config.maxResults;
90
101
  const sources =
91
102
  options?.sources ??
92
103
  (this.config.sources.length > 0 ? this.config.sources : undefined);
93
104
 
94
- log.debugRequest("memories.search", { query, limit, sources, after: options?.after, before: options?.before });
95
-
96
- const response = await this.client.memories.search({
105
+ log.debugRequest("memories.search", {
97
106
  query,
107
+ limit,
98
108
  sources,
99
- options: {
100
- max_results: limit,
101
- ...(options?.after ? { after: options.after } : {}),
102
- ...(options?.before ? { before: options.before } : {}),
103
- },
109
+ after: options?.after,
110
+ before: options?.before,
111
+ userId: options?.userId,
104
112
  });
105
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
+
106
127
  const results: SearchResult[] = response.documents.map((doc) => {
107
- const raw = doc as typeof doc & { highlights?: Array<{ id: string; score: number; text: string }> };
128
+ const raw = doc as typeof doc & {
129
+ highlights?: Array<{ id: string; score: number; text: string }>;
130
+ };
108
131
  return {
109
132
  resourceId: doc.resource_id,
110
133
  title: doc.title ?? null,
@@ -112,7 +135,11 @@ export class HyperspellClient {
112
135
  score: doc.score ?? null,
113
136
  url: (doc.metadata?.url as string | null) ?? null,
114
137
  createdAt: (doc.metadata?.created_at as string | null) ?? null,
115
- highlights: (raw.highlights ?? []).map((h) => ({ id: h.id, score: h.score, text: h.text })),
138
+ highlights: (raw.highlights ?? []).map((h) => ({
139
+ id: h.id,
140
+ score: h.score,
141
+ text: h.text,
142
+ })),
116
143
  };
117
144
  });
118
145
 
@@ -122,25 +149,41 @@ export class HyperspellClient {
122
149
 
123
150
  async searchRaw(
124
151
  query: string,
125
- options?: { limit?: number; sources?: HyperspellSource[]; after?: string; before?: string },
152
+ options?: {
153
+ limit?: number;
154
+ sources?: HyperspellSource[];
155
+ after?: string;
156
+ before?: string;
157
+ userId?: string;
158
+ },
126
159
  ): Promise<Record<string, unknown>> {
127
160
  const limit = options?.limit ?? this.config.maxResults;
128
161
  const sources =
129
162
  options?.sources ??
130
163
  (this.config.sources.length > 0 ? this.config.sources : undefined);
131
164
 
132
- log.debugRequest("memories.search (raw)", { query, limit, sources, after: options?.after, before: options?.before });
133
-
134
- const response = await this.client.memories.search({
165
+ log.debugRequest("memories.search (raw)", {
135
166
  query,
167
+ limit,
136
168
  sources,
137
- options: {
138
- max_results: limit,
139
- ...(options?.after ? { after: options.after } : {}),
140
- ...(options?.before ? { before: options.before } : {}),
141
- },
169
+ after: options?.after,
170
+ before: options?.before,
171
+ userId: options?.userId,
142
172
  });
143
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
+
144
187
  log.debugResponse("memories.search (raw)", {
145
188
  count: response.documents.length,
146
189
  });
@@ -150,7 +193,11 @@ export class HyperspellClient {
150
193
 
151
194
  async searchWithAnswer(
152
195
  query: string,
153
- options?: { limit?: number; sources?: HyperspellSource[] },
196
+ options?: {
197
+ limit?: number;
198
+ sources?: HyperspellSource[];
199
+ userId?: string;
200
+ },
154
201
  ): Promise<SearchWithAnswerResult> {
155
202
  const limit = options?.limit ?? this.config.maxResults;
156
203
  const sources =
@@ -161,16 +208,20 @@ export class HyperspellClient {
161
208
  query,
162
209
  limit,
163
210
  sources,
211
+ userId: options?.userId,
164
212
  });
165
213
 
166
- const response = await this.client.memories.search({
167
- query,
168
- sources,
169
- answer: true,
170
- options: {
171
- max_results: limit,
214
+ const response = await this.client.memories.search(
215
+ {
216
+ query,
217
+ sources,
218
+ answer: true,
219
+ options: {
220
+ max_results: limit,
221
+ },
172
222
  },
173
- });
223
+ this.requestOptions(options?.userId),
224
+ );
174
225
 
175
226
  const documents: SearchResult[] = response.documents.map((doc) => ({
176
227
  resourceId: doc.resource_id,
@@ -201,6 +252,7 @@ export class HyperspellClient {
201
252
  collection?: string;
202
253
  date?: string;
203
254
  metadata?: Record<string, string | number | boolean>;
255
+ userId?: string;
204
256
  },
205
257
  ): Promise<{ resourceId: string }> {
206
258
  log.debugRequest("memories.add", {
@@ -209,19 +261,23 @@ export class HyperspellClient {
209
261
  resourceId: options?.resourceId,
210
262
  collection: options?.collection,
211
263
  date: options?.date,
264
+ userId: options?.userId,
212
265
  });
213
266
 
214
- const result = await this.client.memories.add({
215
- text,
216
- title: options?.title,
217
- resource_id: options?.resourceId,
218
- collection: options?.collection,
219
- date: options?.date,
220
- metadata: {
221
- ...options?.metadata,
222
- openclaw_source: "command",
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
+ },
223
278
  },
224
- });
279
+ this.requestOptions(options?.userId),
280
+ );
225
281
 
226
282
  log.debugResponse("memories.add", { resourceId: result.resource_id });
227
283
  return { resourceId: result.resource_id };
@@ -245,10 +301,15 @@ export class HyperspellClient {
245
301
 
246
302
  async getConnectUrl(
247
303
  integrationId: string,
304
+ options?: { userId?: string },
248
305
  ): Promise<{ url: string; expiresAt: string }> {
249
306
  log.debugRequest("integrations.connect", { integrationId });
250
307
 
251
- const response = await this.client.integrations.connect(integrationId);
308
+ const response = await this.client.integrations.connect(
309
+ integrationId,
310
+ undefined,
311
+ this.requestOptions(options?.userId),
312
+ );
252
313
 
253
314
  log.debugResponse("integrations.connect", { url: response.url });
254
315
  return {
@@ -261,6 +322,7 @@ export class HyperspellClient {
261
322
  source?: HyperspellSource;
262
323
  collection?: string;
263
324
  pageSize?: number;
325
+ userId?: string;
264
326
  }): AsyncGenerator<{
265
327
  resourceId: string;
266
328
  source: HyperspellSource;
@@ -270,6 +332,7 @@ export class HyperspellClient {
270
332
  log.debugRequest("memories.list", {
271
333
  source: options?.source,
272
334
  collection: options?.collection,
335
+ userId: options?.userId,
273
336
  });
274
337
 
275
338
  const params: Record<string, unknown> = {
@@ -278,7 +341,10 @@ export class HyperspellClient {
278
341
  if (options?.source) params.source = options.source;
279
342
  if (options?.collection) params.collection = options.collection;
280
343
 
281
- for await (const memory of this.client.memories.list(params as any)) {
344
+ for await (const memory of this.client.memories.list(
345
+ params as any,
346
+ this.requestOptions(options?.userId),
347
+ )) {
282
348
  yield {
283
349
  resourceId: memory.resource_id,
284
350
  source: memory.source as HyperspellSource,
@@ -291,10 +357,15 @@ export class HyperspellClient {
291
357
  async getMemory(
292
358
  resourceId: string,
293
359
  source: HyperspellSource,
360
+ options?: { userId?: string },
294
361
  ): Promise<Record<string, unknown>> {
295
362
  log.debugRequest("memories.get", { resourceId, source });
296
363
 
297
- const response = await this.client.memories.get(resourceId, { source });
364
+ const response = await this.client.memories.get(
365
+ resourceId,
366
+ { source },
367
+ this.requestOptions(options?.userId),
368
+ );
298
369
  const raw = response as unknown as Record<string, unknown>;
299
370
 
300
371
  log.debugResponse("memories.get", { resourceId, hasData: "data" in raw });
@@ -308,30 +379,35 @@ export class HyperspellClient {
308
379
  title?: string;
309
380
  extract?: Array<"procedure" | "memory" | "mood">;
310
381
  metadata?: Record<string, string | number | boolean>;
382
+ userId?: string;
311
383
  },
312
384
  ): Promise<{ resourceId: string; status: string }> {
313
385
  log.debugRequest("sessions.add", {
314
386
  historyLength: history.length,
315
387
  sessionId: options?.sessionId,
316
388
  extract: options?.extract,
389
+ userId: options?.userId,
317
390
  });
318
391
 
319
- const result = await this.client.sessions.add({
320
- history,
321
- session_id: options?.sessionId,
322
- title: options?.title,
323
- format: "openclaw",
324
- // Cast: SDK 0.35 typing accepts only ["procedure" | "memory"], but the
325
- // backend's mood extractor (hyperspell/hyperspell#581) accepts "mood".
326
- // Remove this cast once the OpenAPI spec is updated.
327
- extract: (options?.extract ?? ["procedure"]) as Array<
328
- "procedure" | "memory"
329
- >,
330
- metadata: {
331
- ...options?.metadata,
332
- openclaw_source: "agent_end",
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
+ },
333
408
  },
334
- });
409
+ this.requestOptions(options?.userId),
410
+ );
335
411
 
336
412
  log.debugResponse("sessions.add", {
337
413
  resourceId: result.resource_id,
@@ -340,10 +416,14 @@ export class HyperspellClient {
340
416
  return { resourceId: result.resource_id, status: result.status };
341
417
  }
342
418
 
343
- async listConnections(): Promise<Connection[]> {
344
- log.debugRequest("connections.list", {});
419
+ async listConnections(options?: {
420
+ userId?: string;
421
+ }): Promise<Connection[]> {
422
+ log.debugRequest("connections.list", { userId: options?.userId });
345
423
 
346
- const response = await this.client.connections.list();
424
+ const response = await this.client.connections.list(
425
+ this.requestOptions(options?.userId),
426
+ );
347
427
 
348
428
  const connections: Connection[] = response.connections.map((conn) => ({
349
429
  id: conn.id,
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
- _cfg: HyperspellConfig,
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
- log.debug(`/getcontext command: "${query}"`)
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
- log.debug(`/remember command: "${truncate(text, 50)}"`)
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,6 +25,18 @@ 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;
@@ -38,6 +50,7 @@ export type HyperspellConfig = {
38
50
  relevanceThreshold: number;
39
51
  debug: boolean;
40
52
  knowledgeGraph: KnowledgeGraphConfig;
53
+ multiUser?: MultiUserConfig;
41
54
  };
42
55
 
43
56
  const ALLOWED_KEYS = [
@@ -53,6 +66,7 @@ const ALLOWED_KEYS = [
53
66
  "relevanceThreshold",
54
67
  "debug",
55
68
  "knowledgeGraph",
69
+ "multiUser",
56
70
  ];
57
71
 
58
72
  const VALID_SOURCES: HyperspellSource[] = [
@@ -135,6 +149,40 @@ function parseSources(raw: string | string[] | undefined): HyperspellSource[] {
135
149
  return sources;
136
150
  }
137
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
+
138
186
  export function parseConfig(raw: unknown): HyperspellConfig {
139
187
  const cfg =
140
188
  raw && typeof raw === "object" && !Array.isArray(raw)
@@ -184,6 +232,7 @@ export function parseConfig(raw: unknown): HyperspellConfig {
184
232
  scanIntervalMinutes: (kgRaw.scanIntervalMinutes as number) ?? 60,
185
233
  batchSize: (kgRaw.batchSize as number) ?? 20,
186
234
  },
235
+ multiUser: parseMultiUser(cfg.multiUser),
187
236
  };
188
237
  }
189
238
 
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
- for await (const mem of client.listMemories()) {
100
- if (stateManager.isProcessed(mem.resourceId)) continue
101
- if (mem.metadata?.graph_entity === "true" || mem.metadata?.graph_entity === true) continue
102
- if ((mem.metadata?.status as string) !== "completed") continue
103
-
104
- let summary = ""
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
- unprocessed.push({
121
- resourceId: mem.resourceId,
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 }],