clawvault 3.4.0 → 3.4.1

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
@@ -267,23 +267,24 @@ clawvault setup --theme neural --canvas --bases
267
267
 
268
268
  ## OpenClaw Integration
269
269
 
270
- For hook-based lifecycle integration with OpenClaw:
270
+ ClawVault integrates with OpenClaw as a plugin package (not the deprecated `openclaw hooks install/enable` flow):
271
271
 
272
272
  ```bash
273
- # Install and enable hook pack
274
- openclaw hooks install clawvault
275
- openclaw hooks enable clawvault
273
+ # Install ClawVault
274
+ npm install -g clawvault
275
+
276
+ # Add ClawVault package path to plugins.load.paths in openclaw.json
277
+ # (You can use `npm root -g` to locate node_modules)
276
278
 
277
- # Verify
278
- openclaw hooks list --verbose
279
- openclaw hooks check
279
+ # Enable plugin + memory slot in openclaw.json:
280
+ # plugins.slots.memory = "clawvault"
281
+ # plugins.entries.clawvault.enabled = true
282
+
283
+ # Verify runtime assumptions
280
284
  clawvault compat
281
285
  ```
282
286
 
283
- The hook automatically:
284
- - Detects context death and injects recovery alerts
285
- - Auto-checkpoints before session resets
286
- - Provides `--profile auto` for context queries
287
+ The plugin provides lifecycle hooks, memory tools, and protocol-safe messaging behavior for OpenClaw sessions.
287
288
 
288
289
  ### MEMORY.md vs Vault
289
290
 
@@ -351,27 +352,26 @@ clawvault compat
351
352
 
352
353
  ## OpenClaw Setup (Canonical)
353
354
 
354
- If you want hook-based lifecycle integration, use this sequence:
355
+ Use this sequence for plugin-based OpenClaw integration:
355
356
 
356
357
  ```bash
357
358
  # Install CLI
358
359
  npm install -g clawvault
359
360
 
360
- # Install and enable hook pack
361
- openclaw hooks install clawvault
362
- openclaw hooks enable clawvault
361
+ # Locate global node_modules
362
+ npm root -g
363
363
 
364
- # Verify
365
- openclaw hooks list --verbose
366
- openclaw hooks info clawvault
367
- openclaw hooks check
368
- clawvault compat
364
+ # In openclaw.json:
365
+ # - add <npm-root>/clawvault to plugins.load.paths
366
+ # - set plugins.slots.memory = "clawvault"
367
+ # - set plugins.entries.clawvault.enabled = true
368
+ # - set plugins.entries.clawvault.config.vaultPath as needed
369
369
  ```
370
370
 
371
371
  Important:
372
372
 
373
- - `clawhub install clawvault` installs skill guidance, but does not replace hook-pack installation.
374
- - After enabling hooks, restart the OpenClaw gateway process so hook registration reloads.
373
+ - `clawhub install clawvault` installs skill guidance, but does not configure OpenClaw plugin loading.
374
+ - After changing plugin config, restart the OpenClaw gateway process.
375
375
 
376
376
  ## Minimal AGENTS.md Additions
377
377
 
@@ -488,10 +488,10 @@ vault/
488
488
  - `qmd` fallback errors:
489
489
  - `qmd` is optional; in-process BM25 search is available without it
490
490
  - if you want fallback compatibility, ensure `qmd --version` works in the same shell
491
- - Hook/plugin not active in OpenClaw:
492
- - run `openclaw hooks install clawvault`
493
- - run `openclaw hooks enable clawvault`
494
- - verify with `openclaw hooks list --verbose`
491
+ - Plugin not active in OpenClaw:
492
+ - verify `plugins.load.paths` includes the ClawVault package path
493
+ - verify `plugins.slots.memory` is `clawvault`
494
+ - verify `plugins.entries.clawvault.enabled` is `true`
495
495
  - OpenClaw integration drift:
496
496
  - run `clawvault compat`
497
497
  - Session transcript corruption:
@@ -833,38 +833,128 @@ function truncateRecapSnippet(snippet) {
833
833
  if (safe.length <= MAX_RECAP_SNIPPET_LENGTH) return safe;
834
834
  return `${safe.slice(0, MAX_RECAP_SNIPPET_LENGTH - 3).trimEnd()}...`;
835
835
  }
