@hyperspell/openclaw-hyperspell 0.7.2 → 0.8.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
@@ -7,9 +7,11 @@ OpenClaw plugin for [Hyperspell](https://hyperspell.com) - Context and memory fo
7
7
  ## Installation
8
8
 
9
9
  ```bash
10
- openclaw plugins install @hyperspell/openclaw-hyperspell
10
+ openclaw plugins install --dangerously-force-unsafe-install @hyperspell/openclaw-hyperspell
11
11
  ```
12
12
 
13
+ > **Note:** The `--dangerously-force-unsafe-install` flag is required because OpenClaw's automated install scanner detects standard operations that are essential to how the plugin works — opening your browser during setup, scheduling background jobs, and securely communicating with the Hyperspell API. These are normal, expected behaviors and the plugin does not execute arbitrary code or access data beyond what is needed to function. The source is fully open and auditable in this repository.
14
+
13
15
  ## Quick Start
14
16
 
15
17
  Run the interactive setup wizard:
@@ -193,70 +195,6 @@ Enable the graph tools by using `hyperspell_network_scan`, `hyperspell_network_w
193
195
 
194
196
  ---
195
197
 
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
198
  ## Troubleshooting
261
199
 
262
200
  ### "No relevant memories found"
package/client.ts CHANGED
@@ -27,7 +27,7 @@ export type Integration = {
27
27
  id: string;
28
28
  name: string;
29
29
  provider: HyperspellSource;
30
- icon: string;
30
+ icon: string | null;
31
31
  };
32
32
 
33
33
  export type Connection = {
@@ -149,6 +149,7 @@ export class HyperspellClient {
149
149
  score: doc.score ?? null,
150
150
  url: (doc.metadata?.url as string | null) ?? null,
151
151
  createdAt: (doc.metadata?.created_at as string | null) ?? null,
152
+ highlights: [],
152
153
  }));
153
154
 
154
155
  log.debugResponse("memories.search (with answer)", {
@@ -290,7 +291,12 @@ export class HyperspellClient {
290
291
  session_id: options?.sessionId,
291
292
  title: options?.title,
292
293
  format: "openclaw",
293
- extract: options?.extract ?? ["procedure"],
294
+ // Cast: SDK 0.35 typing accepts only ["procedure" | "memory"], but the
295
+ // backend's mood extractor (hyperspell/hyperspell#581) accepts "mood".
296
+ // Remove this cast once the OpenAPI spec is updated.
297
+ extract: (options?.extract ?? ["procedure"]) as Array<
298
+ "procedure" | "memory"
299
+ >,
294
300
  metadata: {
295
301
  ...options?.metadata,
296
302
  openclaw_source: "agent_end",
package/commands/setup.ts CHANGED
@@ -332,6 +332,7 @@ async function runSetup(): Promise<void> {
332
332
  apiKey,
333
333
  userId,
334
334
  autoContext: true,
335
+ autoTrace: { enabled: false, extract: ["procedure"] },
335
336
  syncMemories: true,
336
337
  sources: [],
337
338
  maxResults: 10,
package/config.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export type HyperspellSource =
2
- | "collections"
3
2
  | "reddit"
4
3
  | "notion"
5
4
  | "slack"
@@ -8,7 +7,11 @@ export type HyperspellSource =
8
7
  | "box"
9
8
  | "google_drive"
10
9
  | "vault"
11
- | "web_crawler";
10
+ | "web_crawler"
11
+ | "dropbox"
12
+ | "github"
13
+ | "trace"
14
+ | "microsoft_teams";
12
15
 
13
16
  export type KnowledgeGraphConfig = {
14
17
  enabled: boolean;
@@ -49,7 +52,6 @@ const ALLOWED_KEYS = [
49
52
  ];
50
53
 
51
54
  const VALID_SOURCES: HyperspellSource[] = [
52
- "collections",
53
55
  "reddit",
54
56
  "notion",
55
57
  "slack",
@@ -59,6 +61,10 @@ const VALID_SOURCES: HyperspellSource[] = [
59
61
  "google_drive",
60
62
  "vault",
61
63
  "web_crawler",
64
+ "dropbox",
65
+ "github",
66
+ "trace",
67
+ "microsoft_teams",
62
68
  ];
63
69
 
64
70
  function assertAllowedKeys(
package/index.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
2
2
  import { HyperspellClient } from "./client.ts"
3
3
  import { registerCommands } from "./commands/slash.ts"
4
- import { registerWineCommands } from "./commands/wine.ts"
5
4
  import { registerCliCommands } from "./commands/setup.ts"
6
5
  import { parseConfig, hyperspellConfigSchema, getWorkspaceDir } from "./config.ts"
7
6
  import { buildAutoContextHandler } from "./hooks/auto-context.ts"
@@ -10,7 +9,6 @@ import { buildFileSyncHandler, syncMemoriesOnStartup } from "./hooks/memory-sync
10
9
  import { initLogger } from "./logger.ts"
11
10
  import { registerRememberTool } from "./tools/remember.ts"
12
11
  import { registerSearchTool } from "./tools/search.ts"
13
- import { registerSommelierTool, registerSommelierRateTool } from "./tools/sommelier.ts"
14
12
  import { registerNetworkTools } from "./graph/index.ts"
15
13
 
16
14
  export default {
@@ -34,11 +32,6 @@ export default {
34
32
  const rawConfig = api.pluginConfig as Record<string, unknown> | undefined;
35
33
  const hasConfig = rawConfig?.apiKey || process.env.HYPERSPELL_API_KEY;
36
34
 
37
- // SommeliAgent works independently of Hyperspell config — it just needs uv + Spotify
38
- registerSommelierTool(api)
39
- registerSommelierRateTool(api)
40
- registerWineCommands(api)
41
-
42
35
  if (!hasConfig) {
43
36
  api.logger.info(
44
37
  "hyperspell: not configured - run 'openclaw openclaw-hyperspell setup'",
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "id": "openclaw-hyperspell",
3
+ "name": "Hyperspell",
4
+ "description": "Context and memory for your AI agents — search across Notion, Slack, Google Drive, and more",
5
+ "version": "0.7.2",
3
6
  "kind": "memory",
4
7
  "uiHints": {
5
8
  "apiKey": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspell/openclaw-hyperspell",
3
- "version": "0.7.2",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "description": "OpenClaw Hyperspell memory plugin",
6
6
  "license": "MIT",
@@ -14,10 +14,11 @@
14
14
  "graph/",
15
15
  "lib/",
16
16
  "types/",
17
- "sommeliagent/",
18
17
  "openclaw.plugin.json",
19
18
  "README.md"
20
19
  ],
20
+ "author": "Hyperspell <hello@hyperspell.com> (https://hyperspell.com)",
21
+ "homepage": "https://hyperspell.com",
21
22
  "repository": {
22
23
  "type": "git",
23
24
  "url": "git+https://github.com/hyperspell/hyperspell-openclaw.git"
@@ -28,14 +29,12 @@
28
29
  "memory",
29
30
  "rag",
30
31
  "ai",
31
- "plugin",
32
- "sommelier",
33
- "wine"
32
+ "plugin"
34
33
  ],
35
34
  "dependencies": {
36
35
  "@clack/prompts": "^1.0.0",
37
36
  "@sinclair/typebox": "^0.34.0",
38
- "hyperspell": "^0.30.0"
37
+ "hyperspell": "^0.35.1"
39
38
  },
40
39
  "peerDependencies": {
41
40
  "openclaw": ">=2026.1.29"
@@ -49,7 +48,15 @@
49
48
  "extensions": [
50
49
  "./index.ts"
51
50
  ],
52
- "hooks": []
51
+ "hooks": [],
52
+ "compat": {
53
+ "pluginApi": ">=2026.1.29",
54
+ "minGatewayVersion": "2026.1.29"
55
+ },
56
+ "build": {
57
+ "openclawVersion": "2026.3.24-beta.2",
58
+ "pluginSdkVersion": "2026.3.24-beta.2"
59
+ }
53
60
  },
54
61
  "devDependencies": {
55
62
  "typescript": "^5.9.3"
package/commands/wine.ts DELETED
@@ -1,164 +0,0 @@
1
- import { execFile } from "node:child_process"
2
- import { access, constants } from "node:fs/promises"
3
- import { join } from "node:path"
4
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
5
- import { log } from "../logger.ts"
6
- import { runScript, SCRIPTS_DIR } from "../lib/run-script.ts"
7
-
8
- function parseWineArgs(argsStr: string): string[] {
9
- const args: string[] = []
10
- const parts = argsStr.trim().split(/\s+/)
11
-
12
- for (let i = 0; i < parts.length; i++) {
13
- const part = parts[i].toLowerCase()
14
- if (["red", "white", "rose", "orange", "sparkling", "dessert"].includes(part)) {
15
- args.push("--color", part)
16
- } else if (["budget", "mid", "premium", "luxury"].includes(part)) {
17
- args.push("--price", part)
18
- } else if (part === "demo") {
19
- args.push("--demo")
20
- } else if (part === "profile") {
21
- args.push("--profile")
22
- } else if (/^\d+$/.test(part)) {
23
- args.push("--count", String(Math.min(Number.parseInt(part), 10)))
24
- }
25
- }
26
-
27
- return args
28
- }
29
-
30
- export function registerWineCommands(api: OpenClawPluginApi): void {
31
- // /wine [color] [price] [count] [demo] [profile]
32
- api.registerCommand({
33
- name: "wine",
34
- description: "Get wine recommendations based on your Spotify listening habits",
35
- acceptsArgs: true,
36
- requireAuth: false,
37
- handler: async (ctx: { args?: string }) => {
38
- log.debug(`/wine command: "${ctx.args || ""}"`)
39
-
40
- // Check uv
41
- try {
42
- await new Promise<void>((resolve, reject) => {
43
- execFile("uv", ["--version"], { timeout: 5_000 }, (err) => {
44
- if (err) reject(err)
45
- else resolve()
46
- })
47
- })
48
- } catch {
49
- return { text: "SommeliAgent needs `uv` installed. Run: brew install uv" }
50
- }
51
-
52
- const userArgs = ctx.args?.trim() || ""
53
- const scriptArgs = parseWineArgs(userArgs)
54
- const isDemo = scriptArgs.includes("--demo")
55
-
56
- // Check Spotify auth (unless demo mode)
57
- if (!isDemo) {
58
- const tokenPath = join(
59
- process.env.HOME || process.env.USERPROFILE || "",
60
- ".sommeliagent",
61
- "token.json",
62
- )
63
- try {
64
- await access(tokenPath, constants.R_OK)
65
- } catch {
66
- return {
67
- text: `Spotify not connected. Set up:\n1. Create app at https://developer.spotify.com/dashboard\n2. Set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET\n3. Run: uv run ${join(SCRIPTS_DIR, "auth.py")}\n\nOr try: /wine demo`,
68
- }
69
- }
70
- }
71
-
72
- try {
73
- const { stdout, stderr } = await runScript("recommend.py", scriptArgs)
74
-
75
- if (stderr) {
76
- log.debug(`/wine stderr: ${stderr}`)
77
- }
78
-
79
- return { text: stdout }
80
- } catch (err) {
81
- log.error("/wine failed", err)
82
- return {
83
- text: `SommeliAgent failed: ${err instanceof Error ? err.message : String(err)}\n\nTry: /wine demo`,
84
- }
85
- }
86
- },
87
- })
88
-
89
- // /wine-auth - Run Spotify OAuth
90
- api.registerCommand({
91
- name: "wine-auth",
92
- description: "Connect your Spotify account for wine recommendations",
93
- acceptsArgs: false,
94
- requireAuth: false,
95
- handler: async () => {
96
- log.debug("/wine-auth command")
97
-
98
- try {
99
- const { stdout } = await runScript("auth.py", [], 180_000)
100
- return { text: stdout }
101
- } catch (err) {
102
- log.error("/wine-auth failed", err)
103
- return {
104
- text: `Spotify auth failed: ${err instanceof Error ? err.message : String(err)}\n\nMake sure SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET are set.`,
105
- }
106
- }
107
- },
108
- })
109
-
110
- // /wine-rate <wine-id> <1-5> [notes]
111
- api.registerCommand({
112
- name: "wine-rate",
113
- description: "Rate a wine recommendation (improves future suggestions)",
114
- acceptsArgs: true,
115
- requireAuth: false,
116
- handler: async (ctx: { args?: string }) => {
117
- const parts = ctx.args?.trim().split(/\s+/) || []
118
- if (parts.length < 2) {
119
- return { text: 'Usage: /wine-rate <wine-id> <1-5> [notes]\nExample: /wine-rate red-it-001 5 "Incredible tannins"' }
120
- }
121
-
122
- const wineId = parts[0]
123
- const rating = Number.parseInt(parts[1])
124
- if (Number.isNaN(rating) || rating < 1 || rating > 5) {
125
- return { text: "Rating must be 1-5." }
126
- }
127
-
128
- const notes = parts.slice(2).join(" ")
129
- const args = ["--wine-id", wineId, "--rating", String(rating)]
130
- if (notes) args.push("--notes", notes)
131
-
132
- try {
133
- const { stdout } = await runScript("rate.py", args)
134
- return { text: stdout }
135
- } catch (err) {
136
- log.error("/wine-rate failed", err)
137
- return {
138
- text: `Failed to rate: ${err instanceof Error ? err.message : String(err)}`,
139
- }
140
- }
141
- },
142
- })
143
-
144
- // /wine-history - Show past ratings
145
- api.registerCommand({
146
- name: "wine-history",
147
- description: "View your wine rating history and taste profile",
148
- acceptsArgs: false,
149
- requireAuth: false,
150
- handler: async () => {
151
- log.debug("/wine-history command")
152
-
153
- try {
154
- const { stdout } = await runScript("history.py", [])
155
- return { text: stdout }
156
- } catch (err) {
157
- log.error("/wine-history failed", err)
158
- return {
159
- text: `Failed to load history: ${err instanceof Error ? err.message : String(err)}`,
160
- }
161
- }
162
- },
163
- })
164
- }
package/lib/run-script.ts DELETED
@@ -1,32 +0,0 @@
1
- import { execFile } from "node:child_process"
2
- import { dirname, join } from "node:path"
3
- import { fileURLToPath } from "node:url"
4
-
5
- const __dirname = dirname(fileURLToPath(import.meta.url))
6
- export const SCRIPTS_DIR = join(__dirname, "..", "sommeliagent", "scripts")
7
-
8
- const ALLOWED_SCRIPTS = new Set(["recommend.py", "rate.py", "auth.py", "history.py"])
9
-
10
- export function runScript(
11
- scriptName: string,
12
- args: string[],
13
- timeout: number = 30_000,
14
- ): Promise<{ stdout: string; stderr: string }> {
15
- if (!ALLOWED_SCRIPTS.has(scriptName)) {
16
- return Promise.reject(new Error(`Unknown script: ${scriptName}`))
17
- }
18
- return new Promise((resolve, reject) => {
19
- execFile(
20
- "uv",
21
- ["run", join(SCRIPTS_DIR, scriptName), ...args],
22
- { timeout, maxBuffer: 1024 * 512 },
23
- (error, stdout, stderr) => {
24
- if (error) {
25
- reject(new Error(`${scriptName} failed: ${stderr || error.message}`))
26
- } else {
27
- resolve({ stdout, stderr })
28
- }
29
- },
30
- )
31
- })
32
- }
@@ -1,63 +0,0 @@
1
- # Cross-Domain Mapping: Music → Wine
2
-
3
- The core thesis: sensory and aesthetic preferences transfer across domains. Someone who seeks complexity in music seeks complexity in wine. Someone drawn to dark, brooding sounds wants wines with tension and structure, not crowd-pleasers.
4
-
5
- ## Music → Wine Dimension Map
6
-
7
- | Music Signal | Wine Dimension | Logic |
8
- |---|---|---|
9
- | Tempo / energy | Body / weight | High energy → bold, full-bodied; ambient → light, delicate |
10
- | Complexity (jazz, prog, classical) | Complexity (terroir-driven, natural wines) | Listeners who tolerate/seek complexity want wines that challenge |
11
- | Valence (happy vs dark) | Sweetness / dryness spectrum | Bright pop → approachable, off-dry; minor key → dry, tannic, austere |
12
- | Acoustic vs electronic | Old World vs New World | Organic, analog sound → traditional winemaking; synthetic → modern, tech-forward wines |
13
- | Obscurity / niche taste | Obscurity of region/varietal | Niche listeners don't want grocery store Cab Sav |
14
- | Repetition tolerance | Familiarity preference | Loop-heavy listeners → reliable house pours; variety seekers → discovery |
15
- | Lyrical depth | Label story / terroir narrative | People who care about lyrics care about provenance |
16
-
17
- ## Mapping Algorithm
18
-
19
- ### Step 1: Music Profile Aggregation
20
-
21
- From Spotify's audio features API, we aggregate across a user's top 50 tracks:
22
- - **avgValence** (0-1): musical positiveness
23
- - **avgEnergy** (0-1): intensity and activity
24
- - **avgDanceability** (0-1): dance suitability
25
- - **avgAcousticness** (0-1): acoustic vs electronic
26
- - **avgTempo** (BPM): normalized to 0-1 range (/200)
27
- - **avgComplexity** (derived): instrumentalness × 0.3 + time signature variety × 0.3 + (1 - danceability) × 0.4
28
- - **obscurityScore** (derived): 1 - (average track popularity / 100)
29
-
30
- ### Step 2: Wine Dimension Translation
31
-
32
- Each wine dimension is a weighted average of music features:
33
-
34
- **Body** = energy × 0.5 + tempo × 0.3 + (1 - acousticness) × 0.2
35
- **Sweetness** = valence × 0.6 + danceability × 0.3 + (1 - complexity) × 0.1
36
- **Tannin** = (1 - valence) × 0.4 + complexity × 0.3 + energy × 0.3
37
- **Acidity** = complexity × 0.4 + (1 - valence) × 0.3 + obscurity × 0.3
38
- **Complexity** = complexity × 0.5 + obscurity × 0.3 + (1 - danceability) × 0.2
39
- **Fruitiness** = valence × 0.5 + energy × 0.3 + (1 - complexity) × 0.2
40
- **Earthiness** = acousticness × 0.4 + complexity × 0.3 + obscurity × 0.3
41
- **Spiciness** = energy × 0.4 + (1 - valence) × 0.3 + complexity × 0.3
42
-
43
- ### Step 3: Wine Scoring
44
-
45
- Each wine in the database has a profile with the same 8 dimensions. Match score = weighted cosine similarity between target profile and wine profile.
46
-
47
- Weights: body (1.5), sweetness (1.2), complexity (1.5), tannin (1.0), acidity (1.0), fruitiness (0.8), earthiness (0.8), spiciness (0.7).
48
-
49
- ### Step 4: Connection Generation
50
-
51
- For each recommended wine, the system identifies which specific music-wine connections are strongest and generates explanations.
52
-
53
- ## Expansion Signals (Future)
54
-
55
- - **Reading history** → intellectual complexity tolerance
56
- - **Food delivery** → direct cuisine/region pairing data
57
- - **Time of day / mood** → contextual recommendations
58
- - **Weather + location** → seasonal suggestions
59
- - **Instagram aesthetic** → visual preference → label/winemaking philosophy
60
-
61
- ## Philosophy
62
-
63
- The signal doesn't need to be scientifically rigorous. This is entertainment that gets smarter. The delight is in the explanation, not the accuracy. The feedback loop (ratings) quietly builds a genuine cross-domain taste database over time.