clawmem 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.
Files changed (50) hide show
  1. package/AGENTS.md +660 -0
  2. package/CLAUDE.md +660 -0
  3. package/LICENSE +21 -0
  4. package/README.md +993 -0
  5. package/SKILL.md +717 -0
  6. package/bin/clawmem +75 -0
  7. package/package.json +72 -0
  8. package/src/amem.ts +797 -0
  9. package/src/beads.ts +263 -0
  10. package/src/clawmem.ts +1849 -0
  11. package/src/collections.ts +405 -0
  12. package/src/config.ts +178 -0
  13. package/src/consolidation.ts +123 -0
  14. package/src/directory-context.ts +248 -0
  15. package/src/errors.ts +41 -0
  16. package/src/formatter.ts +427 -0
  17. package/src/graph-traversal.ts +247 -0
  18. package/src/hooks/context-surfacing.ts +317 -0
  19. package/src/hooks/curator-nudge.ts +89 -0
  20. package/src/hooks/decision-extractor.ts +639 -0
  21. package/src/hooks/feedback-loop.ts +214 -0
  22. package/src/hooks/handoff-generator.ts +345 -0
  23. package/src/hooks/postcompact-inject.ts +226 -0
  24. package/src/hooks/precompact-extract.ts +314 -0
  25. package/src/hooks/pretool-inject.ts +79 -0
  26. package/src/hooks/session-bootstrap.ts +324 -0
  27. package/src/hooks/staleness-check.ts +130 -0
  28. package/src/hooks.ts +367 -0
  29. package/src/indexer.ts +327 -0
  30. package/src/intent.ts +294 -0
  31. package/src/limits.ts +26 -0
  32. package/src/llm.ts +1175 -0
  33. package/src/mcp.ts +2138 -0
  34. package/src/memory.ts +336 -0
  35. package/src/mmr.ts +93 -0
  36. package/src/observer.ts +269 -0
  37. package/src/openclaw/engine.ts +283 -0
  38. package/src/openclaw/index.ts +221 -0
  39. package/src/openclaw/plugin.json +83 -0
  40. package/src/openclaw/shell.ts +207 -0
  41. package/src/openclaw/tools.ts +304 -0
  42. package/src/profile.ts +346 -0
  43. package/src/promptguard.ts +218 -0
  44. package/src/retrieval-gate.ts +106 -0
  45. package/src/search-utils.ts +127 -0
  46. package/src/server.ts +783 -0
  47. package/src/splitter.ts +325 -0
  48. package/src/store.ts +4062 -0
  49. package/src/validation.ts +67 -0
  50. package/src/watcher.ts +58 -0
