@hyperspell/openclaw-hyperspell 0.4.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/package.json +2 -1
- package/sync/markdown.ts +183 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperspell/openclaw-hyperspell",
|
|
3
|
-
"version": "0.4.
|
|
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
|
+
}
|