@hyperspell/openclaw-hyperspell 0.4.2 → 0.7.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.
package/README.md CHANGED
@@ -149,11 +149,135 @@ This ensures the AI always has access to relevant information from your connecte
149
149
 
150
150
  ## Available Sources
151
151
 
152
+ ### Documents & Storage
152
153
  - `vault` - User-created or synced memories
153
154
  - `notion` - Notion pages and databases
154
- - `slack` - Slack messages
155
- - `google_calendar` - Google Calendar events
156
- - `google_mail` - Gmail messages
157
155
  - `google_drive` - Google Drive files
158
156
  - `box` - Box files
157
+ - `dropbox` - Dropbox files
158
+ - `onedrive` - Microsoft OneDrive files
159
+
160
+ ### Communication
161
+ - `slack` - Slack messages
162
+ - `google_mail` - Gmail messages
163
+
164
+ ### Calendars & Meetings
165
+ - `google_calendar` - Google Calendar events
166
+ - `zoom` - Zoom meeting recordings and transcripts
167
+ - `fathom` - Fathom meeting recordings
168
+ - `fireflies` - Fireflies.ai meeting transcripts
169
+
170
+ ### Project Management
171
+ - `linear` - Linear issues and comments
172
+
173
+ ### CRM
174
+ - `hubspot` - HubSpot contacts, companies, and deals
175
+ - `attio` - Attio CRM contacts, companies, and deals
176
+
177
+ ### Developer Tools
178
+ - `github` - GitHub repositories and commits
179
+
180
+ ### Other
159
181
  - `web_crawler` - Crawled web pages
