@hyperspell/openclaw-hyperspell 0.8.0 → 0.9.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 CHANGED
@@ -7,9 +7,11 @@ OpenClaw plugin for [Hyperspell](https://hyperspell.com) - Context and memory fo
7
7
  ## Installation
8
8
 
9
9
  ```bash
10
- openclaw plugins install @hyperspell/openclaw-hyperspell
10
+ openclaw plugins install --dangerously-force-unsafe-install @hyperspell/openclaw-hyperspell
11
11
  ```
12
12
 
13
+ > **Note:** The `--dangerously-force-unsafe-install` flag is required because OpenClaw's automated install scanner detects standard operations that are essential to how the plugin works — opening your browser during setup, scheduling background jobs, and securely communicating with the Hyperspell API. These are normal, expected behaviors and the plugin does not execute arbitrary code or access data beyond what is needed to function. The source is fully open and auditable in this repository.
14
+
13
15
  ## Quick Start
14
16
 
15
17
  Run the interactive setup wizard:
package/client.ts CHANGED
@@ -27,7 +27,7 @@ export type Integration = {
27
27
  id: string;
28
28
  name: string;
29
29
  provider: HyperspellSource;
30
- icon: string;
30
+ icon: string | null;
31
31
  };
32
32
 
33
33
  export type Connection = {
@@ -37,6 +37,25 @@ export type Connection = {
37
37
  provider: HyperspellSource;
38
38
  };
39
39
 
40
+ export type EmotionalStateResponse = {
41
+ resourceId: string
42
+ summary: string
43
+ extractedAt: string
44
+ sessionId: string | null
45
+ relationshipId: string | null
46
+ status: string
47
+ }
48
+
49
+ export type EmotionalStateLatest = {
50
+ resourceId: string
51
+ summary: string
52
+ extractedAt: string
53
+ sessionId: string | null
54
+ relationshipId: string | null
55
+ }
56
+
57
+ const API_BASE_URL = "https://api.hyperspell.com"
58
+
40
59
  export class HyperspellClient {
41
60
  private client: Hyperspell;
42
61
  private config: HyperspellConfig;
@@ -52,6 +71,17 @@ export class HyperspellClient {
52
71
  );
53
72
  }
54
73
 
74
+ private rawHeaders(): Record<string, string> {
75
+ const headers: Record<string, string> = {
76
+ "Content-Type": "application/json",
77
+ Authorization: `Bearer ${this.config.apiKey}`,
78
+ };
79
+ if (this.config.userId) {
80
+ headers["X-As-User"] = this.config.userId;
81
+ }
82
+ return headers;
83
+ }
84
+
55
85
  async search(
56
86
  query: string,
57
87
  options?: { limit?: number; sources?: HyperspellSource[]; after?: string; before?: string },
@@ -149,6 +179,7 @@ export class HyperspellClient {
149
179
  score: doc.score ?? null,
150
180
  url: (doc.metadata?.url as string | null) ?? null,
151
181
  createdAt: (doc.metadata?.created_at as string | null) ?? null,
182
+ highlights: [],
152
183
  }));
153
184
 
154
185
  log.debugResponse("memories.search (with answer)", {
@@ -290,7 +321,12 @@ export class HyperspellClient {
290
321
  session_id: options?.sessionId,
291
322
  title: options?.title,
292
323
  format: "openclaw",
293
- extract: options?.extract ?? ["procedure"],
324
+ // Cast: SDK 0.35 typing accepts only ["procedure" | "memory"], but the
325
+ // backend's mood extractor (hyperspell/hyperspell#581) accepts "mood".
326
+ // Remove this cast once the OpenAPI spec is updated.
327
+ extract: (options?.extract ?? ["procedure"]) as Array<
328
+ "procedure" | "memory"
329
+ >,
294
330
  metadata: {
295
331
  ...options?.metadata,
296
332
  openclaw_source: "agent_end",
@@ -319,4 +355,106 @@ export class HyperspellClient {
319
355
  log.debugResponse("connections.list", { count: connections.length });
320
356
  return connections;
321
357
  }
358
+
359
+ // -- Emotional State (raw fetch -- not in public SDK) -----------------------
360
+
361
+ async storeEmotionalState(
362
+ conversation: string,
363
+ options?: {
364
+ sessionId?: string;
365
+ relationshipId?: string;
366
+ metadata?: Record<string, string | number | boolean>;
367
+ },
368
+ ): Promise<EmotionalStateResponse> {
369
+ log.debugRequest("emotional-state.store", {
370
+ conversationLength: conversation.length,
371
+ relationshipId: options?.relationshipId,
372
+ });
373
+
374
+ const body: Record<string, unknown> = { conversation };
375
+ if (options?.sessionId) body.session_id = options.sessionId;
376
+ if (options?.relationshipId) body.relationship_id = options.relationshipId;
377
+ if (options?.metadata) body.metadata = options.metadata;
378
+
379
+ const res = await fetch(`${API_BASE_URL}/emotional-state`, {
380
+ method: "POST",
381
+ headers: this.rawHeaders(),
382
+ body: JSON.stringify(body),
383
+ });
384
+
385
+ if (!res.ok) {
386
+ const text = await res.text().catch(() => "");
387
+ throw new Error(`POST /emotional-state failed (${res.status}): ${text}`);
388
+ }
389
+
390
+ const data = await res.json();
391
+ const result: EmotionalStateResponse = {
392
+ resourceId: data.resource_id,
393
+ summary: data.summary,
394
+ extractedAt: data.extracted_at,
395
+ sessionId: data.session_id ?? null,
396
+ relationshipId: data.relationship_id ?? null,
397
+ status: data.status,
398
+ };
399
+
400
+ log.debugResponse("emotional-state.store", { resourceId: result.resourceId });
401
+ return result;
402
+ }
403
+
404
+ async getEmotionalState(relationshipId?: string): Promise<EmotionalStateLatest | null> {
405
+ log.debugRequest("emotional-state.get", { relationshipId });
406
+
407
+ const url = new URL(`${API_BASE_URL}/emotional-state`);
408
+ if (relationshipId) url.searchParams.set("relationship_id", relationshipId);
409
+
410
+ const res = await fetch(url.toString(), {
411
+ method: "GET",
412
+ headers: this.rawHeaders(),
413
+ });
414
+
415
+ if (!res.ok) {
416
+ const text = await res.text().catch(() => "");
417
+ throw new Error(`GET /emotional-state failed (${res.status}): ${text}`);
418
+ }
419
+
420
+ const data = await res.json();
421
+ if (data === null) {
422
+ log.debugResponse("emotional-state.get", { found: false });
423
+ return null;
424
+ }
425
+
426
+ const result: EmotionalStateLatest = {
427
+ resourceId: data.resource_id,
428
+ summary: data.summary,
429
+ extractedAt: data.extracted_at,
430
+ sessionId: data.session_id ?? null,
431
+ relationshipId: data.relationship_id ?? null,
432
+ };
433
+
434
+ log.debugResponse("emotional-state.get", { found: true, resourceId: result.resourceId });
435
+ return result;
436
+ }
437
+
438
+ async deleteEmotionalState(relationshipId?: string): Promise<{ deletedCount: number }> {
439
+ log.debugRequest("emotional-state.delete", { relationshipId });
440
+
441
+ const url = new URL(`${API_BASE_URL}/emotional-state`);
442
+ if (relationshipId) url.searchParams.set("relationship_id", relationshipId);
443
+
444
+ const res = await fetch(url.toString(), {
445
+ method: "DELETE",
446
+ headers: this.rawHeaders(),
447
+ });
448
+
449
+ if (!res.ok) {
450
+ const text = await res.text().catch(() => "");
451
+ throw new Error(`DELETE /emotional-state failed (${res.status}): ${text}`);
452
+ }
453
+
454
+ const data = await res.json();
455
+ const result = { deletedCount: data.deleted_count };
456
+
457
+ log.debugResponse("emotional-state.delete", result);
458
+ return result;
459
+ }
322
460
  }
package/commands/setup.ts CHANGED
@@ -332,6 +332,8 @@ async function runSetup(): Promise<void> {
332
332
  apiKey,
333
333
  userId,
334
334
  autoContext: true,
335
+ autoTrace: { enabled: false, extract: ["procedure"] },
336
+ emotionalContext: false,
335
337
  syncMemories: true,
336
338
  sources: [],
337
339
  maxResults: 10,
package/config.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export type HyperspellSource =
2
- | "collections"
3
2
  | "reddit"
4
3
  | "notion"
5
4
  | "slack"
@@ -8,7 +7,11 @@ export type HyperspellSource =
8
7
  | "box"
9
8
  | "google_drive"
10
9
  | "vault"
11
- | "web_crawler";
10
+ | "web_crawler"
11
+ | "dropbox"
12
+ | "github"
13
+ | "trace"
14
+ | "microsoft_teams";
12
15
 
13
16
  export type KnowledgeGraphConfig = {
14
17
  enabled: boolean;
@@ -27,6 +30,8 @@ export type HyperspellConfig = {
27
30
  userId?: string;
28
31
  autoContext: boolean;
29
32
  autoTrace: AutoTraceConfig;
33
+ emotionalContext: boolean;
34
+ relationshipId?: string;
30
35
  syncMemories: boolean;
31
36
  sources: HyperspellSource[];
32
37
  maxResults: number;
@@ -40,6 +45,8 @@ const ALLOWED_KEYS = [
40
45
  "userId",
41
46
  "autoContext",
42
47
  "autoTrace",
48
+ "emotionalContext",
49
+ "relationshipId",
43
50
  "syncMemories",
44
51
  "sources",
45
52
  "maxResults",
@@ -49,7 +56,6 @@ const ALLOWED_KEYS = [
49
56
  ];
50
57
 
51
58
  const VALID_SOURCES: HyperspellSource[] = [
52
- "collections",
53
59
  "reddit",
54
60
  "notion",
55
61
  "slack",
@@ -59,6 +65,10 @@ const VALID_SOURCES: HyperspellSource[] = [
59
65
  "google_drive",
60
66
  "vault",
61
67
  "web_crawler",
68
+ "dropbox",
69
+ "github",
70
+ "trace",
71
+ "microsoft_teams",
62
72
  ];
63
73
 
64
74
  function assertAllowedKeys(
@@ -162,6 +172,8 @@ export function parseConfig(raw: unknown): HyperspellConfig {
162
172
  | Record<string, string | number | boolean>
163
173
  | undefined,
164
174
  },
175
+ emotionalContext: (cfg.emotionalContext as boolean) ?? false,
176
+ relationshipId: cfg.relationshipId as string | undefined,
165
177
  syncMemories: (cfg.syncMemories as boolean) ?? false,
166
178
  sources: parseSources(cfg.sources as string | string[] | undefined),
167
179
  maxResults: (cfg.maxResults as number) ?? 10,
@@ -0,0 +1,97 @@
1
+ import type { HyperspellClient } from "../client.ts";
2
+ import type { HyperspellConfig } from "../config.ts";
3
+ import { log } from "../logger.ts";
4
+
5
+ type Message = { role?: string; content?: string | unknown };
6
+
7
+ const MIN_MESSAGES = 3;
8
+ const MIN_CONVERSATION_LENGTH = 100;
9
+
10
+ function messagesToTranscript(messages: unknown[]): string {
11
+ return (messages as Message[])
12
+ .filter((m) => m.role && m.content)
13
+ .map((m) => {
14
+ const content =
15
+ typeof m.content === "string" ? m.content : JSON.stringify(m.content);
16
+ return `${m.role}: ${content}`;
17
+ })
18
+ .join("\n");
19
+ }
20
+
21
+ /**
22
+ * Fetch emotional state at session start and inject into context.
23
+ * Runs on `before_agent_start`.
24
+ */
25
+ export function buildEmotionalStateFetchHandler(
26
+ client: HyperspellClient,
27
+ cfg: HyperspellConfig,
28
+ ) {
29
+ return async (_event: Record<string, unknown>) => {
30
+ try {
31
+ const state = await client.getEmotionalState(cfg.relationshipId);
32
+
33
+ if (!state) {
34
+ log.debug("emotional-context: no prior emotional state found");
35
+ return;
36
+ }
37
+
38
+ log.debug(`emotional-context: injecting state from ${state.extractedAt}`);
39
+
40
+ const context = [
41
+ "<hyperspell-emotional-context>",
42
+ "The following captures the emotional register of your relationship with this user from your last interaction. Let it inform your tone — don't reference it explicitly.",
43
+ "",
44
+ state.summary,
45
+ "</hyperspell-emotional-context>",
46
+ ].join("\n");
47
+
48
+ return { prependContext: context };
49
+ } catch (err) {
50
+ log.error("emotional-context fetch failed", err);
51
+ return;
52
+ }
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Extract and store emotional state at session end.
58
+ * Runs on `agent_end` — fire-and-forget.
59
+ */
60
+ export function buildEmotionalStateStoreHandler(
61
+ client: HyperspellClient,
62
+ cfg: HyperspellConfig,
63
+ ) {
64
+ return async (event: Record<string, unknown>) => {
65
+ if (event.success === false) {
66
+ log.debug("emotional-state: skipping — agent ended with error");
67
+ return;
68
+ }
69
+
70
+ const messages = event.messages as unknown[] | undefined;
71
+ if (!messages || messages.length < MIN_MESSAGES) {
72
+ log.debug(
73
+ `emotional-state: skipping — too few messages (${messages?.length ?? 0})`,
74
+ );
75
+ return;
76
+ }
77
+
78
+ const transcript = messagesToTranscript(messages);
79
+ if (transcript.length < MIN_CONVERSATION_LENGTH) {
80
+ log.debug(
81
+ `emotional-state: skipping — conversation too short (${transcript.length} chars)`,
82
+ );
83
+ return;
84
+ }
85
+
86
+ try {
87
+ const result = await client.storeEmotionalState(transcript, {
88
+ relationshipId: cfg.relationshipId,
89
+ metadata: { source: "openclaw_agent_end" },
90
+ });
91
+ log.info(`emotional-state: stored ${result.resourceId}`);
92
+ } catch (err) {
93
+ // Fire-and-forget — never let this break the session
94
+ log.error("emotional-state store failed", err);
95
+ }
96
+ };
97
+ }
package/index.ts CHANGED
@@ -5,6 +5,7 @@ import { registerCliCommands } from "./commands/setup.ts"
5
5
  import { parseConfig, hyperspellConfigSchema, getWorkspaceDir } from "./config.ts"
6
6
  import { buildAutoContextHandler } from "./hooks/auto-context.ts"
7
7
  import { buildAutoTraceHandler } from "./hooks/auto-trace.ts"
8
+ import { buildEmotionalStateFetchHandler, buildEmotionalStateStoreHandler } from "./hooks/emotional-state.ts"
8
9
  import { buildFileSyncHandler, syncMemoriesOnStartup } from "./hooks/memory-sync.ts"
9
10
  import { initLogger } from "./logger.ts"
10
11
  import { registerRememberTool } from "./tools/remember.ts"
@@ -83,6 +84,12 @@ export default {
83
84
  registerSearchTool(api, client, cfg);
84
85
  registerRememberTool(api, client, cfg);
85
86
 
87
+ // Register emotional context hooks (fetch on start, store on end)
88
+ if (cfg.emotionalContext) {
89
+ api.on("before_agent_start", buildEmotionalStateFetchHandler(client, cfg));
90
+ api.on("agent_end", buildEmotionalStateStoreHandler(client, cfg));
91
+ }
92
+
86
93
  // Register auto-context hook
87
94
  if (cfg.autoContext) {
88
95
  const autoContextHandler = buildAutoContextHandler(client, cfg);
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "id": "openclaw-hyperspell",
3
+ "name": "Hyperspell",
4
+ "description": "Context and memory for your AI agents — search across Notion, Slack, Google Drive, and more",
5
+ "version": "0.7.2",
3
6
  "kind": "memory",
4
7
  "uiHints": {
5
8
  "apiKey": {
@@ -24,6 +27,17 @@
24
27
  "help": "Automatically send conversation traces to Hyperspell for memory extraction (procedural, mood) at the end of each session",
25
28
  "advanced": true
26
29
  },
30
+ "emotionalContext": {
31
+ "label": "Emotional Context",
32
+ "help": "Maintain emotional continuity across sessions — remembers not just what happened, but how it felt",
33
+ "advanced": true
34
+ },
35
+ "relationshipId": {
36
+ "label": "Relationship ID",
37
+ "placeholder": "partner-anna",
38
+ "help": "Optional identifier for per-relationship emotional state (e.g. different registers for different people)",
39
+ "advanced": true
40
+ },
27
41
  "sources": {
28
42
  "label": "Sources",
29
43
  "placeholder": "notion,slack,google_drive",
@@ -79,6 +93,12 @@
79
93
  "metadata": { "type": "object" }
80
94
  }
81
95
  },
96
+ "emotionalContext": {
97
+ "type": "boolean"
98
+ },
99
+ "relationshipId": {
100
+ "type": "string"
101
+ },
82
102
  "sources": {
83
103
  "type": "string"
84
104
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspell/openclaw-hyperspell",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw Hyperspell memory plugin",
6
6
  "license": "MIT",
@@ -17,6 +17,8 @@
17
17
  "openclaw.plugin.json",
18
18
  "README.md"
19
19
  ],
20
+ "author": "Hyperspell <hello@hyperspell.com> (https://hyperspell.com)",
21
+ "homepage": "https://hyperspell.com",
20
22
  "repository": {
21
23
  "type": "git",
22
24
  "url": "git+https://github.com/hyperspell/hyperspell-openclaw.git"
@@ -32,7 +34,7 @@
32
34
  "dependencies": {
33
35
  "@clack/prompts": "^1.0.0",
34
36
  "@sinclair/typebox": "^0.34.0",
35
- "hyperspell": "^0.30.0"
37
+ "hyperspell": "^0.35.1"
36
38
  },
37
39
  "peerDependencies": {
38
40
  "openclaw": ">=2026.1.29"
@@ -46,7 +48,15 @@
46
48
  "extensions": [
47
49
  "./index.ts"
48
50
  ],
49
- "hooks": []
51
+ "hooks": [],
52
+ "compat": {
53
+ "pluginApi": ">=2026.1.29",
54
+ "minGatewayVersion": "2026.1.29"
55
+ },
56
+ "build": {
57
+ "openclawVersion": "2026.3.24-beta.2",
58
+ "pluginSdkVersion": "2026.3.24-beta.2"
59
+ }
50
60
  },
51
61
  "devDependencies": {
52
62
  "typescript": "^5.9.3"