@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/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 { resolveUser } from "../lib/sender.ts"
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
- const userId = params.userId ?? resolved?.userId
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: { query: string; limit?: number; after?: string; before?: string; userId?: string },
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