182
+
183
+ ## Knowledge Graph
184
+
185
+ The plugin can automatically build a local knowledge graph from your memories:
186
+
187
+ 1. **Scan** memories for entities (people, organizations, projects, topics)
188
+ 2. **Extract** structured information and relationships
189
+ 3. **Write** entity files to `memory/people/`, `memory/organizations/`, etc.
190
+ 4. **Link** entities via markdown relationship references
191
+
192
+ Enable the graph tools by using `hyperspell_network_scan`, `hyperspell_network_write`, and `hyperspell_network_complete` in your agent workflows.
193
+
194
+ ---
195
+
196
+ ## SommeliAgent 🍷
197
+
198
+ > *"Your playlist says more about your palate than you'd like to admit, darling."*
199
+ > — A Linea, Hyperspell's hidden sommelier
200
+
201
+ An opinionated AI sommelier living inside your memory plugin. She reads your Spotify listening habits, judges them (affectionately), and recommends wines that match the person your music says you are — not the person you think you are.
202
+
203
+ 215 wines. 26 countries. Zero tolerance for boring recommendations.
204
+
205
+ **Requires:** `uv` ([install](https://docs.astral.sh/uv/))
206
+
207
+ ### Setup
208
+
209
+ 1. Create a Spotify app at https://developer.spotify.com/dashboard (redirect URI: `http://localhost:8888/callback`)
210
+ 2. Set environment variables: `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET`
211
+ 3. Authenticate: `/wine-auth`
212
+
213
+ Or skip all that and try demo mode: `/wine demo`
214
+
215
+ ### Slash Commands
216
+
217
+ #### `/wine [options]`
218
+
219
+ Get wine recommendations. Options can be combined:
220
+
221
+ ```
222
+ /wine — recommendations from your Spotify
223
+ /wine demo — use demo profile (no Spotify needed)
224
+ /wine red premium — only red wines, premium price range
225
+ /wine white 5 — 5 white wine recommendations
226
+ /wine demo profile — show full music-to-wine mapping
227
+ ```
228
+
229
+ #### `/wine-auth`
230
+
231
+ Connect your Spotify account.
232
+
233
+ #### `/wine-rate <wine-id> <1-5> [notes]`
234
+
235
+ Rate a recommendation to improve future suggestions.
236
+
237
+ ```
238
+ /wine-rate red-it-001 5 "Incredible tannins, paired perfectly with my Radiohead phase"
239
+ ```
240
+
241
+ #### `/wine-history`
242
+
243
+ View your rating history and derived taste preferences.
244
+
245
+ ### AI Tools
246
+
247
+ The plugin also registers tools the AI can use autonomously:
248
+
249
+ - **hyperspell_sommelier** - Get wine recommendations (with full personality instructions)
250
+ - **hyperspell_sommelier_rate** - Rate wines
251
+
252
+ ### How It Works
253
+
254
+ Your Spotify audio features (energy, valence, complexity, acousticness, tempo) are mapped to wine dimensions (body, sweetness, tannin, acidity, complexity, fruitiness, earthiness, spiciness). The cross-domain mapping is entertainment-first — the comedy comes from a sharp, opinionated sommelier voice diagnosing your personality through your questionable music taste.
255
+
256
+ The sommelier is A Linea — she lives inside Hyperspell and has strong opinions about both your playlist and your palate. She chose Alexander McQueen as her fashion house, so expect the wine recommendations to have a similar aesthetic: beautiful, a little dark, and never boring.
257
+
258
+ See `sommeliagent/references/cross-domain-mappings.md` for the full methodology. 🖤
259
+
260
+ ## Troubleshooting
261
+
262
+ ### "No relevant memories found"
263
+ - Check that your sources are connected: `openclaw openclaw-hyperspell status`
264
+ - Verify your API key is set: `echo $HYPERSPELL_API_KEY`
265
+ - Make sure content has been indexed (initial sync can take a few minutes)
266
+
267
+ ### Memory sync not working
268
+ - Ensure `syncMemories: true` in your config
269
+ - Check that markdown files are in `~/.openclaw/workspace/memory/`
270
+ - Run `/sync` manually to trigger a sync and see any errors
271
+
272
+ ### Auto-context not injecting
273
+ - Verify `autoContext: true` in your config
274
+ - Enable `debug: true` to see what queries are being made
275
+ - Check that you have memories matching your conversation topics
276
+
277
+ ## Contributing
278
+
279
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
280
+
281
+ ## License
282
+
283
+ MIT
package/client.ts CHANGED
@@ -1,213 +1,322 @@
1
- import Hyperspell from "hyperspell"
2
- import type { HyperspellConfig, HyperspellSource } from "./config.ts"
3
- import { log } from "./logger.ts"
1
+ import Hyperspell from "hyperspell";
2
+ import type { HyperspellConfig, HyperspellSource } from "./config.ts";
3
+ import { log } from "./logger.ts";
4
+
5
+ export type Highlight = {
6
+ id: string;
7
+ score: number;
8
+ text: string;
9
+ };
4
10
 
5
11
  export type SearchResult = {
6
- resourceId: string
7
- title: string | null
8
- source: HyperspellSource
9
- score: number | null
10
- url: string | null
11
- createdAt: string | null
12
- }
12
+ resourceId: string;
13
+ title: string | null;
14
+ source: HyperspellSource;
15
+ score: number | null;
16
+ url: string | null;
17
+ createdAt: string | null;
18
+ highlights: Highlight[];
19
+ };
13
20
 
14
21
  export type SearchWithAnswerResult = {
15
- answer: string | null
16
- documents: SearchResult[]
17
- }
22
+ answer: string | null;
23
+ documents: SearchResult[];
24
+ };
18
25
 
19
26
  export type Integration = {
20
- id: string
21
- name: string
22
- provider: HyperspellSource
23
- icon: string
24
- }
27
+ id: string;
28
+ name: string;
29
+ provider: HyperspellSource;
30
+ icon: string;
31
+ };
25
32
 
26
33
  export type Connection = {
27
- id: string
28
- integrationId: string
29
- label: string | null
30
- provider: HyperspellSource
31
- }
34
+ id: string;
35
+ integrationId: string;
36
+ label: string | null;
37
+ provider: HyperspellSource;
38
+ };
32
39
 
