@hyperspell/openclaw-hyperspell 0.1.1 → 0.3.0
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 +33 -15
- package/client.ts +102 -2
- package/commands/setup.ts +536 -0
- package/commands/slash.ts +34 -54
- package/config.ts +75 -3
- package/hooks/memory-sync.ts +63 -0
- package/index.ts +62 -3
- package/openclaw.plugin.json +25 -10
- package/package.json +8 -5
- package/tools/remember.ts +20 -8
- package/tools/search.ts +51 -31
- package/types/openclaw.d.ts +18 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { exec } from "node:child_process"
|
|
2
|
+
import * as fs from "node:fs"
|
|
3
|
+
import * as path from "node:path"
|
|
4
|
+
import { homedir, platform, userInfo } from "node:os"
|
|
5
|
+
import * as p from "@clack/prompts"
|
|
6
|
+
import type { Command } from "commander"
|
|
7
|
+
import Hyperspell from "hyperspell"
|
|
8
|
+
import { syncAllMemoryFiles, getMemoryFiles } from "../sync/markdown.ts"
|
|
9
|
+
import { HyperspellClient } from "../client.ts"
|
|
10
|
+
import { getWorkspaceDir } from "../config.ts"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve OpenClaw state directory, matching OpenClaw's logic.
|
|
14
|
+
* Checks OPENCLAW_STATE_DIR env var, falls back to ~/.openclaw
|
|
15
|
+
*/
|
|
16
|
+
function resolveStateDir(): string {
|
|
17
|
+
const override = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim()
|
|
18
|
+
if (override) {
|
|
19
|
+
return override.startsWith("~")
|
|
20
|
+
? override.replace(/^~(?=$|[\\/])/, homedir())
|
|
21
|
+
: path.resolve(override)
|
|
22
|
+
}
|
|
23
|
+
return path.join(homedir(), ".openclaw")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve OpenClaw config path, matching OpenClaw's logic.
|
|
28
|
+
* Checks OPENCLAW_CONFIG_PATH env var, falls back to $STATE_DIR/openclaw.json
|
|
29
|
+
*/
|
|
30
|
+
function resolveConfigPath(): string {
|
|
31
|
+
const override = process.env.OPENCLAW_CONFIG_PATH?.trim() || process.env.CLAWDBOT_CONFIG_PATH?.trim()
|
|
32
|
+
if (override) {
|
|
33
|
+
return override.startsWith("~")
|
|
34
|
+
? override.replace(/^~(?=$|[\\/])/, homedir())
|
|
35
|
+
: path.resolve(override)
|
|
36
|
+
}
|
|
37
|
+
return path.join(resolveStateDir(), "openclaw.json")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function fetchConnectionSources(client: Hyperspell, userId: string): Promise<string[]> {
|
|
41
|
+
try {
|
|
42
|
+
const userClient = new Hyperspell({
|
|
43
|
+
apiKey: client.apiKey,
|
|
44
|
+
userID: userId,
|
|
45
|
+
})
|
|
46
|
+
const response = await userClient.connections.list()
|
|
47
|
+
const providers = response.connections.map((conn) => conn.provider)
|
|
48
|
+
// Add vault and deduplicate
|
|
49
|
+
const sources = [...new Set(["vault", ...providers])]
|
|
50
|
+
return sources
|
|
51
|
+
} catch (_error) {
|
|
52
|
+
return ["vault"]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function updateConfigSources(configPath: string, sources: string[]): void {
|
|
57
|
+
if (!fs.existsSync(configPath)) return
|
|
58
|
+
|
|
59
|
+
const content = fs.readFileSync(configPath, "utf-8")
|
|
60
|
+
const config = JSON.parse(content)
|
|
61
|
+
|
|
62
|
+
const pluginConfig = config?.plugins?.entries?.["openclaw-hyperspell"]?.config
|
|
63
|
+
if (pluginConfig) {
|
|
64
|
+
pluginConfig.sources = sources.join(",")
|
|
65
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n")
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function openUrl(url: string): Promise<void> {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
let command: string
|
|
72
|
+
switch (platform()) {
|
|
73
|
+
case "darwin":
|
|
74
|
+
command = `open "${url}"`
|
|
75
|
+
break
|
|
76
|
+
case "win32":
|
|
77
|
+
command = `start "" "${url}"`
|
|
78
|
+
break
|
|
79
|
+
default:
|
|
80
|
+
command = `xdg-open "${url}"`
|
|
81
|
+
}
|
|
82
|
+
exec(command, (error) => {
|
|
83
|
+
if (error) reject(error)
|
|
84
|
+
else resolve()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function runSetup(): Promise<void> {
|
|
90
|
+
p.intro("Hyperspell Setup")
|
|
91
|
+
|
|
92
|
+
// Step 1: Check if they have an account
|
|
93
|
+
const hasAccount = await p.confirm({
|
|
94
|
+
message: "Do you already have a Hyperspell account?",
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
if (p.isCancel(hasAccount)) {
|
|
98
|
+
p.cancel("Setup cancelled")
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!hasAccount) {
|
|
103
|
+
p.note(
|
|
104
|
+
"1. Go to https://app.hyperspell.com to create a free account\n" +
|
|
105
|
+
"2. Create a new app for your AI agent\n" +
|
|
106
|
+
"3. Select which integrations you want to connect (Notion, Slack, etc.)",
|
|
107
|
+
"Create an account",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const openSignup = await p.confirm({
|
|
111
|
+
message: "Open app.hyperspell.com in your browser?",
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
if (p.isCancel(openSignup)) {
|
|
115
|
+
p.cancel("Setup cancelled")
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (openSignup) {
|
|
120
|
+
await openUrl("https://app.hyperspell.com")
|
|
121
|
+
p.log.info("Browser opened. Come back when you've created your account.")
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await p.confirm({
|
|
125
|
+
message: "Ready to continue?",
|
|
126
|
+
active: "Yes",
|
|
127
|
+
inactive: "No",
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Step 2: Get API Key
|
|
132
|
+
p.note(
|
|
133
|
+
"1. Go to your app in https://app.hyperspell.com\n" +
|
|
134
|
+
"2. Navigate to Settings > API Keys\n" +
|
|
135
|
+
"3. Create a new API key",
|
|
136
|
+
"API Key",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
const apiKey = await p.text({
|
|
140
|
+
message: "Paste your API key",
|
|
141
|
+
placeholder: "hs_...",
|
|
142
|
+
validate: (value) => {
|
|
143
|
+
if (!value) return "API key is required"
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
if (p.isCancel(apiKey)) {
|
|
148
|
+
p.cancel("Setup cancelled")
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Validate API key
|
|
153
|
+
const s = p.spinner()
|
|
154
|
+
s.start("Validating API key")
|
|
155
|
+
|
|
156
|
+
let client: Hyperspell
|
|
157
|
+
try {
|
|
158
|
+
client = new Hyperspell({ apiKey })
|
|
159
|
+
await client.integrations.list()
|
|
160
|
+
s.stop("API key is valid")
|
|
161
|
+
} catch (_error) {
|
|
162
|
+
s.stop("API key validation failed")
|
|
163
|
+
p.log.error("Please check that your API key is correct and try again.")
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Step 3: User ID
|
|
168
|
+
p.note(
|
|
169
|
+
"Hyperspell is a multi-tenant memory platform. Each user's memories\n" +
|
|
170
|
+
"are stored separately, identified by a User ID.\n\n" +
|
|
171
|
+
"For a personal agent, use your email address or username.",
|
|
172
|
+
"User ID",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
const systemUser = userInfo().username
|
|
176
|
+
const userId = await p.text({
|
|
177
|
+
message: "Enter a User ID for this agent",
|
|
178
|
+
placeholder: systemUser || "your-email@example.com",
|
|
179
|
+
defaultValue: systemUser,
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
if (p.isCancel(userId)) {
|
|
183
|
+
p.cancel("Setup cancelled")
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Step 4: List and connect integrations
|
|
188
|
+
let integrations: Awaited<ReturnType<typeof client.integrations.list>>
|
|
189
|
+
try {
|
|
190
|
+
integrations = await client.integrations.list()
|
|
191
|
+
} catch (_error) {
|
|
192
|
+
integrations = { integrations: [] }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (integrations.integrations.length === 0) {
|
|
196
|
+
p.note(
|
|
197
|
+
"No integrations are configured in your app yet.\n" +
|
|
198
|
+
"Go to https://app.hyperspell.com to add integrations,\n" +
|
|
199
|
+
"then use /connect <source> to connect them.",
|
|
200
|
+
"Connect Your Apps",
|
|
201
|
+
)
|
|
202
|
+
} else {
|
|
203
|
+
const integrationList = integrations.integrations
|
|
204
|
+
.map((int) => `• ${int.name} (${int.provider})`)
|
|
205
|
+
.join("\n")
|
|
206
|
+
|
|
207
|
+
// Get a user token for the connect page
|
|
208
|
+
let connectUrl: string
|
|
209
|
+
try {
|
|
210
|
+
const tokenResponse = await client.auth.userToken({ user_id: userId })
|
|
211
|
+
connectUrl = `https://connect.hyperspell.com?token=${tokenResponse.token}`
|
|
212
|
+
} catch (_error) {
|
|
213
|
+
p.log.error("Could not generate connect URL. You can connect apps later using /connect.")
|
|
214
|
+
connectUrl = ""
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
p.note(
|
|
218
|
+
`Available integrations:\n${integrationList}` +
|
|
219
|
+
(connectUrl ? `\n\nConnect your accounts at:\n${connectUrl}` : ""),
|
|
220
|
+
"Connect Your Apps",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if (connectUrl) {
|
|
224
|
+
const openConnect = await p.confirm({
|
|
225
|
+
message: "Open connection page in your browser?",
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
if (!p.isCancel(openConnect) && openConnect) {
|
|
229
|
+
await openUrl(connectUrl)
|
|
230
|
+
p.log.info("Browser opened. Connect your accounts and come back when done.")
|
|
231
|
+
|
|
232
|
+
await p.confirm({
|
|
233
|
+
message: "Finished connecting accounts?",
|
|
234
|
+
active: "Yes",
|
|
235
|
+
inactive: "Not yet",
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Fetch connected sources
|
|
242
|
+
const s1 = p.spinner()
|
|
243
|
+
s1.start("Fetching connected sources")
|
|
244
|
+
const sources = await fetchConnectionSources(client, userId)
|
|
245
|
+
s1.stop(`Found ${sources.length} sources: ${sources.join(", ")}`)
|
|
246
|
+
|
|
247
|
+
// Step 5: Ask about memory sync
|
|
248
|
+
p.note(
|
|
249
|
+
"OpenClaw can automatically sync markdown files in your workspace's\n" +
|
|
250
|
+
"memory/ directory with Hyperspell. This allows you to:\n\n" +
|
|
251
|
+
"• Store notes and context that persist across sessions\n" +
|
|
252
|
+
"• Have the AI reference your local documentation\n" +
|
|
253
|
+
"• Keep local files in sync with Hyperspell's search",
|
|
254
|
+
"Memory Sync",
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
const syncMemories = await p.confirm({
|
|
258
|
+
message: "Enable automatic memory sync for markdown files?",
|
|
259
|
+
initialValue: true,
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
if (p.isCancel(syncMemories)) {
|
|
263
|
+
p.cancel("Setup cancelled")
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Step 5: Save configuration
|
|
268
|
+
const s2 = p.spinner()
|
|
269
|
+
s2.start("Saving configuration")
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const configPath = resolveConfigPath()
|
|
273
|
+
const openclawDir = path.dirname(configPath)
|
|
274
|
+
const envPath = path.join(openclawDir, ".env")
|
|
275
|
+
|
|
276
|
+
// Ensure directory exists
|
|
277
|
+
if (!fs.existsSync(openclawDir)) {
|
|
278
|
+
fs.mkdirSync(openclawDir, { recursive: true })
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Read existing config or create new one
|
|
282
|
+
let config: Record<string, unknown> = {}
|
|
283
|
+
if (fs.existsSync(configPath)) {
|
|
284
|
+
const existing = fs.readFileSync(configPath, "utf-8")
|
|
285
|
+
config = JSON.parse(existing)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Merge in plugin configuration
|
|
289
|
+
if (!config.plugins) {
|
|
290
|
+
config.plugins = {}
|
|
291
|
+
}
|
|
292
|
+
const plugins = config.plugins as Record<string, unknown>
|
|
293
|
+
if (!plugins.entries) {
|
|
294
|
+
plugins.entries = {}
|
|
295
|
+
}
|
|
296
|
+
const entries = plugins.entries as Record<string, unknown>
|
|
297
|
+
entries["openclaw-hyperspell"] = {
|
|
298
|
+
enabled: true,
|
|
299
|
+
config: {
|
|
300
|
+
apiKey: "${HYPERSPELL_API_KEY}",
|
|
301
|
+
userId,
|
|
302
|
+
sources: sources.join(","),
|
|
303
|
+
autoContext: true,
|
|
304
|
+
syncMemories,
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Write config
|
|
309
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n")
|
|
310
|
+
|
|
311
|
+
// Write or append to .env
|
|
312
|
+
const envLine = `HYPERSPELL_API_KEY=${apiKey}`
|
|
313
|
+
if (fs.existsSync(envPath)) {
|
|
314
|
+
const envContent = fs.readFileSync(envPath, "utf-8")
|
|
315
|
+
if (envContent.includes("HYPERSPELL_API_KEY=")) {
|
|
316
|
+
// Replace existing line
|
|
317
|
+
const updated = envContent.replace(/^HYPERSPELL_API_KEY=.*$/m, envLine)
|
|
318
|
+
fs.writeFileSync(envPath, updated)
|
|
319
|
+
} else {
|
|
320
|
+
// Append new line
|
|
321
|
+
fs.appendFileSync(envPath, (envContent.endsWith("\n") ? "" : "\n") + envLine + "\n")
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
fs.writeFileSync(envPath, envLine + "\n")
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
s2.stop("Configuration saved")
|
|
328
|
+
|
|
329
|
+
p.note(
|
|
330
|
+
`Config: ${configPath}\n` +
|
|
331
|
+
`API Key: ${envPath}`,
|
|
332
|
+
"Files updated",
|
|
333
|
+
)
|
|
334
|
+
} catch (error) {
|
|
335
|
+
s2.stop("Failed to save configuration")
|
|
336
|
+
|
|
337
|
+
// Fall back to showing manual instructions
|
|
338
|
+
const configJson = JSON.stringify(
|
|
339
|
+
{
|
|
340
|
+
plugins: {
|
|
341
|
+
entries: {
|
|
342
|
+
"openclaw-hyperspell": {
|
|
343
|
+
enabled: true,
|
|
344
|
+
config: {
|
|
345
|
+
apiKey: "${HYPERSPELL_API_KEY}",
|
|
346
|
+
userId,
|
|
347
|
+
sources: sources.join(","),
|
|
348
|
+
autoContext: true,
|
|
349
|
+
syncMemories,
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
null,
|
|
356
|
+
2,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
p.note(
|
|
360
|
+
`Add to your openclaw.json:\n\n${configJson}\n\n` +
|
|
361
|
+
`Set the environment variable:\n export HYPERSPELL_API_KEY=${apiKey}`,
|
|
362
|
+
"Manual configuration required",
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Step 7: Sync existing memories if enabled
|
|
367
|
+
if (syncMemories) {
|
|
368
|
+
const workspaceDir = getWorkspaceDir()
|
|
369
|
+
const memoryFiles = getMemoryFiles(workspaceDir)
|
|
370
|
+
|
|
371
|
+
if (memoryFiles.length > 0) {
|
|
372
|
+
const s3 = p.spinner()
|
|
373
|
+
s3.start(`Syncing ${memoryFiles.length} memory file(s)`)
|
|
374
|
+
|
|
375
|
+
const hyperspellClient = new HyperspellClient({
|
|
376
|
+
apiKey,
|
|
377
|
+
userId,
|
|
378
|
+
autoContext: true,
|
|
379
|
+
syncMemories: true,
|
|
380
|
+
sources: [],
|
|
381
|
+
maxResults: 10,
|
|
382
|
+
debug: false,
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
const result = await syncAllMemoryFiles(hyperspellClient, workspaceDir)
|
|
386
|
+
|
|
387
|
+
if (result.failed > 0) {
|
|
388
|
+
s3.stop(`Synced ${result.synced} files, ${result.failed} failed`)
|
|
389
|
+
for (const error of result.errors) {
|
|
390
|
+
p.log.error(` ${error}`)
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
s3.stop(`Synced ${result.synced} memory files`)
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
p.log.info("No memory files found in memory/ directory")
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const syncNote = syncMemories
|
|
401
|
+
? "\n\nMemory sync is enabled — markdown files in memory/ will be\n" +
|
|
402
|
+
"automatically synced to Hyperspell when they change."
|
|
403
|
+
: ""
|
|
404
|
+
|
|
405
|
+
p.note(
|
|
406
|
+
"/getcontext <query> Search your memories for relevant context\n" +
|
|
407
|
+
"/remember <text> Save something directly to your vault\n\n" +
|
|
408
|
+
"To connect more apps, run: openclaw openclaw-hyperspell connect\n\n" +
|
|
409
|
+
"Auto-context is enabled by default — relevant memories are\n" +
|
|
410
|
+
"automatically injected before each AI response." +
|
|
411
|
+
syncNote,
|
|
412
|
+
"How to use Hyperspell",
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
p.outro("Setup complete!")
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function runConnect(pluginConfig: unknown): Promise<void> {
|
|
419
|
+
const config = pluginConfig as Record<string, unknown> | undefined
|
|
420
|
+
|
|
421
|
+
p.intro("Hyperspell Connect")
|
|
422
|
+
|
|
423
|
+
if (!config?.apiKey) {
|
|
424
|
+
p.log.error("Not configured")
|
|
425
|
+
p.note("Run 'openclaw openclaw-hyperspell setup' to configure Hyperspell first.")
|
|
426
|
+
p.outro("")
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const s = p.spinner()
|
|
431
|
+
s.start("Generating connect URL")
|
|
432
|
+
|
|
433
|
+
let client: Hyperspell
|
|
434
|
+
let userId: string
|
|
435
|
+
try {
|
|
436
|
+
client = new Hyperspell({ apiKey: config.apiKey as string })
|
|
437
|
+
userId = (config.userId as string) || userInfo().username || "user"
|
|
438
|
+
const tokenResponse = await client.auth.userToken({ user_id: userId })
|
|
439
|
+
const connectUrl = `https://connect.hyperspell.com?token=${tokenResponse.token}`
|
|
440
|
+
|
|
441
|
+
s.stop("Connect URL ready")
|
|
442
|
+
|
|
443
|
+
await openUrl(connectUrl)
|
|
444
|
+
p.log.success("Browser opened to connect.hyperspell.com")
|
|
445
|
+
|
|
446
|
+
await p.confirm({
|
|
447
|
+
message: "Finished connecting accounts?",
|
|
448
|
+
active: "Yes",
|
|
449
|
+
inactive: "Not yet",
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
// Fetch and update sources
|
|
453
|
+
const s2 = p.spinner()
|
|
454
|
+
s2.start("Updating sources configuration")
|
|
455
|
+
|
|
456
|
+
const sources = await fetchConnectionSources(client, userId)
|
|
457
|
+
const configPath = resolveConfigPath()
|
|
458
|
+
updateConfigSources(configPath, sources)
|
|
459
|
+
|
|
460
|
+
s2.stop(`Sources updated: ${sources.join(", ")}`)
|
|
461
|
+
|
|
462
|
+
p.outro("Done! Restart OpenClaw to apply changes.")
|
|
463
|
+
} catch (_error) {
|
|
464
|
+
s.stop("Failed to generate connect URL")
|
|
465
|
+
p.log.error("Check your API key and try again.")
|
|
466
|
+
p.outro("")
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function runStatus(pluginConfig: unknown): Promise<void> {
|
|
471
|
+
const config = pluginConfig as Record<string, unknown> | undefined
|
|
472
|
+
|
|
473
|
+
p.intro("Hyperspell Status")
|
|
474
|
+
|
|
475
|
+
if (!config?.apiKey) {
|
|
476
|
+
p.log.warn("Not configured")
|
|
477
|
+
p.note("Run 'openclaw openclaw-hyperspell setup' to configure Hyperspell.")
|
|
478
|
+
p.outro("")
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
p.log.success("Configured")
|
|
483
|
+
p.log.info(`User ID: ${config.userId || "(not set)"}`)
|
|
484
|
+
p.log.info(`Auto-Context: ${config.autoContext !== false ? "Enabled" : "Disabled"}`)
|
|
485
|
+
p.log.info(`Memory Sync: ${config.syncMemories ? "Enabled" : "Disabled"}`)
|
|
486
|
+
p.log.info(`Sources Filter: ${config.sources || "(all sources)"}`)
|
|
487
|
+
p.log.info(`Max Results: ${config.maxResults || 10}`)
|
|
488
|
+
|
|
489
|
+
const s = p.spinner()
|
|
490
|
+
s.start("Testing connection")
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
const client = new Hyperspell({ apiKey: config.apiKey as string })
|
|
494
|
+
const integrations = await client.integrations.list()
|
|
495
|
+
s.stop(`Connection OK (${integrations.integrations.length} integrations available)`)
|
|
496
|
+
|
|
497
|
+
if (integrations.integrations.length > 0) {
|
|
498
|
+
const list = integrations.integrations
|
|
499
|
+
.map((int) => `• ${int.name} (${int.provider})`)
|
|
500
|
+
.join("\n")
|
|
501
|
+
p.note(list, "Available integrations")
|
|
502
|
+
}
|
|
503
|
+
} catch (_error) {
|
|
504
|
+
s.stop("Connection failed")
|
|
505
|
+
p.log.error("Check your API key and try again.")
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
p.outro("")
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function registerCliCommands(program: Command, pluginConfig: unknown): void {
|
|
512
|
+
const hyperspellCmd = program
|
|
513
|
+
.command("openclaw-hyperspell")
|
|
514
|
+
.description("Hyperspell — Memory and context for your AI agent")
|
|
515
|
+
|
|
516
|
+
hyperspellCmd
|
|
517
|
+
.command("setup")
|
|
518
|
+
.description("Interactive setup wizard for Hyperspell")
|
|
519
|
+
.action(async () => {
|
|
520
|
+
await runSetup()
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
hyperspellCmd
|
|
524
|
+
.command("status")
|
|
525
|
+
.description("Show Hyperspell connection status")
|
|
526
|
+
.action(async () => {
|
|
527
|
+
await runStatus(pluginConfig)
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
hyperspellCmd
|
|
531
|
+
.command("connect")
|
|
532
|
+
.description("Open the Hyperspell connect page to link your accounts")
|
|
533
|
+
.action(async () => {
|
|
534
|
+
await runConnect(pluginConfig)
|
|
535
|
+
})
|
|
536
|
+
}
|
package/commands/slash.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
2
2
|
import type { HyperspellClient } from "../client.ts"
|
|
3
3
|
import type { HyperspellConfig } from "../config.ts"
|
|
4
|
-
import {
|
|
4
|
+
import { getWorkspaceDir } from "../config.ts"
|
|
5
5
|
import { log } from "../logger.ts"
|
|
6
|
+
import { syncAllMemoryFiles } from "../sync/markdown.ts"
|
|
6
7
|
|
|
7
8
|
function truncate(text: string, maxLength: number): string {
|
|
8
9
|
if (text.length <= maxLength) return text
|
|
@@ -56,59 +57,6 @@ export function registerCommands(
|
|
|
56
57
|
},
|
|
57
58
|
})
|
|
58
59
|
|
|
59
|
-
// /connect <source> - Open connection URL for an integration
|
|
60
|
-
api.registerCommand({
|
|
61
|
-
name: "connect",
|
|
62
|
-
description: "Connect an account to Hyperspell",
|
|
63
|
-
acceptsArgs: true,
|
|
64
|
-
requireAuth: true,
|
|
65
|
-
handler: async (ctx: { args?: string }) => {
|
|
66
|
-
const source = ctx.args?.trim().toLowerCase()
|
|
67
|
-
if (!source) {
|
|
68
|
-
return { text: "Usage: /connect <source>\n\nExamples: /connect notion, /connect slack" }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
log.debug(`/connect command: "${source}"`)
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
const integrations = await client.listIntegrations()
|
|
75
|
-
|
|
76
|
-
// Find matching integration by provider or name
|
|
77
|
-
const integration = integrations.find(
|
|
78
|
-
(int) =>
|
|
79
|
-
int.provider.toLowerCase() === source ||
|
|
80
|
-
int.name.toLowerCase() === source ||
|
|
81
|
-
int.id.toLowerCase() === source,
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
if (!integration) {
|
|
85
|
-
const available = integrations.map((i) => i.provider).join(", ")
|
|
86
|
-
return {
|
|
87
|
-
text: `Integration "${source}" not found.\n\nAvailable: ${available}`,
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const { url } = await client.getConnectUrl(integration.id)
|
|
92
|
-
|
|
93
|
-
// Auto-open in browser
|
|
94
|
-
try {
|
|
95
|
-
await openInBrowser(url)
|
|
96
|
-
return {
|
|
97
|
-
text: `Opening ${integration.name} connection in your browser...`,
|
|
98
|
-
}
|
|
99
|
-
} catch {
|
|
100
|
-
// Fall back to showing the URL if browser open fails
|
|
101
|
-
return {
|
|
102
|
-
text: `Connect your ${integration.name} account:\n${url}`,
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
} catch (err) {
|
|
106
|
-
log.error("/connect failed", err)
|
|
107
|
-
return { text: "Failed to get connect URL. Check logs for details." }
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
})
|
|
111
|
-
|
|
112
60
|
// /remember <text> - Add a new memory
|
|
113
61
|
api.registerCommand({
|
|
114
62
|
name: "remember",
|
|
@@ -136,4 +84,36 @@ export function registerCommands(
|
|
|
136
84
|
}
|
|
137
85
|
},
|
|
138
86
|
})
|
|
87
|
+
|
|
88
|
+
// /sync - Manually sync memory files
|
|
89
|
+
api.registerCommand({
|
|
90
|
+
name: "sync",
|
|
91
|
+
description: "Sync memory/*.md files with Hyperspell",
|
|
92
|
+
acceptsArgs: false,
|
|
93
|
+
requireAuth: true,
|
|
94
|
+
handler: async () => {
|
|
95
|
+
log.debug("/sync command")
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const workspaceDir = getWorkspaceDir()
|
|
99
|
+
const result = await syncAllMemoryFiles(client, workspaceDir)
|
|
100
|
+
|
|
101
|
+
if (result.synced === 0 && result.failed === 0) {
|
|
102
|
+
return { text: "No memory files found in memory/ directory." }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (result.failed > 0) {
|
|
106
|
+
const errors = result.errors.map((e) => ` • ${e}`).join("\n")
|
|
107
|
+
return {
|
|
108
|
+
text: `Synced ${result.synced} files, ${result.failed} failed:\n${errors}`,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { text: `Synced ${result.synced} memory file(s) to Hyperspell.` }
|
|
113
|
+
} catch (err) {
|
|
114
|
+
log.error("/sync failed", err)
|
|
115
|
+
return { text: "Failed to sync memory files. Check logs for details." }
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
})
|
|
139
119
|
}
|