@@ -0,0 +1,283 @@
1
+ /**
2
+ * ClawMem ContextEngine — OpenClaw integration
3
+ *
4
+ * Implements the ContextEngine interface for OpenClaw's plugin system.
5
+ * Phase 1: thin shim that shells out to `clawmem hook <name>` for lifecycle operations.
6
+ *
7
+ * Architecture (per GPT 5.4 High review):
8
+ * - assemble(): minimal pass-through (real retrieval happens in before_prompt_build hook)
9
+ * - afterTurn(): shells out to decision-extractor, handoff-generator, feedback-loop
10
+ * - compact(): shells out to precompact-extract, then delegates to legacy compaction (ownsCompaction: false)
11
+ * - ingest(): no-op (ClawMem captures at turn boundaries, not per-message)
12
+ * - bootstrap(): session registration only (context injection via before_prompt_build hook)
13
+ */
14
+
15
+ import type { ClawMemConfig } from "./shell.js";
16
+ import { execHook, parseHookOutput } from "./shell.js";
17
+
18
+ // =============================================================================
19
+ // Types (matching OpenClaw's ContextEngine interface without importing it)
20
+ // =============================================================================
21
+
22
+ type ContextEngineInfo = {
23
+ id: string;
24
+ name: string;
25
+ version?: string;
26
+ ownsCompaction?: boolean;
27
+ };
28
+
29
+ type AssembleResult = {
30
+ messages: unknown[];
31
+ estimatedTokens: number;
32
+ systemPromptAddition?: string;
33
+ };
34
+
35
+ type CompactResult = {
36
+ ok: boolean;
37
+ compacted: boolean;
38
+ reason?: string;
39
+ result?: {
40
+ summary?: string;
41
+ firstKeptEntryId?: string;
42
+ tokensBefore: number;
43
+ tokensAfter?: number;
44
+ details?: unknown;
45
+ };
46
+ };
47
+
48
+ type IngestResult = { ingested: boolean };
49
+ type IngestBatchResult = { ingestedCount: number };
50
+ type BootstrapResult = {
51
+ bootstrapped: boolean;
52
+ importedMessages?: number;
53
+ reason?: string;
54
+ };
55
+
56
+ type SubagentSpawnPreparation = { rollback: () => void | Promise<void> };
57
+ type SubagentEndReason = "deleted" | "completed" | "swept" | "released";
58
+
59
+ // =============================================================================
60
+ // Logger interface
61
+ // =============================================================================
62
+
63
+ type Logger = {
64
+ debug?: (msg: string) => void;
65
+ info: (msg: string) => void;
66
+ warn: (msg: string) => void;
67
+ error: (msg: string) => void;
68
+ };
69
+
70
+ // =============================================================================
71
+ // ClawMem ContextEngine
72
+ // =============================================================================
73
+
74
+ export class ClawMemContextEngine {
75
+ readonly info: ContextEngineInfo = {
76
+ id: "clawmem",
77
+ name: "ClawMem Memory System",
78
+ version: "0.2.0",
79
+ ownsCompaction: false, // Delegate compaction to legacy engine
80
+ };
81
+
82
+ private bootstrappedSessions = new Set<string>();
83
+
84
+ constructor(
85
+ private readonly cfg: ClawMemConfig,
86
+ private readonly logger: Logger,
87
+ ) {}
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // bootstrap — register session, no context injection (P1 finding: bootstrap
91
+ // return type has no prompt injection field)
92
+ // ---------------------------------------------------------------------------
93
+
94
+ async bootstrap(params: {
95
+ sessionId: string;
96
+ sessionKey?: string;
97
+ sessionFile: string;
98
+ }): Promise<BootstrapResult> {
99
+ this.bootstrappedSessions.add(params.sessionId);
100
+
101
+ // Fire session-bootstrap hook for session registration + side effects
102
+ // (profile refresh, session_log entry). Context is NOT injected here —
103
+ // that happens in the before_prompt_build plugin hook.
104
+ const result = await execHook(this.cfg, "session-bootstrap", {
105
+ session_id: params.sessionId,
106
+ transcript_path: params.sessionFile,
107
+ });
108
+
109
+ if (result.exitCode !== 0) {
110
+ this.logger.warn(`clawmem: bootstrap hook failed: ${result.stderr}`);
111
+ }
112
+
113
+ return { bootstrapped: true };
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // ingest — no-op (ClawMem captures at turn boundaries via afterTurn)
118
+ // ---------------------------------------------------------------------------
119
+
120
+ async ingest(_params: {
121
+ sessionId: string;
122
+ sessionKey?: string;
123
+ message: unknown;
124
+ isHeartbeat?: boolean;
125
+ }): Promise<IngestResult> {
126
+ return { ingested: false };
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // ingestBatch — no-op
131
+ // ---------------------------------------------------------------------------
132
+
133
+ async ingestBatch?(_params: {
134
+ sessionId: string;
135
+ sessionKey?: string;
136
+ messages: unknown[];
137
+ isHeartbeat?: boolean;
138
+ }): Promise<IngestBatchResult> {
139
+ return { ingestedCount: 0 };
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // assemble — minimal pass-through (P1 finding: assemble() lacks the user
144
+ // prompt, so retrieval must happen in before_prompt_build hook instead)
145
+ // ---------------------------------------------------------------------------
146
+
147
+ async assemble(params: {
148
+ sessionId: string;
149
+ sessionKey?: string;
150
+ messages: unknown[];
151
+ tokenBudget?: number;
152
+ }): Promise<AssembleResult> {
153
+ // Pass-through: retrieval already injected via before_prompt_build hook
154
+ return {
155
+ messages: params.messages,
156
+ estimatedTokens: 0, // Caller handles estimation
157
+ };
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // afterTurn — run extraction hooks (decision-extractor, handoff-generator,
162
+ // feedback-loop) in parallel. These process the completed turn and persist
163
+ // decisions, handoff notes, and confidence boosts.
164
+ // ---------------------------------------------------------------------------
165
+
166
+ async afterTurn(params: {
167
+ sessionId: string;
168
+ sessionKey?: string;
169
+ sessionFile: string;
170
+ messages: unknown[];
171
+ prePromptMessageCount: number;
172
+ autoCompactionSummary?: string;
173
+ isHeartbeat?: boolean;
174
+ tokenBudget?: number;
175
+ }): Promise<void> {
176
+ // Skip extraction for heartbeat turns
177
+ if (params.isHeartbeat) return;
178
+
179
+ // Skip if too few messages (no meaningful content to extract)
180
+ const newMessages = params.messages.length - params.prePromptMessageCount;
181
+ if (newMessages < 2) return;
182
+
183
+ const hookInput = {
184
+ session_id: params.sessionId,
185
+ transcript_path: params.sessionFile,
186
+ };
187
+
188
+ // Fire all three Stop hooks in parallel (independent operations)
189
+ const [decisionResult, handoffResult, feedbackResult] = await Promise.allSettled([
190
+ execHook(this.cfg, "decision-extractor", hookInput),
191
+ execHook(this.cfg, "handoff-generator", hookInput),
192
+ execHook(this.cfg, "feedback-loop", hookInput),
193
+ ]);
194
+
195
+ // Log failures but don't throw (fail-open)
196
+ for (const [name, result] of [
197
+ ["decision-extractor", decisionResult],
198
+ ["handoff-generator", handoffResult],
199
+ ["feedback-loop", feedbackResult],
200
+ ] as const) {
201
+ if (result.status === "rejected") {
202
+ this.logger.warn(`clawmem: afterTurn ${name} failed: ${result.reason}`);
203
+ } else if (result.value.exitCode !== 0) {
204
+ this.logger.warn(`clawmem: afterTurn ${name} error: ${result.value.stderr}`);
205
+ }
206
+ }
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // compact — run precompact-extract for state preservation, then return
211
+ // ownsCompaction: false so OpenClaw's legacy compaction still runs.
212
+ // P1 finding: compact() must still perform real compaction; precompact-extract
213
+ // is a side effect, not a compactor. We delegate real compaction to legacy.
214
+ // ---------------------------------------------------------------------------
215
+
216
+ async compact(params: {
217
+ sessionId: string;
218
+ sessionKey?: string;
219
+ sessionFile: string;
220
+ tokenBudget?: number;
221
+ force?: boolean;
222
+ currentTokenCount?: number;
223
+ compactionTarget?: "budget" | "threshold";
224
+ customInstructions?: string;
225
+ }): Promise<CompactResult> {
226
+ // Run precompact-extract to preserve state before compaction
227
+ const extractResult = await execHook(this.cfg, "precompact-extract", {
228
+ session_id: params.sessionId,
229
+ transcript_path: params.sessionFile,
230
+ });
231
+
232
+ if (extractResult.exitCode !== 0) {
233
+ this.logger.warn(`clawmem: precompact-extract failed: ${extractResult.stderr}`);
234
+ }
235
+
236
+ // Return not-compacted — let legacy engine handle actual compaction
237
+ // (ownsCompaction: false means OpenClaw will call legacy compact() after us)
238
+ return {
239
+ ok: true,
240
+ compacted: false,
241
+ reason: "clawmem-precompact-only",
242
+ };
243
+ }
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // prepareSubagentSpawn — placeholder for future per-subagent memory scoping
247
+ // ---------------------------------------------------------------------------
248
+
249
+ async prepareSubagentSpawn?(_params: {
250
+ parentSessionKey: string;
251
+ childSessionKey: string;
252
+ ttlMs?: number;
253
+ }): Promise<SubagentSpawnPreparation | undefined> {
254
+ return undefined;
255
+ }
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // onSubagentEnded — placeholder
259
+ // ---------------------------------------------------------------------------
260
+
261
+ async onSubagentEnded?(_params: {
262
+ childSessionKey: string;
263
+ reason: SubagentEndReason;
264
+ }): Promise<void> {
265
+ // No-op for now
266
+ }
267
+
268
+ // ---------------------------------------------------------------------------
269
+ // dispose — cleanup
270
+ // ---------------------------------------------------------------------------
271
+
272
+ async dispose(): Promise<void> {
273
+ this.bootstrappedSessions.clear();
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // Helper: check if session has been bootstrapped (used by prompt hook)
278
+ // ---------------------------------------------------------------------------
279
+
280
+ isBootstrapped(sessionId: string): boolean {
281
+ return this.bootstrappedSessions.has(sessionId);
282
+ }
283
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * ClawMem OpenClaw Plugin — Entry Point
3
+ *
4
+ * Registers ClawMem as an OpenClaw ContextEngine plugin with:
5
+ * 1. ContextEngine (engine.ts) — lifecycle: afterTurn, compact, bootstrap
6
+ * 2. Plugin hooks — before_prompt_build (retrieval), session_start/end, before_reset
7
+ * 3. Agent tools — clawmem_search, clawmem_get, clawmem_session_log, clawmem_timeline, clawmem_similar
8
+ * 4. Service — starts clawmem serve (REST API) for tool HTTP calls
9
+ *
10
+ * Architecture per GPT 5.4 High review:
11
+ * - Prompt-aware retrieval goes through before_prompt_build (has the user prompt)
12
+ * - Post-turn extraction goes through ContextEngine.afterTurn() (has messages[])
13
+ * - Compaction goes through ContextEngine.compact() → precompact-extract → delegate to legacy
14
+ * - assemble() is minimal pass-through (retrieval already injected via hook)
15
+ */
16
+
17
+ import { ClawMemContextEngine } from "./engine.js";
18
+ import { resolveClawMemBin, execHook, parseHookOutput, extractContext } from "./shell.js";
19
+ import type { ClawMemConfig } from "./shell.js";
20
+ import { createTools } from "./tools.js";
21
+
22
+ // =============================================================================
23
+ // Plugin Definition
24
+ // =============================================================================
25
+
26
+ const PROFILE_BUDGETS: Record<string, number> = {
27
+ speed: 400,
28
+ balanced: 800,
29
+ deep: 1200,
30
+ };
31
+
32
+ const clawmemPlugin = {
33
+ id: "clawmem",
34
+ name: "ClawMem",
35
+ description: "On-device hybrid memory system with composite scoring, graph traversal, and lifecycle management",
36
+ version: "0.2.0",
37
+ kind: "context-engine" as const,
38
+
39
+ register(api: any) {
40
+ // ----- Resolve config -----
41
+ const pluginCfg = (api.pluginConfig || {}) as Record<string, unknown>;
42
+ const profile = (pluginCfg.profile as string) || "balanced";
43
+ const tokenBudget = (pluginCfg.tokenBudget as number) || PROFILE_BUDGETS[profile] || 800;
44
+
45
+ const cfg: ClawMemConfig = {
46
+ clawmemBin: resolveClawMemBin(pluginCfg.clawmemBin as string | undefined),
47
+ tokenBudget,
48
+ profile,
49
+ enableTools: pluginCfg.enableTools !== false,
50
+ servePort: (pluginCfg.servePort as number) || 7438,
51
+ env: {
52
+ ...(pluginCfg.gpuEmbed ? { CLAWMEM_EMBED_URL: pluginCfg.gpuEmbed as string } : {}),
53
+ ...(pluginCfg.gpuLlm ? { CLAWMEM_LLM_URL: pluginCfg.gpuLlm as string } : {}),
54
+ ...(pluginCfg.gpuRerank ? { CLAWMEM_RERANK_URL: pluginCfg.gpuRerank as string } : {}),
55
+ CLAWMEM_PROFILE: profile,
56
+ },
57
+ };
58
+
59
+ const logger = api.logger;
60
+ logger.info(`clawmem: plugin registered (bin: ${cfg.clawmemBin}, profile: ${profile}, budget: ${tokenBudget})`);
61
+
62
+ // ----- Register ContextEngine -----
63
+ const engine = new ClawMemContextEngine(cfg, logger);
64
+ api.registerContextEngine("clawmem", () => engine);
65
+
66
+ // ----- Track first-turn per session -----
67
+ const surfacedSessions = new Set<string>();
68
+
69
+ // ----- Plugin Hook: before_prompt_build -----
70
+ // This is WHERE retrieval happens (P1 finding: assemble() lacks the user prompt)
71
+ api.on("before_prompt_build", async (
72
+ event: { prompt: string; messages?: unknown[] },
73
+ ctx: { sessionId?: string; sessionKey?: string }
74
+ ) => {
75
+ if (!event.prompt || event.prompt.length < 5) return;
76
+
77
+ const sessionId = ctx.sessionId || "unknown";
78
+ const isFirstTurn = !surfacedSessions.has(sessionId);
79
+
80
+ let context = "";
81
+
82
+ // On first turn: run session-bootstrap for profile + handoff + decisions + stale
83
+ if (isFirstTurn) {
84
+ surfacedSessions.add(sessionId);
85
+
86
+ const bootstrapResult = await execHook(cfg, "session-bootstrap", {
87
+ session_id: sessionId,
88
+ });
89
+
90
+ if (bootstrapResult.exitCode === 0) {
91
+ const parsed = parseHookOutput(bootstrapResult.stdout);
92
+ // Session-bootstrap outputs context directly to stdout (not via hookSpecificOutput)
93
+ // It uses the system_message field for SessionStart hooks
94
+ const bootstrapContext = (parsed?.systemMessage as string) || "";
95
+ if (bootstrapContext) {
96
+ context += bootstrapContext + "\n\n";
97
+ }
98
+ } else {
99
+ logger.warn(`clawmem: session-bootstrap failed: ${bootstrapResult.stderr}`);
100
+ }
101
+ }
102
+
103
+ // Every turn: run context-surfacing for prompt-aware retrieval
104
+ const surfacingResult = await execHook(cfg, "context-surfacing", {
105
+ session_id: sessionId,
106
+ prompt: event.prompt,
107
+ });
108
+
109
+ if (surfacingResult.exitCode === 0) {
110
+ const parsed = parseHookOutput(surfacingResult.stdout);
111
+ const surfacedContext = extractContext(parsed);
112
+ if (surfacedContext) {
113
+ context += surfacedContext;
114
+ }
115
+ } else {
116
+ logger.warn(`clawmem: context-surfacing failed: ${surfacingResult.stderr}`);
117
+ }
118
+
119
+ if (!context.trim()) return;
120
+
121
+ return { prependContext: context.trim() };
122
+ }, { priority: 10 }); // Run early to prepend context before other hooks
123
+
124
+ // ----- Plugin Hook: session_start -----
125
+ api.on("session_start", async (
126
+ event: { sessionId: string; sessionKey?: string },
127
+ _ctx: unknown
128
+ ) => {
129
+ logger.info?.(`clawmem: session started ${event.sessionId}`);
130
+ });
131
+
132
+ // ----- Plugin Hook: session_end -----
133
+ api.on("session_end", async (
134
+ event: { sessionId: string; sessionKey?: string; messageCount: number },
135
+ _ctx: unknown
136
+ ) => {
137
+ // Cleanup tracked state
138
+ surfacedSessions.delete(event.sessionId);
139
+ logger.info?.(`clawmem: session ended ${event.sessionId} (${event.messageCount} messages)`);
140
+ });
141
+
142
+ // ----- Plugin Hook: before_reset -----
143
+ // Safety net for /new and /reset — ensure extraction runs before session clears
144
+ api.on("before_reset", async (
145
+ event: { sessionFile?: string; messages?: unknown[]; reason?: string },
146
+ ctx: { sessionId?: string; sessionKey?: string }
147
+ ) => {
148
+ if (!event.sessionFile || !ctx.sessionId) return;
149
+
150
+ // Run extraction hooks before reset clears the session
151
+ const hookInput = {
152
+ session_id: ctx.sessionId,
153
+ transcript_path: event.sessionFile,
154
+ };
155
+
156
+ await Promise.allSettled([
157
+ execHook(cfg, "decision-extractor", hookInput),
158
+ execHook(cfg, "handoff-generator", hookInput),
159
+ execHook(cfg, "feedback-loop", hookInput),
160
+ ]);
161
+
162
+ surfacedSessions.delete(ctx.sessionId);
163
+ });
164
+
165
+ // ----- Plugin Hook: before_compaction -----
166
+ // Fire precompact-extract before compaction starts (additional safety — compact()
167
+ // also runs it, but before_compaction fires earlier in the pipeline)
168
+ api.on("before_compaction", async (
169
+ event: { sessionFile?: string; messageCount: number },
170
+ ctx: { sessionId?: string }
171
+ ) => {
172
+ if (!event.sessionFile || !ctx.sessionId) return;
173
+
174
+ await execHook(cfg, "precompact-extract", {
175
+ session_id: ctx.sessionId,
176
+ transcript_path: event.sessionFile,
177
+ });
178
+ });
179
+
180
+ // ----- Register Tools -----
181
+ if (cfg.enableTools) {
182
+ const tools = createTools(cfg, logger);
183
+ for (const tool of tools) {
184
+ api.registerTool(
185
+ {
186
+ name: tool.name,
187
+ label: tool.label,
188
+ description: tool.description,
189
+ parameters: tool.parameters,
190
+ async execute(toolCallId: string, params: Record<string, unknown>) {
191
+ return tool.execute(toolCallId, params);
192
+ },
193
+ },
194
+ { name: tool.name }
195
+ );
196
+ }
197
+ logger.info(`clawmem: registered ${tools.length} agent tools`);
198
+ }
199
+
200
+ // ----- Register Service (REST API) -----
201
+ let serveChild: import("node:child_process").ChildProcess | null = null;
202
+
203
+ api.registerService({
204
+ id: "clawmem-api",
205
+ async start(svcCtx: { logger: typeof logger }) {
206
+ const { spawnBackground } = await import("./shell.js");
207
+ serveChild = spawnBackground(cfg, ["serve", "--port", String(cfg.servePort)], svcCtx.logger);
208
+ svcCtx.logger.info(`clawmem: REST API spawned (pid=${serveChild.pid})`);
209
+ },
210
+ stop() {
211
+ if (serveChild && !serveChild.killed) {
212
+ serveChild.kill("SIGTERM");
213
+ logger.info("clawmem: REST API service stopped");
214
+ }
215
+ serveChild = null;
216
+ },
217
+ });
218
+ },
219
+ };
220
+
221
+ export default clawmemPlugin;
@@ -0,0 +1,83 @@
1
+ {
2
+ "id": "clawmem",
3
+ "kind": "context-engine",
4
+ "uiHints": {
5
+ "clawmemBin": {
6
+ "label": "ClawMem Binary Path",
7
+ "placeholder": "/usr/local/bin/clawmem",
8
+ "help": "Path to clawmem binary. Auto-detected from PATH if not set."
9
+ },
10
+ "tokenBudget": {
11
+ "label": "Context Token Budget",
12
+ "placeholder": "800",
13
+ "help": "Max tokens for context surfacing injection (default: 800)"
14
+ },
15
+ "profile": {
16
+ "label": "Retrieval Profile",
17
+ "help": "speed (400 tokens) | balanced (800) | deep (1200)"
18
+ },
19
+ "enableTools": {
20
+ "label": "Register Agent Tools",
21
+ "help": "Register ClawMem retrieval tools for agent-initiated queries"
22
+ },
23
+ "servePort": {
24
+ "label": "REST API Port",
25
+ "placeholder": "7438",
26
+ "help": "Port for ClawMem HTTP REST API (used by tools)",
27
+ "advanced": true
28
+ },
29
+ "gpuEmbed": {
30
+ "label": "Embedding Endpoint",
31
+ "placeholder": "http://localhost:8088",
32
+ "help": "URL for granite-embed embedding service",
33
+ "advanced": true
34
+ },
35
+ "gpuLlm": {
36
+ "label": "LLM Endpoint",
37
+ "placeholder": "http://localhost:8089",
38
+ "help": "URL for ClawMem LLM (query expansion, extraction)",
39
+ "advanced": true
40
+ },
41
+ "gpuRerank": {
42
+ "label": "Reranker Endpoint",
43
+ "placeholder": "http://localhost:8090",
44
+ "help": "URL for reranking service",
45
+ "advanced": true
46
+ }
47
+ },
48
+ "configSchema": {
49
+ "type": "object",
50
+ "additionalProperties": false,
51
+ "properties": {
52
+ "clawmemBin": {
53
+ "type": "string"
54
+ },
55
+ "tokenBudget": {
56
+ "type": "number",
57
+ "minimum": 100,
58
+ "maximum": 4000
59
+ },
60
+ "profile": {
61
+ "type": "string",
62
+ "enum": ["speed", "balanced", "deep"]
63
+ },
64
+ "enableTools": {
65
+ "type": "boolean"
66
+ },
67
+ "servePort": {
68
+ "type": "number",
69
+ "minimum": 1024,
70
+ "maximum": 65535
71
+ },
72
+ "gpuEmbed": {
73
+ "type": "string"
74
+ },
75
+ "gpuLlm": {
76
+ "type": "string"
77
+ },
78
+ "gpuRerank": {
79
+ "type": "string"
80
+ }
81
+ }
82
+ }
83
+ }