@hyperspell/openclaw-hyperspell 0.1.0 → 0.2.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 +46 -20
- package/client.ts +92 -1
- package/commands/setup.ts +470 -0
- package/commands/slash.ts +5 -59
- package/config.ts +25 -3
- package/index.ts +38 -1
- package/openclaw.plugin.json +25 -10
- package/package.json +8 -5
- package/tools/remember.ts +20 -8
- package/tools/search.ts +51 -31
- package/types/openclaw.d.ts +17 -0
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# OpenClaw Hyperspell Plugin
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
OpenClaw plugin for [Hyperspell](https://hyperspell.com) - Context and memory for your AI agents.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
@@ -8,19 +10,40 @@ OpenClaw plugin for [Hyperspell](https://hyperspell.com) - Context and memory fo
|
|
|
8
10
|
openclaw plugins install @hyperspell/openclaw-hyperspell
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
##
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
Run the interactive setup wizard:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
openclaw openclaw-hyperspell setup
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The setup wizard will guide you through:
|
|
22
|
+
1. Creating a Hyperspell account (if you don't have one)
|
|
23
|
+
2. Configuring your API key
|
|
24
|
+
3. Setting up your User ID for multi-tenant memory
|
|
25
|
+
4. Connecting your apps (Notion, Slack, Google Drive, etc.)
|
|
26
|
+
|
|
27
|
+
## Manual Configuration
|
|
12
28
|
|
|
13
|
-
|
|
29
|
+
Alternatively, add to your `openclaw.json`:
|
|
14
30
|
|
|
15
31
|
```json
|
|
16
32
|
{
|
|
17
33
|
"plugins": {
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
34
|
+
"entries": {
|
|
35
|
+
"openclaw-hyperspell": {
|
|
36
|
+
"enabled": true,
|
|
37
|
+
"config": {
|
|
38
|
+
"apiKey": "${HYPERSPELL_API_KEY}",
|
|
39
|
+
"userId": "your-email",
|
|
40
|
+
"autoContext": true
|
|
41
|
+
}
|
|
42
|
+
}
|
|
21
43
|
}
|
|
22
44
|
}
|
|
23
45
|
}
|
|
46
|
+
|
|
24
47
|
```
|
|
25
48
|
|
|
26
49
|
Or set the environment variable:
|
|
@@ -29,12 +52,26 @@ Or set the environment variable:
|
|
|
29
52
|
export HYPERSPELL_API_KEY=hs_...
|
|
30
53
|
```
|
|
31
54
|
|
|
55
|
+
## CLI Commands
|
|
56
|
+
|
|
57
|
+
### `openclaw openclaw-hyperspell setup`
|
|
58
|
+
|
|
59
|
+
Interactive setup wizard that walks you through configuration.
|
|
60
|
+
|
|
61
|
+
### `openclaw openclaw-hyperspell status`
|
|
62
|
+
|
|
63
|
+
Check your current configuration and connection status.
|
|
64
|
+
|
|
65
|
+
### `openclaw openclaw-hyperspell connect`
|
|
66
|
+
|
|
67
|
+
Open the Hyperspell connect page to link your accounts (Notion, Slack, Google Drive, etc.)
|
|
68
|
+
|
|
32
69
|
### Options
|
|
33
70
|
|
|
34
71
|
| Option | Type | Default | Description |
|
|
35
72
|
|--------|------|---------|-------------|
|
|
36
73
|
| `apiKey` | string | `${HYPERSPELL_API_KEY}` | Hyperspell API key |
|
|
37
|
-
| `userId` | string | - | User ID
|
|
74
|
+
| `userId` | string | - | User ID (can be your email) |
|
|
38
75
|
| `autoContext` | boolean | `true` | Auto-inject relevant memories before each AI turn |
|
|
39
76
|
| `sources` | string | - | Comma-separated sources to search (e.g., `notion,slack`) |
|
|
40
77
|
| `maxResults` | number | `10` | Maximum memories per context injection |
|
|
@@ -42,22 +79,12 @@ export HYPERSPELL_API_KEY=hs_...
|
|
|
42
79
|
|
|
43
80
|
## Slash Commands
|
|
44
81
|
|
|
45
|
-
### `/
|
|
82
|
+
### `/getcontext <query>`
|
|
46
83
|
|
|
47
84
|
Search your memories for relevant context.
|
|
48
85
|
|
|
49
86
|
```
|
|
50
|
-
/
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### `/connect <source>`
|
|
54
|
-
|
|
55
|
-
Connect an account to Hyperspell. Opens the OAuth flow in your browser.
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
/connect notion
|
|
59
|
-
/connect slack
|
|
60
|
-
/connect google_drive
|
|
87
|
+
/getcontext Q1 budget planning
|
|
61
88
|
```
|
|
62
89
|
|
|
63
90
|
### `/remember <text>`
|
|
@@ -87,12 +114,11 @@ This ensures the AI always has access to relevant information from your connecte
|
|
|
87
114
|
|
|
88
115
|
## Available Sources
|
|
89
116
|
|
|
90
|
-
- `
|
|
117
|
+
- `vault` - User-created memories
|
|
91
118
|
- `notion` - Notion pages and databases
|
|
92
119
|
- `slack` - Slack messages
|
|
93
120
|
- `google_calendar` - Google Calendar events
|
|
94
121
|
- `google_mail` - Gmail messages
|
|
95
122
|
- `google_drive` - Google Drive files
|
|
96
123
|
- `box` - Box files
|
|
97
|
-
- `vault` - Vault documents
|
|
98
124
|
- `web_crawler` - Crawled web pages
|
package/client.ts
CHANGED
|
@@ -11,6 +11,11 @@ export type SearchResult = {
|
|
|
11
11
|
createdAt: string | null
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export type SearchWithAnswerResult = {
|
|
15
|
+
answer: string | null
|
|
16
|
+
documents: SearchResult[]
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
export type Integration = {
|
|
15
20
|
id: string
|
|
16
21
|
name: string
|
|
@@ -18,6 +23,13 @@ export type Integration = {
|
|
|
18
23
|
icon: string
|
|
19
24
|
}
|
|
20
25
|
|
|
26
|
+
export type Connection = {
|
|
27
|
+
id: string
|
|
28
|
+
integrationId: string
|
|
29
|
+
label: string | null
|
|
30
|
+
provider: HyperspellSource
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
export class HyperspellClient {
|
|
22
34
|
private client: Hyperspell
|
|
23
35
|
private config: HyperspellConfig
|
|
@@ -26,8 +38,9 @@ export class HyperspellClient {
|
|
|
26
38
|
this.config = config
|
|
27
39
|
this.client = new Hyperspell({
|
|
28
40
|
apiKey: config.apiKey,
|
|
41
|
+
userID: config.userId,
|
|
29
42
|
})
|
|
30
|
-
log.info(
|
|
43
|
+
log.info(`client initialized${config.userId ? ` for user ${config.userId}` : ""}`)
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
async search(
|
|
@@ -61,6 +74,68 @@ export class HyperspellClient {
|
|
|
61
74
|
return results
|
|
62
75
|
}
|
|
63
76
|
|
|
77
|
+
async searchRaw(
|
|
78
|
+
query: string,
|
|
79
|
+
options?: { limit?: number; sources?: HyperspellSource[] },
|
|
80
|
+
): Promise<Record<string, unknown>> {
|
|
81
|
+
const limit = options?.limit ?? this.config.maxResults
|
|
82
|
+
const sources =
|
|
83
|
+
options?.sources ?? (this.config.sources.length > 0 ? this.config.sources : undefined)
|
|
84
|
+
|
|
85
|
+
log.debugRequest("memories.search (raw)", { query, limit, sources })
|
|
86
|
+
|
|
87
|
+
const response = await this.client.memories.search({
|
|
88
|
+
query,
|
|
89
|
+
sources,
|
|
90
|
+
options: {
|
|
91
|
+
max_results: limit,
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
log.debugResponse("memories.search (raw)", { count: response.documents.length })
|
|
96
|
+
|
|
97
|
+
return response as unknown as Record<string, unknown>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async searchWithAnswer(
|
|
101
|
+
query: string,
|
|
102
|
+
options?: { limit?: number; sources?: HyperspellSource[] },
|
|
103
|
+
): Promise<SearchWithAnswerResult> {
|
|
104
|
+
const limit = options?.limit ?? this.config.maxResults
|
|
105
|
+
const sources =
|
|
106
|
+
options?.sources ?? (this.config.sources.length > 0 ? this.config.sources : undefined)
|
|
107
|
+
|
|
108
|
+
log.debugRequest("memories.search (with answer)", { query, limit, sources })
|
|
109
|
+
|
|
110
|
+
const response = await this.client.memories.search({
|
|
111
|
+
query,
|
|
112
|
+
sources,
|
|
113
|
+
answer: true,
|
|
114
|
+
options: {
|
|
115
|
+
max_results: limit,
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const documents: SearchResult[] = response.documents.map((doc) => ({
|
|
120
|
+
resourceId: doc.resource_id,
|
|
121
|
+
title: doc.title ?? null,
|
|
122
|
+
source: doc.source as HyperspellSource,
|
|
123
|
+
score: doc.score ?? null,
|
|
124
|
+
url: doc.metadata?.url as string | null ?? null,
|
|
125
|
+
createdAt: doc.metadata?.created_at as string | null ?? null,
|
|
126
|
+
}))
|
|
127
|
+
|
|
128
|
+
log.debugResponse("memories.search (with answer)", {
|
|
129
|
+
count: documents.length,
|
|
130
|
+
hasAnswer: !!response.answer,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
answer: response.answer ?? null,
|
|
135
|
+
documents,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
64
139
|
async addMemory(
|
|
65
140
|
text: string,
|
|
66
141
|
options?: { title?: string; metadata?: Record<string, string | number | boolean> },
|
|
@@ -110,4 +185,20 @@ export class HyperspellClient {
|
|
|
110
185
|
expiresAt: response.expires_at,
|
|
111
186
|
}
|
|
112
187
|
}
|
|
188
|
+
|
|
189
|
+
async listConnections(): Promise<Connection[]> {
|
|
190
|
+
log.debugRequest("connections.list", {})
|
|
191
|
+
|
|
192
|
+
const response = await this.client.connections.list()
|
|
193
|
+
|
|
194
|
+
const connections: Connection[] = response.connections.map((conn) => ({
|
|
195
|
+
id: conn.id,
|
|
196
|
+
integrationId: conn.integration_id,
|
|
197
|
+
label: conn.label,
|
|
198
|
+
provider: conn.provider as HyperspellSource,
|
|
199
|
+
}))
|
|
200
|
+
|
|
201
|
+
log.debugResponse("connections.list", { count: connections.length })
|
|
202
|
+
return connections
|
|
203
|
+
}
|
|
113
204
|
}
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { exec } from "node:child_process"
|
|
2
|
+
import * as fs from "node:fs"
|
|
3
|
+
import * as path from "node:path"
|
|
4
|
+
import { homedir, platform, userInfo } from "node:os"
|
|
5
|
+
import * as p from "@clack/prompts"
|
|
6
|
+
import type { Command } from "commander"
|
|
7
|
+
import Hyperspell from "hyperspell"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve OpenClaw state directory, matching OpenClaw's logic.
|
|
11
|
+
* Checks OPENCLAW_STATE_DIR env var, falls back to ~/.openclaw
|
|
12
|
+
*/
|
|
13
|
+
function resolveStateDir(): string {
|
|
14
|
+
const override = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim()
|
|
15
|
+
if (override) {
|
|
16
|
+
return override.startsWith("~")
|
|
17
|
+
? override.replace(/^~(?=$|[\\/])/, homedir())
|
|
18
|
+
: path.resolve(override)
|
|
19
|
+
}
|
|
20
|
+
return path.join(homedir(), ".openclaw")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve OpenClaw config path, matching OpenClaw's logic.
|
|
25
|
+
* Checks OPENCLAW_CONFIG_PATH env var, falls back to $STATE_DIR/openclaw.json
|
|
26
|
+
*/
|
|
27
|
+
function resolveConfigPath(): string {
|
|
28
|
+
const override = process.env.OPENCLAW_CONFIG_PATH?.trim() || process.env.CLAWDBOT_CONFIG_PATH?.trim()
|
|
29
|
+
if (override) {
|
|
30
|
+
return override.startsWith("~")
|
|
31
|
+
? override.replace(/^~(?=$|[\\/])/, homedir())
|
|
32
|
+
: path.resolve(override)
|
|
33
|
+
}
|
|
34
|
+
return path.join(resolveStateDir(), "openclaw.json")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function fetchConnectionSources(client: Hyperspell, userId: string): Promise<string[]> {
|
|
38
|
+
try {
|
|
39
|
+
const userClient = new Hyperspell({
|
|
40
|
+
apiKey: client.apiKey,
|
|
41
|
+
userID: userId,
|
|
42
|
+
})
|
|
43
|
+
const response = await userClient.connections.list()
|
|
44
|
+
const providers = response.connections.map((conn) => conn.provider)
|
|
45
|
+
// Add vault and deduplicate
|
|
46
|
+
const sources = [...new Set(["vault", ...providers])]
|
|
47
|
+
return sources
|
|
48
|
+
} catch (_error) {
|
|
49
|
+
return ["vault"]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function updateConfigSources(configPath: string, sources: string[]): void {
|
|
54
|
+
if (!fs.existsSync(configPath)) return
|
|
55
|
+
|
|
56
|
+
const content = fs.readFileSync(configPath, "utf-8")
|
|
57
|
+
const config = JSON.parse(content)
|
|
58
|
+
|
|
59
|
+
const pluginConfig = config?.plugins?.entries?.["openclaw-hyperspell"]?.config
|
|
60
|
+
if (pluginConfig) {
|
|
61
|
+
pluginConfig.sources = sources.join(",")
|
|
62
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n")
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function openUrl(url: string): Promise<void> {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
let command: string
|
|
69
|
+
switch (platform()) {
|
|
70
|
+
case "darwin":
|
|
71
|
+
command = `open "${url}"`
|
|
72
|
+
break
|
|
73
|
+
case "win32":
|
|
74
|
+
command = `start "" "${url}"`
|
|
75
|
+
break
|
|
76
|
+
default:
|
|
77
|
+
command = `xdg-open "${url}"`
|
|
78
|
+
}
|
|
79
|
+
exec(command, (error) => {
|
|
80
|
+
if (error) reject(error)
|
|
81
|
+
else resolve()
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function runSetup(): Promise<void> {
|
|
87
|
+
p.intro("Hyperspell Setup")
|
|
88
|
+
|
|
89
|
+
// Step 1: Check if they have an account
|
|
90
|
+
const hasAccount = await p.confirm({
|
|
91
|
+
message: "Do you already have a Hyperspell account?",
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
if (p.isCancel(hasAccount)) {
|
|
95
|
+
p.cancel("Setup cancelled")
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!hasAccount) {
|
|
100
|
+
p.note(
|
|
101
|
+
"1. Go to https://app.hyperspell.com to create a free account\n" +
|
|
102
|
+
"2. Create a new app for your AI agent\n" +
|
|
103
|
+
"3. Select which integrations you want to connect (Notion, Slack, etc.)",
|
|
104
|
+
"Create an account",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const openSignup = await p.confirm({
|
|
108
|
+
message: "Open app.hyperspell.com in your browser?",
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
if (p.isCancel(openSignup)) {
|
|
112
|
+
p.cancel("Setup cancelled")
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (openSignup) {
|
|
117
|
+
await openUrl("https://app.hyperspell.com")
|
|
118
|
+
p.log.info("Browser opened. Come back when you've created your account.")
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await p.confirm({
|
|
122
|
+
message: "Ready to continue?",
|
|
123
|
+
active: "Yes",
|
|
124
|
+
inactive: "No",
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Step 2: Get API Key
|
|
129
|
+
p.note(
|
|
130
|
+
"1. Go to your app in https://app.hyperspell.com\n" +
|
|
131
|
+
"2. Navigate to Settings > API Keys\n" +
|
|
132
|
+
"3. Create a new API key",
|
|
133
|
+
"API Key",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
const apiKey = await p.text({
|
|
137
|
+
message: "Paste your API key",
|
|
138
|
+
placeholder: "hs_...",
|
|
139
|
+
validate: (value) => {
|
|
140
|
+
if (!value) return "API key is required"
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
if (p.isCancel(apiKey)) {
|
|
145
|
+
p.cancel("Setup cancelled")
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Validate API key
|
|
150
|
+
const s = p.spinner()
|
|
151
|
+
s.start("Validating API key")
|
|
152
|
+
|
|
153
|
+
let client: Hyperspell
|
|
154
|
+
try {
|
|
155
|
+
client = new Hyperspell({ apiKey })
|
|
156
|
+
await client.integrations.list()
|
|
157
|
+
s.stop("API key is valid")
|
|
158
|
+
} catch (_error) {
|
|
159
|
+
s.stop("API key validation failed")
|
|
160
|
+
p.log.error("Please check that your API key is correct and try again.")
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Step 3: User ID
|
|
165
|
+
p.note(
|
|
166
|
+
"Hyperspell is a multi-tenant memory platform. Each user's memories\n" +
|
|
167
|
+
"are stored separately, identified by a User ID.\n\n" +
|
|
168
|
+
"For a personal agent, use your email address or username.",
|
|
169
|
+
"User ID",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const systemUser = userInfo().username
|
|
173
|
+
const userId = await p.text({
|
|
174
|
+
message: "Enter a User ID for this agent",
|
|
175
|
+
placeholder: systemUser || "your-email@example.com",
|
|
176
|
+
defaultValue: systemUser,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
if (p.isCancel(userId)) {
|
|
180
|
+
p.cancel("Setup cancelled")
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Step 4: List and connect integrations
|
|
185
|
+
let integrations: Awaited<ReturnType<typeof client.integrations.list>>
|
|
186
|
+
try {
|
|
187
|
+
integrations = await client.integrations.list()
|
|
188
|
+
} catch (_error) {
|
|
189
|
+
integrations = { integrations: [] }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (integrations.integrations.length === 0) {
|
|
193
|
+
p.note(
|
|
194
|
+
"No integrations are configured in your app yet.\n" +
|
|
195
|
+
"Go to https://app.hyperspell.com to add integrations,\n" +
|
|
196
|
+
"then use /connect <source> to connect them.",
|
|
197
|
+
"Connect Your Apps",
|
|
198
|
+
)
|
|
199
|
+
} else {
|
|
200
|
+
const integrationList = integrations.integrations
|
|
201
|
+
.map((int) => `• ${int.name} (${int.provider})`)
|
|
202
|
+
.join("\n")
|
|
203
|
+
|
|
204
|
+
// Get a user token for the connect page
|
|
205
|
+
let connectUrl: string
|
|
206
|
+
try {
|
|
207
|
+
const tokenResponse = await client.auth.userToken({ user_id: userId })
|
|
208
|
+
connectUrl = `https://connect.hyperspell.com?token=${tokenResponse.token}`
|
|
209
|
+
} catch (_error) {
|
|
210
|
+
p.log.error("Could not generate connect URL. You can connect apps later using /connect.")
|
|
211
|
+
connectUrl = ""
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
p.note(
|
|
215
|
+
`Available integrations:\n${integrationList}` +
|
|
216
|
+
(connectUrl ? `\n\nConnect your accounts at:\n${connectUrl}` : ""),
|
|
217
|
+
"Connect Your Apps",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if (connectUrl) {
|
|
221
|
+
const openConnect = await p.confirm({
|
|
222
|
+
message: "Open connection page in your browser?",
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
if (!p.isCancel(openConnect) && openConnect) {
|
|
226
|
+
await openUrl(connectUrl)
|
|
227
|
+
p.log.info("Browser opened. Connect your accounts and come back when done.")
|
|
228
|
+
|
|
229
|
+
await p.confirm({
|
|
230
|
+
message: "Finished connecting accounts?",
|
|
231
|
+
active: "Yes",
|
|
232
|
+
inactive: "Not yet",
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Fetch connected sources
|
|
239
|
+
const s1 = p.spinner()
|
|
240
|
+
s1.start("Fetching connected sources")
|
|
241
|
+
const sources = await fetchConnectionSources(client, userId)
|
|
242
|
+
s1.stop(`Found ${sources.length} sources: ${sources.join(", ")}`)
|
|
243
|
+
|
|
244
|
+
// Step 5: Save configuration
|
|
245
|
+
const s2 = p.spinner()
|
|
246
|
+
s2.start("Saving configuration")
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const configPath = resolveConfigPath()
|
|
250
|
+
const openclawDir = path.dirname(configPath)
|
|
251
|
+
const envPath = path.join(openclawDir, ".env")
|
|
252
|
+
|
|
253
|
+
// Ensure directory exists
|
|
254
|
+
if (!fs.existsSync(openclawDir)) {
|
|
255
|
+
fs.mkdirSync(openclawDir, { recursive: true })
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Read existing config or create new one
|
|
259
|
+
let config: Record<string, unknown> = {}
|
|
260
|
+
if (fs.existsSync(configPath)) {
|
|
261
|
+
const existing = fs.readFileSync(configPath, "utf-8")
|
|
262
|
+
config = JSON.parse(existing)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Merge in plugin configuration
|
|
266
|
+
if (!config.plugins) {
|
|
267
|
+
config.plugins = {}
|
|
268
|
+
}
|
|
269
|
+
const plugins = config.plugins as Record<string, unknown>
|
|
270
|
+
if (!plugins.entries) {
|
|
271
|
+
plugins.entries = {}
|
|
272
|
+
}
|
|
273
|
+
const entries = plugins.entries as Record<string, unknown>
|
|
274
|
+
entries["openclaw-hyperspell"] = {
|
|
275
|
+
enabled: true,
|
|
276
|
+
config: {
|
|
277
|
+
apiKey: "${HYPERSPELL_API_KEY}",
|
|
278
|
+
userId,
|
|
279
|
+
sources: sources.join(","),
|
|
280
|
+
autoContext: true,
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Write config
|
|
285
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n")
|
|
286
|
+
|
|
287
|
+
// Write or append to .env
|
|
288
|
+
const envLine = `HYPERSPELL_API_KEY=${apiKey}`
|
|
289
|
+
if (fs.existsSync(envPath)) {
|
|
290
|
+
const envContent = fs.readFileSync(envPath, "utf-8")
|
|
291
|
+
if (envContent.includes("HYPERSPELL_API_KEY=")) {
|
|
292
|
+
// Replace existing line
|
|
293
|
+
const updated = envContent.replace(/^HYPERSPELL_API_KEY=.*$/m, envLine)
|
|
294
|
+
fs.writeFileSync(envPath, updated)
|
|
295
|
+
} else {
|
|
296
|
+
// Append new line
|
|
297
|
+
fs.appendFileSync(envPath, (envContent.endsWith("\n") ? "" : "\n") + envLine + "\n")
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
fs.writeFileSync(envPath, envLine + "\n")
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
s2.stop("Configuration saved")
|
|
304
|
+
|
|
305
|
+
p.note(
|
|
306
|
+
`Config: ${configPath}\n` +
|
|
307
|
+
`API Key: ${envPath}`,
|
|
308
|
+
"Files updated",
|
|
309
|
+
)
|
|
310
|
+
} catch (error) {
|
|
311
|
+
s2.stop("Failed to save configuration")
|
|
312
|
+
|
|
313
|
+
// Fall back to showing manual instructions
|
|
314
|
+
const configJson = JSON.stringify(
|
|
315
|
+
{
|
|
316
|
+
plugins: {
|
|
317
|
+
entries: {
|
|
318
|
+
"openclaw-hyperspell": {
|
|
319
|
+
enabled: true,
|
|
320
|
+
config: {
|
|
321
|
+
apiKey: "${HYPERSPELL_API_KEY}",
|
|
322
|
+
userId,
|
|
323
|
+
sources: sources.join(","),
|
|
324
|
+
autoContext: true,
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
null,
|
|
331
|
+
2,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
p.note(
|
|
335
|
+
`Add to your openclaw.json:\n\n${configJson}\n\n` +
|
|
336
|
+
`Set the environment variable:\n export HYPERSPELL_API_KEY=${apiKey}`,
|
|
337
|
+
"Manual configuration required",
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
p.note(
|
|
342
|
+
"/getcontext <query> Search your memories for relevant context\n" +
|
|
343
|
+
"/remember <text> Save something directly to your vault\n\n" +
|
|
344
|
+
"To connect more apps, run: openclaw openclaw-hyperspell connect\n\n" +
|
|
345
|
+
"Auto-context is enabled by default — relevant memories are\n" +
|
|
346
|
+
"automatically injected before each AI response.",
|
|
347
|
+
"How to use Hyperspell",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
p.outro("Setup complete!")
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function runConnect(pluginConfig: unknown): Promise<void> {
|
|
354
|
+
const config = pluginConfig as Record<string, unknown> | undefined
|
|
355
|
+
|
|
356
|
+
p.intro("Hyperspell Connect")
|
|
357
|
+
|
|
358
|
+
if (!config?.apiKey) {
|
|
359
|
+
p.log.error("Not configured")
|
|
360
|
+
p.note("Run 'openclaw openclaw-hyperspell setup' to configure Hyperspell first.")
|
|
361
|
+
p.outro("")
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const s = p.spinner()
|
|
366
|
+
s.start("Generating connect URL")
|
|
367
|
+
|
|
368
|
+
let client: Hyperspell
|
|
369
|
+
let userId: string
|
|
370
|
+
try {
|
|
371
|
+
client = new Hyperspell({ apiKey: config.apiKey as string })
|
|
372
|
+
userId = (config.userId as string) || userInfo().username || "user"
|
|
373
|
+
const tokenResponse = await client.auth.userToken({ user_id: userId })
|
|
374
|
+
const connectUrl = `https://connect.hyperspell.com?token=${tokenResponse.token}`
|
|
375
|
+
|
|
376
|
+
s.stop("Connect URL ready")
|
|
377
|
+
|
|
378
|
+
await openUrl(connectUrl)
|
|
379
|
+
p.log.success("Browser opened to connect.hyperspell.com")
|
|
380
|
+
|
|
381
|
+
await p.confirm({
|
|
382
|
+
message: "Finished connecting accounts?",
|
|
383
|
+
active: "Yes",
|
|
384
|
+
inactive: "Not yet",
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
// Fetch and update sources
|
|
388
|
+
const s2 = p.spinner()
|
|
389
|
+
s2.start("Updating sources configuration")
|
|
390
|
+
|
|
391
|
+
const sources = await fetchConnectionSources(client, userId)
|
|
392
|
+
const configPath = resolveConfigPath()
|
|
393
|
+
updateConfigSources(configPath, sources)
|
|
394
|
+
|
|
395
|
+
s2.stop(`Sources updated: ${sources.join(", ")}`)
|
|
396
|
+
|
|
397
|
+
p.outro("Done! Restart OpenClaw to apply changes.")
|
|
398
|
+
} catch (_error) {
|
|
399
|
+
s.stop("Failed to generate connect URL")
|
|
400
|
+
p.log.error("Check your API key and try again.")
|
|
401
|
+
p.outro("")
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function runStatus(pluginConfig: unknown): Promise<void> {
|
|
406
|
+
const config = pluginConfig as Record<string, unknown> | undefined
|
|
407
|
+
|
|
408
|
+
p.intro("Hyperspell Status")
|
|
409
|
+
|
|
410
|
+
if (!config?.apiKey) {
|
|
411
|
+
p.log.warn("Not configured")
|
|
412
|
+
p.note("Run 'openclaw openclaw-hyperspell setup' to configure Hyperspell.")
|
|
413
|
+
p.outro("")
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
p.log.success("Configured")
|
|
418
|
+
p.log.info(`User ID: ${config.userId || "(not set)"}`)
|
|
419
|
+
p.log.info(`Auto-Context: ${config.autoContext !== false ? "Enabled" : "Disabled"}`)
|
|
420
|
+
p.log.info(`Sources Filter: ${config.sources || "(all sources)"}`)
|
|
421
|
+
p.log.info(`Max Results: ${config.maxResults || 10}`)
|
|
422
|
+
|
|
423
|
+
const s = p.spinner()
|
|
424
|
+
s.start("Testing connection")
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const client = new Hyperspell({ apiKey: config.apiKey as string })
|
|
428
|
+
const integrations = await client.integrations.list()
|
|
429
|
+
s.stop(`Connection OK (${integrations.integrations.length} integrations available)`)
|
|
430
|
+
|
|
431
|
+
if (integrations.integrations.length > 0) {
|
|
432
|
+
const list = integrations.integrations
|
|
433
|
+
.map((int) => `• ${int.name} (${int.provider})`)
|
|
434
|
+
.join("\n")
|
|
435
|
+
p.note(list, "Available integrations")
|
|
436
|
+
}
|
|
437
|
+
} catch (_error) {
|
|
438
|
+
s.stop("Connection failed")
|
|
439
|
+
p.log.error("Check your API key and try again.")
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
p.outro("")
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function registerCliCommands(program: Command, pluginConfig: unknown): void {
|
|
446
|
+
const hyperspellCmd = program
|
|
447
|
+
.command("openclaw-hyperspell")
|
|
448
|
+
.description("Hyperspell — Memory and context for your AI agent")
|
|
449
|
+
|
|
450
|
+
hyperspellCmd
|
|
451
|
+
.command("setup")
|
|
452
|
+
.description("Interactive setup wizard for Hyperspell")
|
|
453
|
+
.action(async () => {
|
|
454
|
+
await runSetup()
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
hyperspellCmd
|
|
458
|
+
.command("status")
|
|
459
|
+
.description("Show Hyperspell connection status")
|
|
460
|
+
.action(async () => {
|
|
461
|
+
await runStatus(pluginConfig)
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
hyperspellCmd
|
|
465
|
+
.command("connect")
|
|
466
|
+
.description("Open the Hyperspell connect page to link your accounts")
|
|
467
|
+
.action(async () => {
|
|
468
|
+
await runConnect(pluginConfig)
|
|
469
|
+
})
|
|
470
|
+
}
|
package/commands/slash.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
2
2
|
import type { HyperspellClient } from "../client.ts"
|
|
3
3
|
import type { HyperspellConfig } from "../config.ts"
|
|
4
|
-
import { openInBrowser } from "../lib/browser.ts"
|
|
5
4
|
import { log } from "../logger.ts"
|
|
6
5
|
|
|
7
6
|
function truncate(text: string, maxLength: number): string {
|
|
@@ -19,19 +18,19 @@ export function registerCommands(
|
|
|
19
18
|
client: HyperspellClient,
|
|
20
19
|
_cfg: HyperspellConfig,
|
|
21
20
|
): void {
|
|
22
|
-
// /
|
|
21
|
+
// /getcontext <query> - Search memories and show summaries
|
|
23
22
|
api.registerCommand({
|
|
24
|
-
name: "
|
|
23
|
+
name: "getcontext",
|
|
25
24
|
description: "Search your memories for relevant context",
|
|
26
25
|
acceptsArgs: true,
|
|
27
26
|
requireAuth: true,
|
|
28
27
|
handler: async (ctx: { args?: string }) => {
|
|
29
28
|
const query = ctx.args?.trim()
|
|
30
29
|
if (!query) {
|
|
31
|
-
return { text: "Usage: /
|
|
30
|
+
return { text: "Usage: /getcontext <search query>" }
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
log.debug(`/
|
|
33
|
+
log.debug(`/getcontext command: "${query}"`)
|
|
35
34
|
|
|
36
35
|
try {
|
|
37
36
|
const results = await client.search(query, { limit: 5 })
|
|
@@ -50,65 +49,12 @@ export function registerCommands(
|
|
|
50
49
|
text: `Found ${results.length} memories:\n\n${lines.join("\n")}`,
|
|
51
50
|
}
|
|
52
51
|
} catch (err) {
|
|
53
|
-
log.error("/
|
|
52
|
+
log.error("/getcontext failed", err)
|
|
54
53
|
return { text: "Failed to search memories. Check logs for details." }
|
|
55
54
|
}
|
|
56
55
|
},
|
|
57
56
|
})
|
|
58
57
|
|
|
59
|
-
// /connect <source> - Open connection URL for an integration
|
|
60
|
-
api.registerCommand({
|
|
61
|
-
name: "connect",
|
|
62
|
-
description: "Connect an account to Hyperspell",
|
|
63
|
-
acceptsArgs: true,
|
|
64
|
-
requireAuth: true,
|
|
65
|
-
handler: async (ctx: { args?: string }) => {
|
|
66
|
-
const source = ctx.args?.trim().toLowerCase()
|
|
67
|
-
if (!source) {
|
|
68
|
-
return { text: "Usage: /connect <source>\n\nExamples: /connect notion, /connect slack" }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
log.debug(`/connect command: "${source}"`)
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
const integrations = await client.listIntegrations()
|
|
75
|
-
|
|
76
|
-
// Find matching integration by provider or name
|
|
77
|
-
const integration = integrations.find(
|
|
78
|
-
(int) =>
|
|
79
|
-
int.provider.toLowerCase() === source ||
|
|
80
|
-
int.name.toLowerCase() === source ||
|
|
81
|
-
int.id.toLowerCase() === source,
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
if (!integration) {
|
|
85
|
-
const available = integrations.map((i) => i.provider).join(", ")
|
|
86
|
-
return {
|
|
87
|
-
text: `Integration "${source}" not found.\n\nAvailable: ${available}`,
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const { url } = await client.getConnectUrl(integration.id)
|
|
92
|
-
|
|
93
|
-
// Auto-open in browser
|
|
94
|
-
try {
|
|
95
|
-
await openInBrowser(url)
|
|
96
|
-
return {
|
|
97
|
-
text: `Opening ${integration.name} connection in your browser...`,
|
|
98
|
-
}
|
|
99
|
-
} catch {
|
|
100
|
-
// Fall back to showing the URL if browser open fails
|
|
101
|
-
return {
|
|
102
|
-
text: `Connect your ${integration.name} account:\n${url}`,
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
} catch (err) {
|
|
106
|
-
log.error("/connect failed", err)
|
|
107
|
-
return { text: "Failed to get connect URL. Check logs for details." }
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
})
|
|
111
|
-
|
|
112
58
|
// /remember <text> - Add a new memory
|
|
113
59
|
api.registerCommand({
|
|
114
60
|
name: "remember",
|
package/config.ts
CHANGED
|
@@ -62,8 +62,30 @@ function resolveEnvVars(value: string): string {
|
|
|
62
62
|
})
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function parseSources(raw: string | undefined): HyperspellSource[] {
|
|
66
|
-
if (!raw
|
|
65
|
+
function parseSources(raw: string | string[] | undefined): HyperspellSource[] {
|
|
66
|
+
if (!raw) {
|
|
67
|
+
return []
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Handle array input
|
|
71
|
+
if (Array.isArray(raw)) {
|
|
72
|
+
const sources = raw
|
|
73
|
+
.map((s) => String(s).trim().toLowerCase())
|
|
74
|
+
.filter((s) => s.length > 0) as HyperspellSource[]
|
|
75
|
+
|
|
76
|
+
for (const source of sources) {
|
|
77
|
+
if (!VALID_SOURCES.includes(source)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Invalid source "${source}". Valid sources: ${VALID_SOURCES.join(", ")}`,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return sources
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle string input (comma-separated)
|
|
88
|
+
if (typeof raw === "string" && raw.trim() === "") {
|
|
67
89
|
return []
|
|
68
90
|
}
|
|
69
91
|
|
|
@@ -108,7 +130,7 @@ export function parseConfig(raw: unknown): HyperspellConfig {
|
|
|
108
130
|
apiKey,
|
|
109
131
|
userId: cfg.userId as string | undefined,
|
|
110
132
|
autoContext: (cfg.autoContext as boolean) ?? true,
|
|
111
|
-
sources: parseSources(cfg.sources as string | undefined),
|
|
133
|
+
sources: parseSources(cfg.sources as string | string[] | undefined),
|
|
112
134
|
maxResults: (cfg.maxResults as number) ?? 10,
|
|
113
135
|
debug: (cfg.debug as boolean) ?? false,
|
|
114
136
|
}
|
package/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
2
2
|
import { HyperspellClient } from "./client.ts"
|
|
3
3
|
import { registerCommands } from "./commands/slash.ts"
|
|
4
|
+
import { registerCliCommands } from "./commands/setup.ts"
|
|
4
5
|
import { parseConfig, hyperspellConfigSchema } from "./config.ts"
|
|
5
6
|
import { buildAutoContextHandler } from "./hooks/auto-context.ts"
|
|
6
7
|
import { initLogger } from "./logger.ts"
|
|
@@ -10,11 +11,47 @@ import { registerSearchTool } from "./tools/search.ts"
|
|
|
10
11
|
export default {
|
|
11
12
|
id: "openclaw-hyperspell",
|
|
12
13
|
name: "Hyperspell",
|
|
13
|
-
description: "
|
|
14
|
+
description: "Hyperspell gives your Molty context and memory from all your existing data",
|
|
14
15
|
kind: "memory" as const,
|
|
15
16
|
configSchema: hyperspellConfigSchema,
|
|
16
17
|
|
|
17
18
|
register(api: OpenClawPluginApi) {
|
|
19
|
+
// Register CLI commands (openclaw openclaw-hyperspell setup|status|connect)
|
|
20
|
+
api.registerCli(
|
|
21
|
+
(ctx) => {
|
|
22
|
+
registerCliCommands(ctx.program, api.pluginConfig)
|
|
23
|
+
},
|
|
24
|
+
{ commands: ["openclaw-hyperspell"] },
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// Check if configured
|
|
28
|
+
const rawConfig = api.pluginConfig as Record<string, unknown> | undefined
|
|
29
|
+
const hasConfig = rawConfig?.apiKey || process.env.HYPERSPELL_API_KEY
|
|
30
|
+
|
|
31
|
+
if (!hasConfig) {
|
|
32
|
+
api.logger.info("hyperspell: not configured - run 'openclaw openclaw-hyperspell setup'")
|
|
33
|
+
// Still register slash commands so they show up, but they'll return an error
|
|
34
|
+
api.registerCommand({
|
|
35
|
+
name: "getcontext",
|
|
36
|
+
description: "Search your memories for relevant context",
|
|
37
|
+
acceptsArgs: true,
|
|
38
|
+
requireAuth: false,
|
|
39
|
+
handler: async () => {
|
|
40
|
+
return { text: "Hyperspell not configured. Run 'openclaw openclaw-hyperspell setup' first." }
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
api.registerCommand({
|
|
44
|
+
name: "remember",
|
|
45
|
+
description: "Save something to memory",
|
|
46
|
+
acceptsArgs: true,
|
|
47
|
+
requireAuth: false,
|
|
48
|
+
handler: async () => {
|
|
49
|
+
return { text: "Hyperspell not configured. Run 'openclaw openclaw-hyperspell setup' first." }
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
18
55
|
const cfg = parseConfig(api.pluginConfig)
|
|
19
56
|
|
|
20
57
|
initLogger(api.logger, cfg.debug)
|
package/openclaw.plugin.json
CHANGED
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
"userId": {
|
|
12
12
|
"label": "User ID",
|
|
13
13
|
"placeholder": "user_123",
|
|
14
|
-
"help": "
|
|
15
|
-
"advanced":
|
|
14
|
+
"help": "User ID (can be your email)",
|
|
15
|
+
"advanced": false
|
|
16
16
|
},
|
|
17
17
|
"autoContext": {
|
|
18
18
|
"label": "Auto-Context",
|
|
19
|
-
"help": "Inject relevant memories before every AI turn"
|
|
19
|
+
"help": "Inject relevant memories before every AI turn",
|
|
20
|
+
"advanced": true
|
|
20
21
|
},
|
|
21
22
|
"sources": {
|
|
22
23
|
"label": "Sources",
|
|
@@ -40,13 +41,27 @@
|
|
|
40
41
|
"type": "object",
|
|
41
42
|
"additionalProperties": false,
|
|
42
43
|
"properties": {
|
|
43
|
-
"apiKey": {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
"apiKey": {
|
|
45
|
+
"type": "string"
|
|
46
|
+
},
|
|
47
|
+
"userId": {
|
|
48
|
+
"type": "string"
|
|
49
|
+
},
|
|
50
|
+
"autoContext": {
|
|
51
|
+
"type": "boolean"
|
|
52
|
+
},
|
|
53
|
+
"sources": {
|
|
54
|
+
"type": "string"
|
|
55
|
+
},
|
|
56
|
+
"maxResults": {
|
|
57
|
+
"type": "number",
|
|
58
|
+
"minimum": 1,
|
|
59
|
+
"maximum": 20
|
|
60
|
+
},
|
|
61
|
+
"debug": {
|
|
62
|
+
"type": "boolean"
|
|
63
|
+
}
|
|
49
64
|
},
|
|
50
65
|
"required": []
|
|
51
66
|
}
|
|
52
|
-
}
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperspell/openclaw-hyperspell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw Hyperspell memory plugin",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
"plugin"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"
|
|
32
|
-
"@sinclair/typebox": "^0.34.0"
|
|
31
|
+
"@clack/prompts": "^1.0.0",
|
|
32
|
+
"@sinclair/typebox": "^0.34.0",
|
|
33
|
+
"hyperspell": "^0.30.0"
|
|
33
34
|
},
|
|
34
35
|
"peerDependencies": {
|
|
35
36
|
"openclaw": ">=2026.1.29"
|
|
@@ -40,9 +41,11 @@
|
|
|
40
41
|
"lint:fix": "bunx @biomejs/biome check --write ."
|
|
41
42
|
},
|
|
42
43
|
"openclaw": {
|
|
43
|
-
"extensions": [
|
|
44
|
+
"extensions": [
|
|
45
|
+
"./index.ts"
|
|
46
|
+
]
|
|
44
47
|
},
|
|
45
48
|
"devDependencies": {
|
|
46
49
|
"typescript": "^5.9.3"
|
|
47
50
|
}
|
|
48
|
-
}
|
|
51
|
+
}
|
package/tools/remember.ts
CHANGED
|
@@ -26,16 +26,28 @@ export function registerRememberTool(
|
|
|
26
26
|
) {
|
|
27
27
|
log.debug(`remember tool: "${params.text.slice(0, 50)}..."`)
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
try {
|
|
30
|
+
await client.addMemory(params.text, {
|
|
31
|
+
title: params.title,
|
|
32
|
+
metadata: { source: "openclaw_tool" },
|
|
33
|
+
})
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
const preview =
|
|
36
|
+
params.text.length > 80 ? `${params.text.slice(0, 80)}…` : params.text
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: "text" as const, text: `Stored: "${preview}"` }],
|
|
40
|
+
}
|
|
41
|
+
} catch (err) {
|
|
42
|
+
log.error("remember tool failed", err)
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: "text" as const,
|
|
47
|
+
text: `Failed to store memory: ${err instanceof Error ? err.message : String(err)}`,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
}
|
|
39
51
|
}
|
|
40
52
|
},
|
|
41
53
|
},
|
package/tools/search.ts
CHANGED
|
@@ -28,42 +28,62 @@ export function registerSearchTool(
|
|
|
28
28
|
const limit = params.limit ?? 5
|
|
29
29
|
log.debug(`search tool: query="${params.query}" limit=${limit}`)
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
try {
|
|
32
|
+
const response = await client.searchRaw(params.query, { limit })
|
|
33
|
+
const documents = (response.documents ?? []) as Array<{
|
|
34
|
+
source: string
|
|
35
|
+
resource_id: string
|
|
36
|
+
score?: number
|
|
37
|
+
summary?: string
|
|
38
|
+
title?: string
|
|
39
|
+
metadata?: Record<string, unknown>
|
|
40
|
+
highlights?: Array<{ text: string }>
|
|
41
|
+
data?: Array<{ text: string }>
|
|
42
|
+
}>
|
|
32
43
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
if (documents.length === 0) {
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{ type: "text" as const, text: "No relevant memories found." },
|
|
48
|
+
],
|
|
49
|
+
}
|
|
38
50
|
}
|
|
39
|
-
}
|
|
40
51
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
const formattedDocs = documents
|
|
53
|
+
.map((doc, i) => {
|
|
54
|
+
const relevance = doc.score
|
|
55
|
+
? `${Math.round(doc.score * 100)}%`
|
|
56
|
+
: "N/A"
|
|
57
|
+
const title = doc.title || "(untitled)"
|
|
58
|
+
const summary = doc.summary || "(no summary)"
|
|
59
|
+
return `${i + 1}. Source: ${doc.source}\n Title: ${title}\n Summary: ${summary}\n Relevance: ${relevance}`
|
|
60
|
+
})
|
|
61
|
+
.join("\n\n")
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
const text = `Found ${documents.length} memories:\n\n${formattedDocs}`
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: "text" as const,
|
|
69
|
+
text,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
details: {
|
|
73
|
+
count: documents.length,
|
|
74
|
+
documents,
|
|
56
75
|
},
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
log.error("search tool failed", err)
|
|
79
|
+
return {
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: "text" as const,
|
|
83
|
+
text: `Search failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
}
|
|
67
87
|
}
|
|
68
88
|
},
|
|
69
89
|
},
|
package/types/openclaw.d.ts
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
declare module "openclaw/plugin-sdk" {
|
|
2
|
+
import type { Command } from "commander"
|
|
3
|
+
|
|
4
|
+
export interface OpenClawPluginCliContext {
|
|
5
|
+
program: Command
|
|
6
|
+
config: unknown
|
|
7
|
+
workspaceDir?: string
|
|
8
|
+
logger: {
|
|
9
|
+
info: (message: string, ...args: unknown[]) => void
|
|
10
|
+
warn: (message: string, ...args: unknown[]) => void
|
|
11
|
+
error: (message: string, ...args: unknown[]) => void
|
|
12
|
+
debug: (message: string, ...args: unknown[]) => void
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type OpenClawPluginCliRegistrar = (ctx: OpenClawPluginCliContext) => void | Promise<void>
|
|
17
|
+
|
|
2
18
|
export interface OpenClawPluginApi {
|
|
3
19
|
pluginConfig: unknown
|
|
4
20
|
logger: {
|
|
@@ -14,6 +30,7 @@ declare module "openclaw/plugin-sdk" {
|
|
|
14
30
|
requireAuth: boolean
|
|
15
31
|
handler: (ctx: { args?: string; senderId?: string; channel?: string }) => Promise<{ text: string }>
|
|
16
32
|
}): void
|
|
33
|
+
registerCli(registrar: OpenClawPluginCliRegistrar, opts?: { commands?: string[] }): void
|
|
17
34
|
registerTool<T = unknown>(
|
|
18
35
|
options: {
|
|
19
36
|
name: string
|