@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/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
- })