@hyperspell/openclaw-hyperspell 0.2.0 → 0.3.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/client.ts +10 -1
- package/commands/setup.ts +67 -1
- package/commands/slash.ts +34 -0
- package/config.ts +50 -0
- package/hooks/memory-sync.ts +63 -0
- package/index.ts +24 -2
- package/openclaw.plugin.json +8 -0
- package/package.json +3 -3
- package/types/openclaw.d.ts +1 -0
package/client.ts
CHANGED
|
@@ -138,16 +138,25 @@ export class HyperspellClient {
|
|
|
138
138
|
|
|
139
139
|
async addMemory(
|
|
140
140
|
text: string,
|
|
141
|
-
options?: {
|
|
141
|
+
options?: {
|
|
142
|
+
title?: string
|
|
143
|
+
resourceId?: string
|
|
144
|
+
collection?: string
|
|
145
|
+
metadata?: Record<string, string | number | boolean>
|
|
146
|
+
},
|
|
142
147
|
): Promise<{ resourceId: string }> {
|
|
143
148
|
log.debugRequest("memories.add", {
|
|
144
149
|
textLength: text.length,
|
|
145
150
|
title: options?.title,
|
|
151
|
+
resourceId: options?.resourceId,
|
|
152
|
+
collection: options?.collection,
|
|
146
153
|
})
|
|
147
154
|
|
|
148
155
|
const result = await this.client.memories.add({
|
|
149
156
|
text,
|
|
150
157
|
title: options?.title,
|
|
158
|
+
resource_id: options?.resourceId,
|
|
159
|
+
collection: options?.collection,
|
|
151
160
|
metadata: {
|
|
152
161
|
...options?.metadata,
|
|
153
162
|
openclaw_source: "command",
|
package/commands/setup.ts
CHANGED
|
@@ -5,6 +5,9 @@ import { homedir, platform, 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
|
+
import { syncAllMemoryFiles, getMemoryFiles } from "../sync/markdown.ts"
|
|
9
|
+
import { HyperspellClient } from "../client.ts"
|
|
10
|
+
import { getWorkspaceDir } from "../config.ts"
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Resolve OpenClaw state directory, matching OpenClaw's logic.
|
|
@@ -241,6 +244,26 @@ async function runSetup(): Promise<void> {
|
|
|
241
244
|
const sources = await fetchConnectionSources(client, userId)
|
|
242
245
|
s1.stop(`Found ${sources.length} sources: ${sources.join(", ")}`)
|
|
243
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
|
+
|
|
244
267
|
// Step 5: Save configuration
|
|
245
268
|
const s2 = p.spinner()
|
|
246
269
|
s2.start("Saving configuration")
|
|
@@ -278,6 +301,7 @@ async function runSetup(): Promise<void> {
|
|
|
278
301
|
userId,
|
|
279
302
|
sources: sources.join(","),
|
|
280
303
|
autoContext: true,
|
|
304
|
+
syncMemories,
|
|
281
305
|
},
|
|
282
306
|
}
|
|
283
307
|
|
|
@@ -322,6 +346,7 @@ async function runSetup(): Promise<void> {
|
|
|
322
346
|
userId,
|
|
323
347
|
sources: sources.join(","),
|
|
324
348
|
autoContext: true,
|
|
349
|
+
syncMemories,
|
|
325
350
|
},
|
|
326
351
|
},
|
|
327
352
|
},
|
|
@@ -338,12 +363,52 @@ async function runSetup(): Promise<void> {
|
|
|
338
363
|
)
|
|
339
364
|
}
|
|
340
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
|
+
|
|
341
405
|
p.note(
|
|
342
406
|
"/getcontext <query> Search your memories for relevant context\n" +
|
|
343
407
|
"/remember <text> Save something directly to your vault\n\n" +
|
|
344
408
|
"To connect more apps, run: openclaw openclaw-hyperspell connect\n\n" +
|
|
345
409
|
"Auto-context is enabled by default — relevant memories are\n" +
|
|
346
|
-
"automatically injected before each AI response."
|
|
410
|
+
"automatically injected before each AI response." +
|
|
411
|
+
syncNote,
|
|
347
412
|
"How to use Hyperspell",
|
|
348
413
|
)
|
|
349
414
|
|
|
@@ -417,6 +482,7 @@ async function runStatus(pluginConfig: unknown): Promise<void> {
|
|
|
417
482
|
p.log.success("Configured")
|
|
418
483
|
p.log.info(`User ID: ${config.userId || "(not set)"}`)
|
|
419
484
|
p.log.info(`Auto-Context: ${config.autoContext !== false ? "Enabled" : "Disabled"}`)
|
|
485
|
+
p.log.info(`Memory Sync: ${config.syncMemories ? "Enabled" : "Disabled"}`)
|
|
420
486
|
p.log.info(`Sources Filter: ${config.sources || "(all sources)"}`)
|
|
421
487
|
p.log.info(`Max Results: ${config.maxResults || 10}`)
|
|
422
488
|
|
package/commands/slash.ts
CHANGED
|
@@ -1,7 +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 { getWorkspaceDir } from "../config.ts"
|
|
4
5
|
import { log } from "../logger.ts"
|
|
6
|
+
import { syncAllMemoryFiles } from "../sync/markdown.ts"
|
|
5
7
|
|
|
6
8
|
function truncate(text: string, maxLength: number): string {
|
|
7
9
|
if (text.length <= maxLength) return text
|
|
@@ -82,4 +84,36 @@ export function registerCommands(
|
|
|
82
84
|
}
|
|
83
85
|
},
|
|
84
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
|
+
})
|
|
85
119
|
}
|
package/config.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type HyperspellConfig = {
|
|
|
14
14
|
apiKey: string
|
|
15
15
|
userId?: string
|
|
16
16
|
autoContext: boolean
|
|
17
|
+
syncMemories: boolean
|
|
17
18
|
sources: HyperspellSource[]
|
|
18
19
|
maxResults: number
|
|
19
20
|
debug: boolean
|
|
@@ -23,6 +24,7 @@ const ALLOWED_KEYS = [
|
|
|
23
24
|
"apiKey",
|
|
24
25
|
"userId",
|
|
25
26
|
"autoContext",
|
|
27
|
+
"syncMemories",
|
|
26
28
|
"sources",
|
|
27
29
|
"maxResults",
|
|
28
30
|
"debug",
|
|
@@ -130,6 +132,7 @@ export function parseConfig(raw: unknown): HyperspellConfig {
|
|
|
130
132
|
apiKey,
|
|
131
133
|
userId: cfg.userId as string | undefined,
|
|
132
134
|
autoContext: (cfg.autoContext as boolean) ?? true,
|
|
135
|
+
syncMemories: (cfg.syncMemories as boolean) ?? false,
|
|
133
136
|
sources: parseSources(cfg.sources as string | string[] | undefined),
|
|
134
137
|
maxResults: (cfg.maxResults as number) ?? 10,
|
|
135
138
|
debug: (cfg.debug as boolean) ?? false,
|
|
@@ -139,3 +142,50 @@ export function parseConfig(raw: unknown): HyperspellConfig {
|
|
|
139
142
|
export const hyperspellConfigSchema = {
|
|
140
143
|
parse: parseConfig,
|
|
141
144
|
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get the workspace directory from OpenClaw config
|
|
148
|
+
*/
|
|
149
|
+
export function getWorkspaceDir(): string {
|
|
150
|
+
const { homedir } = require("node:os")
|
|
151
|
+
const fs = require("node:fs")
|
|
152
|
+
const path = require("node:path")
|
|
153
|
+
|
|
154
|
+
// Resolve config path
|
|
155
|
+
const override = process.env.OPENCLAW_CONFIG_PATH?.trim() || process.env.CLAWDBOT_CONFIG_PATH?.trim()
|
|
156
|
+
let configPath: string
|
|
157
|
+
if (override) {
|
|
158
|
+
configPath = override.startsWith("~")
|
|
159
|
+
? override.replace(/^~(?=$|[\\/])/, homedir())
|
|
160
|
+
: path.resolve(override)
|
|
161
|
+
} else {
|
|
162
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim()
|
|
163
|
+
const resolvedStateDir = stateDir
|
|
164
|
+
? (stateDir.startsWith("~") ? stateDir.replace(/^~(?=$|[\\/])/, homedir()) : path.resolve(stateDir))
|
|
165
|
+
: path.join(homedir(), ".openclaw")
|
|
166
|
+
configPath = path.join(resolvedStateDir, "openclaw.json")
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Read workspace from config
|
|
170
|
+
if (fs.existsSync(configPath)) {
|
|
171
|
+
try {
|
|
172
|
+
const content = fs.readFileSync(configPath, "utf-8")
|
|
173
|
+
const config = JSON.parse(content)
|
|
174
|
+
const workspace = config?.agents?.defaults?.workspace
|
|
175
|
+
if (workspace) {
|
|
176
|
+
return workspace.startsWith("~")
|
|
177
|
+
? workspace.replace(/^~(?=$|[\\/])/, homedir())
|
|
178
|
+
: workspace
|
|
179
|
+
}
|
|
180
|
+
} catch (_e) {
|
|
181
|
+
// Fall back to default
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Default workspace
|
|
186
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim()
|
|
187
|
+
const resolvedStateDir = stateDir
|
|
188
|
+
? (stateDir.startsWith("~") ? stateDir.replace(/^~(?=$|[\\/])/, homedir()) : path.resolve(stateDir))
|
|
189
|
+
: path.join(homedir(), ".openclaw")
|
|
190
|
+
return path.join(resolvedStateDir, "workspace")
|
|
191
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as path from "node:path"
|
|
2
|
+
import type { HyperspellClient } from "../client.ts"
|
|
3
|
+
import type { HyperspellConfig } from "../config.ts"
|
|
4
|
+
import { getWorkspaceDir } from "../config.ts"
|
|
5
|
+
import { log } from "../logger.ts"
|
|
6
|
+
import { syncMarkdownFile, syncAllMemoryFiles } from "../sync/markdown.ts"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build a handler for file change events that syncs markdown files to Hyperspell
|
|
10
|
+
*/
|
|
11
|
+
export function buildFileSyncHandler(client: HyperspellClient, _cfg: HyperspellConfig) {
|
|
12
|
+
const workspaceDir = getWorkspaceDir()
|
|
13
|
+
const memoryDir = path.join(workspaceDir, "memory")
|
|
14
|
+
|
|
15
|
+
return async (event: Record<string, unknown>) => {
|
|
16
|
+
const filePath = event.file_path as string | undefined
|
|
17
|
+
if (!filePath) return
|
|
18
|
+
|
|
19
|
+
// Only process markdown files in the workspace's memory directory
|
|
20
|
+
if (!filePath.startsWith(memoryDir) || !filePath.endsWith(".md")) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const fileName = path.basename(filePath)
|
|
25
|
+
log.info(`Memory file changed: ${fileName}`)
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = await syncMarkdownFile(client, filePath)
|
|
29
|
+
if (result.success) {
|
|
30
|
+
log.info(`Synced ${fileName} -> ${result.resourceId}`)
|
|
31
|
+
} else {
|
|
32
|
+
log.error(`Failed to sync ${fileName}: ${result.error}`)
|
|
33
|
+
}
|
|
34
|
+
} catch (err) {
|
|
35
|
+
log.error(`Error syncing ${fileName}`, err)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Sync all existing memory files on startup
|
|
42
|
+
*/
|
|
43
|
+
export async function syncMemoriesOnStartup(
|
|
44
|
+
client: HyperspellClient,
|
|
45
|
+
workspaceDir: string,
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
log.info("Syncing existing memory files...")
|
|
48
|
+
|
|
49
|
+
const result = await syncAllMemoryFiles(client, workspaceDir)
|
|
50
|
+
|
|
51
|
+
if (result.synced > 0) {
|
|
52
|
+
log.info(`Synced ${result.synced} memory files`)
|
|
53
|
+
}
|
|
54
|
+
if (result.failed > 0) {
|
|
55
|
+
log.error(`Failed to sync ${result.failed} files:`)
|
|
56
|
+
for (const error of result.errors) {
|
|
57
|
+
log.error(` - ${error}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (result.synced === 0 && result.failed === 0) {
|
|
61
|
+
log.info("No memory files found in memory/ directory")
|
|
62
|
+
}
|
|
63
|
+
}
|
package/index.ts
CHANGED
|
@@ -2,8 +2,9 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
|
2
2
|
import { HyperspellClient } from "./client.ts"
|
|
3
3
|
import { registerCommands } from "./commands/slash.ts"
|
|
4
4
|
import { registerCliCommands } from "./commands/setup.ts"
|
|
5
|
-
import { parseConfig, hyperspellConfigSchema } from "./config.ts"
|
|
5
|
+
import { parseConfig, hyperspellConfigSchema, getWorkspaceDir } from "./config.ts"
|
|
6
6
|
import { buildAutoContextHandler } from "./hooks/auto-context.ts"
|
|
7
|
+
import { buildFileSyncHandler, syncMemoriesOnStartup } from "./hooks/memory-sync.ts"
|
|
7
8
|
import { initLogger } from "./logger.ts"
|
|
8
9
|
import { registerRememberTool } from "./tools/remember.ts"
|
|
9
10
|
import { registerSearchTool } from "./tools/search.ts"
|
|
@@ -49,6 +50,15 @@ export default {
|
|
|
49
50
|
return { text: "Hyperspell not configured. Run 'openclaw openclaw-hyperspell setup' first." }
|
|
50
51
|
},
|
|
51
52
|
})
|
|
53
|
+
api.registerCommand({
|
|
54
|
+
name: "sync",
|
|
55
|
+
description: "Sync memory/*.md files with Hyperspell",
|
|
56
|
+
acceptsArgs: false,
|
|
57
|
+
requireAuth: false,
|
|
58
|
+
handler: async () => {
|
|
59
|
+
return { text: "Hyperspell not configured. Run 'openclaw openclaw-hyperspell setup' first." }
|
|
60
|
+
},
|
|
61
|
+
})
|
|
52
62
|
return
|
|
53
63
|
}
|
|
54
64
|
|
|
@@ -68,14 +78,26 @@ export default {
|
|
|
68
78
|
api.on("before_agent_start", autoContextHandler)
|
|
69
79
|
}
|
|
70
80
|
|
|
81
|
+
// Register memory sync hook
|
|
82
|
+
if (cfg.syncMemories) {
|
|
83
|
+
const fileSyncHandler = buildFileSyncHandler(client, cfg)
|
|
84
|
+
api.on("file_changed", fileSyncHandler)
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
// Register slash commands
|
|
72
88
|
registerCommands(api, client, cfg)
|
|
73
89
|
|
|
74
90
|
// Register service for lifecycle management
|
|
75
91
|
api.registerService({
|
|
76
92
|
id: "openclaw-hyperspell",
|
|
77
|
-
start: () => {
|
|
93
|
+
start: async () => {
|
|
78
94
|
api.logger.info("hyperspell: connected")
|
|
95
|
+
|
|
96
|
+
// Sync memories on startup if enabled
|
|
97
|
+
if (cfg.syncMemories) {
|
|
98
|
+
const workspaceDir = getWorkspaceDir()
|
|
99
|
+
await syncMemoriesOnStartup(client, workspaceDir)
|
|
100
|
+
}
|
|
79
101
|
},
|
|
80
102
|
stop: () => {
|
|
81
103
|
api.logger.info("hyperspell: stopped")
|
package/openclaw.plugin.json
CHANGED
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
"label": "Debug Logging",
|
|
36
36
|
"help": "Enable verbose debug logs for API calls and responses",
|
|
37
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
|
|
38
43
|
}
|
|
39
44
|
},
|
|
40
45
|
"configSchema": {
|
|
@@ -60,6 +65,9 @@
|
|
|
60
65
|
},
|
|
61
66
|
"debug": {
|
|
62
67
|
"type": "boolean"
|
|
68
|
+
},
|
|
69
|
+
"syncMemories": {
|
|
70
|
+
"type": "boolean"
|
|
63
71
|
}
|
|
64
72
|
},
|
|
65
73
|
"required": []
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperspell/openclaw-hyperspell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw Hyperspell memory plugin",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
],
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "https://github.com/hyperspell/openclaw-hyperspell.git"
|
|
20
|
+
"url": "git+https://github.com/hyperspell/openclaw-hyperspell.git"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
|
23
23
|
"openclaw",
|
|
@@ -48,4 +48,4 @@
|
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"typescript": "^5.9.3"
|
|
50
50
|
}
|
|
51
|
-
}
|
|
51
|
+
}
|
package/types/openclaw.d.ts
CHANGED