@hyperspell/openclaw-hyperspell 0.12.0 → 0.13.1

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.
Files changed (50) hide show
  1. package/README.md +37 -1
  2. package/dist/client.js +341 -0
  3. package/dist/commands/setup.js +568 -0
  4. package/dist/commands/slash.js +159 -0
  5. package/dist/config.js +349 -0
  6. package/{graph/cron.ts → dist/graph/cron.js} +11 -15
  7. package/dist/graph/index.js +4 -0
  8. package/dist/graph/ops.js +221 -0
  9. package/dist/graph/state.js +68 -0
  10. package/dist/graph/tools.js +87 -0
  11. package/dist/hooks/auto-context.js +199 -0
  12. package/dist/hooks/auto-trace.js +143 -0
  13. package/dist/hooks/emotional-state.js +155 -0
  14. package/dist/hooks/memory-sync.js +54 -0
  15. package/dist/hooks/startup-orientation.js +236 -0
  16. package/dist/index.js +132 -0
  17. package/dist/lib/browser.js +29 -0
  18. package/dist/lib/sender.js +173 -0
  19. package/dist/lib/voice-id.js +22 -0
  20. package/dist/logger.js +32 -0
  21. package/dist/sync/markdown.js +151 -0
  22. package/dist/tools/remember.js +97 -0
  23. package/dist/tools/search.js +87 -0
  24. package/openclaw.plugin.json +25 -1
  25. package/package.json +7 -12
  26. package/client.ts +0 -566
  27. package/commands/setup.ts +0 -673
  28. package/commands/slash.ts +0 -198
  29. package/config.test.ts +0 -202
  30. package/config.ts +0 -497
  31. package/graph/index.ts +0 -5
  32. package/graph/ops.ts +0 -259
  33. package/graph/state.ts +0 -79
  34. package/graph/tools.ts +0 -117
  35. package/hooks/auto-context.ts +0 -272
  36. package/hooks/auto-trace.test.ts +0 -81
  37. package/hooks/auto-trace.ts +0 -197
  38. package/hooks/emotional-state.test.ts +0 -160
  39. package/hooks/emotional-state.ts +0 -179
  40. package/hooks/memory-sync.ts +0 -65
  41. package/index.ts +0 -152
  42. package/lib/browser.ts +0 -31
  43. package/lib/sender.test.ts +0 -234
  44. package/lib/sender.ts +0 -234
  45. package/lib/voice-id.ts +0 -39
  46. package/logger.ts +0 -41
  47. package/sync/markdown.ts +0 -186
  48. package/tools/remember.ts +0 -132
  49. package/tools/search.ts +0 -131
  50. package/types/openclaw.d.ts +0 -76
package/README.md CHANGED
@@ -35,6 +35,9 @@ Add to your `openclaw.json`:
35
35
  ```json
36
36
  {
37
37
  "plugins": {
38
+ "slots": {
39
+ "memory": "openclaw-hyperspell"
40
+ },
38
41
  "entries": {
39
42
  "openclaw-hyperspell": {
40
43
  "enabled": true,
@@ -43,8 +46,14 @@ Add to your `openclaw.json`:
43
46
  "userId": "your-email",
44
47
  "autoContext": true,
45
48
  "syncMemories": true,
46
- "sources": "vault,notion,slack"
49
+ "sources": "vault,notion,slack",
50
+ "dreaming": {
51
+ "enabled": true
52
+ }
47
53
  }
54
+ },
55
+ "memory-core": {
56
+ "enabled": true
48
57
  }
49
58
  }
50
59
  }
@@ -57,6 +66,28 @@ Set the environment variable in `~/.openclaw/.env`:
57
66
  HYPERSPELL_API_KEY=hs_...
58
67
  ```
59
68
 
