@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/README.md +127 -3
- package/client.ts +311 -241
- package/commands/setup.ts +8 -54
- package/commands/wine.ts +164 -0
- package/config.ts +242 -175
- package/hooks/auto-context.ts +17 -11
- package/hooks/auto-trace.ts +127 -0
- package/index.ts +122 -100
- package/lib/browser.ts +10 -6
- package/lib/run-script.ts +32 -0
- package/openclaw.plugin.json +110 -87
- package/package.json +8 -4
- package/sommeliagent/references/cross-domain-mappings.md +63 -0
- package/sommeliagent/scripts/auth.py +222 -0
- package/sommeliagent/scripts/history.py +72 -0
- package/sommeliagent/scripts/rate.py +76 -0
- package/sommeliagent/scripts/recommend.py +777 -0
- package/sommeliagent/scripts/test_recommend.py +459 -0
- package/sommeliagent/scripts/wine_db.py +3224 -0
- package/tools/remember.ts +6 -2
- package/tools/search.ts +9 -3
- package/tools/sommelier.ts +254 -0
package/commands/setup.ts
CHANGED
|
@@ -1,45 +1,18 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
512
|
+
await openInBrowser(connectUrl)
|
|
559
513
|
p.log.success("Browser opened to connect.hyperspell.com")
|
|
560
514
|
|
|
561
515
|
await p.confirm({
|
package/commands/wine.ts
ADDED
|
@@ -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
|
+
}
|