@hyperspell/openclaw-hyperspell 0.9.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 +166 -60
- package/commands/slash.ts +94 -15
- package/config.test.ts +202 -0
- package/config.ts +212 -0
- package/graph/ops.ts +34 -28
- package/graph/tools.ts +1 -1
- package/hooks/auto-context.ts +178 -11
- package/hooks/auto-trace.ts +16 -2
- package/hooks/memory-sync.ts +5 -3
- package/index.ts +12 -6
- package/lib/sender.test.ts +234 -0
- package/lib/sender.ts +234 -0
- package/lib/voice-id.ts +39 -0
- package/openclaw.plugin.json +37 -0
- package/package.json +3 -2
- package/sync/markdown.ts +4 -1
- package/tools/remember.ts +113 -41
- package/tools/search.ts +114 -81
- package/types/openclaw.d.ts +16 -0
package/tools/search.ts
CHANGED
|
@@ -1,98 +1,131 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox"
|
|
2
|
-
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
3
2
|
import type { HyperspellClient } from "../client.ts"
|
|
4
|
-
import type { HyperspellConfig } from "../config.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
|
-
export function
|
|
8
|
-
api: OpenClawPluginApi,
|
|
7
|
+
export function createSearchToolFactory(
|
|
9
8
|
client: HyperspellClient,
|
|
10
|
-
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"Search through the user's connected sources (Notion, Slack, Gmail, Google Drive, etc.) for relevant information.",
|
|
18
|
-
parameters: Type.Object({
|
|
19
|
-
query: Type.String({ description: "Search query" }),
|
|
20
|
-
limit: Type.Optional(
|
|
21
|
-
Type.Number({ description: "Max results (default: 5)" }),
|
|
22
|
-
),
|
|
23
|
-
after: Type.Optional(
|
|
24
|
-
Type.String({ description: "Only return memories created on or after this date (ISO 8601 or YYYY-MM-DD)" }),
|
|
25
|
-
),
|
|
26
|
-
before: Type.Optional(
|
|
27
|
-
Type.String({ description: "Only return memories created before this date (ISO 8601 or YYYY-MM-DD)" }),
|
|
28
|
-
),
|
|
29
|
-
}),
|
|
30
|
-
async execute(
|
|
31
|
-
_toolCallId: string,
|
|
32
|
-
params: { query: string; limit?: number; after?: string; before?: string },
|
|
33
|
-
) {
|
|
34
|
-
const limit = params.limit ?? 5
|
|
35
|
-
log.debug(`search tool: query="${params.query}" limit=${limit} after=${params.after ?? "none"} before=${params.before ?? "none"}`)
|
|
9
|
+
cfg: HyperspellConfig,
|
|
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)."
|
|
36
16
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
17
|
+
return (ctx: Record<string, unknown>) => ({
|
|
18
|
+
name: "hyperspell_search",
|
|
19
|
+
label: "Memory Search",
|
|
20
|
+
description:
|
|
21
|
+
"Search through the user's connected sources (Notion, Slack, Gmail, Google Drive, etc.) for relevant information.",
|
|
22
|
+
parameters: Type.Object({
|
|
23
|
+
query: Type.String({ description: "Search query" }),
|
|
24
|
+
limit: Type.Optional(
|
|
25
|
+
Type.Number({ description: "Max results (default: 5)" }),
|
|
26
|
+
),
|
|
27
|
+
after: Type.Optional(
|
|
28
|
+
Type.String({ description: "Only return memories created on or after this date (ISO 8601 or YYYY-MM-DD)" }),
|
|
29
|
+
),
|
|
30
|
+
before: Type.Optional(
|
|
31
|
+
Type.String({ description: "Only return memories created before this date (ISO 8601 or YYYY-MM-DD)" }),
|
|
32
|
+
),
|
|
33
|
+
userId: Type.Optional(
|
|
34
|
+
Type.String({
|
|
35
|
+
description:
|
|
36
|
+
"Search as a specific user (e.g. 'ben', 'shared'). Omit to search as current sender.",
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
scope: Type.Optional(Type.String({ description: scopeDescription })),
|
|
40
|
+
}),
|
|
41
|
+
async execute(
|
|
42
|
+
_toolCallId: string,
|
|
43
|
+
params: {
|
|
44
|
+
query: string
|
|
45
|
+
limit?: number
|
|
46
|
+
after?: string
|
|
47
|
+
before?: string
|
|
48
|
+
userId?: string
|
|
49
|
+
scope?: string
|
|
50
|
+
},
|
|
51
|
+
) {
|
|
52
|
+
const limit = params.limit ?? 5
|
|
53
|
+
const resolved = resolveUser(ctx, cfg)
|
|
54
|
+
const userId = params.userId ?? resolved?.userId
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
}
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
? `${Math.round(doc.score * 100)}%`
|
|
62
|
-
: "N/A"
|
|
63
|
-
const title = doc.title || "(untitled)"
|
|
64
|
-
const summary = doc.summary || "(no summary)"
|
|
65
|
-
return `${i + 1}. Source: ${doc.source}\n Title: ${title}\n Summary: ${summary}\n Relevance: ${relevance}`
|
|
66
|
-
})
|
|
67
|
-
.join("\n\n")
|
|
70
|
+
log.debug(
|
|
71
|
+
`search tool: query="${params.query}" limit=${limit} after=${params.after ?? "none"} before=${params.before ?? "none"} userId=${userId} scope=${params.scope ?? "any"}`,
|
|
72
|
+
)
|
|
68
73
|
|
|
69
|
-
|
|
74
|
+
try {
|
|
75
|
+
const response = await client.searchRaw(params.query, {
|
|
76
|
+
limit,
|
|
77
|
+
after: params.after,
|
|
78
|
+
before: params.before,
|
|
79
|
+
userId,
|
|
80
|
+
filter,
|
|
81
|
+
})
|
|
82
|
+
const documents = (response.documents ?? []) as Array<{
|
|
83
|
+
source: string
|
|
84
|
+
resource_id: string
|
|
85
|
+
score?: number
|
|
86
|
+
summary?: string
|
|
87
|
+
title?: string
|
|
88
|
+
metadata?: Record<string, unknown>
|
|
89
|
+
highlights?: Array<{ text: string }>
|
|
90
|
+
data?: Array<{ text: string }>
|
|
91
|
+
}>
|
|
70
92
|
|
|
93
|
+
if (documents.length === 0) {
|
|
71
94
|
return {
|
|
72
95
|
content: [
|
|
73
|
-
{
|
|
74
|
-
type: "text" as const,
|
|
75
|
-
text,
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
details: {
|
|
79
|
-
count: documents.length,
|
|
80
|
-
documents,
|
|
81
|
-
},
|
|
82
|
-
}
|
|
83
|
-
} catch (err) {
|
|
84
|
-
log.error("search tool failed", err)
|
|
85
|
-
return {
|
|
86
|
-
content: [
|
|
87
|
-
{
|
|
88
|
-
type: "text" as const,
|
|
89
|
-
text: `Search failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
90
|
-
},
|
|
96
|
+
{ type: "text" as const, text: "No relevant memories found." },
|
|
91
97
|
],
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
|
-
|
|
100
|
+
|
|
101
|
+
const formattedDocs = documents
|
|
102
|
+
.map((doc, i) => {
|
|
103
|
+
const relevance = doc.score
|
|
104
|
+
? `${Math.round(doc.score * 100)}%`
|
|
105
|
+
: "N/A"
|
|
106
|
+
const title = doc.title || "(untitled)"
|
|
107
|
+
const summary = doc.summary || "(no summary)"
|
|
108
|
+
return `${i + 1}. Source: ${doc.source}\n Title: ${title}\n Summary: ${summary}\n Relevance: ${relevance}`
|
|
109
|
+
})
|
|
110
|
+
.join("\n\n")
|
|
111
|
+
|
|
112
|
+
const text = `Found ${documents.length} memories:\n\n${formattedDocs}`
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: "text" as const, text }],
|
|
116
|
+
details: { count: documents.length, documents },
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
log.error("search tool failed", err)
|
|
120
|
+
return {
|
|
121
|
+
content: [
|
|
122
|
+
{
|
|
123
|
+
type: "text" as const,
|
|
124
|
+
text: `Search failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
}
|
|
128
|
+
}
|
|
95
129
|
},
|
|
96
|
-
|
|
97
|
-
)
|
|
130
|
+
})
|
|
98
131
|
}
|
package/types/openclaw.d.ts
CHANGED
|
@@ -48,6 +48,22 @@ declare module "openclaw/plugin-sdk" {
|
|
|
48
48
|
},
|
|
49
49
|
meta: { name: string },
|
|
50
50
|
): void
|
|
51
|
+
registerTool<T = unknown>(
|
|
52
|
+
factory: (ctx: Record<string, unknown>) => {
|
|
53
|
+
name: string
|
|
54
|
+
label: string
|
|
55
|
+
description: string
|
|
56
|
+
parameters: unknown
|
|
57
|
+
execute: (
|
|
58
|
+
toolCallId: string,
|
|
59
|
+
params: T,
|
|
60
|
+
) => Promise<{
|
|
61
|
+
content: Array<{ type: "text"; text: string }>
|
|
62
|
+
details?: Record<string, unknown>
|
|
63
|
+
}>
|
|
64
|
+
},
|
|
65
|
+
meta: { name: string },
|
|
66
|
+
): void
|
|
51
67
|
on(event: string, handler: (event: Record<string, unknown>, ctx?: Record<string, unknown>) => Promise<{ prependContext?: string } | void> | void): void
|
|
52
68
|
registerService(options: {
|
|
53
69
|
id: string
|