@hyperspell/openclaw-hyperspell 0.12.0 → 0.13.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.
Files changed (50) hide show
  1. package/README.md +37 -1
  2. package/dist/client.js +341 -0
  3. package/dist/commands/setup.js +568 -0
  4. package/dist/commands/slash.js +159 -0
  5. package/dist/config.js +349 -0
  6. package/{graph/cron.ts → dist/graph/cron.js} +11 -15
  7. package/dist/graph/index.js +4 -0
  8. package/dist/graph/ops.js +221 -0
  9. package/dist/graph/state.js +68 -0
  10. package/dist/graph/tools.js +87 -0
  11. package/dist/hooks/auto-context.js +199 -0
  12. package/dist/hooks/auto-trace.js +143 -0
  13. package/dist/hooks/emotional-state.js +155 -0
  14. package/dist/hooks/memory-sync.js +54 -0
  15. package/dist/hooks/startup-orientation.js +236 -0
  16. package/dist/index.js +132 -0
  17. package/dist/lib/browser.js +29 -0
  18. package/dist/lib/sender.js +173 -0
  19. package/dist/lib/voice-id.js +22 -0
  20. package/dist/logger.js +32 -0
  21. package/dist/sync/markdown.js +151 -0
  22. package/dist/tools/remember.js +97 -0
  23. package/dist/tools/search.js +87 -0
  24. package/openclaw.plugin.json +25 -1
  25. package/package.json +7 -12
  26. package/client.ts +0 -566
  27. package/commands/setup.ts +0 -673
  28. package/commands/slash.ts +0 -198
  29. package/config.test.ts +0 -202
  30. package/config.ts +0 -497
  31. package/graph/index.ts +0 -5
  32. package/graph/ops.ts +0 -259
  33. package/graph/state.ts +0 -79
  34. package/graph/tools.ts +0 -117
  35. package/hooks/auto-context.ts +0 -272
  36. package/hooks/auto-trace.test.ts +0 -81
  37. package/hooks/auto-trace.ts +0 -197
  38. package/hooks/emotional-state.test.ts +0 -160
  39. package/hooks/emotional-state.ts +0 -179
  40. package/hooks/memory-sync.ts +0 -65
  41. package/index.ts +0 -152
  42. package/lib/browser.ts +0 -31
  43. package/lib/sender.test.ts +0 -234
  44. package/lib/sender.ts +0 -234
  45. package/lib/voice-id.ts +0 -39
  46. package/logger.ts +0 -41
  47. package/sync/markdown.ts +0 -186
  48. package/tools/remember.ts +0 -132
  49. package/tools/search.ts +0 -131
  50. package/types/openclaw.d.ts +0 -76
