@hyperspell/openclaw-hyperspell 0.3.1 → 0.4.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 +46 -11
- package/package.json +2 -1
- package/sync/markdown.ts +183 -0
package/README.md
CHANGED
|
@@ -23,10 +23,12 @@ The setup wizard will guide you through:
|
|
|
23
23
|
2. Configuring your API key
|
|
24
24
|
3. Setting up your User ID for multi-tenant memory
|
|
25
25
|
4. Connecting your apps (Notion, Slack, Google Drive, etc.)
|
|
26
|
+
5. Enabling memory sync for local markdown files
|
|
27
|
+
6. Syncing existing memory files to Hyperspell
|
|
26
28
|
|
|
27
29
|
## Manual Configuration
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
Add to your `openclaw.json`:
|
|
30
32
|
|
|
31
33
|
```json
|
|
32
34
|
{
|
|
@@ -37,26 +39,27 @@ Alternatively, add to your `openclaw.json`:
|
|
|
37
39
|
"config": {
|
|
38
40
|
"apiKey": "${HYPERSPELL_API_KEY}",
|
|
39
41
|
"userId": "your-email",
|
|
40
|
-
"autoContext": true
|
|
42
|
+
"autoContext": true,
|
|
43
|
+
"syncMemories": true,
|
|
44
|
+
"sources": "vault,notion,slack"
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
|
-
|
|
47
50
|
```
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
Set the environment variable in `~/.openclaw/.env`:
|
|
50
53
|
|
|
51
54
|
```bash
|
|
52
|
-
|
|
55
|
+
HYPERSPELL_API_KEY=hs_...
|
|
53
56
|
```
|
|
54
57
|
|
|
55
58
|
## CLI Commands
|
|
56
59
|
|
|
57
60
|
### `openclaw openclaw-hyperspell setup`
|
|
58
61
|
|
|
59
|
-
Interactive setup wizard that walks you through configuration.
|
|
62
|
+
Interactive setup wizard that walks you through configuration, connecting apps, and syncing memory files.
|
|
60
63
|
|
|
61
64
|
### `openclaw openclaw-hyperspell status`
|
|
62
65
|
|
|
@@ -64,16 +67,17 @@ Check your current configuration and connection status.
|
|
|
64
67
|
|
|
65
68
|
### `openclaw openclaw-hyperspell connect`
|
|
66
69
|
|
|
67
|
-
Open the Hyperspell connect page to link your accounts (Notion, Slack, Google Drive, etc.)
|
|
70
|
+
Open the Hyperspell connect page to link your accounts (Notion, Slack, Google Drive, etc.). After connecting, your sources are automatically updated in the config.
|
|
68
71
|
|
|
69
|
-
|
|
72
|
+
## Configuration Options
|
|
70
73
|
|
|
71
74
|
| Option | Type | Default | Description |
|
|
72
75
|
|--------|------|---------|-------------|
|
|
73
76
|
| `apiKey` | string | `${HYPERSPELL_API_KEY}` | Hyperspell API key |
|
|
74
|
-
| `userId` | string | - | User ID (can be your email) |
|
|
77
|
+
| `userId` | string | - | User ID for multi-tenant memory (can be your email) |
|
|
75
78
|
| `autoContext` | boolean | `true` | Auto-inject relevant memories before each AI turn |
|
|
76
|
-
| `
|
|
79
|
+
| `syncMemories` | boolean | `false` | Sync markdown files in `workspace/memory/` to Hyperspell |
|
|
80
|
+
| `sources` | string | - | Comma-separated sources to search (e.g., `vault,notion,slack`) |
|
|
77
81
|
| `maxResults` | number | `10` | Maximum memories per context injection |
|
|
78
82
|
| `debug` | boolean | `false` | Enable verbose logging |
|
|
79
83
|
|
|
@@ -95,6 +99,37 @@ Save something to memory.
|
|
|
95
99
|
/remember Meeting with Alice: discussed Q1 budget, need to follow up on headcount
|
|
96
100
|
```
|
|
97
101
|
|
|
102
|
+
### `/sync`
|
|
103
|
+
|
|
104
|
+
Manually sync all markdown files in `workspace/memory/` to Hyperspell.
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
/sync
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Memory Sync
|
|
111
|
+
|
|
112
|
+
When `syncMemories: true`, the plugin syncs markdown files from your agent's workspace memory directory (e.g., `~/.openclaw/workspace/memory/`) to Hyperspell. This includes all `.md` files in subdirectories.
|
|
113
|
+
|
|
114
|
+
**How it works:**
|
|
115
|
+
|
|
116
|
+
- Each markdown file is uploaded to Hyperspell as a memory in the `openclaw` collection
|
|
117
|
+
- The returned `resource_id` is stored in the file's YAML frontmatter as `hyperspell_id`
|
|
118
|
+
- On subsequent syncs, files with an existing `hyperspell_id` are updated rather than duplicated
|
|
119
|
+
- Files are synced automatically on startup and when they change
|
|
120
|
+
|
|
121
|
+
**Example frontmatter after sync:**
|
|
122
|
+
|
|
123
|
+
```markdown
|
|
124
|
+
---
|
|
125
|
+
title: Meeting Notes
|
|
126
|
+
hyperspell_id: abc123-def456
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
# Meeting Notes
|
|
130
|
+
...
|
|
131
|
+
```
|
|
132
|
+
|
|
98
133
|
## AI Tools
|
|
99
134
|
|
|
100
135
|
The plugin registers tools that the AI can use autonomously:
|
|
@@ -114,7 +149,7 @@ This ensures the AI always has access to relevant information from your connecte
|
|
|
114
149
|
|
|
115
150
|
## Available Sources
|
|
116
151
|
|
|
117
|
-
- `vault` - User-created memories
|
|
152
|
+
- `vault` - User-created or synced memories
|
|
118
153
|
- `notion` - Notion pages and databases
|
|
119
154
|
- `slack` - Slack messages
|
|
120
155
|
- `google_calendar` - Google Calendar events
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperspell/openclaw-hyperspell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw Hyperspell memory plugin",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"commands/",
|
|
11
11
|
"hooks/",
|
|
12
12
|
"tools/",
|
|
13
|
+
"sync/",
|
|
13
14
|
"lib/",
|
|
14
15
|
"types/",
|
|
15
16
|
"openclaw.plugin.json",
|
package/sync/markdown.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import * as fs from "node:fs"
|
|
2
|
+
import * as path from "node:path"
|
|
3
|
+
import type { HyperspellClient } from "../client.ts"
|
|
4
|
+
import { log } from "../logger.ts"
|
|
5
|
+
|
|
6
|
+
const FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---\n?/
|
|
7
|
+
|
|
8
|
+
interface MarkdownFile {
|
|
9
|
+
filePath: string
|
|
10
|
+
title: string
|
|
11
|
+
content: string
|
|
12
|
+
hyperspellId: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse frontmatter from markdown content
|
|
17
|
+
*/
|
|
18
|
+
function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
|
|
19
|
+
const match = content.match(FRONTMATTER_REGEX)
|
|
20
|
+
if (!match) {
|
|
21
|
+
return { frontmatter: {}, body: content }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const frontmatterText = match[1]
|
|
25
|
+
const body = content.slice(match[0].length)
|
|
26
|
+
const frontmatter: Record<string, string> = {}
|
|
27
|
+
|
|
28
|
+
for (const line of frontmatterText.split("\n")) {
|
|
29
|
+
const colonIndex = line.indexOf(":")
|
|
30
|
+
if (colonIndex > 0) {
|
|
31
|
+
const key = line.slice(0, colonIndex).trim()
|
|
32
|
+
const value = line.slice(colonIndex + 1).trim()
|
|
33
|
+
frontmatter[key] = value
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { frontmatter, body }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Serialize frontmatter back to string
|
|
42
|
+
*/
|
|
43
|
+
function serializeFrontmatter(frontmatter: Record<string, string>): string {
|
|
44
|
+
const lines = Object.entries(frontmatter).map(([key, value]) => `${key}: ${value}`)
|
|
45
|
+
return `---\n${lines.join("\n")}\n---\n`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Read a markdown file and parse its content
|
|
50
|
+
*/
|
|
51
|
+
function readMarkdownFile(filePath: string): MarkdownFile | null {
|
|
52
|
+
try {
|
|
53
|
+
const content = fs.readFileSync(filePath, "utf-8")
|
|
54
|
+
const { frontmatter, body } = parseFrontmatter(content)
|
|
55
|
+
const title = frontmatter.title || path.basename(filePath, ".md")
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
filePath,
|
|
59
|
+
title,
|
|
60
|
+
content: body.trim(),
|
|
61
|
+
hyperspellId: frontmatter.hyperspell_id || null,
|
|
62
|
+
}
|
|
63
|
+
} catch (err) {
|
|
64
|
+
log.error(`Failed to read markdown file: ${filePath}`, err)
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Update the hyperspell_id in the frontmatter of a markdown file
|
|
71
|
+
*/
|
|
72
|
+
function updateFrontmatterId(filePath: string, hyperspellId: string): void {
|
|
73
|
+
try {
|
|
74
|
+
const content = fs.readFileSync(filePath, "utf-8")
|
|
75
|
+
const { frontmatter, body } = parseFrontmatter(content)
|
|
76
|
+
|
|
77
|
+
frontmatter.hyperspell_id = hyperspellId
|
|
78
|
+
|
|
79
|
+
const newContent = serializeFrontmatter(frontmatter) + body
|
|
80
|
+
fs.writeFileSync(filePath, newContent)
|
|
81
|
+
|
|
82
|
+
log.debug(`Updated frontmatter in ${filePath} with hyperspell_id: ${hyperspellId}`)
|
|
83
|
+
} catch (err) {
|
|
84
|
+
log.error(`Failed to update frontmatter in ${filePath}`, err)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get all markdown files from the memory directory, including subdirectories
|
|
90
|
+
*/
|
|
91
|
+
export function getMemoryFiles(workspaceDir: string): string[] {
|
|
92
|
+
const memoryDir = path.join(workspaceDir, "memory")
|
|
93
|
+
|
|
94
|
+
if (!fs.existsSync(memoryDir)) {
|
|
95
|
+
return []
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const results: string[] = []
|
|
99
|
+
|
|
100
|
+
function walk(dir: string): void {
|
|
101
|
+
try {
|
|
102
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
103
|
+
const fullPath = path.join(dir, entry.name)
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
walk(fullPath)
|
|
106
|
+
} else if (entry.name.endsWith(".md")) {
|
|
107
|
+
results.push(fullPath)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
log.error(`Failed to read directory: ${dir}`, err)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
walk(memoryDir)
|
|
116
|
+
return results
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Sync a single markdown file to Hyperspell
|
|
121
|
+
*/
|
|
122
|
+
export async function syncMarkdownFile(
|
|
123
|
+
client: HyperspellClient,
|
|
124
|
+
filePath: string,
|
|
125
|
+
): Promise<{ success: boolean; resourceId?: string; error?: string }> {
|
|
126
|
+
const file = readMarkdownFile(filePath)
|
|
127
|
+
if (!file) {
|
|
128
|
+
return { success: false, error: "Failed to read file" }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!file.content) {
|
|
132
|
+
return { success: false, error: "File has no content" }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const result = await client.addMemory(file.content, {
|
|
137
|
+
title: file.title,
|
|
138
|
+
resourceId: file.hyperspellId || undefined,
|
|
139
|
+
collection: "openclaw",
|
|
140
|
+
metadata: {
|
|
141
|
+
openclaw_source: "memory_sync",
|
|
142
|
+
file_path: filePath,
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Update frontmatter with new resource ID if it changed or was newly created
|
|
147
|
+
if (result.resourceId !== file.hyperspellId) {
|
|
148
|
+
updateFrontmatterId(filePath, result.resourceId)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { success: true, resourceId: result.resourceId }
|
|
152
|
+
} catch (err) {
|
|
153
|
+
const errorMsg = err instanceof Error ? err.message : String(err)
|
|
154
|
+
log.error(`Failed to sync ${filePath}`, err)
|
|
155
|
+
return { success: false, error: errorMsg }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Sync all markdown files in the memory directory
|
|
161
|
+
*/
|
|
162
|
+
export async function syncAllMemoryFiles(
|
|
163
|
+
client: HyperspellClient,
|
|
164
|
+
workspaceDir: string,
|
|
165
|
+
): Promise<{ synced: number; failed: number; errors: string[] }> {
|
|
166
|
+
const files = getMemoryFiles(workspaceDir)
|
|
167
|
+
let synced = 0
|
|
168
|
+
let failed = 0
|
|
169
|
+
const errors: string[] = []
|
|
170
|
+
|
|
171
|
+
for (const filePath of files) {
|
|
172
|
+
const result = await syncMarkdownFile(client, filePath)
|
|
173
|
+
if (result.success) {
|
|
174
|
+
synced++
|
|
175
|
+
log.info(`Synced: ${path.basename(filePath)} -> ${result.resourceId}`)
|
|
176
|
+
} else {
|
|
177
|
+
failed++
|
|
178
|
+
errors.push(`${path.basename(filePath)}: ${result.error}`)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { synced, failed, errors }
|
|
183
|
+
}
|