@hyperspell/openclaw-hyperspell 0.11.1 → 0.13.0
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 +37 -1
- package/dist/client.js +341 -0
- package/dist/commands/setup.js +569 -0
- package/dist/commands/slash.js +159 -0
- package/dist/config.js +349 -0
- package/{graph/cron.ts → dist/graph/cron.js} +11 -15
- package/dist/graph/index.js +4 -0
- package/dist/graph/ops.js +221 -0
- package/dist/graph/state.js +68 -0
- package/dist/graph/tools.js +87 -0
- package/dist/hooks/auto-context.js +199 -0
- package/dist/hooks/auto-trace.js +141 -0
- package/dist/hooks/emotional-state.js +155 -0
- package/dist/hooks/memory-sync.js +54 -0
- package/dist/hooks/startup-orientation.js +156 -0
- package/dist/index.js +132 -0
- package/dist/lib/browser.js +29 -0
- package/dist/lib/sender.js +173 -0
- package/dist/lib/voice-id.js +22 -0
- package/dist/logger.js +32 -0
- package/dist/sync/markdown.js +151 -0
- package/dist/tools/remember.js +97 -0
- package/dist/tools/search.js +87 -0
- package/openclaw.plugin.json +17 -1
- package/package.json +7 -12
- package/client.ts +0 -566
- package/commands/setup.ts +0 -673
- package/commands/slash.ts +0 -198
- package/config.test.ts +0 -202
- package/config.ts +0 -497
- package/graph/index.ts +0 -5
- package/graph/ops.ts +0 -259
- package/graph/state.ts +0 -79
- package/graph/tools.ts +0 -117
- package/hooks/auto-context.ts +0 -272
- package/hooks/auto-trace.test.ts +0 -81
- package/hooks/auto-trace.ts +0 -197
- package/hooks/emotional-state.test.ts +0 -160
- package/hooks/emotional-state.ts +0 -179
- package/hooks/memory-sync.ts +0 -65
- package/index.ts +0 -152
- package/lib/browser.ts +0 -31
- package/lib/sender.test.ts +0 -234
- package/lib/sender.ts +0 -234
- package/lib/voice-id.ts +0 -39
- package/logger.ts +0 -41
- package/sync/markdown.ts +0 -186
- package/tools/remember.ts +0 -132
- package/tools/search.ts +0 -131
- package/types/openclaw.d.ts +0 -76
package/commands/slash.ts
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
2
|
-
import type { HyperspellClient } from "../client.ts"
|
|
3
|
-
import type { CanReadScope, HyperspellConfig } from "../config.ts"
|
|
4
|
-
import { getWorkspaceDir } from "../config.ts"
|
|
5
|
-
import {
|
|
6
|
-
buildScopeFilter,
|
|
7
|
-
getCanReadScopes,
|
|
8
|
-
getDefaultWriteScope,
|
|
9
|
-
resolveRole,
|
|
10
|
-
resolveUser,
|
|
11
|
-
routeWrite,
|
|
12
|
-
} from "../lib/sender.ts"
|
|
13
|
-
import { log } from "../logger.ts"
|
|
14
|
-
import { syncAllMemoryFiles } from "../sync/markdown.ts"
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Strip a `#scope-name` prefix from free text. Returns the scope and the
|
|
18
|
-
* remainder. Used by /remember and /getcontext to let users narrow or route
|
|
19
|
-
* via a single keystroke.
|
|
20
|
-
*/
|
|
21
|
-
function parseScopePrefix(text: string): { scope?: string; rest: string } {
|
|
22
|
-
const m = text.match(/^#(\S+)\s+(.*)$/)
|
|
23
|
-
if (!m) return { rest: text }
|
|
24
|
-
return { scope: m[1], rest: m[2] }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function truncate(text: string, maxLength: number): string {
|
|
28
|
-
if (text.length <= maxLength) return text
|
|
29
|
-
return `${text.slice(0, maxLength)}…`
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function formatScore(score: number | null): string {
|
|
33
|
-
if (score === null) return ""
|
|
34
|
-
return ` (${Math.round(score * 100)}%)`
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function registerCommands(
|
|
38
|
-
api: OpenClawPluginApi,
|
|
39
|
-
client: HyperspellClient,
|
|
40
|
-
cfg: HyperspellConfig,
|
|
41
|
-
): void {
|
|
42
|
-
// /getcontext <query> - Search memories and show summaries
|
|
43
|
-
api.registerCommand({
|
|
44
|
-
name: "getcontext",
|
|
45
|
-
description: "Search your memories for relevant context",
|
|
46
|
-
acceptsArgs: true,
|
|
47
|
-
requireAuth: true,
|
|
48
|
-
handler: async (ctx: { args?: string; senderId?: string; channel?: string }) => {
|
|
49
|
-
const rawArgs = ctx.args?.trim()
|
|
50
|
-
if (!rawArgs) {
|
|
51
|
-
return { text: "Usage: /getcontext [#scope] <search query>" }
|
|
52
|
-
}
|
|
53
|
-
const { scope: requestedScope, rest: query } = parseScopePrefix(rawArgs)
|
|
54
|
-
|
|
55
|
-
const resolved = resolveUser(ctx as Record<string, unknown>, cfg)
|
|
56
|
-
const userId = resolved?.userId
|
|
57
|
-
|
|
58
|
-
// Build scope filter: intersect requested scope (if any) with caller's canRead.
|
|
59
|
-
let filter: Record<string, unknown> | undefined
|
|
60
|
-
if (cfg.multiUser?.scoping) {
|
|
61
|
-
const canRead = getCanReadScopes(resolved, cfg)
|
|
62
|
-
const allowed: CanReadScope[] = requestedScope
|
|
63
|
-
? canRead.includes("*") || canRead.includes(requestedScope)
|
|
64
|
-
? [requestedScope]
|
|
65
|
-
: []
|
|
66
|
-
: canRead
|
|
67
|
-
filter = buildScopeFilter(allowed, resolved?.userId ?? "")
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
log.debug(
|
|
71
|
-
`/getcontext command: "${query}" userId=${userId} scope=${requestedScope ?? "any"}`,
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
const results = await client.search(query, { limit: 5, userId, filter })
|
|
76
|
-
|
|
77
|
-
if (results.length === 0) {
|
|
78
|
-
return { text: `No memories found for: "${query}"` }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const lines = results.map((r, i) => {
|
|
82
|
-
const title = r.title ? truncate(r.title, 60) : `[${r.source}]`
|
|
83
|
-
const score = formatScore(r.score)
|
|
84
|
-
return `${i + 1}. ${title}${score}`
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
text: `Found ${results.length} memories:\n\n${lines.join("\n")}`,
|
|
89
|
-
}
|
|
90
|
-
} catch (err) {
|
|
91
|
-
log.error("/getcontext failed", err)
|
|
92
|
-
return { text: "Failed to search memories. Check logs for details." }
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
// /remember <text> - Add a new memory
|
|
98
|
-
api.registerCommand({
|
|
99
|
-
name: "remember",
|
|
100
|
-
description: "Save something to memory",
|
|
101
|
-
acceptsArgs: true,
|
|
102
|
-
requireAuth: true,
|
|
103
|
-
handler: async (ctx: { args?: string; senderId?: string; channel?: string }) => {
|
|
104
|
-
const rawArgs = ctx.args?.trim()
|
|
105
|
-
if (!rawArgs) {
|
|
106
|
-
return { text: "Usage: /remember [#scope] <text to remember>" }
|
|
107
|
-
}
|
|
108
|
-
const { scope: requestedScope, rest: text } = parseScopePrefix(rawArgs)
|
|
109
|
-
|
|
110
|
-
const resolved = resolveUser(ctx as Record<string, unknown>, cfg)
|
|
111
|
-
const scopingEnabled = !!cfg.multiUser?.scoping
|
|
112
|
-
const availableScopes = cfg.multiUser?.scoping?.scopes ?? []
|
|
113
|
-
|
|
114
|
-
// Scope resolution: prefix > role default > global default > "private"
|
|
115
|
-
const scope = scopingEnabled
|
|
116
|
-
? (requestedScope ?? getDefaultWriteScope(resolved, cfg))
|
|
117
|
-
: "private"
|
|
118
|
-
|
|
119
|
-
// Validate scope is in declared vocabulary
|
|
120
|
-
if (
|
|
121
|
-
scopingEnabled &&
|
|
122
|
-
requestedScope &&
|
|
123
|
-
availableScopes.length > 0 &&
|
|
124
|
-
!availableScopes.includes(scope)
|
|
125
|
-
) {
|
|
126
|
-
return {
|
|
127
|
-
text: `Unknown scope "${scope}". Available: ${availableScopes.join(", ")}.`,
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// canWriteScopes enforcement
|
|
132
|
-
if (scopingEnabled) {
|
|
133
|
-
const role = resolveRole(resolved, cfg)
|
|
134
|
-
if (role?.canWriteScopes && !role.canWriteScopes.includes(scope)) {
|
|
135
|
-
return { text: `You cannot write to scope "${scope}".` }
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const { userId, collection } = scopingEnabled
|
|
140
|
-
? routeWrite(resolved, scope, cfg)
|
|
141
|
-
: { userId: resolved?.userId, collection: undefined }
|
|
142
|
-
|
|
143
|
-
log.debug(
|
|
144
|
-
`/remember command: "${truncate(text, 50)}" userId=${userId} scope=${scope}`,
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
await client.addMemory(text, {
|
|
149
|
-
metadata: { source: "openclaw_command" },
|
|
150
|
-
collection,
|
|
151
|
-
userId,
|
|
152
|
-
scope: scopingEnabled ? scope : undefined,
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
const preview = truncate(text, 60)
|
|
156
|
-
const scopeHint = scopingEnabled ? ` [${scope}]` : ""
|
|
157
|
-
return { text: `Remembered${scopeHint}: "${preview}"` }
|
|
158
|
-
} catch (err) {
|
|
159
|
-
log.error("/remember failed", err)
|
|
160
|
-
return { text: "Failed to save memory. Check logs for details." }
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
// /sync - Manually sync memory files
|
|
166
|
-
api.registerCommand({
|
|
167
|
-
name: "sync",
|
|
168
|
-
description: "Sync memory/*.md files with Hyperspell",
|
|
169
|
-
acceptsArgs: false,
|
|
170
|
-
requireAuth: true,
|
|
171
|
-
handler: async () => {
|
|
172
|
-
log.debug("/sync command")
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
const workspaceDir = getWorkspaceDir()
|
|
176
|
-
const result = await syncAllMemoryFiles(client, workspaceDir, {
|
|
177
|
-
userId: cfg.multiUser?.sharedUserId,
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
if (result.synced === 0 && result.failed === 0) {
|
|
181
|
-
return { text: "No memory files found in memory/ directory." }
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (result.failed > 0) {
|
|
185
|
-
const errors = result.errors.map((e) => ` • ${e}`).join("\n")
|
|
186
|
-
return {
|
|
187
|
-
text: `Synced ${result.synced} files, ${result.failed} failed:\n${errors}`,
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return { text: `Synced ${result.synced} memory file(s) to Hyperspell.` }
|
|
192
|
-
} catch (err) {
|
|
193
|
-
log.error("/sync failed", err)
|
|
194
|
-
return { text: "Failed to sync memory files. Check logs for details." }
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
})
|
|
198
|
-
}
|
package/config.test.ts
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict"
|
|
2
|
-
import { test } from "node:test"
|
|
3
|
-
import { parseConfig } from "./config.ts"
|
|
4
|
-
|
|
5
|
-
const base = {
|
|
6
|
-
apiKey: "test-key",
|
|
7
|
-
userId: "u1",
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
test("parseConfig — no multiUser returns no scoping", () => {
|
|
11
|
-
const cfg = parseConfig(base)
|
|
12
|
-
assert.equal(cfg.multiUser, undefined)
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
test("parseConfig — multiUser without scoping is valid", () => {
|
|
16
|
-
const cfg = parseConfig({
|
|
17
|
-
...base,
|
|
18
|
-
multiUser: {
|
|
19
|
-
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
20
|
-
sharedUserId: "shared",
|
|
21
|
-
},
|
|
22
|
-
})
|
|
23
|
-
assert.equal(cfg.multiUser?.scoping, undefined)
|
|
24
|
-
assert.equal(cfg.multiUser?.sharedUserId, "shared")
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test("parseConfig — scoping disabled returns no scoping", () => {
|
|
28
|
-
const cfg = parseConfig({
|
|
29
|
-
...base,
|
|
30
|
-
multiUser: {
|
|
31
|
-
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
32
|
-
scoping: {
|
|
33
|
-
enabled: false,
|
|
34
|
-
defaultScope: "private",
|
|
35
|
-
scopes: ["private"],
|
|
36
|
-
roles: {},
|
|
37
|
-
users: {},
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
})
|
|
41
|
-
assert.equal(cfg.multiUser?.scoping, undefined)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
test("parseConfig — empty scopes array throws", () => {
|
|
45
|
-
assert.throws(
|
|
46
|
-
() =>
|
|
47
|
-
parseConfig({
|
|
48
|
-
...base,
|
|
49
|
-
multiUser: {
|
|
50
|
-
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
51
|
-
scoping: {
|
|
52
|
-
enabled: true,
|
|
53
|
-
defaultScope: "private",
|
|
54
|
-
scopes: [],
|
|
55
|
-
roles: {},
|
|
56
|
-
users: {},
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
}),
|
|
60
|
-
/scoping\.scopes/,
|
|
61
|
-
)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
test("parseConfig — defaultScope not in scopes throws", () => {
|
|
65
|
-
assert.throws(
|
|
66
|
-
() =>
|
|
67
|
-
parseConfig({
|
|
68
|
-
...base,
|
|
69
|
-
multiUser: {
|
|
70
|
-
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
71
|
-
scoping: {
|
|
72
|
-
enabled: true,
|
|
73
|
-
defaultScope: "nonexistent",
|
|
74
|
-
scopes: ["private", "family"],
|
|
75
|
-
roles: {},
|
|
76
|
-
users: {},
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
}),
|
|
80
|
-
/defaultScope/,
|
|
81
|
-
)
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
test("parseConfig — role canRead with unknown scope throws", () => {
|
|
85
|
-
assert.throws(
|
|
86
|
-
() =>
|
|
87
|
-
parseConfig({
|
|
88
|
-
...base,
|
|
89
|
-
multiUser: {
|
|
90
|
-
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
91
|
-
scoping: {
|
|
92
|
-
enabled: true,
|
|
93
|
-
defaultScope: "private",
|
|
94
|
-
scopes: ["private", "family"],
|
|
95
|
-
roles: {
|
|
96
|
-
weird: { canRead: ["private", "ghost"], defaultWriteScope: "private" },
|
|
97
|
-
},
|
|
98
|
-
users: {},
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
}),
|
|
102
|
-
/canRead.*ghost/,
|
|
103
|
-
)
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
test("parseConfig — role defaultWriteScope not in scopes throws", () => {
|
|
107
|
-
assert.throws(
|
|
108
|
-
() =>
|
|
109
|
-
parseConfig({
|
|
110
|
-
...base,
|
|
111
|
-
multiUser: {
|
|
112
|
-
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
113
|
-
scoping: {
|
|
114
|
-
enabled: true,
|
|
115
|
-
defaultScope: "private",
|
|
116
|
-
scopes: ["private", "family"],
|
|
117
|
-
roles: {
|
|
118
|
-
weird: { canRead: ["family"], defaultWriteScope: "nonexistent" },
|
|
119
|
-
},
|
|
120
|
-
users: {},
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
}),
|
|
124
|
-
/defaultWriteScope/,
|
|
125
|
-
)
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
test("parseConfig — canWriteScopes with unknown scope throws", () => {
|
|
129
|
-
assert.throws(
|
|
130
|
-
() =>
|
|
131
|
-
parseConfig({
|
|
132
|
-
...base,
|
|
133
|
-
multiUser: {
|
|
134
|
-
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
135
|
-
scoping: {
|
|
136
|
-
enabled: true,
|
|
137
|
-
defaultScope: "private",
|
|
138
|
-
scopes: ["private", "family"],
|
|
139
|
-
roles: {
|
|
140
|
-
weird: {
|
|
141
|
-
canRead: ["*"],
|
|
142
|
-
defaultWriteScope: "private",
|
|
143
|
-
canWriteScopes: ["ghost"],
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
users: {},
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
}),
|
|
150
|
-
/canWriteScopes.*ghost/,
|
|
151
|
-
)
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
test("parseConfig — user role keying into unknown role throws", () => {
|
|
155
|
-
assert.throws(
|
|
156
|
-
() =>
|
|
157
|
-
parseConfig({
|
|
158
|
-
...base,
|
|
159
|
-
multiUser: {
|
|
160
|
-
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
161
|
-
scoping: {
|
|
162
|
-
enabled: true,
|
|
163
|
-
defaultScope: "private",
|
|
164
|
-
scopes: ["private", "family"],
|
|
165
|
-
roles: {
|
|
166
|
-
parent: { canRead: ["*"], defaultWriteScope: "private" },
|
|
167
|
-
},
|
|
168
|
-
users: { u1: { role: "nonexistent" } },
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
}),
|
|
172
|
-
/does not key into scoping\.roles/,
|
|
173
|
-
)
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
test("parseConfig — valid scoping parses", () => {
|
|
177
|
-
const cfg = parseConfig({
|
|
178
|
-
...base,
|
|
179
|
-
multiUser: {
|
|
180
|
-
senderMap: { "u1-phone": { userId: "u1", name: "U1" } },
|
|
181
|
-
scoping: {
|
|
182
|
-
enabled: true,
|
|
183
|
-
defaultScope: "private",
|
|
184
|
-
scopes: ["private", "family", "parent_only"],
|
|
185
|
-
roles: {
|
|
186
|
-
parent: { canRead: ["*"], defaultWriteScope: "private" },
|
|
187
|
-
kid: { canRead: ["family"], defaultWriteScope: "family" },
|
|
188
|
-
},
|
|
189
|
-
users: {
|
|
190
|
-
u1: { role: "parent" },
|
|
191
|
-
},
|
|
192
|
-
collections: { family: "household" },
|
|
193
|
-
voiceId: { enabled: true, confidenceThreshold: 0.8 },
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
})
|
|
197
|
-
assert.equal(cfg.multiUser?.scoping?.enabled, true)
|
|
198
|
-
assert.deepEqual(cfg.multiUser?.scoping?.scopes, ["private", "family", "parent_only"])
|
|
199
|
-
assert.equal(cfg.multiUser?.scoping?.roles.parent?.defaultWriteScope, "private")
|
|
200
|
-
assert.equal(cfg.multiUser?.scoping?.collections?.family, "household")
|
|
201
|
-
assert.equal(cfg.multiUser?.scoping?.voiceId?.confidenceThreshold, 0.8)
|
|
202
|
-
})
|