@hyperspell/openclaw-hyperspell 0.10.0 → 0.11.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/client.ts +29 -3
- package/commands/slash.ts +84 -13
- package/config.test.ts +202 -0
- package/config.ts +163 -0
- package/hooks/auto-context.ts +20 -6
- package/hooks/auto-trace.ts +4 -0
- package/lib/sender.test.ts +234 -0
- package/lib/sender.ts +178 -20
- package/lib/voice-id.ts +39 -0
- package/openclaw.plugin.json +37 -0
- package/package.json +3 -2
- package/tools/remember.ts +69 -5
- package/tools/search.ts +34 -5
package/tools/remember.ts
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox"
|
|
2
2
|
import type { HyperspellClient } from "../client.ts"
|
|
3
3
|
import type { HyperspellConfig } from "../config.ts"
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getDefaultWriteScope,
|
|
6
|
+
resolveRole,
|
|
7
|
+
resolveUser,
|
|
8
|
+
routeWrite,
|
|
9
|
+
} from "../lib/sender.ts"
|
|
5
10
|
import { log } from "../logger.ts"
|
|
6
11
|
|
|
7
12
|
export function createRememberToolFactory(
|
|
8
13
|
client: HyperspellClient,
|
|
9
14
|
cfg: HyperspellConfig,
|
|
10
15
|
) {
|
|
16
|
+
const scopingEnabled = !!cfg.multiUser?.scoping
|
|
17
|
+
const availableScopes = cfg.multiUser?.scoping?.scopes ?? []
|
|
18
|
+
const scopeDescription = scopingEnabled
|
|
19
|
+
? `Privacy scope for this memory. Available: ${availableScopes.join(", ")}. Defaults to the user's role default.`
|
|
20
|
+
: "Privacy scope (only used when scoping is enabled in config)."
|
|
21
|
+
|
|
11
22
|
return (ctx: Record<string, unknown>) => ({
|
|
12
23
|
name: "hyperspell_remember",
|
|
13
24
|
label: "Memory Store",
|
|
@@ -26,24 +37,77 @@ export function createRememberToolFactory(
|
|
|
26
37
|
"Store for a specific user or 'shared' for everyone. Omit to store for current sender.",
|
|
27
38
|
}),
|
|
28
39
|
),
|
|
40
|
+
scope: Type.Optional(
|
|
41
|
+
Type.String({ description: scopeDescription }),
|
|
42
|
+
),
|
|
29
43
|
}),
|
|
30
44
|
async execute(
|
|
31
45
|
_toolCallId: string,
|
|
32
|
-
params: { text: string; title?: string; date?: string; userId?: string },
|
|
46
|
+
params: { text: string; title?: string; date?: string; userId?: string; scope?: string },
|
|
33
47
|
) {
|
|
34
|
-
// Resolve userId: explicit param > sender resolution > config default
|
|
35
48
|
const resolved = resolveUser(ctx, cfg)
|
|
36
|
-
|
|
49
|
+
|
|
50
|
+
// Scope resolution: explicit param > role default > global default > "private"
|
|
51
|
+
const scope =
|
|
52
|
+
params.scope ??
|
|
53
|
+
(scopingEnabled ? getDefaultWriteScope(resolved, cfg) : "private")
|
|
54
|
+
|
|
55
|
+
// Validate scope is in declared vocabulary (if scoping is enabled)
|
|
56
|
+
if (
|
|
57
|
+
scopingEnabled &&
|
|
58
|
+
availableScopes.length > 0 &&
|
|
59
|
+
!availableScopes.includes(scope)
|
|
60
|
+
) {
|
|
61
|
+
return {
|
|
62
|
+
content: [
|
|
63
|
+
{
|
|
64
|
+
type: "text" as const,
|
|
65
|
+
text: `Unknown scope "${scope}". Available: ${availableScopes.join(", ")}.`,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// canWriteScopes enforcement (role-level deny list)
|
|
72
|
+
if (scopingEnabled) {
|
|
73
|
+
const role = resolveRole(resolved, cfg)
|
|
74
|
+
if (role?.canWriteScopes && !role.canWriteScopes.includes(scope)) {
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: "text" as const,
|
|
79
|
+
text: `You cannot write to scope "${scope}".`,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Route: explicit userId override takes precedence; otherwise derive from scope
|
|
87
|
+
let userId: string | undefined
|
|
88
|
+
let collection: string | undefined
|
|
89
|
+
if (params.userId) {
|
|
90
|
+
userId = params.userId
|
|
91
|
+
} else if (scopingEnabled) {
|
|
92
|
+
const routed = routeWrite(resolved, scope, cfg)
|
|
93
|
+
userId = routed.userId
|
|
94
|
+
collection = routed.collection
|
|
95
|
+
} else {
|
|
96
|
+
userId = resolved?.userId
|
|
97
|
+
}
|
|
98
|
+
|
|
37
99
|
log.debug(
|
|
38
|
-
`remember tool: "${params.text.slice(0, 50)}..." date=${params.date ?? "now"} userId=${userId}`,
|
|
100
|
+
`remember tool: "${params.text.slice(0, 50)}..." date=${params.date ?? "now"} userId=${userId} scope=${scope}`,
|
|
39
101
|
)
|
|
40
102
|
|
|
41
103
|
try {
|
|
42
104
|
await client.addMemory(params.text, {
|
|
43
105
|
title: params.title,
|
|
44
106
|
date: params.date,
|
|
107
|
+
collection,
|
|
45
108
|
metadata: { source: "openclaw_tool" },
|
|
46
109
|
userId,
|
|
110
|
+
scope: scopingEnabled ? scope : undefined,
|
|
47
111
|
})
|
|
48
112
|
|
|
49
113
|
const preview =
|
package/tools/search.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox"
|
|
2
2
|
import type { HyperspellClient } from "../client.ts"
|
|
3
|
-
import type { HyperspellConfig } from "../config.ts"
|
|
4
|
-
import { resolveUser } from "../lib/sender.ts"
|
|
3
|
+
import type { CanReadScope, HyperspellConfig } from "../config.ts"
|
|
4
|
+
import { buildScopeFilter, getCanReadScopes, resolveUser } from "../lib/sender.ts"
|
|
5
5
|
import { log } from "../logger.ts"
|
|
6
6
|
|
|
7
7
|
export function createSearchToolFactory(
|
|
8
8
|
client: HyperspellClient,
|
|
9
9
|
cfg: HyperspellConfig,
|
|
10
10
|
) {
|
|
11
|
+
const scopingEnabled = !!cfg.multiUser?.scoping
|
|
12
|
+
const availableScopes = cfg.multiUser?.scoping?.scopes ?? []
|
|
13
|
+
const scopeDescription = scopingEnabled
|
|
14
|
+
? `Narrow search to a single privacy scope. Available: ${availableScopes.join(", ")}. Omit to search all scopes visible to the caller's role.`
|
|
15
|
+
: "Privacy scope (only used when scoping is enabled in config)."
|
|
16
|
+
|
|
11
17
|
return (ctx: Record<string, unknown>) => ({
|
|
12
18
|
name: "hyperspell_search",
|
|
13
19
|
label: "Memory Search",
|
|
@@ -30,17 +36,39 @@ export function createSearchToolFactory(
|
|
|
30
36
|
"Search as a specific user (e.g. 'ben', 'shared'). Omit to search as current sender.",
|
|
31
37
|
}),
|
|
32
38
|
),
|
|
39
|
+
scope: Type.Optional(Type.String({ description: scopeDescription })),
|
|
33
40
|
}),
|
|
34
41
|
async execute(
|
|
35
42
|
_toolCallId: string,
|
|
36
|
-
params: {
|
|
43
|
+
params: {
|
|
44
|
+
query: string
|
|
45
|
+
limit?: number
|
|
46
|
+
after?: string
|
|
47
|
+
before?: string
|
|
48
|
+
userId?: string
|
|
49
|
+
scope?: string
|
|
50
|
+
},
|
|
37
51
|
) {
|
|
38
52
|
const limit = params.limit ?? 5
|
|
39
|
-
// Resolve userId: explicit param > sender resolution > config default
|
|
40
53
|
const resolved = resolveUser(ctx, cfg)
|
|
41
54
|
const userId = params.userId ?? resolved?.userId
|
|
55
|
+
|
|
56
|
+
// Build scope filter: intersect requested scope (if any) with caller's canRead
|
|
57
|
+
let filter: Record<string, unknown> | undefined
|
|
58
|
+
if (scopingEnabled) {
|
|
59
|
+
const canRead = getCanReadScopes(resolved, cfg)
|
|
60
|
+
const allowed: CanReadScope[] = params.scope
|
|
61
|
+
? canRead.includes("*")
|
|
62
|
+
? [params.scope]
|
|
63
|
+
: canRead.includes(params.scope)
|
|
64
|
+
? [params.scope]
|
|
65
|
+
: [] // requested scope not allowed → match nothing
|
|
66
|
+
: canRead
|
|
67
|
+
filter = buildScopeFilter(allowed, resolved?.userId ?? "")
|
|
68
|
+
}
|
|
69
|
+
|
|
42
70
|
log.debug(
|
|
43
|
-
`search tool: query="${params.query}" limit=${limit} after=${params.after ?? "none"} before=${params.before ?? "none"} userId=${userId}`,
|
|
71
|
+
`search tool: query="${params.query}" limit=${limit} after=${params.after ?? "none"} before=${params.before ?? "none"} userId=${userId} scope=${params.scope ?? "any"}`,
|
|
44
72
|
)
|
|
45
73
|
|
|
46
74
|
try {
|
|
@@ -49,6 +77,7 @@ export function createSearchToolFactory(
|
|
|
49
77
|
after: params.after,
|
|
50
78
|
before: params.before,
|
|
51
79
|
userId,
|
|
80
|
+
filter,
|
|
52
81
|
})
|
|
53
82
|
const documents = (response.documents ?? []) as Array<{
|
|
54
83
|
source: string
|