package/graph/ops.ts DELETED
@@ -1,259 +0,0 @@
1
- import * as fs from "node:fs"
2
- import * as path from "node:path"
3
- import type { HyperspellClient } from "../client.ts"
4
- import type { HyperspellConfig } from "../config.ts"
5
- import { getAllUserIds } from "../lib/sender.ts"
6
- import { log } from "../logger.ts"
7
- import { NetworkStateManager } from "./state.ts"
8
-
9
- const ENTITY_TYPES = ["people", "projects", "organizations", "topics"] as const
10
- export type EntityType = (typeof ENTITY_TYPES)[number]
11
-
12
- export interface SourceMemories {
13
- [source: string]: string[]
14
- }
15
-
16
- export interface ScannedMemory {
17
- resourceId: string
18
- source: string
19
- title: string | null
20
- summary: string
21
- }
22
-
23
- export interface WriteEntityParams {
24
- type: EntityType
25
- slug: string
26
- name: string
27
- description: string
28
- relationships?: string[]
29
- sourceMemories?: SourceMemories
30
- email?: string
31
- phone?: string
32
- domain?: string
33
- }
34
-
35
- export function slugify(name: string): string {
36
- return name
37
- .toLowerCase()
38
- .trim()
39
- .replace(/[^a-z0-9]+/g, "-")
40
- .replace(/^-|-$/g, "")
41
- }
42
-
43
- function summarizeMemoryData(data: unknown[], source: string): string {
44
- if (!Array.isArray(data) || data.length === 0) return ""
45
-
46
- const items = data as Array<Record<string, unknown>>
47
-
48
- if (source === "slack" || source === "google_mail") {
49
- const senders = new Map<string, string>()
50
- const messages: string[] = []
51
- for (const item of items) {
52
- const sender = item.sender as Record<string, unknown> | undefined
53
- if (sender?.name && sender?.email) {
54
- senders.set(String(sender.email), String(sender.name))
55
- }
56
- if (item.content && messages.length < 5) {
57
- messages.push(String(item.content).slice(0, 200))
58
- }
59
- }
60
- const parts: string[] = []
61
- if (senders.size > 0) {
62
- parts.push(`Participants: ${[...senders.entries()].map(([e, n]) => `${n} <${e}>`).join(", ")}`)
63
- }
64
- if (messages.length > 0) {
65
- parts.push(`Recent messages:\n${messages.join("\n---\n")}`)
66
- }
67
- return parts.join("\n\n")
68
- }
69
-
70
- if (source === "notion") {
71
- const parts: string[] = []
72
- for (const item of items.slice(0, 10)) {
73
- if (item.__type === "Title" && item.text) {
74
- parts.push(`## ${item.text}`)
75
- } else if (item.__type === "Markdown" && item.text) {
76
- parts.push(String(item.text).slice(0, 300))
77
- } else if (item.__type === "Table" && item.table_rows) {
78
- const rows = item.table_rows as string[][]
79
- if (rows[0]) parts.push(`Table: ${rows[0].join(" | ")}`)
80
- }
81
- }
82
- return parts.join("\n")
83
- }
84
-
85
- const parts: string[] = []
86
- for (const item of items.slice(0, 10)) {
87
- if (item.text) {
88
- parts.push(String(item.text).slice(0, 300))
89
- }
90
- }
91
- return parts.join("\n")
92
- }
93
-
94
- export async function scanMemories(
95
- client: HyperspellClient,
96
- stateManager: NetworkStateManager,
97
- batchSize: number,
98
- cfg?: HyperspellConfig,
99
- ): Promise<ScannedMemory[]> {
100
- const unprocessed: ScannedMemory[] = []
101
- const userIds = cfg ? getAllUserIds(cfg) : [undefined]
102
-
103
- outer: for (const userId of userIds) {
104
- for await (const mem of client.listMemories({ userId })) {
105
- if (stateManager.isProcessed(mem.resourceId)) continue
106
- if (mem.metadata?.graph_entity === "true" || mem.metadata?.graph_entity === true) continue
107
- if ((mem.metadata?.status as string) !== "completed") continue
108
-
109
- let summary = ""
110
- try {
111
- const full = await client.getMemory(mem.resourceId, mem.source as import("../config.ts").HyperspellSource, { userId })
112
- const data = full.data as unknown[] | undefined
113
-
114
- const participants = full.participants as Array<{ name?: string; email?: string }> | undefined
115
- const participantLine = participants?.length
116
- ? `Participants: ${participants.map((p) => `${p.name || ""} <${p.email || ""}>`).join(", ")}`
117
- : ""
118
-
119
- const dataSummary = data ? summarizeMemoryData(data, mem.source) : ""
120
- summary = [participantLine, dataSummary].filter(Boolean).join("\n\n")
121
- } catch {
122
- summary = "(content unavailable)"
123
- }
124
-
125
- unprocessed.push({
126
- resourceId: mem.resourceId,
127
- source: mem.source,
128
- title: mem.title,
129
- summary: summary.slice(0, 1000),
130
- })
131
-
132
- if (unprocessed.length >= batchSize) break outer
133
- }
134
- }
135
-
136
- return unprocessed
137
- }
138
-
139
- export function formatScanResults(memories: ScannedMemory[], processedCount: number, lastScan: string | null): string {
140
- if (memories.length === 0) {
141
- return `No unprocessed memories found. ${processedCount} memories already processed. Last scan: ${lastScan || "never"}`
142
- }
143
-
144
- const formatted = memories
145
- .map((m) => {
146
- const lines = [`[${m.source}] ${m.title || "(untitled)"} (id: ${m.resourceId})`]
147
- if (m.summary) lines.push(m.summary)
148
- return lines.join("\n")
149
- })
150
- .join("\n\n---\n\n")
151
-
152
- return `Found ${memories.length} unprocessed memories:\n\n${formatted}`
153
- }
154
-
155
- export function writeEntity(workspaceDir: string, params: WriteEntityParams): string {
156
- const slug = slugify(params.slug)
157
- const dir = path.join(workspaceDir, "memory", params.type)
158
- const filePath = path.join(dir, `${slug}.md`)
159
-
160
- fs.mkdirSync(dir, { recursive: true })
161
-
162
- // Check for existing file and merge
163
- let existingSourceMemories: SourceMemories = {}
164
- let existingRelationships: string[] = []
165
- let hyperspellId = ""
166
-
167
- if (fs.existsSync(filePath)) {
168
- const existing = fs.readFileSync(filePath, "utf-8")
169
- const fmMatch = existing.match(/^---\n([\s\S]*?)\n---\n?/)
170
- if (fmMatch) {
171
- const fmText = fmMatch[1]
172
- for (const line of fmText.split("\n")) {
173
- const idx = line.indexOf(":")
174
- if (idx <= 0) continue
175
- const key = line.slice(0, idx).trim()
176
- const val = line.slice(idx + 1).trim()
177
- if (key === "hyperspell_id") hyperspellId = val
178
- if (key === "source_memories") {
179
- try { existingSourceMemories = JSON.parse(val) } catch {}
180
- }
181
- if (key === "relationships") {
182
- try { existingRelationships = JSON.parse(val) } catch {}
183
- }
184
- }
185
- }
186
- }
187
-
188
- // Merge source memories
189
- const mergedSources: SourceMemories = { ...existingSourceMemories }
190
- if (params.sourceMemories) {
191
- for (const [source, ids] of Object.entries(params.sourceMemories)) {
192
- const existing = mergedSources[source] || []
193
- const merged = [...new Set([...existing, ...ids])]
194
- mergedSources[source] = merged
195
- }
196
- }
197
-
198
- // Merge relationships
199
- const mergedRelationships = [
200
- ...new Set([...existingRelationships, ...(params.relationships || [])]),
201
- ]
202
-
203
- // Build frontmatter
204
- const fm: Record<string, string> = {
205
- title: params.name,
206
- type: params.type.slice(0, -1), // "people" → "person"
207
- graph_entity: "true",
208
- source_memories: JSON.stringify(mergedSources),
209
- last_extracted: new Date().toISOString(),
210
- }
211
- if (hyperspellId) fm.hyperspell_id = hyperspellId
212
- if (mergedRelationships.length > 0) {
213
- fm.relationships = JSON.stringify(mergedRelationships)
214
- }
215
- if (params.email) fm.email = params.email
216
- if (params.phone) fm.phone = params.phone
217
- if (params.domain) fm.domain = params.domain
218
-
219
- // Build body
220
- const bodyParts = [`# ${params.name}\n`, params.description]
221
-
222
- const contactParts: string[] = []
223
- if (params.email) contactParts.push(`- Email: ${params.email}`)
224
- if (params.phone) contactParts.push(`- Phone: ${params.phone}`)
225
- if (params.domain) contactParts.push(`- Domain: ${params.domain}`)
226
- if (contactParts.length > 0) {
227
- bodyParts.push("\n## Contact\n")
228
- bodyParts.push(...contactParts)
229
- }
230
-
231
- if (mergedRelationships.length > 0) {
232
- bodyParts.push("\n## Relationships\n")
233
- for (const rel of mergedRelationships) {
234
- const [relationship, target] = rel.split(":")
235
- if (target) {
236
- const targetName = target.split("/").pop()?.replace(/-/g, " ") || target
237
- bodyParts.push(`- ${relationship}: [${targetName}](../${target}.md)`)
238
- } else {
239
- bodyParts.push(`- ${rel}`)
240
- }
241
- }
242
- }
243
-
244
- // Build file content
245
- const fmLines = Object.entries(fm).map(([k, v]) => `${k}: ${v}`)
246
- const content = `---\n${fmLines.join("\n")}\n---\n${bodyParts.join("\n")}\n`
247
-
248
- fs.writeFileSync(filePath, content)
249
- log.info(`Wrote entity: ${params.type}/${slug}.md`)
250
-
251
- return `${params.type}/${slug}.md`
252
- }
253
-
254
- export function completeMemories(stateManager: NetworkStateManager, memoryIds: string[]): { newCount: number; totalCount: number } {
255
- const newCount = stateManager.markProcessed(memoryIds)
256
- stateManager.updateLastScan()
257
- stateManager.save()
258
- return { newCount, totalCount: stateManager.getProcessedCount() }
259
- }
package/graph/state.ts DELETED
@@ -1,79 +0,0 @@
1
- import * as fs from "node:fs"
2
- import * as path from "node:path"
3
- import { log } from "../logger.ts"
4
-
5
- const STATE_VERSION = 1
6
- const STATE_FILENAME = ".network-state.json"
7
-
8
- export interface NetworkState {
9
- processedIds: Record<string, string> // resource_id → ISO timestamp
10
- lastScanAt: string | null
11
- version: number
12
- }
13
-
14
- export class NetworkStateManager {
15
- private statePath: string
16
- private state: NetworkState
17
-
18
- constructor(workspaceDir: string) {
19
- const memoryDir = path.join(workspaceDir, "memory")
20
- fs.mkdirSync(memoryDir, { recursive: true })
21
- this.statePath = path.join(memoryDir, STATE_FILENAME)
22
- this.state = this.load()
23
- }
24
-
25
- private load(): NetworkState {
26
- try {
27
- if (fs.existsSync(this.statePath)) {
28
- const raw = fs.readFileSync(this.statePath, "utf-8")
29
- const parsed = JSON.parse(raw)
30
- if (parsed.version === STATE_VERSION) {
31
- return parsed
32
- }
33
- log.warn(`Network state version mismatch (got ${parsed.version}, want ${STATE_VERSION}), resetting`)
34
- }
35
- } catch (err) {
36
- log.warn("Failed to load network state, starting fresh", err)
37
- }
38
- return { processedIds: {}, lastScanAt: null, version: STATE_VERSION }
39
- }
40
-
41
- save(): void {
42
- const tmpPath = this.statePath + ".tmp"
43
- try {
44
- fs.writeFileSync(tmpPath, JSON.stringify(this.state, null, 2))
45
- fs.renameSync(tmpPath, this.statePath)
46
- } catch (err) {
47
- log.error("Failed to save network state", err)
48
- try { fs.unlinkSync(tmpPath) } catch {}
49
- }
50
- }
51
-
52
- isProcessed(resourceId: string): boolean {
53
- return resourceId in this.state.processedIds
54
- }
55
-
56
- markProcessed(resourceIds: string[]): number {
57
- let count = 0
58
- const now = new Date().toISOString()
59
- for (const id of resourceIds) {
60
- if (!(id in this.state.processedIds)) {
61
- this.state.processedIds[id] = now
62
- count++
63
- }
64
- }
65
- return count
66
- }
67
-
68
- updateLastScan(): void {
69
- this.state.lastScanAt = new Date().toISOString()
70
- }
71
-
72
- getProcessedCount(): number {
73
- return Object.keys(this.state.processedIds).length
74
- }
75
-
76
- getLastScanAt(): string | null {
77
- return this.state.lastScanAt
78
- }
79
- }
package/graph/tools.ts DELETED
@@ -1,117 +0,0 @@
1
- import { Type } from "@sinclair/typebox"
2
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
- import type { HyperspellClient } from "../client.ts"
4
- import type { HyperspellConfig } from "../config.ts"
5
- import { getWorkspaceDir } from "../config.ts"
6
- import { log } from "../logger.ts"
7
- import { NetworkStateManager } from "./state.ts"
8
- import { scanMemories, formatScanResults, writeEntity, completeMemories } from "./ops.ts"
9
- import type { EntityType, SourceMemories } from "./ops.ts"
10
-
11
- const ENTITY_TYPES = ["people", "projects", "organizations", "topics"] as const
12
-
13
- export function registerNetworkTools(
14
- api: OpenClawPluginApi,
15
- client: HyperspellClient,
16
- cfg: HyperspellConfig,
17
- ): void {
18
- const workspaceDir = getWorkspaceDir()
19
- const stateManager = new NetworkStateManager(workspaceDir)
20
-
21
- api.registerTool(
22
- {
23
- name: "hyperspell_network_scan",
24
- label: "Memory Network Scan",
25
- description:
26
- "Scan Hyperspell memories and return a batch of unprocessed ones with their content summaries. Use this to find new memories that need entity extraction.",
27
- parameters: Type.Object({
28
- batchSize: Type.Optional(
29
- Type.Number({ description: "Max memories to return (default: 20)" }),
30
- ),
31
- }),
32
- async execute(_toolCallId: string, params: { batchSize?: number }) {
33
- const batchSize = params.batchSize ?? cfg.knowledgeGraph.batchSize
34
- try {
35
- const memories = await scanMemories(client, stateManager, batchSize, cfg)
36
- const text = formatScanResults(memories, stateManager.getProcessedCount(), stateManager.getLastScanAt())
37
- return {
38
- content: [{ type: "text" as const, text }],
39
- details: { count: memories.length, memories },
40
- }
41
- } catch (err) {
42
- log.error("network scan failed", err)
43
- return {
44
- content: [{ type: "text" as const, text: `Scan failed: ${err instanceof Error ? err.message : String(err)}` }],
45
- }
46
- }
47
- },
48
- },
49
- { name: "hyperspell_network_scan" },
50
- )
51
-
52
- api.registerTool(
53
- {
54
- name: "hyperspell_network_write",
55
- label: "Memory Network Write",
56
- description:
57
- "Write or update an entity file in the memory network. Creates markdown files in memory/people/, memory/projects/, memory/organizations/, or memory/topics/.",
58
- parameters: Type.Object({
59
- type: Type.Union(ENTITY_TYPES.map((t) => Type.Literal(t)), {
60
- description: "Entity type: people, projects, organizations, or topics",
61
- }),
62
- slug: Type.String({ description: "URL-safe identifier (lowercase with hyphens, e.g. 'alice-chen')" }),
63
- name: Type.String({ description: "Display name of the entity" }),
64
- description: Type.String({ description: "Description of the entity" }),
65
- relationships: Type.Optional(
66
- Type.Array(Type.String(), {
67
- description: "Relationships in format 'relationship:type/slug', e.g. 'works-at:organizations/hyperspell'",
68
- }),
69
- ),
70
- sourceMemories: Type.Optional(
71
- Type.Record(Type.String(), Type.Array(Type.String()), {
72
- description: "Source memories by provider, e.g. { slack: ['C073WR69EPM'], google_mail: ['19bbe68026553623'] }",
73
- }),
74
- ),
75
- email: Type.Optional(Type.String({ description: "Email address (for people)" })),
76
- phone: Type.Optional(Type.String({ description: "Phone number (for people)" })),
77
- domain: Type.Optional(Type.String({ description: "Domain/homepage (for organizations)" })),
78
- }),
79
- async execute(
80
- _toolCallId: string,
81
- params: { type: EntityType; slug: string; name: string; description: string; relationships?: string[]; sourceMemories?: SourceMemories; email?: string; phone?: string; domain?: string },
82
- ) {
83
- try {
84
- const result = writeEntity(workspaceDir, params)
85
- return { content: [{ type: "text" as const, text: `Wrote ${result} (${params.name})` }] }
86
- } catch (err) {
87
- log.error(`network write failed: ${params.type}/${params.slug}`, err)
88
- return { content: [{ type: "text" as const, text: `Write failed: ${err instanceof Error ? err.message : String(err)}` }] }
89
- }
90
- },
91
- },
92
- { name: "hyperspell_network_write" },
93
- )
94
-
95
- api.registerTool(
96
- {
97
- name: "hyperspell_network_complete",
98
- label: "Memory Network Complete",
99
- description: "Mark a batch of memory IDs as processed so they won't appear in future scans.",
100
- parameters: Type.Object({
101
- memoryIds: Type.Array(Type.String(), { description: "List of resource_ids to mark as processed" }),
102
- }),
103
- async execute(_toolCallId: string, params: { memoryIds: string[] }) {
104
- try {
105
- const { newCount, totalCount } = completeMemories(stateManager, params.memoryIds)
106
- return {
107
- content: [{ type: "text" as const, text: `Marked ${newCount} new memories as processed (${totalCount} total). Last scan: ${stateManager.getLastScanAt()}` }],
108
- }
109
- } catch (err) {
110
- log.error("network complete failed", err)
111
- return { content: [{ type: "text" as const, text: `Complete failed: ${err instanceof Error ? err.message : String(err)}` }] }
112
- }
113
- },
114
- },
115
- { name: "hyperspell_network_complete" },
116
- )
117
- }