69
+ ### Running alongside Dreaming
70
+
71
+ OpenClaw's memory slot (`plugins.slots.memory`) is exclusive, but the `memory-core` dreaming engine is an explicit exception: it can sidecar-load alongside the slot owner so Dreaming keeps running while Hyperspell handles remote search and storage.
72
+
73
+ To enable this, all four settings must be true at once:
74
+
75
+ | Setting | Value |
76
+ |---|---|
77
+ | `plugins.slots.memory` | `openclaw-hyperspell` |
78
+ | `plugins.entries.openclaw-hyperspell.enabled` | `true` |
79
+ | `plugins.entries.openclaw-hyperspell.config.dreaming.enabled` | `true` |
80
+ | `plugins.entries.memory-core.enabled` | `true` |
81
+
82
+ The `dreaming.enabled: true` flag on the Hyperspell plugin's config is what tells OpenClaw to load `memory-core` as a sidecar (see `resolveDreamingSidecarEngineId` in OpenClaw's plugin loader). Without it, `memory-core` is treated as a normal memory plugin and the slot policy disables it.
83
+
84
+ **Two distinct memory tracks:**
85
+
86
+ - **Hyperspell** — remote, cross-source (Slack, Notion, Drive, Gmail, vault, ...). Runs on every agent turn via `autoContext` and `autoTrace`. Owns the memory slot.
87
+ - **Dreaming** — local, consolidates daily notes and session transcripts into `workspace/MEMORY.md` on cron (light every 6h, deep nightly, REM weekly). Does not read Hyperspell's remote memories. Reads only local files the agent runner writes.
88
+
89
+ The two systems don't share state at runtime. They can be enabled independently.
90
+
60
91
  ## CLI Commands
61
92
 
62
93
  ### `openclaw openclaw-hyperspell setup`
