@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/commands/setup.ts CHANGED
@@ -1,45 +1,18 @@
1
- import { exec, execFileSync } from "node:child_process"
1
+ import { execFileSync } from "node:child_process"
2
2
  import * as fs from "node:fs"
3
3
  import * as path from "node:path"
4
- import { homedir, platform, userInfo } from "node:os"
4
+ import { userInfo } from "node:os"
5
5
  import * as p from "@clack/prompts"
6
6
  import type { Command } from "commander"
7
7
  import Hyperspell from "hyperspell"
8
8
  import { syncAllMemoryFiles, getMemoryFiles } from "../sync/markdown.ts"
9
+ import { openInBrowser } from "../lib/browser.ts"
9
10
  import { HyperspellClient } from "../client.ts"
10
- import { getWorkspaceDir, parseConfig } from "../config.ts"
11
+ import { getWorkspaceDir, parseConfig, resolveConfigPath } from "../config.ts"
11
12
  import { buildExtractionPrompt, CRON_JOB_NAME } from "../graph/cron.ts"
12
13
  import { NetworkStateManager } from "../graph/state.ts"
13
14
  import { scanMemories, formatScanResults, completeMemories } from "../graph/ops.ts"
14
15
 
15
- /**
16
- * Resolve OpenClaw state directory, matching OpenClaw's logic.
17
- * Checks OPENCLAW_STATE_DIR env var, falls back to ~/.openclaw
18
- */
19
- function resolveStateDir(): string {
20
- const override = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim()
21
- if (override) {
22
- return override.startsWith("~")
23
- ? override.replace(/^~(?=$|[\\/])/, homedir())
24
- : path.resolve(override)
25
- }
26
- return path.join(homedir(), ".openclaw")
27
- }
28
-
29
- /**
30
- * Resolve OpenClaw config path, matching OpenClaw's logic.
31
- * Checks OPENCLAW_CONFIG_PATH env var, falls back to $STATE_DIR/openclaw.json
32
- */
33
- function resolveConfigPath(): string {
34
- const override = process.env.OPENCLAW_CONFIG_PATH?.trim() || process.env.CLAWDBOT_CONFIG_PATH?.trim()
35
- if (override) {
36
- return override.startsWith("~")
37
- ? override.replace(/^~(?=$|[\\/])/, homedir())
38
- : path.resolve(override)
39
- }
40
- return path.join(resolveStateDir(), "openclaw.json")
41
- }
42
-
43
16
  async function fetchConnectionSources(client: Hyperspell, userId: string): Promise<string[]> {
44
17
  try {
45
18
  const userClient = new Hyperspell({
@@ -69,26 +42,6 @@ function updateConfigSources(configPath: string, sources: string[]): void {
69
42
  }
70
43
  }
71
44
 
72
- function openUrl(url: string): Promise<void> {
73
- return new Promise((resolve, reject) => {
74
- let command: string
75
- switch (platform()) {
76
- case "darwin":
77
- command = `open "${url}"`
78
- break
79
- case "win32":
80
- command = `start "" "${url}"`
81
- break
82
- default:
83
- command = `xdg-open "${url}"`
84
- }
85
- exec(command, (error) => {
86
- if (error) reject(error)
87
- else resolve()
88
- })
89
- })
90
- }
91
-
92
45
  async function runSetup(): Promise<void> {
93
46
  p.intro("Hyperspell Setup")
94
47
 
@@ -120,7 +73,7 @@ async function runSetup(): Promise<void> {
120
73
  }
121
74
 
122
75
  if (openSignup) {
123
- await openUrl("https://app.hyperspell.com")
76
+ await openInBrowser("https://app.hyperspell.com")
124
77
  p.log.info("Browser opened. Come back when you've created your account.")
125
78
  }
126
79
 
@@ -229,7 +182,7 @@ async function runSetup(): Promise<void> {
229
182
  })
230
183
 
231
184
  if (!p.isCancel(openConnect) && openConnect) {
232
- await openUrl(connectUrl)
185
+ await openInBrowser(connectUrl)
233
186
  p.log.info("Browser opened. Connect your accounts and come back when done.")
234
187
 
235
188
  await p.confirm({
@@ -382,6 +335,7 @@ async function runSetup(): Promise<void> {
382
335
  syncMemories: true,
383
336
  sources: [],
384
337
  maxResults: 10,
338
+ relevanceThreshold: 0.6,
385
339
  debug: false,
386
340
  knowledgeGraph: { enabled: false, scanIntervalMinutes: 60, batchSize: 20 },
387
341
  })
@@ -555,7 +509,7 @@ async function runConnect(pluginConfig: unknown): Promise<void> {
555
509
 
556
510
  s.stop("Connect URL ready")
557
511
 
558
- await openUrl(connectUrl)
512
+ await openInBrowser(connectUrl)
559
513
  p.log.success("Browser opened to connect.hyperspell.com")
560
514
 
561
515
  await p.confirm({
@@ -0,0 +1,164 @@
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
+ }