@hyperspell/openclaw-hyperspell 0.5.0 → 0.7.2

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/index.ts CHANGED
@@ -1,113 +1,135 @@
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"
4
5
  import { registerCliCommands } from "./commands/setup.ts"
5
6
  import { parseConfig, hyperspellConfigSchema, getWorkspaceDir } from "./config.ts"
6
7
  import { buildAutoContextHandler } from "./hooks/auto-context.ts"
8
+ import { buildAutoTraceHandler } from "./hooks/auto-trace.ts"
7
9
  import { buildFileSyncHandler, syncMemoriesOnStartup } from "./hooks/memory-sync.ts"
8
10
  import { initLogger } from "./logger.ts"
9
11
  import { registerRememberTool } from "./tools/remember.ts"
10
12
  import { registerSearchTool } from "./tools/search.ts"
13
+ import { registerSommelierTool, registerSommelierRateTool } from "./tools/sommelier.ts"
11
14
  import { registerNetworkTools } from "./graph/index.ts"
12
15
 
13
16
  export default {
14
- id: "openclaw-hyperspell",
15
- name: "Hyperspell",
16
- description: "Hyperspell gives your Molty context and memory from all your existing data",
17
- kind: "memory" as const,
18
- configSchema: hyperspellConfigSchema,
19
-
20
- register(api: OpenClawPluginApi) {
21
- // Register CLI commands (openclaw openclaw-hyperspell setup|status|connect)
22
- api.registerCli(
23
- (ctx) => {
24
- registerCliCommands(ctx.program, api.pluginConfig)
25
- },
26
- { commands: ["openclaw-hyperspell"] },
27
- )
28
-
29
- // Check if configured
30
- const rawConfig = api.pluginConfig as Record<string, unknown> | undefined
31
- const hasConfig = rawConfig?.apiKey || process.env.HYPERSPELL_API_KEY
32
-
33
- if (!hasConfig) {
34
- api.logger.info("hyperspell: not configured - run 'openclaw openclaw-hyperspell setup'")
35
- // Still register slash commands so they show up, but they'll return an error
36
- api.registerCommand({
37
- name: "getcontext",
38
- description: "Search your memories for relevant context",
39
- acceptsArgs: true,
40
- requireAuth: false,
41
- handler: async () => {
42
- return { text: "Hyperspell not configured. Run 'openclaw openclaw-hyperspell setup' first." }
43
- },
44
- })
45
- api.registerCommand({
46
- name: "remember",
47
- description: "Save something to memory",
48
- acceptsArgs: true,
49
- requireAuth: false,
50
- handler: async () => {
51
- return { text: "Hyperspell not configured. Run 'openclaw openclaw-hyperspell setup' first." }
52
- },
53
- })
54
- api.registerCommand({
55
- name: "sync",
56
- description: "Sync memory/*.md files with Hyperspell",
57
- acceptsArgs: false,
58
- requireAuth: false,
59
- handler: async () => {
60
- return { text: "Hyperspell not configured. Run 'openclaw openclaw-hyperspell setup' first." }
61
- },
62
- })
63
- return
64
- }
65
-
66
- const cfg = parseConfig(api.pluginConfig)
67
-
68
- initLogger(api.logger, cfg.debug)
69
-
70
- const client = new HyperspellClient(cfg)
71
-
72
- // Register AI tools
73
- registerSearchTool(api, client, cfg)
74
- registerRememberTool(api, client, cfg)
75
-
76
- // Register auto-context hook
77
- if (cfg.autoContext) {
78
- const autoContextHandler = buildAutoContextHandler(client, cfg)
79
- api.on("before_agent_start", autoContextHandler)
80
- }
81
-
82
- // Register memory sync hook
83
- if (cfg.syncMemories) {
84
- const fileSyncHandler = buildFileSyncHandler(client, cfg)
85
- api.on("file_changed", fileSyncHandler)
86
- }
87
-
88
- // Register memory network tools
89
- if (cfg.knowledgeGraph.enabled) {
90
- registerNetworkTools(api, client, cfg)
91
- }
92
-
93
- // Register slash commands
94
- registerCommands(api, client, cfg)
95
-
96
- // Register service for lifecycle management
97
- api.registerService({
98
- id: "openclaw-hyperspell",
99
- start: async () => {
100
- api.logger.info("hyperspell: connected")
101
-
102
- // Sync memories on startup if enabled
103
- if (cfg.syncMemories) {
104
- const workspaceDir = getWorkspaceDir()
105
- await syncMemoriesOnStartup(client, workspaceDir)
106
- }
107
- },
108
- stop: () => {
109
- api.logger.info("hyperspell: stopped")
110
- },
111
- })
112
- },
113
- }
17
+ id: "openclaw-hyperspell",
18
+ name: "Hyperspell",
19
+ description:
20
+ "Hyperspell gives your Molty context and memory from all your existing data",
21
+ kind: "memory" as const,
22
+ configSchema: hyperspellConfigSchema,
23
+
24
+ register(api: OpenClawPluginApi) {
25
+ // Register CLI commands (openclaw openclaw-hyperspell setup|status|connect)
26
+ api.registerCli(
27
+ (ctx) => {
28
+ registerCliCommands(ctx.program, api.pluginConfig);
29
+ },
30
+ { commands: ["openclaw-hyperspell"] },
31
+ );
32
+
33
+ // Check if configured
34
+ const rawConfig = api.pluginConfig as Record<string, unknown> | undefined;
35
+ const hasConfig = rawConfig?.apiKey || process.env.HYPERSPELL_API_KEY;
36
+
37
+ // SommeliAgent works independently of Hyperspell config — it just needs uv + Spotify
38
+ registerSommelierTool(api)
39
+ registerSommelierRateTool(api)
40
+ registerWineCommands(api)
41
+
42
+ if (!hasConfig) {
43
+ api.logger.info(
44
+ "hyperspell: not configured - run 'openclaw openclaw-hyperspell setup'",
45
+ )
46
+ // Still register slash commands so they show up, but they'll return an error
47
+ api.registerCommand({
48
+ name: "getcontext",
49
+ description: "Search your memories for relevant context",
50
+ acceptsArgs: true,
51
+ requireAuth: false,
52
+ handler: async () => {
53
+ return {
54
+ text: "Hyperspell not configured. Run 'openclaw openclaw-hyperspell setup' first.",
55
+ }
56
+ },
57
+ })
58
+ api.registerCommand({
59
+ name: "remember",
60
+ description: "Save something to memory",
61
+ acceptsArgs: true,
62
+ requireAuth: false,
63
+ handler: async () => {
64
+ return {
65
+ text: "Hyperspell not configured. Run 'openclaw openclaw-hyperspell setup' first.",
66
+ }
67
+ },
68
+ })
69
+ api.registerCommand({
70
+ name: "sync",
71
+ description: "Sync memory/*.md files with Hyperspell",
72
+ acceptsArgs: false,
73
+ requireAuth: false,
74
+ handler: async () => {
75
+ return {
76
+ text: "Hyperspell not configured. Run 'openclaw openclaw-hyperspell setup' first.",
77
+ }
78
+ },
79
+ })
80
+ return
81
+ }
82
+
83
+ const cfg = parseConfig(api.pluginConfig);
84
+
85
+ initLogger(api.logger, cfg.debug);
86
+
87
+ const client = new HyperspellClient(cfg);
88
+
89
+ // Register AI tools
90
+ registerSearchTool(api, client, cfg);
91
+ registerRememberTool(api, client, cfg);
92
+
93
+ // Register auto-context hook
94
+ if (cfg.autoContext) {
95
+ const autoContextHandler = buildAutoContextHandler(client, cfg);
96
+ api.on("before_agent_start", autoContextHandler);
97
+ }
98
+
99
+ // Register auto-trace hook (send conversations to Hyperspell on session end)
100
+ if (cfg.autoTrace.enabled) {
101
+ api.on("agent_end", buildAutoTraceHandler(client, cfg));
102
+ }
103
+
104
+ // Register memory sync hook
105
+ if (cfg.syncMemories) {
106
+ const fileSyncHandler = buildFileSyncHandler(client, cfg);
107
+ api.on("file_changed", fileSyncHandler);
108
+ }
109
+
110
+ // Register memory network tools
111
+ if (cfg.knowledgeGraph.enabled) {
112
+ registerNetworkTools(api, client, cfg);
113
+ }
114
+
115
+ // Register slash commands
116
+ registerCommands(api, client, cfg);
117
+
118
+ // Register service for lifecycle management
119
+ api.registerService({
120
+ id: "openclaw-hyperspell",
121
+ start: async () => {
122
+ api.logger.info("hyperspell: connected");
123
+
124
+ // Sync memories on startup if enabled
125
+ if (cfg.syncMemories) {
126
+ const workspaceDir = getWorkspaceDir();
127
+ await syncMemoriesOnStartup(client, workspaceDir);
128
+ }
129
+ },
130
+ stop: () => {
131
+ api.logger.info("hyperspell: stopped");
132
+ },
133
+ });
134
+ },
135
+ };
package/lib/browser.ts CHANGED
@@ -1,22 +1,26 @@
1
- import { exec } from "node:child_process"
1
+ import { execFile } from "node:child_process"
2
2
  import { platform } from "node:os"
