@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 CHANGED
@@ -138,16 +138,25 @@ export class HyperspellClient {
138
138
 
139
139
  async addMemory(
140
140
  text: string,
141
- options?: { title?: string; metadata?: Record<string, string | number | boolean> },
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")
@@ -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.2.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
+ }
@@ -17,6 +17,7 @@ declare module "openclaw/plugin-sdk" {
17
17
 
18
18
  export interface OpenClawPluginApi {
19
19
  pluginConfig: unknown
20
+ workspaceDir?: string
20
21
  logger: {
21
22
  info: (message: string, ...args: unknown[]) => void
22
23
  warn: (message: string, ...args: unknown[]) => void