@hyperspell/openclaw-hyperspell 0.1.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 ADDED
@@ -0,0 +1,98 @@
1
+ # OpenClaw Hyperspell Plugin
2
+
3
+ OpenClaw plugin for [Hyperspell](https://hyperspell.com) - Context and memory for your AI agents.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ openclaw plugins install @hyperspell/openclaw-hyperspell
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ Add to your `openclaw.json`:
14
+
15
+ ```json
16
+ {
17
+ "plugins": {
18
+ "openclaw-hyperspell": {
19
+ "apiKey": "${HYPERSPELL_API_KEY}",
20
+ "autoContext": true
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ Or set the environment variable:
27
+
28
+ ```bash
29
+ export HYPERSPELL_API_KEY=hs_...
30
+ ```
31
+
32
+ ### Options
33
+
34
+ | Option | Type | Default | Description |
35
+ |--------|------|---------|-------------|
36
+ | `apiKey` | string | `${HYPERSPELL_API_KEY}` | Hyperspell API key |
37
+ | `userId` | string | - | User ID to scope searches (for non-JWT API keys) |
38
+ | `autoContext` | boolean | `true` | Auto-inject relevant memories before each AI turn |
39
+ | `sources` | string | - | Comma-separated sources to search (e.g., `notion,slack`) |
40
+ | `maxResults` | number | `10` | Maximum memories per context injection |
41
+ | `debug` | boolean | `false` | Enable verbose logging |
42
+
43
+ ## Slash Commands
44
+
45
+ ### `/context <query>`
46
+
47
+ Search your memories for relevant context.
48
+
49
+ ```
50
+ /context Q1 budget planning
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
61
+ ```
62
+
63
+ ### `/remember <text>`
64
+
65
+ Save something to memory.
66
+
67
+ ```
68
+ /remember Meeting with Alice: discussed Q1 budget, need to follow up on headcount
69
+ ```
70
+
71
+ ## AI Tools
72
+
73
+ The plugin registers tools that the AI can use autonomously:
74
+
75
+ - **hyperspell_search** - Search through connected sources
76
+ - **hyperspell_remember** - Save information to memory
77
+
78
+ ## Auto-Context
79
+
80
+ When `autoContext: true` (default), the plugin automatically:
81
+
82
+ 1. Intercepts each user message before the AI responds
83
+ 2. Searches Hyperspell for relevant memories
84
+ 3. Injects matching context into the AI's prompt
85
+
86
+ This ensures the AI always has access to relevant information from your connected sources.
87
+
88
+ ## Available Sources
89
+
90
+ - `collections` - User-created collections
91
+ - `notion` - Notion pages and databases
92
+ - `slack` - Slack messages
93
+ - `google_calendar` - Google Calendar events
94
+ - `google_mail` - Gmail messages
95
+ - `google_drive` - Google Drive files
96
+ - `box` - Box files
97
+ - `vault` - Vault documents
98
+ - `web_crawler` - Crawled web pages
package/client.ts ADDED
@@ -0,0 +1,113 @@
1
+ import Hyperspell from "hyperspell"
2
+ import type { HyperspellConfig, HyperspellSource } from "./config.ts"
3
+ import { log } from "./logger.ts"
4
+
5
+ export type SearchResult = {
6
+ resourceId: string
7
+ title: string | null
8
+ source: HyperspellSource
9
+ score: number | null
10
+ url: string | null
11
+ createdAt: string | null
12
+ }
13
+
14
+ export type Integration = {
15
+ id: string
16
+ name: string
17
+ provider: HyperspellSource
18
+ icon: string
19
+ }
20
+
21
+ export class HyperspellClient {
22
+ private client: Hyperspell
23
+ private config: HyperspellConfig
24
+
25
+ constructor(config: HyperspellConfig) {
26
+ this.config = config
27
+ this.client = new Hyperspell({
28
+ apiKey: config.apiKey,
29
+ })
30
+ log.info("client initialized")
31
+ }
32
+
33
+ async search(
34
+ query: string,
35
+ options?: { limit?: number; sources?: HyperspellSource[] },
36
+ ): Promise<SearchResult[]> {
37
+ const limit = options?.limit ?? this.config.maxResults
38
+ const sources =
39
+ options?.sources ?? (this.config.sources.length > 0 ? this.config.sources : undefined)
40
+
41
+ log.debugRequest("memories.search", { query, limit, sources })
42
+
43
+ const response = await this.client.memories.search({
44
+ query,
45
+ sources,
46
+ options: {
47
+ max_results: limit,
48
+ },
49
+ })
50
+
51
+ const results: SearchResult[] = response.documents.map((doc) => ({
52
+ resourceId: doc.resource_id,
53
+ title: doc.title ?? null,
54
+ source: doc.source as HyperspellSource,
55
+ score: doc.score ?? null,
56
+ url: doc.metadata?.url as string | null ?? null,
57
+ createdAt: doc.metadata?.created_at as string | null ?? null,
58
+ }))
59
+
60
+ log.debugResponse("memories.search", { count: results.length })
61
+ return results
62
+ }
63
+
64
+ async addMemory(
65
+ text: string,
66
+ options?: { title?: string; metadata?: Record<string, string | number | boolean> },
67
+ ): Promise<{ resourceId: string }> {
68
+ log.debugRequest("memories.add", {
69
+ textLength: text.length,
70
+ title: options?.title,
71
+ })
72
+
73
+ const result = await this.client.memories.add({
74
+ text,
75
+ title: options?.title,
76
+ metadata: {
77
+ ...options?.metadata,
78
+ openclaw_source: "command",
79
+ },
80
+ })
81
+
82
+ log.debugResponse("memories.add", { resourceId: result.resource_id })
83
+ return { resourceId: result.resource_id }
84
+ }
85
+
86
+ async listIntegrations(): Promise<Integration[]> {
87
+ log.debugRequest("integrations.list", {})
88
+
89
+ const response = await this.client.integrations.list()
90
+
91
+ const integrations: Integration[] = response.integrations.map((int) => ({
92
+ id: int.id,
93
+ name: int.name,
94
+ provider: int.provider as HyperspellSource,
95
+ icon: int.icon,
96
+ }))
97
+
98
+ log.debugResponse("integrations.list", { count: integrations.length })
99
+ return integrations
100
+ }
101
+
102
+ async getConnectUrl(integrationId: string): Promise<{ url: string; expiresAt: string }> {
103
+ log.debugRequest("integrations.connect", { integrationId })
104
+
105
+ const response = await this.client.integrations.connect(integrationId)
106
+
107
+ log.debugResponse("integrations.connect", { url: response.url })
108
+ return {
109
+ url: response.url,
110
+ expiresAt: response.expires_at,
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,139 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
2
+ import type { HyperspellClient } from "../client.ts"
3
+ import type { HyperspellConfig } from "../config.ts"
4
+ import { openInBrowser } from "../lib/browser.ts"
5
+ import { log } from "../logger.ts"
6
+
7
+ function truncate(text: string, maxLength: number): string {
8
+ if (text.length <= maxLength) return text
9
+ return `${text.slice(0, maxLength)}…`
10
+ }
11
+
12
+ function formatScore(score: number | null): string {
13
+ if (score === null) return ""
14
+ return ` (${Math.round(score * 100)}%)`
15
+ }
16
+
17
+ export function registerCommands(
18
+ api: OpenClawPluginApi,
19
+ client: HyperspellClient,
20
+ _cfg: HyperspellConfig,
21
+ ): void {
22
+ // /context <query> - Search memories and show summaries
23
+ api.registerCommand({
24
+ name: "context",
25
+ description: "Search your memories for relevant context",
26
+ acceptsArgs: true,
27
+ requireAuth: true,
28
+ handler: async (ctx: { args?: string }) => {
29
+ const query = ctx.args?.trim()
30
+ if (!query) {
31
+ return { text: "Usage: /context <search query>" }
32
+ }
33
+
34
+ log.debug(`/context command: "${query}"`)
35
+
36
+ try {
37
+ const results = await client.search(query, { limit: 5 })
38
+
39
+ if (results.length === 0) {
40
+ return { text: `No memories found for: "${query}"` }
41
+ }
42
+
43
+ const lines = results.map((r, i) => {
44
+ const title = r.title ? truncate(r.title, 60) : `[${r.source}]`
45
+ const score = formatScore(r.score)
46
+ return `${i + 1}. ${title}${score}`
47
+ })
48
+
49
+ return {
50
+ text: `Found ${results.length} memories:\n\n${lines.join("\n")}`,
51
+ }
52
+ } catch (err) {
53
+ log.error("/context failed", err)
54
+ return { text: "Failed to search memories. Check logs for details." }
55
+ }
56
+ },
57
+ })
58
+
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
+ // /remember <text> - Add a new memory
113
+ api.registerCommand({
114
+ name: "remember",
115
+ description: "Save something to memory",
116
+ acceptsArgs: true,
117
+ requireAuth: true,
118
+ handler: async (ctx: { args?: string }) => {
119
+ const text = ctx.args?.trim()
120
+ if (!text) {
121
+ return { text: "Usage: /remember <text to remember>" }
122
+ }
123
+
124
+ log.debug(`/remember command: "${truncate(text, 50)}"`)
125
+
126
+ try {
127
+ await client.addMemory(text, {
128
+ metadata: { source: "openclaw_command" },
129
+ })
130
+
131
+ const preview = truncate(text, 60)
132
+ return { text: `Remembered: "${preview}"` }
133
+ } catch (err) {
134
+ log.error("/remember failed", err)
135
+ return { text: "Failed to save memory. Check logs for details." }
136
+ }
137
+ },
138
+ })
139
+ }
package/config.ts ADDED
@@ -0,0 +1,119 @@
1
+ export type HyperspellSource =
2
+ | "collections"
3
+ | "reddit"
4
+ | "notion"
5
+ | "slack"
6
+ | "google_calendar"
7
+ | "google_mail"
8
+ | "box"
9
+ | "google_drive"
10
+ | "vault"
11
+ | "web_crawler"
12
+
13
+ export type HyperspellConfig = {
14
+ apiKey: string
15
+ userId?: string
16
+ autoContext: boolean
17
+ sources: HyperspellSource[]
18
+ maxResults: number
19
+ debug: boolean
20
+ }
21
+
22
+ const ALLOWED_KEYS = [
23
+ "apiKey",
24
+ "userId",
25
+ "autoContext",
26
+ "sources",
27
+ "maxResults",
28
+ "debug",
29
+ ]
30
+
31
+ const VALID_SOURCES: HyperspellSource[] = [
32
+ "collections",
33
+ "reddit",
34
+ "notion",
35
+ "slack",
36
+ "google_calendar",
37
+ "google_mail",
38
+ "box",
39
+ "google_drive",
40
+ "vault",
41
+ "web_crawler",
42
+ ]
43
+
44
+ function assertAllowedKeys(
45
+ value: Record<string, unknown>,
46
+ allowed: string[],
47
+ label: string,
48
+ ): void {
49
+ const unknown = Object.keys(value).filter((k) => !allowed.includes(k))
50
+ if (unknown.length > 0) {
51
+ throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`)
52
+ }
53
+ }
54
+
55
+ function resolveEnvVars(value: string): string {
56
+ return value.replace(/\$\{([^}]+)\}/g, (_, envVar: string) => {
57
+ const envValue = process.env[envVar]
58
+ if (!envValue) {
59
+ throw new Error(`Environment variable ${envVar} is not set`)
60
+ }
61
+ return envValue
62
+ })
63
+ }
64
+
65
+ function parseSources(raw: string | undefined): HyperspellSource[] {
66
+ if (!raw || raw.trim() === "") {
67
+ return []
68
+ }
69
+
70
+ const sources = raw
71
+ .split(",")
72
+ .map((s) => s.trim().toLowerCase())
73
+ .filter((s) => s.length > 0) as HyperspellSource[]
74
+
75
+ for (const source of sources) {
76
+ if (!VALID_SOURCES.includes(source)) {
77
+ throw new Error(
78
+ `Invalid source "${source}". Valid sources: ${VALID_SOURCES.join(", ")}`,
79
+ )
80
+ }
81
+ }
82
+
83
+ return sources
84
+ }
85
+
86
+ export function parseConfig(raw: unknown): HyperspellConfig {
87
+ const cfg =
88
+ raw && typeof raw === "object" && !Array.isArray(raw)
89
+ ? (raw as Record<string, unknown>)
90
+ : {}
91
+
92
+ if (Object.keys(cfg).length > 0) {
93
+ assertAllowedKeys(cfg, ALLOWED_KEYS, "hyperspell config")
94
+ }
95
+
96
+ const apiKey =
97
+ typeof cfg.apiKey === "string" && cfg.apiKey.length > 0
98
+ ? resolveEnvVars(cfg.apiKey)
99
+ : process.env.HYPERSPELL_API_KEY
100
+
101
+ if (!apiKey) {
102
+ throw new Error(
103
+ "hyperspell: apiKey is required (set in plugin config or HYPERSPELL_API_KEY env var)",
104
+ )
105
+ }
106
+
107
+ return {
108
+ apiKey,
109
+ userId: cfg.userId as string | undefined,
110
+ autoContext: (cfg.autoContext as boolean) ?? true,
111
+ sources: parseSources(cfg.sources as string | undefined),
112
+ maxResults: (cfg.maxResults as number) ?? 10,
113
+ debug: (cfg.debug as boolean) ?? false,
114
+ }
115
+ }
116
+
117
+ export const hyperspellConfigSchema = {
118
+ parse: parseConfig,
119
+ }
@@ -0,0 +1,76 @@
1
+ import type { HyperspellClient, SearchResult } from "../client.ts"
2
+ import type { HyperspellConfig } from "../config.ts"
3
+ import { log } from "../logger.ts"
4
+
5
+ function formatRelativeTime(isoTimestamp: string): string {
6
+ try {
7
+ const dt = new Date(isoTimestamp)
8
+ const now = new Date()
9
+ const seconds = (now.getTime() - dt.getTime()) / 1000
10
+ const minutes = seconds / 60
11
+ const hours = seconds / 3600
12
+ const days = seconds / 86400
13
+
14
+ if (minutes < 30) return "just now"
15
+ if (minutes < 60) return `${Math.floor(minutes)}mins ago`
16
+ if (hours < 24) return `${Math.floor(hours)} hrs ago`
17
+ if (days < 7) return `${Math.floor(days)}d ago`
18
+
19
+ const month = dt.toLocaleString("en", { month: "short" })
20
+ if (dt.getFullYear() === now.getFullYear()) {
21
+ return `${dt.getDate()} ${month}`
22
+ }
23
+ return `${dt.getDate()} ${month}, ${dt.getFullYear()}`
24
+ } catch {
25
+ return ""
26
+ }
27
+ }
28
+
29
+ function formatContext(results: SearchResult[], maxResults: number): string | null {
30
+ const limited = results.slice(0, maxResults)
31
+
32
+ if (limited.length === 0) return null
33
+
34
+ const lines = limited.map((r) => {
35
+ const title = r.title ?? `[${r.source}]`
36
+ const timeStr = r.createdAt ? formatRelativeTime(r.createdAt) : ""
37
+ const pct = r.score != null ? `[${Math.round(r.score * 100)}%]` : ""
38
+ const prefix = timeStr ? `[${timeStr}]` : ""
39
+ return `- ${prefix} ${title} ${pct}`.trim()
40
+ })
41
+
42
+ const intro =
43
+ "The following is context from the user's connected sources. Reference it only when relevant to the conversation."
44
+ const disclaimer =
45
+ "Use this context naturally when relevant — including indirect connections — but don't force it into every response or make assumptions beyond what's stated."
46
+
47
+ return `<hyperspell-context>\n${intro}\n\n## Relevant Memories (with relevance %)\n${lines.join("\n")}\n\n${disclaimer}\n</hyperspell-context>`
48
+ }
49
+
50
+ export function buildAutoContextHandler(
51
+ client: HyperspellClient,
52
+ cfg: HyperspellConfig,
53
+ ) {
54
+ return async (event: Record<string, unknown>) => {
55
+ const prompt = event.prompt as string | undefined
56
+ if (!prompt || prompt.length < 5) return
57
+
58
+ log.debug(`auto-context: searching for "${prompt.slice(0, 50)}..."`)
59
+
60
+ try {
61
+ const results = await client.search(prompt, { limit: cfg.maxResults })
62
+ const context = formatContext(results, cfg.maxResults)
63
+
64
+ if (!context) {
65
+ log.debug("auto-context: no relevant memories found")
66
+ return
67
+ }
68
+
69
+ log.debug(`auto-context: injecting ${results.length} memories`)
70
+ return { prependContext: context }
71
+ } catch (err) {
72
+ log.error("auto-context failed", err)
73
+ return
74
+ }
75
+ }
76
+ }
package/index.ts ADDED
@@ -0,0 +1,48 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
2
+ import { HyperspellClient } from "./client.ts"
3
+ import { registerCommands } from "./commands/slash.ts"
4
+ import { parseConfig, hyperspellConfigSchema } from "./config.ts"
5
+ import { buildAutoContextHandler } from "./hooks/auto-context.ts"
6
+ import { initLogger } from "./logger.ts"
7
+ import { registerRememberTool } from "./tools/remember.ts"
8
+ import { registerSearchTool } from "./tools/search.ts"
9
+
10
+ export default {
11
+ id: "openclaw-hyperspell",
12
+ name: "Hyperspell",
13
+ description: "OpenClaw powered by Hyperspell - RAG-as-a-service for your connected sources",
14
+ kind: "memory" as const,
15
+ configSchema: hyperspellConfigSchema,
16
+
17
+ register(api: OpenClawPluginApi) {
18
+ const cfg = parseConfig(api.pluginConfig)
19
+
20
+ initLogger(api.logger, cfg.debug)
21
+
22
+ const client = new HyperspellClient(cfg)
23
+
24
+ // Register AI tools
25
+ registerSearchTool(api, client, cfg)
26
+ registerRememberTool(api, client, cfg)
27
+
28
+ // Register auto-context hook
29
+ if (cfg.autoContext) {
30
+ const autoContextHandler = buildAutoContextHandler(client, cfg)
31
+ api.on("before_agent_start", autoContextHandler)
32
+ }
33
+
34
+ // Register slash commands
35
+ registerCommands(api, client, cfg)
36
+
37
+ // Register service for lifecycle management
38
+ api.registerService({
39
+ id: "openclaw-hyperspell",
40
+ start: () => {
41
+ api.logger.info("hyperspell: connected")
42
+ },
43
+ stop: () => {
44
+ api.logger.info("hyperspell: stopped")
45
+ },
46
+ })
47
+ },
48
+ }
package/lib/browser.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { exec } from "node:child_process"
2
+ import { platform } from "node:os"
3
+
4
+ export function openInBrowser(url: string): Promise<void> {
5
+ return new Promise((resolve, reject) => {
6
+ let command: string
7
+
8
+ switch (platform()) {
9
+ case "darwin":
10
+ command = `open "${url}"`
11
+ break
12
+ case "win32":
13
+ command = `start "" "${url}"`
14
+ break
15
+ default:
16
+ command = `xdg-open "${url}"`
17
+ }
18
+
19
+ exec(command, (error) => {
20
+ if (error) {
21
+ reject(error)
22
+ } else {
23
+ resolve()
24
+ }
25
+ })
26
+ })
27
+ }
package/logger.ts ADDED
@@ -0,0 +1,41 @@
1
+ type Logger = {
2
+ info: (message: string, ...args: unknown[]) => void
3
+ warn: (message: string, ...args: unknown[]) => void
4
+ error: (message: string, ...args: unknown[]) => void
5
+ debug: (message: string, ...args: unknown[]) => void
6
+ }
7
+
8
+ let _logger: Logger = console
9
+ let _debug = false
10
+
11
+ export function initLogger(logger: Logger, debug: boolean): void {
12
+ _logger = logger
13
+ _debug = debug
14
+ }
15
+
16
+ export const log = {
17
+ info: (message: string, ...args: unknown[]) => {
18
+ _logger.info(`hyperspell: ${message}`, ...args)
19
+ },
20
+ warn: (message: string, ...args: unknown[]) => {
21
+ _logger.warn(`hyperspell: ${message}`, ...args)
22
+ },
23
+ error: (message: string, ...args: unknown[]) => {
24
+ _logger.error(`hyperspell: ${message}`, ...args)
25
+ },
26
+ debug: (message: string, ...args: unknown[]) => {
27
+ if (_debug) {
28
+ _logger.debug(`hyperspell: ${message}`, ...args)
29
+ }
30
+ },
31
+ debugRequest: (method: string, params: unknown) => {
32
+ if (_debug) {
33
+ _logger.debug(`hyperspell: [${method}] request`, params)
34
+ }
35
+ },
36
+ debugResponse: (method: string, result: unknown) => {
37
+ if (_debug) {
38
+ _logger.debug(`hyperspell: [${method}] response`, result)
39
+ }
40
+ },
41
+ }
@@ -0,0 +1,52 @@
1
+ {
2
+ "id": "openclaw-hyperspell",
3
+ "kind": "memory",
4
+ "uiHints": {
5
+ "apiKey": {
6
+ "label": "Hyperspell API Key",
7
+ "sensitive": true,
8
+ "placeholder": "hs_...",
9
+ "help": "Your API key from app.hyperspell.com (or use ${HYPERSPELL_API_KEY})"
10
+ },
11
+ "userId": {
12
+ "label": "User ID",
13
+ "placeholder": "user_123",
14
+ "help": "Optional user ID to scope searches. Required for non-JWT API keys.",
15
+ "advanced": true
16
+ },
17
+ "autoContext": {
18
+ "label": "Auto-Context",
19
+ "help": "Inject relevant memories before every AI turn"
20
+ },
21
+ "sources": {
22
+ "label": "Sources",
23
+ "placeholder": "notion,slack,google_drive",
24
+ "help": "Comma-separated list of sources to search. Leave empty for all sources.",
25
+ "advanced": true
26
+ },
27
+ "maxResults": {
28
+ "label": "Max Results",
29
+ "placeholder": "10",
30
+ "help": "Maximum memories injected into context per turn",
31
+ "advanced": true
32
+ },
33
+ "debug": {
34
+ "label": "Debug Logging",
35
+ "help": "Enable verbose debug logs for API calls and responses",
36
+ "advanced": true
37
+ }
38
+ },
39
+ "configSchema": {
40
+ "type": "object",
41
+ "additionalProperties": false,
42
+ "properties": {
43
+ "apiKey": { "type": "string" },
44
+ "userId": { "type": "string" },
45
+ "autoContext": { "type": "boolean" },
46
+ "sources": { "type": "string" },
47
+ "maxResults": { "type": "number", "minimum": 1, "maximum": 20 },
48
+ "debug": { "type": "boolean" }
49
+ },
50
+ "required": []
51
+ }
52
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@hyperspell/openclaw-hyperspell",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "OpenClaw Hyperspell memory plugin",
6
+ "license": "MIT",
7
+ "main": "./index.ts",
8
+ "files": [
9
+ "*.ts",
10
+ "commands/",
11
+ "hooks/",
12
+ "tools/",
13
+ "lib/",
14
+ "types/",
15
+ "openclaw.plugin.json",
16
+ "README.md"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/hyperspell/openclaw-hyperspell.git"
21
+ },
22
+ "keywords": [
23
+ "openclaw",
24
+ "hyperspell",
25
+ "memory",
26
+ "rag",
27
+ "ai",
28
+ "plugin"
29
+ ],
30
+ "dependencies": {
31
+ "hyperspell": "^0.30.0",
32
+ "@sinclair/typebox": "^0.34.0"
33
+ },
34
+ "peerDependencies": {
35
+ "openclaw": ">=2026.1.29"
36
+ },
37
+ "scripts": {
38
+ "check-types": "tsc --noEmit",
39
+ "lint": "bunx @biomejs/biome ci .",
40
+ "lint:fix": "bunx @biomejs/biome check --write ."
41
+ },
42
+ "openclaw": {
43
+ "extensions": ["./index.ts"]
44
+ },
45
+ "devDependencies": {
46
+ "typescript": "^5.9.3"
47
+ }
48
+ }
@@ -0,0 +1,44 @@
1
+ import { Type } from "@sinclair/typebox"
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
+ import type { HyperspellClient } from "../client.ts"
4
+ import type { HyperspellConfig } from "../config.ts"
5
+ import { log } from "../logger.ts"
6
+
7
+ export function registerRememberTool(
8
+ api: OpenClawPluginApi,
9
+ client: HyperspellClient,
10
+ _cfg: HyperspellConfig,
11
+ ): void {
12
+ api.registerTool(
13
+ {
14
+ name: "hyperspell_remember",
15
+ label: "Memory Store",
16
+ description: "Save important information to the user's memory.",
17
+ parameters: Type.Object({
18
+ text: Type.String({ description: "Information to remember" }),
19
+ title: Type.Optional(
20
+ Type.String({ description: "Optional title for the memory" }),
21
+ ),
22
+ }),
23
+ async execute(
24
+ _toolCallId: string,
25
+ params: { text: string; title?: string },
26
+ ) {
27
+ log.debug(`remember tool: "${params.text.slice(0, 50)}..."`)
28
+
29
+ await client.addMemory(params.text, {
30
+ title: params.title,
31
+ metadata: { source: "openclaw_tool" },
32
+ })
33
+
34
+ const preview =
35
+ params.text.length > 80 ? `${params.text.slice(0, 80)}…` : params.text
36
+
37
+ return {
38
+ content: [{ type: "text" as const, text: `Stored: "${preview}"` }],
39
+ }
40
+ },
41
+ },
42
+ { name: "hyperspell_remember" },
43
+ )
44
+ }
@@ -0,0 +1,72 @@
1
+ import { Type } from "@sinclair/typebox"
2
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
+ import type { HyperspellClient } from "../client.ts"
4
+ import type { HyperspellConfig } from "../config.ts"
5
+ import { log } from "../logger.ts"
6
+
7
+ export function registerSearchTool(
8
+ api: OpenClawPluginApi,
9
+ client: HyperspellClient,
10
+ _cfg: HyperspellConfig,
11
+ ): void {
12
+ api.registerTool(
13
+ {
14
+ name: "hyperspell_search",
15
+ label: "Memory Search",
16
+ description:
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
+ }),
24
+ async execute(
25
+ _toolCallId: string,
26
+ params: { query: string; limit?: number },
27
+ ) {
28
+ const limit = params.limit ?? 5
29
+ log.debug(`search tool: query="${params.query}" limit=${limit}`)
30
+
31
+ const results = await client.search(params.query, { limit })
32
+
33
+ if (results.length === 0) {
34
+ return {
35
+ content: [
36
+ { type: "text" as const, text: "No relevant memories found." },
37
+ ],
38
+ }
39
+ }
40
+
41
+ const text = results
42
+ .map((r, i) => {
43
+ const title = r.title ?? `[${r.source}]`
44
+ const score = r.score
45
+ ? ` (${Math.round(r.score * 100)}%)`
46
+ : ""
47
+ return `${i + 1}. ${title}${score}`
48
+ })
49
+ .join("\n")
50
+
51
+ return {
52
+ content: [
53
+ {
54
+ type: "text" as const,
55
+ text: `Found ${results.length} memories:\n\n${text}`,
56
+ },
57
+ ],
58
+ details: {
59
+ count: results.length,
60
+ memories: results.map((r) => ({
61
+ resourceId: r.resourceId,
62
+ title: r.title,
63
+ source: r.source,
64
+ score: r.score,
65
+ })),
66
+ },
67
+ }
68
+ },
69
+ },
70
+ { name: "hyperspell_search" },
71
+ )
72
+ }
@@ -0,0 +1,42 @@
1
+ declare module "openclaw/plugin-sdk" {
2
+ export interface OpenClawPluginApi {
3
+ pluginConfig: unknown
4
+ logger: {
5
+ info: (message: string, ...args: unknown[]) => void
6
+ warn: (message: string, ...args: unknown[]) => void
7
+ error: (message: string, ...args: unknown[]) => void
8
+ debug: (message: string, ...args: unknown[]) => void
9
+ }
10
+ registerCommand(options: {
11
+ name: string
12
+ description: string
13
+ acceptsArgs: boolean
14
+ requireAuth: boolean
15
+ handler: (ctx: { args?: string; senderId?: string; channel?: string }) => Promise<{ text: string }>
16
+ }): void
17
+ registerTool<T = unknown>(
18
+ options: {
19
+ name: string
20
+ label: string
21
+ description: string
22
+ parameters: unknown
23
+ execute: (
24
+ toolCallId: string,
25
+ params: T,
26
+ ) => Promise<{
27
+ content: Array<{ type: "text"; text: string }>
28
+ details?: Record<string, unknown>
29
+ }>
30
+ },
31
+ meta: { name: string },
32
+ ): void
33
+ on(event: string, handler: (event: Record<string, unknown>, ctx?: Record<string, unknown>) => Promise<{ prependContext?: string } | void> | void): void
34
+ registerService(options: {
35
+ id: string
36
+ start: () => void
37
+ stop: () => void
38
+ }): void
39
+ }
40
+
41
+ export function stringEnum<T extends string>(values: readonly T[]): unknown
42
+ }