836
+ function isRecord2(value) {
837
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
838
+ }
839
+ function parseTopLevelJson(output) {
840
+ const text = output.trim();
841
+ if (!text) return null;
842
+ const tryParse = (candidate) => {
843
+ try {
844
+ const parsed = JSON.parse(candidate);
845
+ return isRecord2(parsed) ? parsed : null;
846
+ } catch {
847
+ return null;
848
+ }
849
+ };
850
+ const direct = tryParse(text);
851
+ if (direct) return direct;
852
+ const findJsonEnd = (start) => {
853
+ const open = text[start];
854
+ if (open !== "{" && open !== "[") return -1;
855
+ const stack = [open];
856
+ let inString = false;
857
+ let escaped = false;
858
+ for (let i = start + 1; i < text.length; i += 1) {
859
+ const ch = text[i];
860
+ if (inString) {
861
+ if (escaped) {
862
+ escaped = false;
863
+ continue;
864
+ }
865
+ if (ch === "\\") {
866
+ escaped = true;
867
+ continue;
868
+ }
869
+ if (ch === '"') {
870
+ inString = false;
871
+ }
872
+ continue;
873
+ }
874
+ if (ch === '"') {
875
+ inString = true;
876
+ continue;
877
+ }
878
+ if (ch === "{" || ch === "[") {
879
+ stack.push(ch);
880
+ continue;
881
+ }
882
+ if (ch === "}" || ch === "]") {
883
+ const expected = ch === "}" ? "{" : "[";
884
+ const top = stack[stack.length - 1];
885
+ if (top !== expected) return -1;
886
+ stack.pop();
887
+ if (stack.length === 0) {
888
+ return i;
889
+ }
890
+ }
891
+ }
892
+ return -1;
893
+ };
894
+ for (let start = 0; start < text.length; start += 1) {
895
+ const ch = text[start];
896
+ if (ch !== "{" && ch !== "[") continue;
897
+ const end = findJsonEnd(start);
898
+ if (end < 0) continue;
899
+ const parsed = tryParse(text.slice(start, end + 1));
900
+ if (parsed) return parsed;
901
+ }
902
+ return null;
903
+ }
904
+ function resolveEntryArray(parsed, keys) {
905
+ for (const key of keys) {
906
+ const value = parsed[key];
907
+ if (!Array.isArray(value)) continue;
908
+ return value.filter((item) => isRecord2(item));
909
+ }
910
+ return [];
911
+ }
836
912
  function parseContextJson(output, maxResults) {
837
- try {
838
- const parsed = JSON.parse(output);
839
- if (!parsed || !Array.isArray(parsed.context)) return [];
840
- return parsed.context.slice(0, maxResults).map((entry) => ({
841
- title: sanitizeForDisplay(entry.title ?? "Untitled"),
842
- path: sanitizeForDisplay(entry.path ?? ""),
843
- age: sanitizeForDisplay(entry.age ?? "unknown age"),
844
- snippet: truncateSnippet(String(entry.snippet ?? "")),
845
- score: Number.isFinite(Number(entry.score)) ? Number(entry.score) : 0
846
- })).filter((entry) => entry.snippet.length > 0);
847
- } catch {
913
+ const parsed = parseTopLevelJson(output);
914
+ if (!parsed) {
848
915
  return [];
849
916
  }
917
+ const rows = resolveEntryArray(parsed, ["context", "results", "entries", "memories"]);
918
+ return rows.slice(0, maxResults).map((entry) => {
919
+ const nestedDocument = isRecord2(entry.document) ? entry.document : null;
920
+ const title = sanitizeForDisplay(
921
+ entry.title ?? nestedDocument?.title ?? nestedDocument?.id ?? entry.path ?? "Untitled"
922
+ );
923
+ const resolvedPath = sanitizeForDisplay(
924
+ entry.path ?? entry.relPath ?? nestedDocument?.path ?? ""
925
+ );
926
+ const resolvedAge = sanitizeForDisplay(entry.age ?? entry.modified ?? "unknown age");
927
+ const snippetSource = String(
928
+ entry.snippet ?? entry.text ?? entry.content ?? nestedDocument?.snippet ?? nestedDocument?.content ?? ""
929
+ );
930
+ return {
931
+ title,
932
+ path: resolvedPath,
933
+ age: resolvedAge,
934
+ snippet: truncateSnippet(snippetSource),
935
+ score: Number.isFinite(Number(entry.score)) ? Number(entry.score) : 0
936
+ };
937
+ }).filter((entry) => entry.snippet.length > 0);
850
938
  }
851
939
  function parseSessionRecapJson(output, maxResults) {
852
- try {
853
- const parsed = JSON.parse(output);
854
- if (!parsed || !Array.isArray(parsed.messages)) return [];
855
- return parsed.messages.map((entry) => {
856
- const role = typeof entry.role === "string" ? entry.role.toLowerCase() : "";
857
- if (role !== "user" && role !== "assistant") return null;
858
- const text = truncateRecapSnippet(typeof entry.text === "string" ? entry.text : "");
859
- if (!text) return null;
860
- return {
861
- role: role === "user" ? "User" : "Assistant",
862
- text
863
- };
864
- }).filter((entry) => Boolean(entry)).slice(-maxResults);
865
- } catch {
940
+ const parsed = parseTopLevelJson(output);
941
+ if (!parsed) {
866
942
  return [];
867
943
  }
944
+ const rows = resolveEntryArray(parsed, ["messages", "turns", "recap"]);
945
+ return rows.map((entry) => {
946
+ const role = typeof entry.role === "string" ? entry.role.toLowerCase() : "";
947
+ const normalizedRole = role === "user" || role === "human" ? "User" : role === "assistant" || role === "ai" ? "Assistant" : "";
948
+ if (!normalizedRole) return null;
949
+ const text = truncateRecapSnippet(
950
+ typeof entry.text === "string" ? entry.text : typeof entry.content === "string" ? entry.content : ""
951
+ );
952
+ if (!text) return null;
953
+ return {
954
+ role: normalizedRole,
955
+ text
956
+ };
957
+ }).filter((entry) => Boolean(entry)).slice(-maxResults);
868
958
  }
869
959
  function formatSessionContextInjection(recapEntries, memoryEntries) {
870
960
  const lines = [
@@ -1132,8 +1222,8 @@ function toSafeFilePath(vaultPath, relPath) {
1132
1222
  if (!mapped || mapped.includes("..")) {
1133
1223
  throw new Error("Invalid memory path");
1134
1224
  }
1135
- if (mapped !== "MEMORY.md" && !mapped.startsWith("memory/")) {
1136
- throw new Error("memory_get only allows MEMORY.md or memory/* paths");
1225
+ if (!mapped.toLowerCase().endsWith(".md")) {
1226
+ throw new Error("memory_get only allows Markdown note paths inside the vault");
1137
1227
  }
1138
1228
  const absolute = path4.resolve(vaultPath, mapped);
1139
1229
  const vaultRootWithSep = vaultPath.endsWith(path4.sep) ? vaultPath : `${vaultPath}${path4.sep}`;
@@ -1260,8 +1350,17 @@ function buildToolSchema(properties, required = []) {
1260
1350
  additionalProperties: false
1261
1351
  };
1262
1352
  }
1353
+ function resolveToolInput(toolCallIdOrInput, maybeInput) {
1354
+ if (maybeInput && typeof maybeInput === "object" && !Array.isArray(maybeInput)) {
1355
+ return maybeInput;
1356
+ }
1357
+ if (toolCallIdOrInput && typeof toolCallIdOrInput === "object" && !Array.isArray(toolCallIdOrInput)) {
1358
+ return toolCallIdOrInput;
1359
+ }
1360
+ return {};
1361
+ }
1263
1362
  function createMemorySearchToolFactory(memoryManager) {
1264
- return () => {
1363
+ return (_toolContext) => {
1265
1364
  const inputSchema = buildToolSchema({
1266
1365
  query: {
1267
1366
  type: "string",
@@ -1284,7 +1383,8 @@ function createMemorySearchToolFactory(memoryManager) {
1284
1383
  description: "Optional OpenClaw session key for scoped recall."
1285
1384
  }
1286
1385
  }, ["query"]);
1287
- const execute = async (input) => {
1386
+ const execute = async (toolCallIdOrInput, maybeInput) => {
1387
+ const input = resolveToolInput(toolCallIdOrInput, maybeInput);
1288
1388
  const query = typeof input.query === "string" ? input.query : "";
1289
1389
  if (!query.trim()) {
1290
1390
  return { query, count: 0, results: [] };
@@ -1301,6 +1401,7 @@ function createMemorySearchToolFactory(memoryManager) {
1301
1401
  };
1302
1402
  };
1303
1403
  return {
1404
+ label: "Memory Search",
1304
1405
  name: "memory_search",
1305
1406
  description: "Search ClawVault memory for relevant snippets before answering.",
1306
1407
  inputSchema,
@@ -1313,11 +1414,15 @@ function createMemorySearchToolFactory(memoryManager) {
1313
1414
  };
1314
1415
  }
1315
1416
  function createMemoryGetToolFactory(memoryManager) {
1316
- return () => {
1417
+ return (_toolContext) => {
1317
1418
  const inputSchema = buildToolSchema({
1419
+ path: {
1420
+ type: "string",
1421
+ description: "Relative path from memory_search result (for OpenClaw compatibility)."
1422
+ },
1318
1423
  relPath: {
1319
1424
  type: "string",
1320
- description: "Relative path from memory_search result (e.g. memory/2026-01-01.md)."
1425
+ description: "Alias of path (e.g. memory/2026-01-01.md)."
1321
1426
  },
1322
1427
  from: {
1323
1428
  type: "number",
@@ -1330,9 +1435,11 @@ function createMemoryGetToolFactory(memoryManager) {
1330
1435
  maximum: 400,
1331
1436
  description: "Optional number of lines to read."
1332
1437
  }
1333
- }, ["relPath"]);
1334
- const execute = async (input) => {
1335
- const relPath = typeof input.relPath === "string" ? input.relPath : "";
1438
+ });
1439
+ inputSchema.anyOf = [{ required: ["path"] }, { required: ["relPath"] }];
1440
+ const execute = async (toolCallIdOrInput, maybeInput) => {
1441
+ const input = resolveToolInput(toolCallIdOrInput, maybeInput);
1442
+ const relPath = typeof input.path === "string" ? input.path : typeof input.relPath === "string" ? input.relPath : "";
1336
1443
  if (!relPath.trim()) {
1337
1444
  return { path: relPath, text: "" };
1338
1445
  }
@@ -1343,6 +1450,7 @@ function createMemoryGetToolFactory(memoryManager) {
1343
1450
  });
1344
1451
  };
1345
1452
  return {
1453
+ label: "Memory Get",
1346
1454
  name: "memory_get",
1347
1455
  description: "Read a specific memory file or line range from ClawVault.",
1348
1456
  inputSchema,
@@ -1509,11 +1617,27 @@ async function fetchMemoryContextEntries(options) {
1509
1617
  vaultPath
1510
1618
  ];
1511
1619
  const contextResult = runClawvault(contextArgs, options.pluginConfig, { timeoutMs: 25e3 });
1512
- if (!contextResult.success) {
1620
+ if (contextResult.success) {
1621
+ return {
1622
+ entries: parseContextJson(contextResult.output, maxResults),
1623
+ vaultPath
1624
+ };
1625
+ }
1626
+ const fallbackSearchArgs = [
1627
+ "search",
1628
+ prompt,
1629
+ "--json",
1630
+ "-n",
1631
+ String(maxResults),
1632
+ "-v",
1633
+ vaultPath
1634
+ ];
1635
+ const fallbackSearchResult = runClawvault(fallbackSearchArgs, options.pluginConfig, { timeoutMs: 25e3 });
1636
+ if (!fallbackSearchResult.success) {
1513
1637
  return { entries: [], vaultPath };
1514
1638
  }
1515
1639
  return {
1516
- entries: parseContextJson(contextResult.output, maxResults),
1640
+ entries: parseContextJson(fallbackSearchResult.output, maxResults),
1517
1641
  vaultPath
1518
1642
  };
1519
1643
  }
@@ -2007,8 +2131,10 @@ async function registerOpenClawPlugin(api) {
2007
2131
  warn: api.logger.warn
2008
2132
  }
2009
2133
  });
2010
- api.registerTool(createMemorySearchToolFactory(memoryManager), { name: "memory_search" });
2011
- api.registerTool(createMemoryGetToolFactory(memoryManager), { name: "memory_get" });
2134
+ const memorySearchTool = createMemorySearchToolFactory(memoryManager)();
2135
+ const memoryGetTool = createMemoryGetToolFactory(memoryManager)();
2136
+ api.registerTool(memorySearchTool, { name: "memory_search" });
2137
+ api.registerTool(memoryGetTool, { name: "memory_get" });
2012
2138
  api.on("before_prompt_build", createBeforePromptBuildHandler({
2013
2139
  pluginConfig,
2014
2140
  runtimeState
package/dist/index.js CHANGED
@@ -60,7 +60,7 @@ import {
60
60
  openclaw_plugin_default,
61
61
  plausibilityScore,
62
62
  registerMemorySlot
63
- } from "./chunk-QYQAGBTM.js";
63
+ } from "./chunk-PLNK37JD.js";
64
64
  import {
65
65
  buildRecallResult,
66
66
  classifyRecallQuery
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createMemorySlotPlugin,
3
3
  openclaw_plugin_default
4
- } from "./chunk-QYQAGBTM.js";
4
+ } from "./chunk-PLNK37JD.js";
5
5
  import "./chunk-RL2L6I6K.js";
6
6
  import "./chunk-NSXYM6EZ.js";
7
7
  import "./chunk-35JCYSRR.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawvault",
3
- "version": "3.4.0",
3
+ "version": "3.4.1",
4
4
  "description": "Structured memory system for AI agents — typed storage, knowledge graph, context profiles, canvas dashboards, neural graph themes, and Obsidian-native task views. An elephant never forgets. 🐘",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",