33
40
  export class HyperspellClient {
34
- private client: Hyperspell
35
- private config: HyperspellConfig
36
-
37
- constructor(config: HyperspellConfig) {
38
- this.config = config
39
- this.client = new Hyperspell({
40
- apiKey: config.apiKey,
41
- userID: config.userId,
42
- })
43
- log.info(`client initialized${config.userId ? ` for user ${config.userId}` : ""}`)
44
- }
45
-
46
- async search(
47
- query: string,
48
- options?: { limit?: number; sources?: HyperspellSource[] },
49
- ): Promise<SearchResult[]> {
50
- const limit = options?.limit ?? this.config.maxResults
51
- const sources =
52
- options?.sources ?? (this.config.sources.length > 0 ? this.config.sources : undefined)
53
-
54
- log.debugRequest("memories.search", { query, limit, sources })
55
-
56
- const response = await this.client.memories.search({
57
- query,
58
- sources,
59
- options: {
60
- max_results: limit,
61
- },
62
- })
63
-
64
- const results: SearchResult[] = response.documents.map((doc) => ({
65
- resourceId: doc.resource_id,
66
- title: doc.title ?? null,
67
- source: doc.source as HyperspellSource,
68
- score: doc.score ?? null,
69
- url: doc.metadata?.url as string | null ?? null,
70
- createdAt: doc.metadata?.created_at as string | null ?? null,
71
- }))
72
-
73
- log.debugResponse("memories.search", { count: results.length })
74
- return results
75
- }
76
-
77
- async searchRaw(
78
- query: string,
79
- options?: { limit?: number; sources?: HyperspellSource[] },
80
- ): Promise<Record<string, unknown>> {
81
- const limit = options?.limit ?? this.config.maxResults
82
- const sources =
83
- options?.sources ?? (this.config.sources.length > 0 ? this.config.sources : undefined)
84
-
85
- log.debugRequest("memories.search (raw)", { query, limit, sources })
86
-
87
- const response = await this.client.memories.search({
88
- query,
89
- sources,
90
- options: {
91
- max_results: limit,
92
- },
93
- })
94
-
95
- log.debugResponse("memories.search (raw)", { count: response.documents.length })
96
-
97
- return response as unknown as Record<string, unknown>
98
- }
99
-
100
- async searchWithAnswer(
101
- query: string,
102
- options?: { limit?: number; sources?: HyperspellSource[] },
103
- ): Promise<SearchWithAnswerResult> {
104
- const limit = options?.limit ?? this.config.maxResults
105
- const sources =
106
- options?.sources ?? (this.config.sources.length > 0 ? this.config.sources : undefined)
107
-
108
- log.debugRequest("memories.search (with answer)", { query, limit, sources })
109
-
110
- const response = await this.client.memories.search({
111
- query,
112
- sources,
113
- answer: true,
114
- options: {
115
- max_results: limit,
116
- },
117
- })
118
-
119
- const documents: SearchResult[] = response.documents.map((doc) => ({
120
- resourceId: doc.resource_id,
121
- title: doc.title ?? null,
122
- source: doc.source as HyperspellSource,
123
- score: doc.score ?? null,
124
- url: doc.metadata?.url as string | null ?? null,
125
- createdAt: doc.metadata?.created_at as string | null ?? null,
126
- }))
127
-
128
- log.debugResponse("memories.search (with answer)", {
129
- count: documents.length,
130
- hasAnswer: !!response.answer,
131
- })
132
-
133
- return {
134
- answer: response.answer ?? null,
135
- documents,
136
- }
137
- }
138
-
139
- async addMemory(
140
- text: string,
141
- options?: {
142
- title?: string
143
- resourceId?: string
144
- collection?: string
145
- metadata?: Record<string, string | number | boolean>
146
- },
147
- ): Promise<{ resourceId: string }> {
148
- log.debugRequest("memories.add", {
149
- textLength: text.length,
150
- title: options?.title,
151
- resourceId: options?.resourceId,
152
- collection: options?.collection,
153
- })
154
-
155
- const result = await this.client.memories.add({
156
- text,
157
- title: options?.title,
158
- resource_id: options?.resourceId,
159
- collection: options?.collection,
160
- metadata: {
161
- ...options?.metadata,
162
- openclaw_source: "command",
163
- },
164
- })
165
-
166
- log.debugResponse("memories.add", { resourceId: result.resource_id })
167
- return { resourceId: result.resource_id }
168
- }
169
-
170
- async listIntegrations(): Promise<Integration[]> {
171
- log.debugRequest("integrations.list", {})
172
-
173
- const response = await this.client.integrations.list()
174
-
175
- const integrations: Integration[] = response.integrations.map((int) => ({
176
- id: int.id,
177
- name: int.name,
178
- provider: int.provider as HyperspellSource,
179
- icon: int.icon,
180
- }))
181
-
182
- log.debugResponse("integrations.list", { count: integrations.length })
183
- return integrations
184
- }
185
-
186
- async getConnectUrl(integrationId: string): Promise<{ url: string; expiresAt: string }> {
187
- log.debugRequest("integrations.connect", { integrationId })
188
-
189
- const response = await this.client.integrations.connect(integrationId)
190
-
191
- log.debugResponse("integrations.connect", { url: response.url })
192
- return {
193
- url: response.url,
194
- expiresAt: response.expires_at,
195
- }
196
- }
197
-
198
- async listConnections(): Promise<Connection[]> {
199
- log.debugRequest("connections.list", {})
200
-
201
- const response = await this.client.connections.list()
202
-
203
- const connections: Connection[] = response.connections.map((conn) => ({
204
- id: conn.id,
205
- integrationId: conn.integration_id,
206
- label: conn.label,
207
- provider: conn.provider as HyperspellSource,
208
- }))
209
-
210
- log.debugResponse("connections.list", { count: connections.length })
211
- return connections
212
- }
41
+ private client: Hyperspell;
42
+ private config: HyperspellConfig;
43
+
44
+ constructor(config: HyperspellConfig) {
45
+ this.config = config;
46
+ this.client = new Hyperspell({
47
+ apiKey: config.apiKey,
48
+ userID: config.userId,
49
+ });
50
+ log.info(
51
+ `client initialized${config.userId ? ` for user ${config.userId}` : ""}`,
52
+ );
53
+ }
54
+
55
+ async search(
56
+ query: string,
57
+ options?: { limit?: number; sources?: HyperspellSource[]; after?: string; before?: string },
58
+ ): Promise<SearchResult[]> {
59
+ const limit = options?.limit ?? this.config.maxResults;
60
+ const sources =
61
+ options?.sources ??
62
+ (this.config.sources.length > 0 ? this.config.sources : undefined);
63
+
64
+ log.debugRequest("memories.search", { query, limit, sources, after: options?.after, before: options?.before });
65
+
66
+ const response = await this.client.memories.search({
67
+ query,
68
+ sources,
69
+ options: {
70
+ max_results: limit,
71
+ ...(options?.after ? { after: options.after } : {}),
72
+ ...(options?.before ? { before: options.before } : {}),
73
+ },
74
+ });
75
+
76
+ const results: SearchResult[] = response.documents.map((doc) => {
77
+ const raw = doc as typeof doc & { highlights?: Array<{ id: string; score: number; text: string }> };
78
+ return {
79
+ resourceId: doc.resource_id,
80
+ title: doc.title ?? null,
81
+ source: doc.source as HyperspellSource,
82
+ score: doc.score ?? null,
83
+ url: (doc.metadata?.url as string | null) ?? null,
84
+ createdAt: (doc.metadata?.created_at as string | null) ?? null,
85
+ highlights: (raw.highlights ?? []).map((h) => ({ id: h.id, score: h.score, text: h.text })),
86
+ };
87
+ });
88
+
89
+ log.debugResponse("memories.search", { count: results.length });
90
+ return results;
91
+ }
92
+
93
+ async searchRaw(
94
+ query: string,
95
+ options?: { limit?: number; sources?: HyperspellSource[]; after?: string; before?: string },
96
+ ): Promise<Record<string, unknown>> {
97
+ const limit = options?.limit ?? this.config.maxResults;
98
+ const sources =
99
+ options?.sources ??
100
+ (this.config.sources.length > 0 ? this.config.sources : undefined);
101
+
102
+ log.debugRequest("memories.search (raw)", { query, limit, sources, after: options?.after, before: options?.before });
103
+
104
+ const response = await this.client.memories.search({
105
+ query,
106
+ sources,
107
+ options: {
108
+ max_results: limit,
109
+ ...(options?.after ? { after: options.after } : {}),
110
+ ...(options?.before ? { before: options.before } : {}),
111
+ },
112
+ });
113
+
114
+ log.debugResponse("memories.search (raw)", {
115
+ count: response.documents.length,
116
+ });
117
+
118
+ return response as unknown as Record<string, unknown>;
119
+ }
120
+
121
+ async searchWithAnswer(
122
+ query: string,
123
+ options?: { limit?: number; sources?: HyperspellSource[] },
124
+ ): Promise<SearchWithAnswerResult> {
125
+ const limit = options?.limit ?? this.config.maxResults;
126
+ const sources =
127
+ options?.sources ??
128
+ (this.config.sources.length > 0 ? this.config.sources : undefined);
129
+
130
+ log.debugRequest("memories.search (with answer)", {
131
+ query,
132
+ limit,
133
+ sources,
134
+ });
135
+
136
+ const response = await this.client.memories.search({
137
+ query,
138
+ sources,
139
+ answer: true,
140
+ options: {
141
+ max_results: limit,
142
+ },
143
+ });
144
+
145
+ const documents: SearchResult[] = response.documents.map((doc) => ({
146
+ resourceId: doc.resource_id,
147
+ title: doc.title ?? null,
148
+ source: doc.source as HyperspellSource,
149
+ score: doc.score ?? null,
150
+ url: (doc.metadata?.url as string | null) ?? null,
151
+ createdAt: (doc.metadata?.created_at as string | null) ?? null,
152
+ }));
153
+
154
+ log.debugResponse("memories.search (with answer)", {
155
+ count: documents.length,
156
+ hasAnswer: !!response.answer,
157
+ });
158
+
159
+ return {
160
+ answer: response.answer ?? null,
161
+ documents,
162
+ };
163
+ }
164
+
165
+ async addMemory(
166
+ text: string,
167
+ options?: {
168
+ title?: string;
169
+ resourceId?: string;
170
+ collection?: string;
171
+ date?: string;
172
+ metadata?: Record<string, string | number | boolean>;
173
+ },
174
+ ): Promise<{ resourceId: string }> {
175
+ log.debugRequest("memories.add", {
176
+ textLength: text.length,
177
+ title: options?.title,
178
+ resourceId: options?.resourceId,
179
+ collection: options?.collection,
180
+ date: options?.date,
181
+ });
182
+
183
+ const result = await this.client.memories.add({
184
+ text,
185
+ title: options?.title,
186
+ resource_id: options?.resourceId,
187
+ collection: options?.collection,
188
+ date: options?.date,
189
+ metadata: {
190
+ ...options?.metadata,
191
+ openclaw_source: "command",
192
+ },
193
+ });
194
+
195
+ log.debugResponse("memories.add", { resourceId: result.resource_id });
196
+ return { resourceId: result.resource_id };
197
+ }
198
+
199
+ async listIntegrations(): Promise<Integration[]> {
200
+ log.debugRequest("integrations.list", {});
201
+
202
+ const response = await this.client.integrations.list();
203
+
204
+ const integrations: Integration[] = response.integrations.map((int) => ({
205
+ id: int.id,
206
+ name: int.name,
207
+ provider: int.provider as HyperspellSource,
208
+ icon: int.icon,
209
+ }));
210
+
211
+ log.debugResponse("integrations.list", { count: integrations.length });
212
+ return integrations;
213
+ }
214
+
215
+ async getConnectUrl(
216
+ integrationId: string,
217
+ ): Promise<{ url: string; expiresAt: string }> {
218
+ log.debugRequest("integrations.connect", { integrationId });
219
+
220
+ const response = await this.client.integrations.connect(integrationId);
221
+
222
+ log.debugResponse("integrations.connect", { url: response.url });
223
+ return {
224
+ url: response.url,
225
+ expiresAt: response.expires_at,
226
+ };
227
+ }
228
+
229
+ async *listMemories(options?: {
230
+ source?: HyperspellSource;
231
+ collection?: string;
232
+ pageSize?: number;
233
+ }): AsyncGenerator<{
234
+ resourceId: string;
235
+ source: HyperspellSource;
236
+ title: string | null;
237
+ metadata: Record<string, unknown>;
238
+ }> {
239
+ log.debugRequest("memories.list", {
240
+ source: options?.source,
241
+ collection: options?.collection,
242
+ });
243
+
244
+ const params: Record<string, unknown> = {
245
+ size: options?.pageSize ?? 100,
246
+ };
247
+ if (options?.source) params.source = options.source;
248
+ if (options?.collection) params.collection = options.collection;
249
+
250
+ for await (const memory of this.client.memories.list(params as any)) {
251
+ yield {
252
+ resourceId: memory.resource_id,
253
+ source: memory.source as HyperspellSource,
254
+ title: memory.title ?? null,
255
+ metadata: (memory.metadata ?? {}) as Record<string, unknown>,
256
+ };
257
+ }
258
+ }
259
+
260
+ async getMemory(
261
+ resourceId: string,
262
+ source: HyperspellSource,
263
+ ): Promise<Record<string, unknown>> {
264
+ log.debugRequest("memories.get", { resourceId, source });
265
+
266
+ const response = await this.client.memories.get(resourceId, { source });
267
+ const raw = response as unknown as Record<string, unknown>;
268
+
269
+ log.debugResponse("memories.get", { resourceId, hasData: "data" in raw });
270
+ return raw;
271
+ }
272
+
273
+ async sendTrace(
274
+ history: string,
275
+ options?: {
276
+ sessionId?: string;
277
+ title?: string;
278
+ extract?: Array<"procedure" | "memory" | "mood">;
279
+ metadata?: Record<string, string | number | boolean>;
280
+ },
281
+ ): Promise<{ resourceId: string; status: string }> {
282
+ log.debugRequest("sessions.add", {
283
+ historyLength: history.length,
284
+ sessionId: options?.sessionId,
285
+ extract: options?.extract,
286
+ });
287
+
288
+ const result = await this.client.sessions.add({
289
+ history,
290
+ session_id: options?.sessionId,
291
+ title: options?.title,
292
+ format: "openclaw",
293
+ extract: options?.extract ?? ["procedure"],
294
+ metadata: {
295
+ ...options?.metadata,
296
+ openclaw_source: "agent_end",
297
+ },
298
+ });
299
+
300
+ log.debugResponse("sessions.add", {
301
+ resourceId: result.resource_id,
302
+ status: result.status,
303
+ });
304
+ return { resourceId: result.resource_id, status: result.status };
305
+ }
306
+
307
+ async listConnections(): Promise<Connection[]> {
308
+ log.debugRequest("connections.list", {});
309
+
310
+ const response = await this.client.connections.list();
311
+
312
+ const connections: Connection[] = response.connections.map((conn) => ({
313
+ id: conn.id,
314
+ integrationId: conn.integration_id,
315
+ label: conn.label,
316
+ provider: conn.provider as HyperspellSource,
317
+ }));
318
+
319
+ log.debugResponse("connections.list", { count: connections.length });
320
+ return connections;
321
+ }
213
322
  }