@octamem/octamem-openclaw 1.0.3 → 1.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octamem/octamem-openclaw",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "type": "module",
5
5
  "description": "OctaMem personal memory plugin for OpenClaw",
6
6
  "license": "MIT",
package/src/client.ts CHANGED
@@ -1,152 +1,102 @@
1
1
  import { logRequest } from "./logger.ts"
2
2
  import type {
3
3
  AddResult,
4
- ChatContext,
5
- LastQa,
4
+ MemoryData,
6
5
  OctaMemConfig,
7
6
  SearchResult,
8
7
  } from "./types.ts"
9
8
 
10
- /**
11
- * OctaMem Memory API base URL.
12
- * API key must be sent in a header (Authorization: Bearer <key> or X-API-Key), not in the body.
13
- * - POST /search — body: { query, previousContext? }
14
- * - POST /add — body: { content, previousContext? }
15
- * - POST /details — body: {} or omitted
16
- */
17
9
  const API_BASE = "http://platform.octamem.com/api/memory"
18
10
 
19
- type ApiBody = { success?: boolean; error?: string; message?: string; [k: string]: unknown }
20
-
21
- /** Prefer API message for user-facing error; no hard-coded error codes. */
22
- function apiErrorMessage(data: ApiBody | null): string | null {
23
- if (!data || typeof data !== "object") return null
24
- if (typeof data.message === "string" && data.message.length > 0) return data.message
25
- if (typeof data.error === "string" && data.error.length > 0) return data.error
26
- return null
11
+ /** Safely coerce any value to a string handles objects, arrays, numbers, null, etc. */
12
+ function asString(val: unknown): string {
13
+ if (val === null || val === undefined || val === "") return ""
14
+ if (typeof val === "string") return val
15
+ return JSON.stringify(val)
27
16
  }
28
17
 
29
- /**
30
- * Client for the OctaMem personal-memory API (search, add, details).
31
- * Requires config.memories.personal.apiKey to be set.
32
- */
33
18
  export class OctaMemClient {
34
19
  private readonly apiKey: string
35
20
 
36
21
  constructor(config: OctaMemConfig) {
37
22
  const key = config.memories.personal?.apiKey
38
- if (!key) throw new Error("Personal memory API key not configured")
23
+ if (!key) throw new Error("OctaMem API key missing")
39
24
  this.apiKey = key
40
25
  }
41
26
 
42
- /** Headers for Memory API: key in Authorization Bearer (recommended). */
43
- private authHeaders(): Record<string, string> {
44
- return {
45
- "Content-Type": "application/json",
46
- Authorization: `Bearer ${this.apiKey}`,
47
- }
48
- }
49
-
50
- /** Returns true if config has a valid personal memory API key. */
51
27
  static isConfigured(config: OctaMemConfig): boolean {
52
28
  return Boolean(config.memories.personal?.apiKey)
53
29
  }
54
30
 
55
- /** Serializes chat context for API previousContext field. */
56
- private contextStr(ctx: ChatContext): string {
57
- return ctx
58
- .flatMap((t) => [
59
- `user: ${t.user}`,
60
- t.assistant ? `assistant: ${t.assistant}` : null,
61
- ])
62
- .filter(Boolean)
63
- .join(" | ")
31
+ private headers() {
32
+ return {
33
+ "Content-Type": "application/json",
34
+ Authorization: `Bearer ${this.apiKey}`,
35
+ }
64
36
  }
65
37
 
66
- /**
67
- * Searches personal memory; returns the raw API response for the LLM.
68
- * @param query - Search query text.
69
- * @param context - Recent user/assistant turns (previousContext sent to API).
70
- */
71
- async search(query: string, context: ChatContext = []): Promise<SearchResult> {
38
+ async search(query: string, previousContext: string): Promise<SearchResult> {
72
39
  try {
73
- const previousContext = this.contextStr(context)
74
40
  const payload = { query, previousContext }
75
- logRequest("POST /api/memory/search", payload)
41
+ logRequest("search", payload)
42
+
76
43
  const res = await fetch(`${API_BASE}/search`, {
77
44
  method: "POST",
78
- headers: this.authHeaders(),
45
+ headers: this.headers(),
79
46
  body: JSON.stringify(payload),
80
47
  })
81
- const data = (await res.json().catch(() => null)) as ApiBody | null
82
- const apiMsg = apiErrorMessage(data)
83
- if (!res.ok) {
84
- return { success: false, query, error: apiMsg ?? `API ${res.status}` }
48
+
49
+ const json = (await res.json().catch(() => null)) as Record<
50
+ string,
51
+ unknown
52
+ > | null
53
+
54
+ if (!res.ok || json?.success === false) {
55
+ const error =
56
+ (json?.message as string) ||
57
+ (json?.error as string) ||
58
+ "Search failed"
59
+ return { success: false, query, error }
85
60
  }
86
- if (data && data.success === false) {
87
- return { success: false, query, error: apiMsg ?? "Request failed" }
61
+
62
+ const raw = (json?.data ?? {}) as Record<string, unknown>
63
+ const data: MemoryData = {
64
+ semantic_memory: asString(raw.semantic_memory),
65
+ episodic_memory: asString(raw.episodic_memory),
66
+ procedural_memory: asString(raw.procedural_memory),
88
67
  }
89
- return { success: true, data: data ?? undefined, query }
68
+
69
+ return { success: true, data, query }
90
70
  } catch (e) {
91
- return { success: false, query, error: e instanceof Error ? e.message : String(e) }
71
+ return { success: false, query, error: String(e) }
92
72
  }
93
73
  }
94
74
 
95
- /**
96
- * Stores content in personal memory. If lastQa is provided, stores a Q&A pair instead of raw content.
97
- * @param content - Text to store (or ignored when lastQa is set).
98
- * @param metadata - Optional metadata (e.g. category, source).
99
- * @param context - Recent chat turns for API previousContext.
100
- * @param lastQa - If set, stores "User: ... Assistant: ..." instead of content.
101
- */
102
- async add(
103
- content: string,
104
- metadata: Record<string, unknown> = {},
105
- context: ChatContext = [],
106
- lastQa?: LastQa,
107
- ): Promise<AddResult> {
75
+ async add(content: string, previousContext: string): Promise<AddResult> {
108
76
  try {
109
- const body = lastQa
110
- ? `User: ${lastQa.query}\nAssistant: ${lastQa.response}`
111
- : content
112
- const previousContext = this.contextStr(context)
113
- const payload = { content: body, previousContext }
114
- logRequest("POST /api/memory/add", payload)
77
+ const payload = { content, previousContext }
78
+ logRequest("add", payload)
79
+
115
80
  const res = await fetch(`${API_BASE}/add`, {
116
81
  method: "POST",
117
- headers: this.authHeaders(),
82
+ headers: this.headers(),
118
83
  body: JSON.stringify(payload),
119
84
  })
120
- const data = (await res.json().catch(() => null)) as ApiBody | null
121
- const apiMsg = apiErrorMessage(data)
122
- if (!res.ok) {
123
- return { success: false, id: "", message: apiMsg ?? `API ${res.status}` }
124
- }
125
- if (data && data.success === false) {
126
- return { success: false, id: "", message: apiMsg ?? "Request failed" }
127
- }
128
- return { success: true, id: `mem_${Date.now()}`, message: "Memory stored successfully" }
129
- } catch (e) {
130
- return { success: false, id: "", message: e instanceof Error ? e.message : "Unknown error" }
131
- }
132
- }
133
85
 
134
- /** Fetches account/details for the configured personal memory. */
135
- async details(): Promise<{ success: boolean; data?: unknown; error?: string }> {
136
- try {
137
- const res = await fetch(`${API_BASE}/details`, {
138
- method: "POST",
139
- headers: this.authHeaders(),
140
- body: "{}",
141
- })
142
- const data = (await res.json().catch(() => null)) as ApiBody | null
143
- const apiMsg = apiErrorMessage(data)
144
- if (!res.ok) {
145
- return { success: false, error: apiMsg ?? `API ${res.status}` }
86
+ const json = (await res.json().catch(() => null)) as Record<
87
+ string,
88
+ unknown
89
+ > | null
90
+
91
+ if (!res.ok || json?.success === false) {
92
+ const message =
93
+ (json?.message as string) || (json?.error as string) || "Add failed"
94
+ return { success: false, message }
146
95
  }
147
- return { success: true, data: data ?? undefined }
96
+
97
+ return { success: true, message: (json?.message as string) || "Stored" }
148
98
  } catch (e) {
149
- return { success: false, error: e instanceof Error ? e.message : String(e) }
99
+ return { success: false, message: String(e) }
150
100
  }
151
101
  }
152
102
  }
package/src/config.ts CHANGED
@@ -1,78 +1,29 @@
1
1
  import type { OctaMemConfig } from "./types.ts"
2
2
 
3
- const ALLOWED_KEYS = ["memories", "toolDescription", "addToolDescription", "autoRecall", "autoCapture"]
4
-
5
- /**
6
- * Replaces ${VAR} placeholders in a string with process.env[VAR].
7
- * @throws If a referenced env var is not set.
8
- */
9
- function resolveEnv(s: string): string {
10
- return s.replace(/\$\{([^}]+)\}/g, (_, key: string) => {
11
- const v = process.env[key]
12
- if (!v) throw new Error(`Env ${key} not set`)
13
- return v
14
- })
15
- }
16
-
17
- /**
18
- * Parses and validates raw plugin config into OctaMemConfig.
19
- * Only known keys are allowed; personal memory apiKey supports ${ENV} substitution.
20
- * @returns Normalized config with memories.personal set only if apiKey is valid.
21
- */
22
- export function parseConfig(raw: unknown): OctaMemConfig {
23
- const o = raw && typeof raw === "object" && !Array.isArray(raw)
24
- ? (raw as Record<string, unknown>)
25
- : {}
26
-
27
- if (Object.keys(o).length > 0) {
28
- const bad = Object.keys(o).filter((k) => !ALLOWED_KEYS.includes(k))
29
- if (bad.length) throw new Error(`Unknown config: ${bad.join(", ")}`)
30
- }
31
-
32
- const memories: OctaMemConfig["memories"] = {}
33
- const personalRaw = o.memories && typeof o.memories === "object"
34
- ? (o.memories as Record<string, unknown>).personal
35
- : undefined
36
-
37
- if (personalRaw && typeof personalRaw === "object") {
38
- const p = personalRaw as Record<string, unknown>
39
- if (typeof p.apiKey === "string" && p.apiKey.length > 0) {
40
- try {
41
- memories.personal = {
42
- apiKey: resolveEnv(p.apiKey),
43
- description: typeof p.description === "string" ? p.description : undefined,
44
- }
45
- } catch {
46
- // skip invalid personal config
47
- }
48
- }
49
- }
3
+ export function parseConfig(raw: any): OctaMemConfig {
4
+ const apiKey =
5
+ raw?.memories?.personal?.apiKey || process.env.OCTAMEM_PERSONAL_KEY || ""
50
6
 
51
7
  return {
52
- memories,
53
- toolDescription: typeof o.toolDescription === "string" ? o.toolDescription : undefined,
54
- addToolDescription: typeof o.addToolDescription === "string" ? o.addToolDescription : undefined,
8
+ memories: apiKey ? { personal: { apiKey } } : {},
55
9
  }
56
10
  }
57
11
 
58
- /** OpenClaw config schema and parser for the OctaMem plugin. */
59
12
  export const configSchema = {
60
13
  jsonSchema: {
61
14
  type: "object",
62
- additionalProperties: false,
63
15
  properties: {
64
16
  memories: {
65
17
  type: "object",
66
18
  properties: {
67
19
  personal: {
68
20
  type: "object",
69
- properties: { apiKey: { type: "string" }, description: { type: "string" } },
70
- required: ["apiKey"],
21
+ properties: {
22
+ apiKey: { type: "string" },
23
+ },
71
24
  },
72
25
  },
73
26
  },
74
- toolDescription: { type: "string" },
75
- addToolDescription: { type: "string" },
76
27
  },
77
28
  },
78
29
  parse: parseConfig,
package/src/context.ts CHANGED
@@ -1,145 +1,180 @@
1
- import fs from "node:fs"
2
- import path from "node:path"
3
1
  import type { ChatContext } from "./types.ts"
4
2
 
5
- /** Max number of user/assistant pairs sent as previousContext to the OctaMem API (search/add). */
6
- export const PREVIOUS_CONTEXT_PAIRS = 3
3
+ export const MAX_CONTEXT_PAIRS = 3
7
4
 
8
- const OCTAMEM_TAG = /<octamem-context>[\s\S]*?<\/octamem-context>\s*/g
9
- /** Sender (untrusted metadata) block: "Sender (untrusted metadata):" plus fenced block. */
10
- const SENDER_METADATA = /Sender\s*\(untrusted\s+metadata\)\s*:\s*```[\s\S]*?```\s*/gi
11
- /** Leading [timestamp] or [anything] prefix. */
12
- const BRACKET_PREFIX = /^\[[^\]]*\]\s*/
13
- /** Leading stray ] (e.g. from some assistant message formats). */
14
- const LEADING_BRACKET = /^\s*\]\s*/
5
+ /**
6
+ * Strips OpenClaw-injected wrappers from message text.
7
+ *
8
+ * OpenClaw wraps user prompts with:
9
+ * 1. A "Sender (untrusted metadata):" block containing a fenced ```json ... ``` section
10
+ * 2. A "[timestamp]" prefix like [Thu 2026-03-19 18:01 GMT+5:30]
11
+ *
12
+ * OpenClaw wraps assistant messages with:
13
+ * 3. [[tag]] markers like [[reply_to_current]]
14
+ *
15
+ * This function removes all three using plain string operations.
16
+ */
17
+ export function stripOpenClawWrapper(text: string): string {
18
+ let s = text.trim()
19
+
20
+ // 1. Strip "Sender (untrusted metadata):\n```json\n{...}\n```" block
21
+ const senderMarker = "Sender (untrusted metadata):"
22
+ const senderIdx = s.indexOf(senderMarker)
23
+ if (senderIdx !== -1) {
24
+ const fenceOpen = s.indexOf("```", senderIdx + senderMarker.length)
25
+ if (fenceOpen !== -1) {
26
+ const fenceClose = s.indexOf("```", fenceOpen + 3)
27
+ if (fenceClose !== -1) {
28
+ s = s.slice(fenceClose + 3).trim()
29
+ }
30
+ }
31
+ }
32
+
33
+ // 2. Strip [timestamp] prefix — only if it starts with [ and closes within 80 chars
34
+ if (s.startsWith("[")) {
35
+ const close = s.indexOf("]")
36
+ if (close !== -1 && close < 80) {
37
+ s = s.slice(close + 1).trim()
38
+ }
39
+ }
40
+
41
+ // 3. Strip [[tag]] markers anywhere in text (e.g. [[reply_to_current]])
42
+ let safety = 10
43
+ while (s.includes("[[") && s.includes("]]") && safety-- > 0) {
44
+ const start = s.indexOf("[[")
45
+ const end = s.indexOf("]]", start)
46
+ if (end === -1) break
47
+ s = (s.slice(0, start) + s.slice(end + 2)).trim()
48
+ }
49
+
50
+ // 4. Strip leading stray ] that can remain after other stripping
51
+ if (s.startsWith("]")) {
52
+ s = s.slice(1).trim()
53
+ }
54
+
55
+ return s
56
+ }
57
+
58
+ const GREETINGS = new Set([
59
+ "hi",
60
+ "hello",
61
+ "hey",
62
+ "hola",
63
+ "howdy",
64
+ "yo",
65
+ "sup",
66
+ "greetings",
67
+ "good morning",
68
+ "good afternoon",
69
+ "good evening",
70
+ "good night",
71
+ "whats up",
72
+ "what's up",
73
+ "how are you",
74
+ "how's it going",
75
+ ])
76
+
77
+ const SESSION_STARTUP_MARKERS = [
78
+ "a new session was started",
79
+ "session startup sequence",
80
+ "run your session startup",
81
+ ]
82
+
83
+ function stripTrailingPunctuation(s: string): string {
84
+ let end = s.length
85
+ while (end > 0 && "!.,?;:' \t\n\"".includes(s[end - 1])) end--
86
+ return s.slice(0, end)
87
+ }
88
+
89
+ export function isGreeting(text: string): boolean {
90
+ const normalized = stripTrailingPunctuation(text.toLowerCase().trim())
91
+ if (!normalized) return true
92
+ if (GREETINGS.has(normalized)) return true
93
+ return false
94
+ }
15
95
 
16
- /** Session startup / system instruction text; exclude from previousContext and skip memory search. */
17
96
  export function isSessionStartup(text: string): boolean {
18
- const t = text.trim().toLowerCase()
19
- return (
20
- t.startsWith("a new session was started") ||
21
- t.includes("session startup sequence") ||
22
- t.includes("run your session startup")
23
- )
97
+ const lower = text.trim().toLowerCase()
98
+ for (const marker of SESSION_STARTUP_MARKERS) {
99
+ if (lower.includes(marker)) return true
100
+ }
101
+ return false
24
102
  }
25
103
 
26
- /**
27
- * Extracts plain text from an OpenClaw message (string content or array of text parts).
28
- */
29
- export function getMessageText(msg: Record<string, unknown>): string {
30
- if (typeof msg.content === "string") return msg.content
104
+ /** Returns true if this text should NOT be sent to OctaMem (greeting, startup, or empty). */
105
+ export function shouldSkip(text: string): boolean {
106
+ if (!text || !text.trim()) return true
107
+ return isGreeting(text) || isSessionStartup(text)
108
+ }
109
+
110
+ /** Extracts raw text from an OpenClaw message object. */
111
+ function getRawMessageText(msg: Record<string, unknown>): string {
112
+ if (typeof msg.content === "string") return msg.content.trim()
31
113
  if (Array.isArray(msg.content)) {
32
114
  return (msg.content as Array<{ type?: string; text?: string }>)
33
115
  .filter((p) => p.type === "text" && typeof p.text === "string")
34
116
  .map((p) => p.text as string)
35
117
  .join(" ")
118
+ .trim()
36
119
  }
37
120
  return ""
38
121
  }
39
122
 
40
- /**
41
- * Strips OpenClaw metadata from a raw string (e.g. prompt or message content).
42
- */
43
- function stripMetadata(s: string): string {
44
- return s
45
- .replace(OCTAMEM_TAG, "")
46
- .replace(SENDER_METADATA, "")
47
- .replace(BRACKET_PREFIX, "")
48
- .replace(LEADING_BRACKET, "")
49
- .trim()
50
- }
51
-
52
- /**
53
- * Returns only the user or assistant utterance: no metadata, no timestamps.
54
- * Use for previousContext and for stored content (add).
55
- */
56
- export function getCleanMessageText(msg: Record<string, unknown>): string {
57
- return stripMetadata(getMessageText(msg))
58
- }
59
-
60
- /** Cleans a raw prompt string (e.g. for search query). */
61
- export function getCleanPrompt(prompt: string): string {
62
- return stripMetadata(prompt)
123
+ /** Extracts clean text from an OpenClaw message, with metadata wrappers stripped. */
124
+ export function getMessageText(msg: Record<string, unknown>): string {
125
+ return stripOpenClawWrapper(getRawMessageText(msg))
63
126
  }
64
127
 
65
128
  /**
66
- * Returns user/assistant turns (oldest first) with clean text only. Used to build previousContext.
129
+ * Collects user/assistant Q&A pairs from messages, skipping greetings and startup.
130
+ * Returns pairs in chronological order.
67
131
  */
68
- function getTurns(messages: unknown[]): { role: string; text: string }[] {
69
- const out: { role: string; text: string }[] = []
70
- for (let i = messages.length - 1; i >= 0; i--) {
71
- const m = messages[i] as Record<string, unknown>
132
+ function collectPairs(messages: unknown[]): ChatContext {
133
+ const turns: { role: string; text: string }[] = []
134
+ for (const raw of messages) {
135
+ const m = raw as Record<string, unknown>
72
136
  const role = m?.role as string
73
137
  if (role !== "user" && role !== "assistant") continue
74
- const text = getCleanMessageText(m)
75
- if (text.length < 2) continue
76
- out.push({ role, text })
77
- }
78
- return out.reverse()
79
- }
80
-
81
- /**
82
- * Loads message objects from the session JSONL file; returns [] on missing file or parse error.
83
- */
84
- function loadSession(stateDir: string, sessionId: string): unknown[] {
85
- try {
86
- const file = path.join(stateDir, "agents", "main", "sessions", `${sessionId}.jsonl`)
87
- return fs
88
- .readFileSync(file, "utf-8")
89
- .split("\n")
90
- .filter((l) => l.trim())
91
- .map((l) => JSON.parse(l) as Record<string, unknown>)
92
- .filter((r) => r.type === "message" && r.message)
93
- .map((r) => r.message)
94
- } catch {
95
- return []
138
+ const text = getMessageText(m)
139
+ if (!text) continue
140
+ turns.push({ role, text })
96
141
  }
97
- }
98
142
 
99
- /**
100
- * Builds ChatContext (user/assistant pairs) from in-memory messages or by loading the session file.
101
- * Used for auto-recall and post-response save to provide conversation context to the API.
102
- * @param messages - Event messages if available.
103
- * @param stateDir - OpenClaw state dir (for session file path).
104
- * @param sessionId - Session id (for session file name).
105
- */
106
- export function getRecentContext(
107
- messages: unknown[] | undefined,
108
- stateDir: string | undefined,
109
- sessionId: string | undefined,
110
- ): ChatContext {
111
- const msgs =
112
- messages?.length
113
- ? messages
114
- : stateDir && sessionId
115
- ? loadSession(stateDir, sessionId)
116
- : []
117
- const turns = getTurns(msgs)
118
- const ctx: ChatContext = []
143
+ const pairs: ChatContext = []
119
144
  for (let i = 0; i < turns.length; i++) {
120
- const t = turns[i]
121
- if (t.role !== "user") continue
122
- if (isSessionStartup(t.text)) {
145
+ if (turns[i].role !== "user") continue
146
+ if (shouldSkip(turns[i].text)) {
123
147
  if (turns[i + 1]?.role === "assistant") i++
124
148
  continue
125
149
  }
126
150
  const next = turns[i + 1]
127
151
  if (next?.role === "assistant") {
128
- ctx.push({ user: t.text, assistant: next.text })
152
+ pairs.push({ user: turns[i].text, assistant: next.text })
129
153
  i++
130
154
  } else {
131
- ctx.push({ user: t.text, assistant: null })
155
+ pairs.push({ user: turns[i].text, assistant: null })
132
156
  }
133
157
  }
134
- return ctx
158
+
159
+ return pairs
135
160
  }
136
161
 
137
162
  /**
138
- * Returns at most PREVIOUS_CONTEXT_PAIRS for the API previousContext field.
139
- * @param ctx - Full chat context (oldest first).
140
- * @param excludeLast - If true, omit the last pair (use when storing that pair as content).
163
+ * Builds the previousContext string from conversation messages.
164
+ * Format: "user: <query> | assistant: <answer> | user: <query2> | assistant: <answer2>"
165
+ * Max 3 pairs. Pass excludeLast=true when storing (to exclude the pair being stored).
141
166
  */
142
- export function getPreviousContextForApi(ctx: ChatContext, excludeLast: boolean): ChatContext {
143
- const base = excludeLast ? ctx.slice(0, -1) : ctx
144
- return base.slice(-PREVIOUS_CONTEXT_PAIRS)
167
+ export function buildPreviousContext(
168
+ messages: unknown[],
169
+ excludeLast: boolean,
170
+ ): string {
171
+ const pairs = collectPairs(messages)
172
+ const base = excludeLast ? pairs.slice(0, -1) : pairs
173
+ const recent = base.slice(-MAX_CONTEXT_PAIRS)
174
+
175
+ if (recent.length === 0) return ""
176
+
177
+ return recent
178
+ .map((t) => `user: ${t.user} | assistant: ${t.assistant ?? ""}`)
179
+ .join(" | ")
145
180
  }
package/src/index.ts CHANGED
@@ -3,327 +3,181 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
3
3
  import { OctaMemClient } from "./client.ts"
4
4
  import { configSchema, parseConfig } from "./config.ts"
5
5
  import {
6
- resolveOpenClawConfigPath,
7
- writeOctaMemPluginConfig,
8
- type OctaMemPluginConfig,
9
- } from "./openclaw-config.ts"
10
- import { getCleanMessageText, getCleanPrompt, isSessionStartup, getPreviousContextForApi, getRecentContext } from "./context.ts"
11
-
12
- const TOOL_DESC =
13
- "Get memories from OctaMem (personal memory)."
14
- const ADD_DESC =
15
- "Save information to personal memory: preferences, facts, names, interests, context. Be proactive when the user shares something worth remembering."
16
-
17
- /** Builds the content array for an OpenClaw tool response (single text part). */
6
+ buildPreviousContext,
7
+ getMessageText,
8
+ shouldSkip,
9
+ stripOpenClawWrapper,
10
+ } from "./context.ts"
11
+ import { logLLMResponse } from "./logger.ts"
12
+ import type { MemoryData } from "./types.ts"
13
+
18
14
  function textContent(text: string) {
19
15
  return [{ type: "text" as const, text }]
20
16
  }
21
17
 
22
- /** Message shown when memory API is unavailable. Explicit instruction so the LLM tells the user. */
23
- function memoryUnavailable(reason: string) {
24
- return `Inform the user in your reply: Personal memory is temporarily unavailable. ${reason}`
18
+ function hasMemory(data: MemoryData): boolean {
19
+ return Boolean(
20
+ data.semantic_memory || data.episodic_memory || data.procedural_memory,
21
+ )
25
22
  }
26
23
 
27
- const MAX_MEMORY_SECTION_LEN = 1200
28
-
29
- /** Formats API memory response as readable text (used only for CLI/tool display). */
30
- function formatMemoryBlock(raw: unknown): string {
31
- const data = raw && typeof raw === "object" && "data" in (raw as object)
32
- ? (raw as { data?: Record<string, unknown> }).data
33
- : (raw as Record<string, unknown> | null)
34
- if (!data || typeof data !== "object") return "No memory data."
35
- const lines: string[] = []
36
- const add = (label: string, value: unknown) => {
37
- if (value == null || value === "") return
38
- const s = typeof value === "string" ? value : String(value)
39
- const trimmed = s.length > MAX_MEMORY_SECTION_LEN ? s.slice(0, MAX_MEMORY_SECTION_LEN) + "…" : s
40
- lines.push(`${label}: ${trimmed}`)
41
- }
42
- add("Semantic", (data as Record<string, unknown>).semantic_memory)
43
- add("Episodic", (data as Record<string, unknown>).episodic_memory)
44
- add("Procedural", (data as Record<string, unknown>).procedural_memory)
45
- return lines.length ? lines.join("\n\n") : "No memory data."
24
+ function formatMemoryForLLM(data: MemoryData): string {
25
+ return [
26
+ `semantic_memory: ${data.semantic_memory || "(empty)"}`,
27
+ `episodic_memory: ${data.episodic_memory || "(empty)"}`,
28
+ `procedural_memory: ${data.procedural_memory || "(empty)"}`,
29
+ ].join("\n\n")
46
30
  }
47
31
 
48
- /** True when OctaMem data has no memory content (semantic/episodic/procedural all empty). */
49
- function isMemoryDataEmpty(data: Record<string, unknown>): boolean {
50
- const s = (v: unknown) => (typeof v === "string" ? v : v != null ? String(v) : "").trim()
51
- return (
52
- !s((data as Record<string, unknown>).semantic_memory) &&
53
- !s((data as Record<string, unknown>).episodic_memory) &&
54
- !s((data as Record<string, unknown>).procedural_memory)
55
- )
56
- }
32
+ const MEMORY_INSTRUCTION = `[OctaMem Memory System STRICT]
33
+ You are connected to OctaMem, a personal memory system.
34
+ You MUST answer the user's question ONLY based on the memory data provided below.
35
+ Do NOT use your internal training data or general knowledge.
36
+ If the memory data does not contain relevant information, clearly tell the user that you don't have that information in your memory.
37
+ Be conversational and natural, but NEVER fabricate information that is not present in the memory data.`
38
+
39
+ const NO_MEMORY_INSTRUCTION = `[OctaMem Memory System — STRICT]
40
+ No relevant memory was found for this query.
41
+ You MUST inform the user that you don't have any information about this in your memory.
42
+ Do NOT answer from your internal knowledge — say you don't know.`
57
43
 
58
- /**
59
- * OpenClaw plugin: OctaMem personal memory (search, add, auto-recall, auto-capture).
60
- * Registers tools octamem_get / octamem_add, CLI (octamem status/search/details/add), and lifecycle hooks.
61
- */
62
44
  export default {
63
45
  id: "octamem-openclaw",
64
46
  name: "OctaMem",
65
- description: "Personal memory for OpenClaw",
47
+ description: "Personal memory using OctaMem",
66
48
  kind: "memory" as const,
67
49
  configSchema,
68
50
 
69
- /** Registers CLI (always), then tools and hooks when personal memory is configured. */
70
51
  register(api: OpenClawPluginApi) {
71
52
  const config = parseConfig(api.pluginConfig)
72
- const configured = OctaMemClient.isConfigured(config)
73
- const client = configured ? new OctaMemClient(config) : null
74
- const stateDir = api.runtime.state.resolveStateDir()
75
- const toolDesc = config.toolDescription ?? TOOL_DESC
76
- const addToolDesc = config.addToolDescription ?? ADD_DESC
77
-
78
- // CLI is always registered so users can run `openclaw octamem configure` without editing config.
79
- api.registerCli(
80
- ({ program }) => {
81
- const octamem = program.command("octamem").description("OctaMem memory commands")
82
-
83
- octamem
84
- .command("configure")
85
- .description("Write OctaMem plugin config into OpenClaw config file")
86
- .argument("[apiKey]", "Personal API key (or set OCTAMEM_PERSONAL_KEY)")
87
- .option("--tool-desc-get <description>", "Custom description for the get/search tool")
88
- .option("--tool-desc-add <description>", "Custom description for the add/store tool")
89
- .action(async (apiKeyArg: string, opts: { toolDescGet?: string; toolDescAdd?: string }) => {
90
- const apiKey =
91
- (apiKeyArg && apiKeyArg.trim()) || process.env.OCTAMEM_PERSONAL_KEY || ""
92
- if (!apiKey) {
93
- console.log(
94
- "Usage: openclaw octamem configure <apiKey>\n Or set OCTAMEM_PERSONAL_KEY and run: openclaw octamem configure",
95
- )
96
- return
97
- }
98
- const configPath = resolveOpenClawConfigPath()
99
- const written = writeOctaMemPluginConfig(configPath, {
100
- apiKey: apiKey.trim(),
101
- ...(opts.toolDescGet !== undefined && { toolDescription: opts.toolDescGet }),
102
- ...(opts.toolDescAdd !== undefined && { addToolDescription: opts.toolDescAdd }),
103
- })
104
- console.log(`Config written to ${written}\nRestart OpenClaw to apply.`)
105
- })
106
-
107
- octamem.command("status").description("Show status").action(() => {
108
- if (configured) {
109
- console.log("\n# OctaMem Status\n\nPersonal memory: configured (read/write)\n")
110
- } else {
111
- console.log(
112
- "\n# OctaMem Status\n\nNot configured. Run: openclaw octamem configure <apiKey>\n",
113
- )
53
+
54
+ if (!OctaMemClient.isConfigured(config)) {
55
+ console.log("[OctaMem] Not configured — missing API key")
56
+ return
57
+ }
58
+
59
+ const client = new OctaMemClient(config)
60
+
61
+ // ── Manual Tool: Search ──
62
+ api.registerTool({
63
+ name: "octamem_search",
64
+ label: "OctaMem Search",
65
+ description:
66
+ "Search OctaMem memory for relevant information about a query.",
67
+ parameters: Type.Object({
68
+ query: Type.String(),
69
+ }),
70
+
71
+ async execute(_, { query }) {
72
+ const result = await client.search(query, "")
73
+
74
+ if (!result.success) {
75
+ return {
76
+ content: textContent(`Memory search failed: ${result.error}`),
77
+ details: { success: false },
114
78
  }
115
- })
116
-
117
- if (client) {
118
- octamem
119
- .command("search")
120
- .description("Search personal memory")
121
- .argument("<query>", "Search query")
122
- .action(async (query: string) => {
123
- const r = await client.search(query)
124
- if (!r.success) return console.log("Error:", r.error ?? "Search failed")
125
- const raw = r.data
126
- console.log("\n# Search Results (personal)\n")
127
- console.log(typeof raw === "string" ? raw : JSON.stringify(raw ?? {}, null, 2))
128
- })
129
- octamem.command("details").description("Memory details").action(async () => {
130
- const r = await client.details()
131
- if (!r.success) return console.log("Error:", r.error)
132
- console.log("\n# Memory Details (personal)\n\n", JSON.stringify(r.data, null, 2))
133
- })
134
- octamem
135
- .command("add")
136
- .description("Add to personal memory")
137
- .argument("<content>", "Content")
138
- .action(async (content: string) => {
139
- const r = await client.add(content)
140
- if (!r.success) return console.log("Error:", r.message)
141
- console.log("Stored (ID:", r.id + ")")
142
- })
79
+ }
80
+
81
+ return {
82
+ content: textContent(formatMemoryForLLM(result.data)),
83
+ details: { success: true, data: result.data },
143
84
  }
144
85
  },
145
- { commands: ["octamem"] },
146
- )
147
-
148
- if (!client) return
149
-
150
- api.registerTool(
151
- {
152
- name: "octamem_get",
153
- label: "OctaMem Get",
154
- description: toolDesc,
155
- parameters: Type.Object({
156
- query: Type.String({ description: "Search query" }),
157
- }),
158
- async execute(_, params) {
159
- const result = await client.search(params.query)
160
- if (!result.success) {
161
- return { content: textContent(memoryUnavailable(result.error ?? "Search failed.")), details: undefined }
162
- }
163
- const raw = result.data
164
- const text =
165
- typeof raw === "string"
166
- ? raw
167
- : JSON.stringify(raw === undefined || raw === null ? {} : raw, null, 2)
86
+ })
87
+
88
+ // ── Manual Tool: Add ──
89
+ api.registerTool({
90
+ name: "octamem_add",
91
+ label: "OctaMem Add",
92
+ description: "Store information into OctaMem memory.",
93
+ parameters: Type.Object({
94
+ content: Type.String(),
95
+ }),
96
+
97
+ async execute(_, { content }) {
98
+ const result = await client.add(content, "")
99
+
100
+ if (!result.success) {
168
101
  return {
169
- content: textContent(text),
170
- details: { memoryType: "personal", data: raw },
102
+ content: textContent(`Failed to store: ${result.message}`),
103
+ details: { success: false },
171
104
  }
172
- },
105
+ }
106
+
107
+ return {
108
+ content: textContent("Stored successfully"),
109
+ details: { success: true },
110
+ }
173
111
  },
174
- { name: "octamem_get" },
175
- )
176
-
177
- api.registerTool(
178
- {
179
- name: "octamem_add",
180
- label: "OctaMem Add",
181
- description: addToolDesc,
182
- parameters: Type.Object({
183
- content: Type.String({ description: "Information to store" }),
184
- category: Type.Optional(Type.String({ description: "Category: preference, fact, name, context, interest, reminder" })),
185
- }),
186
- async execute(_, params) {
187
- const meta = params.category ? { category: params.category } : {}
188
- const result = await client.add(params.content, meta)
189
- if (!result.success) {
190
- return { content: textContent(memoryUnavailable(result.message)), details: undefined }
191
- }
192
- const preview = params.content.length > 80 ? `${params.content.slice(0, 80)}...` : params.content
112
+ })
113
+
114
+ // ── Auto Search: runs before the LLM sees the user's message ──
115
+ api.on("before_prompt_build", async (event) => {
116
+ try {
117
+ const prompt = stripOpenClawWrapper(event.prompt ?? "")
118
+ if (shouldSkip(prompt)) return
119
+
120
+ const messages = event.messages ?? []
121
+ const previousContext = buildPreviousContext(messages, false)
122
+
123
+ const result = await client.search(prompt, previousContext)
124
+
125
+ if (!result.success) return
126
+
127
+ if (hasMemory(result.data)) {
128
+ const memory = formatMemoryForLLM(result.data)
193
129
  return {
194
- content: textContent(`Stored to personal memory: "${preview}"`),
195
- details: { id: result.id, memoryType: "personal" },
130
+ appendSystemContext: `${MEMORY_INSTRUCTION}\n\n${memory}`,
196
131
  }
197
- },
198
- },
199
- { name: "octamem_add" },
200
- )
201
-
202
- let lastPrompt = ""
203
- let lastTime = 0
204
-
205
- /** Current turn prompt from event (before_prompt_build or before_agent_start). */
206
- function getEventPrompt(event: { prompt?: string; messages?: unknown[] }): string {
207
- if (typeof event.prompt === "string" && event.prompt.length > 0) return event.prompt
208
- const msgs = event.messages ?? []
209
- for (let i = msgs.length - 1; i >= 0; i--) {
210
- const m = msgs[i] as Record<string, unknown>
211
- if (m?.role === "user") return getCleanMessageText(m)
132
+ }
133
+
134
+ return {
135
+ appendSystemContext: NO_MEMORY_INSTRUCTION,
136
+ }
137
+ } catch {
138
+ // Don't block the LLM if memory search fails
212
139
  }
213
- return ""
214
- }
140
+ })
141
+
142
+ // ── Auto Save: runs after the LLM has responded ──
143
+ api.on("agent_end", async (event) => {
144
+ try {
145
+ if (!event.success) return
215
146
 
216
- // Auto-recall and auto-capture are always on (part of the memory system).
217
- {
218
- const injectMemory = async (
219
- event: { prompt?: string; messages?: unknown[] },
220
- ctx: { sessionId?: string } | undefined,
221
- ) => {
222
- const prompt = getEventPrompt(event)
223
- if (!prompt || prompt.length < 5) return
224
- const now = Date.now()
225
- if (prompt === lastPrompt && now - lastTime < 2000) return
226
- lastPrompt = prompt
227
- lastTime = now
228
- try {
229
- const clean = getCleanPrompt(prompt)
230
- if (isSessionStartup(clean)) return
231
- // Use session file for context (same source as add) so previousContext matches add's flow.
232
- const chatCtx =
233
- stateDir && ctx?.sessionId
234
- ? getRecentContext(undefined, stateDir, ctx.sessionId)
235
- : getRecentContext(event.messages, stateDir, ctx?.sessionId)
236
- const lastPair = chatCtx[chatCtx.length - 1]
237
- const excludeLast = lastPair?.user === clean && lastPair?.assistant === null
238
- const prevCtx = getPreviousContextForApi(chatCtx, excludeLast)
239
- const r = await client.search(clean, prevCtx)
240
- if (!r.success) {
241
- const msg = r.error ?? "Search failed."
242
- return {
243
- appendSystemContext: `\n\n### Personal Memory\nIn your response, tell the user: Personal memory is temporarily unavailable. ${msg}`,
244
- }
147
+ const messages = event.messages ?? []
148
+
149
+ let userText = ""
150
+ let assistantText = ""
151
+
152
+ for (let i = messages.length - 1; i >= 0; i--) {
153
+ const m = messages[i] as Record<string, unknown>
154
+ if (!userText && m.role === "user") {
155
+ userText = getMessageText(m)
245
156
  }
246
- const raw = r.data
247
- // Pass search response as-is to the LLM (inner data payload as JSON).
248
- const dataPayload =
249
- raw && typeof raw === "object" && "data" in (raw as object)
250
- ? (raw as { data?: unknown }).data
251
- : raw
252
- const hasPayload = dataPayload != null && typeof dataPayload === "object"
253
- const isEmpty =
254
- !hasPayload ||
255
- (hasPayload && isMemoryDataEmpty(dataPayload as Record<string, unknown>))
256
- const block =
257
- hasPayload && !isEmpty
258
- ? JSON.stringify(dataPayload, null, 2)
259
- : isEmpty
260
- ? "No memory data."
261
- : typeof raw === "string"
262
- ? raw
263
- : JSON.stringify(raw ?? {}, null, 2)
264
- const sourceRule =
265
- "Personal memory is OctaMem only. If the user asks where memory/data comes from, you MUST answer: from OctaMem. Never say local, workspace, or not Octamem."
266
- const summarizeInstruction =
267
- "The following is the raw memory search result from OctaMem. Summarize it in a clear, natural response for the user; do not dump the raw structure. "
268
- const memoryOnlyInstruction =
269
- "[SOURCE: All memory below is from OctaMem only. When asked where memory comes from, always answer: from OctaMem. Never say 'local', 'workspace', or 'not Octamem'.] " +
270
- summarizeInstruction +
271
- "For 'who is X?', 'what do you know about X?', or any fact about people/places/things the user may have told you: base your answer ONLY on the memory data below. Do not use general knowledge or invent details. " +
272
- (isEmpty
273
- ? "[CRITICAL: The section below is EMPTY (No memory data). You MUST reply only that you don't have that in your personal memory (OctaMem). Do not guess, invent, or use your training knowledge.] "
274
- : "") +
275
- "When asked the source of memory data, reply: It comes from OctaMem. Never say local or workspace.\n\n"
276
- const memoryBlockContent = isEmpty
277
- ? "No memory data.\n[If you answer from general knowledge here, you are wrong. Reply only: I don't have that in my personal memory.]"
278
- : block
279
- const prepend = isEmpty
280
- ? `\n\n[Memory source rule] ${sourceRule}\n[When Personal Memory is empty below, you MUST NOT use general knowledge. Say only: I don't have that in my personal memory.]\n`
281
- : `\n\n[Memory source rule] ${sourceRule}\n`
282
- return {
283
- prependSystemContext: prepend,
284
- appendSystemContext: `\n\n### Personal Memory (OctaMem)\n${memoryOnlyInstruction}${memoryBlockContent}`,
157
+ if (!assistantText && m.role === "assistant") {
158
+ assistantText = getMessageText(m)
285
159
  }
286
- } catch {
287
- // skip
160
+ if (userText && assistantText) break
288
161
  }
289
- }
290
162
 
291
- api.on("before_prompt_build", injectMemory)
292
- }
163
+ if (!userText || !assistantText) return
164
+ if (shouldSkip(userText)) return
293
165
 
294
- // Memory of OpenClaw: save every user query + agent/LLM response to personal memory.
295
- api.on("agent_end", async (event, ctx) => {
296
- if (!event.success) return
297
- const msgs = event.messages ?? []
298
- let userText = ""
299
- for (let i = msgs.length - 1; i >= 0; i--) {
300
- const m = msgs[i] as Record<string, unknown>
301
- if (m.role === "user") {
302
- userText = getCleanMessageText(m)
303
- break
304
- }
305
- }
306
- if (!userText || userText.length < 5) return
307
- if (isSessionStartup(userText)) return
308
- let assistantText = ""
309
- for (let i = msgs.length - 1; i >= 0; i--) {
310
- const m = msgs[i] as Record<string, unknown>
311
- if (m.role === "assistant") {
312
- assistantText = getCleanMessageText(m)
313
- break
314
- }
315
- }
316
- if (!assistantText) return
317
- const chatCtx = getRecentContext(event.messages, stateDir, ctx?.sessionId)
318
- const prevCtx = getPreviousContextForApi(chatCtx, true)
319
- // Store only the assistant reply as plain text (no "User: ... Assistant: ..." wrapper).
320
- try {
321
- await client.add(assistantText, { source: "post_response" }, prevCtx)
166
+ logLLMResponse(assistantText)
167
+
168
+ const content = `user: ${userText} | assistant: ${assistantText}`
169
+ const previousContext = buildPreviousContext(messages, true)
170
+
171
+ await client.add(content, previousContext)
322
172
  } catch {
323
- // skip
173
+ // Don't crash if memory save fails
324
174
  }
325
175
  })
326
176
 
327
- api.registerService({ id: "octamem-openclaw", start: () => {}, stop: () => {} })
177
+ api.registerService({
178
+ id: "octamem-openclaw",
179
+ start: () => {},
180
+ stop: () => {},
181
+ })
328
182
  },
329
183
  }
package/src/logger.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  import fs from "node:fs"
2
- import path from "node:path"
3
2
  import os from "node:os"
3
+ import path from "node:path"
4
4
 
5
5
  const LOG_PATH = path.join(os.homedir(), ".openclaw", "octamem-api.log")
6
6
 
7
- /** Redact apiKey in payload for safe logging. */
8
7
  function redactPayload(obj: Record<string, unknown>): Record<string, unknown> {
9
8
  const out = { ...obj }
10
9
  if (typeof out.apiKey === "string" && out.apiKey.length > 0) {
@@ -13,11 +12,11 @@ function redactPayload(obj: Record<string, unknown>): Record<string, unknown> {
13
12
  return out
14
13
  }
15
14
 
16
- /**
17
- * Appends a formatted log line for an API request to ~/.openclaw/octamem-api.log.
18
- * Payload is JSON-formatted; apiKey is redacted.
19
- */
20
- export function logRequest(endpoint: string, payload: Record<string, unknown>): void {
15
+ /** Log an outgoing API request (endpoint + payload). */
16
+ export function logRequest(
17
+ endpoint: string,
18
+ payload: Record<string, unknown>,
19
+ ): void {
21
20
  try {
22
21
  const ts = new Date().toISOString()
23
22
  const safe = redactPayload(payload)
@@ -29,16 +28,11 @@ export function logRequest(endpoint: string, payload: Record<string, unknown>):
29
28
  }
30
29
  }
31
30
 
32
- /**
33
- * Appends the API response to ~/.openclaw/octamem-api.log (same file as request log).
34
- */
35
- export function logResponse(endpoint: string, response: unknown): void {
31
+ /** Log the LLM's response shown to the user. */
32
+ export function logLLMResponse(assistantText: string): void {
36
33
  try {
37
34
  const ts = new Date().toISOString()
38
- const body = typeof response === "object" && response !== null
39
- ? JSON.stringify(response, null, 2)
40
- : String(response)
41
- const block = `--- ${ts} ${endpoint} response ---\n${body}\n`
35
+ const block = `--- ${ts} llm-response ---\n${assistantText}\n`
42
36
  fs.appendFileSync(LOG_PATH, block)
43
37
  } catch {
44
38
  // ignore log errors
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs"
2
- import path from "node:path"
3
2
  import os from "node:os"
3
+ import path from "node:path"
4
4
 
5
5
  const CONFIG_DIR = ".openclaw"
6
6
  const CONFIG_FILES = ["openclaw.json", "config.json"]
@@ -32,7 +32,9 @@ export function resolveOpenClawConfigPath(): string {
32
32
  /**
33
33
  * Reads and parses the OpenClaw config file. Returns empty object if missing or invalid.
34
34
  */
35
- export function readOpenClawConfig(configPath: string): Record<string, unknown> {
35
+ export function readOpenClawConfig(
36
+ configPath: string,
37
+ ): Record<string, unknown> {
36
38
  try {
37
39
  const raw = fs.readFileSync(configPath, "utf-8")
38
40
  const data = JSON.parse(raw)
@@ -52,27 +54,47 @@ export function writeOctaMemPluginConfig(
52
54
  options: OctaMemPluginConfig,
53
55
  ): string {
54
56
  const data = readOpenClawConfig(configPath)
55
- const plugins = (data.plugins && typeof data.plugins === "object"
56
- ? { ...(data.plugins as Record<string, unknown>) }
57
- : {}) as Record<string, unknown>
57
+ const plugins = (
58
+ data.plugins && typeof data.plugins === "object"
59
+ ? { ...(data.plugins as Record<string, unknown>) }
60
+ : {}
61
+ ) as Record<string, unknown>
58
62
 
59
63
  plugins.enabled = true
60
- plugins.slots = { ...(plugins.slots as Record<string, unknown>), memory: "octamem-openclaw" }
61
- const entries = (plugins.entries && typeof plugins.entries === "object"
62
- ? { ...(plugins.entries as Record<string, unknown>) }
63
- : {}) as Record<string, unknown>
64
+ plugins.slots = {
65
+ ...(plugins.slots as Record<string, unknown>),
66
+ memory: "octamem-openclaw",
67
+ }
68
+ const entries = (
69
+ plugins.entries && typeof plugins.entries === "object"
70
+ ? { ...(plugins.entries as Record<string, unknown>) }
71
+ : {}
72
+ ) as Record<string, unknown>
64
73
 
65
74
  const existing = entries["octamem-openclaw"]
66
- const existingConfig = existing && typeof existing === "object" && (existing as Record<string, unknown>).config && typeof (existing as Record<string, unknown>).config === "object"
67
- ? { ...((existing as Record<string, unknown>).config as Record<string, unknown>) }
68
- : {}
75
+ const existingConfig =
76
+ existing &&
77
+ typeof existing === "object" &&
78
+ (existing as Record<string, unknown>).config &&
79
+ typeof (existing as Record<string, unknown>).config === "object"
80
+ ? {
81
+ ...((existing as Record<string, unknown>).config as Record<
82
+ string,
83
+ unknown
84
+ >),
85
+ }
86
+ : {}
69
87
  entries["octamem-openclaw"] = {
70
88
  enabled: true,
71
89
  config: {
72
90
  ...existingConfig,
73
91
  memories: { personal: { apiKey: options.apiKey } },
74
- ...(options.toolDescription !== undefined && { toolDescription: options.toolDescription }),
75
- ...(options.addToolDescription !== undefined && { addToolDescription: options.addToolDescription }),
92
+ ...(options.toolDescription !== undefined && {
93
+ toolDescription: options.toolDescription,
94
+ }),
95
+ ...(options.addToolDescription !== undefined && {
96
+ addToolDescription: options.addToolDescription,
97
+ }),
76
98
  },
77
99
  }
78
100
  plugins.entries = entries
@@ -80,10 +102,12 @@ export function writeOctaMemPluginConfig(
80
102
 
81
103
  // So only OctaMem is used for memory: disable core memory tools (file-based).
82
104
  const coreMemoryToolsToDeny = ["memory_search", "memory_get"]
83
- const tools = (data.tools && typeof data.tools === "object"
84
- ? { ...(data.tools as Record<string, unknown>) }
85
- : {}) as Record<string, unknown>
86
- let deny = Array.isArray(tools.deny) ? [...(tools.deny as string[])] : []
105
+ const tools = (
106
+ data.tools && typeof data.tools === "object"
107
+ ? { ...(data.tools as Record<string, unknown>) }
108
+ : {}
109
+ ) as Record<string, unknown>
110
+ const deny = Array.isArray(tools.deny) ? [...(tools.deny as string[])] : []
87
111
  for (const id of coreMemoryToolsToDeny) {
88
112
  if (!deny.includes(id)) deny.push(id)
89
113
  }
package/src/types.ts CHANGED
@@ -1,35 +1,21 @@
1
- /**
2
- * Plugin configuration shape (from OpenClaw plugin config).
3
- * Only personal memory is supported; apiKey may use ${ENV_VAR} substitution.
4
- */
5
1
  export type OctaMemConfig = {
6
- memories: { personal?: { apiKey: string; description?: string } }
7
- /** Description for the get/search tool (user-facing). */
8
- toolDescription?: string
9
- /** Description for the add/store tool (user-facing). */
10
- addToolDescription?: string
2
+ memories: { personal?: { apiKey: string } }
11
3
  }
12
4
 
13
- /** A single user/assistant turn in chat history. */
14
5
  export type ChatTurn = { user: string; assistant: string | null }
15
6
 
16
- /** Ordered list of chat turns, used as context for search/add API calls. */
17
7
  export type ChatContext = ChatTurn[]
18
8
 
19
- /** Result of a search request; data is the raw API response (passed through to LLM). */
20
- export type SearchResult = {
21
- success: boolean
22
- data?: unknown
23
- query: string
24
- error?: string
9
+ export type MemoryData = {
10
+ semantic_memory: string
11
+ episodic_memory: string
12
+ procedural_memory: string
25
13
  }
26
14
 
27
- /** Result of an add (store) request; message holds success or error text. */
28
- export type AddResult = {
29
- success: boolean
30
- id: string
31
- message: string
32
- }
15
+ export type SearchResult =
16
+ | { success: true; data: MemoryData; query: string }
17
+ | { success: false; error: string; query: string }
33
18
 
34
- /** Last user question and assistant reply; used when auto-saving a Q&A pair. */
35
- export type LastQa = { query: string; response: string }
19
+ export type AddResult =
20
+ | { success: true; message: string }
21
+ | { success: false; message: string }