3
3
 
4
4
  export function openInBrowser(url: string): Promise<void> {
5
5
  return new Promise((resolve, reject) => {
6
- let command: string
6
+ let file: string
7
+ let args: string[]
7
8
 
8
9
  switch (platform()) {
9
10
  case "darwin":
10
- command = `open "${url}"`
11
+ file = "open"
12
+ args = [url]
11
13
  break
12
14
  case "win32":
13
- command = `start "" "${url}"`
15
+ file = "cmd"
16
+ args = ["/c", "start", "", url]
14
17
  break
15
18
  default:
16
- command = `xdg-open "${url}"`
19
+ file = "xdg-open"
20
+ args = [url]
17
21
  }
18
22
 
19
- exec(command, (error) => {
23
+ execFile(file, args, (error) => {
20
24
  if (error) {
21
25
  reject(error)
22
26
  } else {
@@ -0,0 +1,32 @@
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,88 +1,111 @@
1
1
  {
2
- "id": "openclaw-hyperspell",
3
- "kind": "memory",
4
- "uiHints": {
5
- "apiKey": {
6
- "label": "Hyperspell API Key",
7
- "sensitive": true,
8
- "placeholder": "hs_...",
9
- "help": "Your API key from app.hyperspell.com (or use ${HYPERSPELL_API_KEY})"
10
- },
11
- "userId": {
12
- "label": "User ID",
13
- "placeholder": "user_123",
14
- "help": "User ID (can be your email)",
15
- "advanced": false
16
- },
17
- "autoContext": {
18
- "label": "Auto-Context",
19
- "help": "Inject relevant memories before every AI turn",
20
- "advanced": true
21
- },
22
- "sources": {
23
- "label": "Sources",
24
- "placeholder": "notion,slack,google_drive",
25
- "help": "Comma-separated list of sources to search. Leave empty for all sources.",
26
- "advanced": true
27
- },
28
- "maxResults": {
29
- "label": "Max Results",
30
- "placeholder": "10",
31
- "help": "Maximum memories injected into context per turn",
32
- "advanced": true
33
- },
34
- "debug": {
35
- "label": "Debug Logging",
36
- "help": "Enable verbose debug logs for API calls and responses",
37
- "advanced": true
38
- },
39
- "syncMemories": {
40
- "label": "Sync Memory Files",
41
- "help": "Automatically sync markdown files in workspace/memory/ to Hyperspell",
42
- "advanced": true
43
- },
44
- "knowledgeGraph": {
45
- "label": "Memory Network",
46
- "help": "Extract entities (people, projects, orgs, topics) from memories into structured markdown files. Requires a cron job for periodic scanning.",
47
- "advanced": true
48
- }
49
- },
50
- "configSchema": {
51
- "type": "object",
52
- "additionalProperties": false,
53
- "properties": {
54
- "apiKey": {
55
- "type": "string"
56
- },
57
- "userId": {
58
- "type": "string"
59
- },
60
- "autoContext": {
61
- "type": "boolean"
62
- },
63
- "sources": {
64
- "type": "string"
65
- },
66
- "maxResults": {
67
- "type": "number",
68
- "minimum": 1,
69
- "maximum": 20
70
- },
71
- "debug": {
72
- "type": "boolean"
73
- },
74
- "syncMemories": {
75
- "type": "boolean"
76
- },
77
- "knowledgeGraph": {
78
- "type": "object",
79
- "properties": {
80
- "enabled": { "type": "boolean" },
81
- "scanIntervalMinutes": { "type": "number", "minimum": 5, "maximum": 1440 },
82
- "batchSize": { "type": "number", "minimum": 5, "maximum": 100 }
83
- }
84
- }
85
- },
86
- "required": []
87
- }
88
- }
2
+ "id": "openclaw-hyperspell",
3
+ "kind": "memory",
4
+ "uiHints": {
5
+ "apiKey": {
6
+ "label": "Hyperspell API Key",
7
+ "sensitive": true,
8
+ "placeholder": "hs_...",
9
+ "help": "Your API key from app.hyperspell.com (or use ${HYPERSPELL_API_KEY})"
10
+ },
11
+ "userId": {
12
+ "label": "User ID",
13
+ "placeholder": "user_123",
14
+ "help": "User ID (can be your email)",
15
+ "advanced": false
16
+ },
17
+ "autoContext": {
18
+ "label": "Auto-Context",
19
+ "help": "Inject relevant memories before every AI turn",
20
+ "advanced": true
21
+ },
22
+ "autoTrace": {
23
+ "label": "Auto-Trace",
24
+ "help": "Automatically send conversation traces to Hyperspell for memory extraction (procedural, mood) at the end of each session",
25
+ "advanced": true
26
+ },
27
+ "sources": {
28
+ "label": "Sources",
29
+ "placeholder": "notion,slack,google_drive",
30
+ "help": "Comma-separated list of sources to search. Leave empty for all sources.",
31
+ "advanced": true
32
+ },
33
+ "maxResults": {
34
+ "label": "Max Results",
35
+ "placeholder": "10",
36
+ "help": "Maximum memories injected into context per turn",
37
+ "advanced": true
38
+ },
39
+ "debug": {
40
+ "label": "Debug Logging",
41
+ "help": "Enable verbose debug logs for API calls and responses",
42
+ "advanced": true
43
+ },
44
+ "syncMemories": {
45
+ "label": "Sync Memory Files",
46
+ "help": "Automatically sync markdown files in workspace/memory/ to Hyperspell",
47
+ "advanced": true
48
+ },
49
+ "knowledgeGraph": {
50
+ "label": "Memory Network",
51
+ "help": "Extract entities (people, projects, orgs, topics) from memories into structured markdown files. Requires a cron job for periodic scanning.",
52
+ "advanced": true
53
+ }
54
+ },
55
+ "configSchema": {
56
+ "type": "object",
57
+ "additionalProperties": false,
58
+ "properties": {
59
+ "apiKey": {
60
+ "type": "string"
61
+ },
62
+ "userId": {
63
+ "type": "string"
64
+ },
65
+ "autoContext": {
66
+ "type": "boolean"
67
+ },
68
+ "autoTrace": {
69
+ "type": "object",
70
+ "properties": {
71
+ "enabled": { "type": "boolean" },
72
+ "extract": {
73
+ "type": "array",
74
+ "items": {
75
+ "type": "string",
76
+ "enum": ["procedure", "memory", "mood"]
77
+ }
78
+ },
79
+ "metadata": { "type": "object" }
80
+ }
81
+ },
82
+ "sources": {
83
+ "type": "string"
84
+ },
85
+ "maxResults": {
86
+ "type": "number",
87
+ "minimum": 1,
88
+ "maximum": 20
89
+ },
90
+ "debug": {
91
+ "type": "boolean"
92
+ },
93
+ "syncMemories": {
94
+ "type": "boolean"
95
+ },
96
+ "knowledgeGraph": {
97
+ "type": "object",
98
+ "properties": {
99
+ "enabled": { "type": "boolean" },
100
+ "scanIntervalMinutes": {
101
+ "type": "number",
102
+ "minimum": 5,
103
+ "maximum": 1440
104
+ },
105
+ "batchSize": { "type": "number", "minimum": 5, "maximum": 100 }
106
+ }
107
+ }
108
+ },
109
+ "required": []
110
+ }
111
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspell/openclaw-hyperspell",
3
- "version": "0.5.0",
3
+ "version": "0.7.2",
4
4
  "type": "module",
5
5
  "description": "OpenClaw Hyperspell memory plugin",
6
6
  "license": "MIT",
@@ -14,12 +14,13 @@
14
14
  "graph/",
15
15
  "lib/",
16
16
  "types/",
17
+ "sommeliagent/",
17
18
  "openclaw.plugin.json",
18
19
  "README.md"
19
20
  ],
20
21
  "repository": {
21
22
  "type": "git",
22
- "url": "git+https://github.com/hyperspell/openclaw-hyperspell.git"
23
+ "url": "git+https://github.com/hyperspell/hyperspell-openclaw.git"
23
24
  },
24
25
  "keywords": [
25
26
  "openclaw",
@@ -27,7 +28,9 @@
27
28
  "memory",
28
29
  "rag",
29
30
  "ai",
30
- "plugin"
31
+ "plugin",
32
+ "sommelier",
33
+ "wine"
31
34
  ],
32
35
  "dependencies": {
33
36
  "@clack/prompts": "^1.0.0",
@@ -45,7 +48,8 @@
45
48
  "openclaw": {
46
49
  "extensions": [
47
50
  "./index.ts"
48
- ]
51
+ ],
52
+ "hooks": []
49
53
  },
50
54
  "devDependencies": {
51
55
  "typescript": "^5.9.3"
@@ -0,0 +1,63 @@
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.