@@ -82,6 +113,7 @@ Open the Hyperspell connect page to link your accounts (Notion, Slack, Google Dr
82
113
  | `sources` | string | - | Comma-separated sources to search (e.g., `vault,notion,slack`) |
83
114
  | `maxResults` | number | `10` | Maximum memories per context injection |
84
115
  | `debug` | boolean | `false` | Enable verbose logging |
116
+ | `dreaming.enabled` | boolean | `false` | Allow `memory-core` to sidecar-load so Dreaming can consolidate local session transcripts into `workspace/MEMORY.md`. See [Running alongside Dreaming](#running-alongside-dreaming). |
85
117
 
86
118
  ## Slash Commands
87
119
 
@@ -212,6 +244,10 @@ Enable the graph tools by using `hyperspell_network_scan`, `hyperspell_network_w
212
244
  - Enable `debug: true` to see what queries are being made
213
245
  - Check that you have memories matching your conversation topics
214
246
 
247
+ ### Enabling `memory-core` disabled Hyperspell
248
+ - This means `memory-core` was placed in `plugins.slots.memory`. The slot should stay on `openclaw-hyperspell`; `memory-core` rides alongside as a sidecar.
249
+ - Restore: set `plugins.slots.memory = "openclaw-hyperspell"`, set both plugins `enabled: true`, and add `dreaming: { enabled: true }` to the Hyperspell config. See [Running alongside Dreaming](#running-alongside-dreaming).
250
+
215
251
  ## Contributing
216
252
 
217
253
  See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
package/dist/client.js ADDED
@@ -0,0 +1,341 @@
1
+ import Hyperspell from "hyperspell";
2
+ import { normalizeScope } from "./config.js";
3
+ import { log } from "./logger.js";
4
+ const API_BASE_URL = "https://api.hyperspell.com";
5
+ export class HyperspellClient {
6
+ client;
7
+ config;
8
+ constructor(config) {
9
+ this.config = config;
10
+ this.client = new Hyperspell({
11
+ apiKey: config.apiKey,
12
+ userID: config.userId,
13
+ });
14
+ log.info(`client initialized${config.userId ? ` for user ${config.userId}` : ""}`);
15
+ }
16
+ rawHeaders() {
17
+ const headers = {
18
+ "Content-Type": "application/json",
19
+ Authorization: `Bearer ${this.config.apiKey}`,
20
+ };
21
+ if (this.config.userId) {
22
+ headers["X-As-User"] = this.config.userId;
23
+ }
24
+ return headers;
25
+ }
26
+ requestOptions(userId) {
27
+ if (!userId)
28
+ return undefined;
29
+ return { headers: { "X-As-User": userId } };
30
+ }
31
+ async search(query, options) {
32
+ const limit = options?.limit ?? this.config.maxResults;
33
+ const sources = options?.sources ??
34
+ (this.config.sources.length > 0 ? this.config.sources : undefined);
35
+ log.debugRequest("memories.search", {
36
+ query,
37
+ limit,
38
+ sources,
39
+ after: options?.after,
40
+ before: options?.before,
41
+ userId: options?.userId,
42
+ filter: options?.filter,
43
+ });
44
+ const response = await this.client.memories.search({
45
+ query,
46
+ sources,
47
+ options: {
48
+ max_results: limit,
49
+ ...(options?.after ? { after: options.after } : {}),
50
+ ...(options?.before ? { before: options.before } : {}),
51
+ ...(options?.filter ? { filter: options.filter } : {}),
52
+ },
53
+ }, this.requestOptions(options?.userId));
54
+ const results = response.documents.map((doc) => {
55
+ const raw = doc;
56
+ return {
57
+ resourceId: doc.resource_id,
58
+ title: doc.title ?? null,
59
+ source: doc.source,
60
+ score: doc.score ?? null,
61
+ url: doc.metadata?.url ?? null,
62
+ createdAt: doc.metadata?.created_at ?? null,
63
+ highlights: (raw.highlights ?? []).map((h) => ({
64
+ id: h.id,
65
+ score: h.score,
66
+ text: h.text,
67
+ })),
68
+ };
69
+ });
70
+ log.debugResponse("memories.search", { count: results.length });
71
+ return results;
72
+ }
73
+ async searchRaw(query, options) {
74
+ const limit = options?.limit ?? this.config.maxResults;
75
+ const sources = options?.sources ??
76
+ (this.config.sources.length > 0 ? this.config.sources : undefined);
77
+ log.debugRequest("memories.search (raw)", {
78
+ query,
79
+ limit,
80
+ sources,
81
+ after: options?.after,
82
+ before: options?.before,
83
+ userId: options?.userId,
84
+ filter: options?.filter,
85
+ });
86
+ const response = await this.client.memories.search({
87
+ query,
88
+ sources,
89
+ options: {
90
+ max_results: limit,
91
+ ...(options?.after ? { after: options.after } : {}),
92
+ ...(options?.before ? { before: options.before } : {}),
93
+ ...(options?.filter ? { filter: options.filter } : {}),
94
+ },
95
+ }, this.requestOptions(options?.userId));
96
+ log.debugResponse("memories.search (raw)", {
97
+ count: response.documents.length,
98
+ });
99
+ return response;
100
+ }
101
+ async searchWithAnswer(query, options) {
102
+ const limit = options?.limit ?? this.config.maxResults;
103
+ const sources = options?.sources ??
104
+ (this.config.sources.length > 0 ? this.config.sources : undefined);
105
+ log.debugRequest("memories.search (with answer)", {
106
+ query,
107
+ limit,
108
+ sources,
109
+ userId: options?.userId,
110
+ filter: options?.filter,
111
+ });
112
+ const response = await this.client.memories.search({
113
+ query,
114
+ sources,
115
+ answer: true,
116
+ options: {
117
+ max_results: limit,
118
+ ...(options?.filter ? { filter: options.filter } : {}),
119
+ },
120
+ }, this.requestOptions(options?.userId));
121
+ const documents = response.documents.map((doc) => ({
122
+ resourceId: doc.resource_id,
123
+ title: doc.title ?? null,
124
+ source: doc.source,
125
+ score: doc.score ?? null,
126
+ url: doc.metadata?.url ?? null,
127
+ createdAt: doc.metadata?.created_at ?? null,
128
+ highlights: [],
129
+ }));
130
+ log.debugResponse("memories.search (with answer)", {
131
+ count: documents.length,
132
+ hasAnswer: !!response.answer,
133
+ });
134
+ return {
135
+ answer: response.answer ?? null,
136
+ documents,
137
+ };
138
+ }
139
+ async addMemory(text, options) {
140
+ log.debugRequest("memories.add", {
141
+ textLength: text.length,
142
+ title: options?.title,
143
+ resourceId: options?.resourceId,
144
+ collection: options?.collection,
145
+ date: options?.date,
146
+ userId: options?.userId,
147
+ scope: options?.scope,
148
+ });
149
+ const result = await this.client.memories.add({
150
+ text,
151
+ title: options?.title,
152
+ resource_id: options?.resourceId,
153
+ collection: options?.collection,
154
+ date: options?.date,
155
+ metadata: {
156
+ openclaw_source: "command",
157
+ ...options?.metadata,
158
+ ...(options?.userId ? { openclaw_user: options.userId } : {}),
159
+ ...(options?.scope
160
+ ? { openclaw_scope: normalizeScope(options.scope) }
161
+ : {}),
162
+ },
163
+ }, this.requestOptions(options?.userId));
164
+ log.debugResponse("memories.add", { resourceId: result.resource_id });
165
+ return { resourceId: result.resource_id };
166
+ }
167
+ async listIntegrations() {
168
+ log.debugRequest("integrations.list", {});
169
+ const response = await this.client.integrations.list();
170
+ const integrations = response.integrations.map((int) => ({
171
+ id: int.id,
172
+ name: int.name,
173
+ provider: int.provider,
174
+ icon: int.icon,
175
+ }));
176
+ log.debugResponse("integrations.list", { count: integrations.length });
177
+ return integrations;
178
+ }
179
+ async getConnectUrl(integrationId, options) {
180
+ log.debugRequest("integrations.connect", { integrationId });
181
+ const response = await this.client.integrations.connect(integrationId, undefined, this.requestOptions(options?.userId));
182
+ log.debugResponse("integrations.connect", { url: response.url });
183
+ return {
184
+ url: response.url,
185
+ expiresAt: response.expires_at,
186
+ };
187
+ }
188
+ async *listMemories(options) {
189
+ log.debugRequest("memories.list", {
190
+ source: options?.source,
191
+ collection: options?.collection,
192
+ userId: options?.userId,
193
+ });
194
+ const params = {
195
+ size: options?.pageSize ?? 100,
196
+ };
197
+ if (options?.source)
198
+ params.source = options.source;
199
+ if (options?.collection)
200
+ params.collection = options.collection;
201
+ for await (const memory of this.client.memories.list(params, this.requestOptions(options?.userId))) {
202
+ yield {
203
+ resourceId: memory.resource_id,
204
+ source: memory.source,
205
+ title: memory.title ?? null,
206
+ metadata: (memory.metadata ?? {}),
207
+ };
208
+ }
209
+ }
210
+ async getMemory(resourceId, source, options) {
211
+ log.debugRequest("memories.get", { resourceId, source });
212
+ const response = await this.client.memories.get(resourceId, { source }, this.requestOptions(options?.userId));
213
+ const raw = response;
214
+ log.debugResponse("memories.get", { resourceId, hasData: "data" in raw });
215
+ return raw;
216
+ }
217
+ async sendTrace(history, options) {
218
+ log.debugRequest("sessions.add", {
219
+ historyLength: history.length,
220
+ sessionId: options?.sessionId,
221
+ extract: options?.extract,
222
+ userId: options?.userId,
223
+ scope: options?.scope,
224
+ });
225
+ const result = await this.client.sessions.add({
226
+ history,
227
+ session_id: options?.sessionId,
228
+ title: options?.title,
229
+ format: "openclaw",
230
+ // Cast: SDK 0.35 typing accepts only ["procedure" | "memory"], but the
231
+ // backend's mood extractor (hyperspell/hyperspell#581) accepts "mood".
232
+ // Remove this cast once the OpenAPI spec is updated.
233
+ extract: (options?.extract ?? ["procedure"]),
234
+ metadata: {
235
+ openclaw_source: "agent_end",
236
+ ...options?.metadata,
237
+ ...(options?.userId ? { openclaw_user: options.userId } : {}),
238
+ ...(options?.scope
239
+ ? { openclaw_scope: normalizeScope(options.scope) }
240
+ : {}),
241
+ },
242
+ }, this.requestOptions(options?.userId));
243
+ log.debugResponse("sessions.add", {
244
+ resourceId: result.resource_id,
245
+ status: result.status,
246
+ });
247
+ return { resourceId: result.resource_id, status: result.status };
248
+ }
249
+ async listConnections(options) {
250
+ log.debugRequest("connections.list", { userId: options?.userId });
251
+ const response = await this.client.connections.list(this.requestOptions(options?.userId));
252
+ const connections = response.connections.map((conn) => ({
253
+ id: conn.id,
254
+ integrationId: conn.integration_id,
255
+ label: conn.label,
256
+ provider: conn.provider,
257
+ }));
258
+ log.debugResponse("connections.list", { count: connections.length });
259
+ return connections;
260
+ }
261
+ // -- Emotional State (raw fetch -- not in public SDK) -----------------------
262
+ async storeEmotionalState(conversation, options) {
263
+ log.debugRequest("emotional-state.store", {
264
+ conversationLength: conversation.length,
265
+ relationshipId: options?.relationshipId,
266
+ });
267
+ const body = { conversation };
268
+ if (options?.sessionId)
269
+ body.session_id = options.sessionId;
270
+ if (options?.relationshipId)
271
+ body.relationship_id = options.relationshipId;
272
+ if (options?.metadata)
273
+ body.metadata = options.metadata;
274
+ const res = await fetch(`${API_BASE_URL}/emotional-state`, {
275
+ method: "POST",
276
+ headers: this.rawHeaders(),
277
+ body: JSON.stringify(body),
278
+ });
279
+ if (!res.ok) {
280
+ const text = await res.text().catch(() => "");
281
+ throw new Error(`POST /emotional-state failed (${res.status}): ${text}`);
282
+ }
283
+ const data = await res.json();
284
+ const result = {
285
+ resourceId: data.resource_id,
286
+ summary: data.summary,
287
+ extractedAt: data.extracted_at,
288
+ sessionId: data.session_id ?? null,
289
+ relationshipId: data.relationship_id ?? null,
290
+ status: data.status,
291
+ };
292
+ log.debugResponse("emotional-state.store", { resourceId: result.resourceId });
293
+ return result;
294
+ }
295
+ async getEmotionalState(relationshipId) {
296
+ log.debugRequest("emotional-state.get", { relationshipId });
297
+ const url = new URL(`${API_BASE_URL}/emotional-state`);
298
+ if (relationshipId)
299
+ url.searchParams.set("relationship_id", relationshipId);
300
+ const res = await fetch(url.toString(), {
301
+ method: "GET",
302
+ headers: this.rawHeaders(),
303
+ });
304
+ if (!res.ok) {
305
+ const text = await res.text().catch(() => "");
306
+ throw new Error(`GET /emotional-state failed (${res.status}): ${text}`);
307
+ }
308
+ const data = await res.json();
309
+ if (data === null) {
310
+ log.debugResponse("emotional-state.get", { found: false });
311
+ return null;
312
+ }
313
+ const result = {
314
+ resourceId: data.resource_id,
315
+ summary: data.summary,
316
+ extractedAt: data.extracted_at,
317
+ sessionId: data.session_id ?? null,
318
+ relationshipId: data.relationship_id ?? null,
319
+ };
320
+ log.debugResponse("emotional-state.get", { found: true, resourceId: result.resourceId });
321
+ return result;
322
+ }
323
+ async deleteEmotionalState(relationshipId) {
324
+ log.debugRequest("emotional-state.delete", { relationshipId });
325
+ const url = new URL(`${API_BASE_URL}/emotional-state`);
326
+ if (relationshipId)
327
+ url.searchParams.set("relationship_id", relationshipId);
328
+ const res = await fetch(url.toString(), {
329
+ method: "DELETE",
330
+ headers: this.rawHeaders(),
331
+ });
332
+ if (!res.ok) {
333
+ const text = await res.text().catch(() => "");
334
+ throw new Error(`DELETE /emotional-state failed (${res.status}): ${text}`);
335
+ }
336
+ const data = await res.json();
337
+ const result = { deletedCount: data.deleted_count };
338
+ log.debugResponse("emotional-state.delete", result);
339
+ return result;
340
+ }
341
+ }