@unblocklabs/unblock-memory 0.1.1 → 0.1.2

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.
@@ -1,8 +1,4 @@
1
1
  import type { QMDStore } from "@unblocklabs/qmd";
2
- export declare const DEFAULT_CLUSTER_LIMIT = 20;
3
- export declare const MAX_CLUSTER_LIMIT = 50;
4
- export declare const DEFAULT_MEMBER_LIMIT = 20;
5
- export declare const MAX_MEMBER_LIMIT = 50;
6
2
  type AnalysisDatabase = QMDStore["internal"]["db"];
7
3
  export type MemoryReclusterOptions = {
8
4
  space?: {
@@ -26,7 +22,7 @@ export type AnalysisRunner = (params: {
26
22
  options?: MemoryReclusterOptions;
27
23
  signal?: AbortSignal;
28
24
  }) => Promise<void>;
29
- export type MemoryAnalysisMember = {
25
+ type MemoryAnalysisMember = {
30
26
  hash: string;
31
27
  seq: number;
32
28
  probability: number;
@@ -52,7 +48,7 @@ export type MemoryAnalysisSummary = {
52
48
  stale: boolean;
53
49
  staleSince: string | null;
54
50
  };
55
- export type MemoryClusterSummary = {
51
+ type MemoryClusterSummary = {
56
52
  clusterId: string;
57
53
  size: number;
58
54
  availableSize: number;
@@ -1,9 +1,9 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
- export const DEFAULT_CLUSTER_LIMIT = 20;
4
- export const MAX_CLUSTER_LIMIT = 50;
5
- export const DEFAULT_MEMBER_LIMIT = 20;
6
- export const MAX_MEMBER_LIMIT = 50;
3
+ const DEFAULT_CLUSTER_LIMIT = 20;
4
+ const MAX_CLUSTER_LIMIT = 50;
5
+ const DEFAULT_MEMBER_LIMIT = 20;
6
+ const MAX_MEMBER_LIMIT = 50;
7
7
  const MAX_EXCERPT_BYTES = 2_000;
8
8
  const MAX_TOTAL_EXCERPT_BYTES = 12_000;
9
9
  const MAX_ALIASES_PER_MEMBER = 5;
@@ -47,6 +47,19 @@ export function ensureMemoryAnalysisSchema(db) {
47
47
 
48
48
  CREATE INDEX IF NOT EXISTS idx_memory_analysis_memberships_cluster
49
49
  ON memory_analysis_memberships(run_id, cluster_id, representative_rank);
50
+
51
+ CREATE VIEW IF NOT EXISTS memory_analysis_available_memberships AS
52
+ SELECT
53
+ m.run_id, m.hash, m.seq, m.cluster_id, m.probability, m.outlier_score,
54
+ m.x, m.y, m.representative_rank, cv.pos, cv.chunk_len, c.doc
55
+ FROM memory_analysis_memberships m
56
+ JOIN content_vectors cv ON cv.hash = m.hash AND cv.seq = m.seq
57
+ JOIN content c ON c.hash = m.hash
58
+ WHERE EXISTS (
59
+ SELECT 1
60
+ FROM documents d
61
+ WHERE d.hash = m.hash AND d.active = 1
62
+ );
50
63
  `);
51
64
  }
52
65
  export function markMemoryAnalysisStale(db) {
@@ -137,10 +150,7 @@ export function latestAnalysisRunId(db) {
137
150
  function count(db, sql, runId) {
138
151
  return db.prepare(sql).get(runId)?.count ?? 0;
139
152
  }
140
- export function readAnalysisSummary(db) {
141
- const run = latestRun(db);
142
- if (!run)
143
- return undefined;
153
+ function analysisSummary(db, run) {
144
154
  const clusters = count(db, "SELECT COUNT(*) AS count FROM memory_analysis_clusters WHERE run_id = ?", run.id);
145
155
  const members = count(db, "SELECT COUNT(*) AS count FROM memory_analysis_memberships WHERE run_id = ?", run.id);
146
156
  const expectedNonNoise = count(db, "SELECT COALESCE(SUM(size), 0) AS count FROM memory_analysis_clusters WHERE run_id = ?", run.id);
@@ -171,6 +181,14 @@ export function readAnalysisSummary(db) {
171
181
  staleSince: run.stale_at,
172
182
  };
173
183
  }
184
+ export function readAnalysisSummary(db) {
185
+ const run = latestRun(db);
186
+ return run ? analysisSummary(db, run) : undefined;
187
+ }
188
+ function latestValidRun(db) {
189
+ const run = latestRun(db);
190
+ return run && analysisSummary(db, run) ? run : undefined;
191
+ }
174
192
  function sourcePaths(db, hash, limit) {
175
193
  if (limit <= 0)
176
194
  return [];
@@ -202,16 +220,9 @@ function members(db, runId, clusterId, limit, maxExcerptBytes = MAX_EXCERPT_BYTE
202
220
  const rows = db.prepare(`
203
221
  SELECT
204
222
  m.hash, m.seq, m.probability, m.outlier_score, m.x, m.y,
205
- m.representative_rank, cv.pos, cv.chunk_len, c.doc
206
- FROM memory_analysis_memberships m
207
- JOIN content_vectors cv ON cv.hash = m.hash AND cv.seq = m.seq
208
- JOIN content c ON c.hash = m.hash
223
+ m.representative_rank, m.pos, m.chunk_len, m.doc
224
+ FROM memory_analysis_available_memberships m
209
225
  WHERE m.run_id = ? AND m.cluster_id = ?
210
- AND EXISTS (
211
- SELECT 1
212
- FROM documents d
213
- WHERE d.hash = m.hash AND d.active = 1
214
- )
215
226
  ORDER BY ${noiseOrder}
216
227
  LIMIT ?
217
228
  `).all(runId, clusterId, limit);
@@ -240,15 +251,8 @@ function members(db, runId, clusterId, limit, maxExcerptBytes = MAX_EXCERPT_BYTE
240
251
  function availableSize(db, runId, clusterId) {
241
252
  return db.prepare(`
242
253
  SELECT COUNT(*) AS count
243
- FROM memory_analysis_memberships m
244
- JOIN content_vectors cv ON cv.hash = m.hash AND cv.seq = m.seq
245
- JOIN content c ON c.hash = m.hash
246
- WHERE m.run_id = ? AND m.cluster_id = ?
247
- AND EXISTS (
248
- SELECT 1
249
- FROM documents d
250
- WHERE d.hash = m.hash AND d.active = 1
251
- )
254
+ FROM memory_analysis_available_memberships
255
+ WHERE run_id = ? AND cluster_id = ?
252
256
  `).get(runId, clusterId)?.count ?? 0;
253
257
  }
254
258
  function readMetadata(run) {
@@ -295,22 +299,20 @@ function noiseRow(db, runId) {
295
299
  SELECT
296
300
  -1 AS cluster_id,
297
301
  COUNT(*) AS size,
298
- COUNT(CASE WHEN cv.hash IS NOT NULL AND c.hash IS NOT NULL AND EXISTS (
299
- SELECT 1
300
- FROM documents d
301
- WHERE d.hash = m.hash AND d.active = 1
302
- ) THEN 1 END) AS available_size,
302
+ (
303
+ SELECT COUNT(*)
304
+ FROM memory_analysis_available_memberships available
305
+ WHERE available.run_id = ? AND available.cluster_id = -1
306
+ ) AS available_size,
303
307
  COALESCE(AVG(m.probability), 0) AS mean_probability
304
308
  FROM memory_analysis_memberships m
305
- LEFT JOIN content_vectors cv ON cv.hash = m.hash AND cv.seq = m.seq
306
- LEFT JOIN content c ON c.hash = m.hash
307
309
  WHERE m.run_id = ? AND m.cluster_id = -1
308
310
  HAVING COUNT(*) > 0
309
- `).get(runId);
311
+ `).get(runId, runId);
310
312
  }
311
313
  export function readClusters(db, requestedLimit = DEFAULT_CLUSTER_LIMIT) {
312
- const run = latestRun(db);
313
- if (!run || !readAnalysisSummary(db)) {
314
+ const run = latestValidRun(db);
315
+ if (!run) {
314
316
  return { status: "not_analyzed", ...readMetadata(), clusters: [], noise: null };
315
317
  }
316
318
  const limit = Math.max(1, Math.min(MAX_CLUSTER_LIMIT, Math.floor(requestedLimit)));
@@ -318,19 +320,14 @@ export function readClusters(db, requestedLimit = DEFAULT_CLUSTER_LIMIT) {
318
320
  SELECT
319
321
  c.cluster_id,
320
322
  c.size,
321
- COUNT(CASE WHEN cv.hash IS NOT NULL AND content_row.hash IS NOT NULL AND EXISTS (
322
- SELECT 1
323
- FROM documents d
324
- WHERE d.hash = m.hash AND d.active = 1
325
- ) THEN 1 END) AS available_size,
323
+ (
324
+ SELECT COUNT(*)
325
+ FROM memory_analysis_available_memberships available
326
+ WHERE available.run_id = c.run_id AND available.cluster_id = c.cluster_id
327
+ ) AS available_size,
326
328
  c.mean_probability
327
329
  FROM memory_analysis_clusters c
328
- LEFT JOIN memory_analysis_memberships m
329
- ON m.run_id = c.run_id AND m.cluster_id = c.cluster_id
330
- LEFT JOIN content_vectors cv ON cv.hash = m.hash AND cv.seq = m.seq
331
- LEFT JOIN content content_row ON content_row.hash = m.hash
332
330
  WHERE c.run_id = ?
333
- GROUP BY c.cluster_id, c.size, c.mean_probability
334
331
  ORDER BY c.size DESC, c.cluster_id
335
332
  LIMIT ?
336
333
  `).all(run.id, limit);
@@ -366,8 +363,8 @@ function resolveClusterId(db, runId, reference) {
366
363
  return clusterIds.find((row) => clusterReference(runId, row.cluster_id) === reference)?.cluster_id;
367
364
  }
368
365
  export function readCluster(db, clusterReferenceId, requestedLimit = DEFAULT_MEMBER_LIMIT) {
369
- const run = latestRun(db);
370
- if (!run || !readAnalysisSummary(db)) {
366
+ const run = latestValidRun(db);
367
+ if (!run) {
371
368
  return { status: "not_analyzed", ...readMetadata() };
372
369
  }
373
370
  const metadata = readMetadata(run);
@@ -1,5 +1,6 @@
1
1
  import { Type } from "typebox";
2
- import { jsonResult, readStringParam } from "openclaw/plugin-sdk/agent-runtime";
2
+ import { Value } from "typebox/value";
3
+ import { jsonResult } from "openclaw/plugin-sdk/agent-runtime";
3
4
  import { resolveConfig } from "./config.js";
4
5
  import { QmdMemoryRuntime } from "./runtime.js";
5
6
  function getContext(ctx) {
@@ -8,11 +9,16 @@ function getContext(ctx) {
8
9
  return undefined;
9
10
  return { cfg, agentId: ctx.agentId };
10
11
  }
11
- function toolParams(value) {
12
- return value !== null && typeof value === "object" && !Array.isArray(value)
13
- ? Object.fromEntries(Object.entries(value))
14
- : {};
15
- }
12
+ const searchParameters = Type.Object({
13
+ query: Type.String({ pattern: "\\S" }),
14
+ maxResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20 })),
15
+ minScore: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })),
16
+ }, { additionalProperties: false });
17
+ const getParameters = Type.Object({
18
+ path: Type.String({ pattern: "\\S" }),
19
+ from: Type.Optional(Type.Integer({ minimum: 1 })),
20
+ lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 1000 })),
21
+ }, { additionalProperties: false });
16
22
  function createSearchTool(runtime, ctx) {
17
23
  const active = getContext(ctx);
18
24
  if (!active)
@@ -21,18 +27,10 @@ function createSearchTool(runtime, ctx) {
21
27
  name: "memory_search",
22
28
  label: "Memory Search",
23
29
  description: "Search canonical Markdown memory with semantic vector retrieval.",
24
- parameters: Type.Object({
25
- query: Type.String(),
26
- maxResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20 })),
27
- minScore: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })),
28
- }),
30
+ parameters: searchParameters,
29
31
  async execute(_toolCallId, params, signal) {
30
- const raw = toolParams(params);
31
- const query = readStringParam(raw, "query");
32
- if (!query)
33
- throw new Error("query is required");
34
- const maxResults = typeof raw.maxResults === "number" ? raw.maxResults : undefined;
35
- const minScore = typeof raw.minScore === "number" ? raw.minScore : undefined;
32
+ const { query: untrimmedQuery, maxResults, minScore } = Value.Parse(searchParameters, params);
33
+ const query = untrimmedQuery.trim();
36
34
  const { manager, error } = await runtime.getMemorySearchManager(active);
37
35
  if (!manager)
38
36
  return jsonResult({ results: [], error: error ?? "memory unavailable" });
@@ -49,23 +47,17 @@ function createGetTool(runtime, ctx) {
49
47
  name: "memory_get",
50
48
  label: "Memory Get",
51
49
  description: "Read an exact qmd:// path returned by memory_search.",
52
- parameters: Type.Object({
53
- path: Type.String(),
54
- from: Type.Optional(Type.Integer({ minimum: 1 })),
55
- lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 1000 })),
56
- }),
50
+ parameters: getParameters,
57
51
  async execute(_toolCallId, params) {
58
- const raw = toolParams(params);
59
- const path = readStringParam(raw, "path");
60
- if (!path)
61
- throw new Error("path is required");
52
+ const { path: untrimmedPath, from, lines } = Value.Parse(getParameters, params);
53
+ const path = untrimmedPath.trim();
62
54
  const { manager, error } = await runtime.getMemorySearchManager(active);
63
55
  if (!manager)
64
56
  return jsonResult({ status: "unavailable", error: error ?? "memory unavailable" });
65
57
  return jsonResult(await manager.readFile({
66
58
  relPath: path,
67
- from: typeof raw.from === "number" ? raw.from : undefined,
68
- lines: typeof raw.lines === "number" ? raw.lines : undefined,
59
+ from,
60
+ lines,
69
61
  }));
70
62
  },
71
63
  };
@@ -86,80 +78,6 @@ const reclusterParameters = Type.Object({
86
78
  }, { additionalProperties: false })),
87
79
  seed: Type.Optional(Type.Integer({ minimum: 0, maximum: 4_294_967_295 })),
88
80
  }, { additionalProperties: false });
89
- function requireObject(value, name) {
90
- if (!value || typeof value !== "object" || Array.isArray(value)) {
91
- throw new Error(`${name} must be an object`);
92
- }
93
- return Object.fromEntries(Object.entries(value));
94
- }
95
- function optionalNumber(value, key, constraints) {
96
- const candidate = value[key];
97
- if (candidate === undefined)
98
- return undefined;
99
- const valid = typeof candidate === "number" && Number.isFinite(candidate) &&
100
- candidate >= constraints.minimum &&
101
- (constraints.maximum === undefined || candidate <= constraints.maximum) &&
102
- (!constraints.integer || Number.isInteger(candidate));
103
- if (!valid)
104
- throw new Error(`${key} is invalid`);
105
- return candidate;
106
- }
107
- function requireOnlyKeys(value, allowed) {
108
- const unexpected = Object.keys(value).find((key) => !allowed.includes(key));
109
- if (unexpected)
110
- throw new Error(`${unexpected} is not allowed`);
111
- }
112
- function parseReclusterOptions(params) {
113
- const raw = toolParams(params);
114
- requireOnlyKeys(raw, ["space", "hdbscan", "seed"]);
115
- const options = {};
116
- if (raw.space !== undefined) {
117
- const space = requireObject(raw.space, "space");
118
- requireOnlyKeys(space, ["method", "nComponents", "nNeighbors", "minDist"]);
119
- const method = space.method;
120
- if (method !== undefined && method !== "umap" && method !== "none") {
121
- throw new Error("space.method is invalid");
122
- }
123
- options.space = {
124
- ...(method === undefined ? {} : { method }),
125
- ...optionalEntry("nComponents", optionalNumber(space, "nComponents", { minimum: 2, maximum: 100, integer: true })),
126
- ...optionalEntry("nNeighbors", optionalNumber(space, "nNeighbors", { minimum: 2, maximum: 200, integer: true })),
127
- ...optionalEntry("minDist", optionalNumber(space, "minDist", { minimum: 0, maximum: 1 })),
128
- };
129
- }
130
- if (raw.hdbscan !== undefined) {
131
- const hdbscan = requireObject(raw.hdbscan, "hdbscan");
132
- requireOnlyKeys(hdbscan, [
133
- "minClusterSize",
134
- "minSamples",
135
- "clusterSelectionMethod",
136
- "clusterSelectionEpsilon",
137
- "allowSingleCluster",
138
- ]);
139
- const method = hdbscan.clusterSelectionMethod;
140
- if (method !== undefined && method !== "eom" && method !== "leaf") {
141
- throw new Error("hdbscan.clusterSelectionMethod is invalid");
142
- }
143
- const allowSingleCluster = hdbscan.allowSingleCluster;
144
- if (allowSingleCluster !== undefined && typeof allowSingleCluster !== "boolean") {
145
- throw new Error("hdbscan.allowSingleCluster is invalid");
146
- }
147
- options.hdbscan = {
148
- ...optionalEntry("minClusterSize", optionalNumber(hdbscan, "minClusterSize", { minimum: 2, maximum: 100_000, integer: true })),
149
- ...optionalEntry("minSamples", optionalNumber(hdbscan, "minSamples", { minimum: 1, maximum: 100_000, integer: true })),
150
- ...(method === undefined ? {} : { clusterSelectionMethod: method }),
151
- ...optionalEntry("clusterSelectionEpsilon", optionalNumber(hdbscan, "clusterSelectionEpsilon", { minimum: 0 })),
152
- ...(allowSingleCluster === undefined ? {} : { allowSingleCluster }),
153
- };
154
- }
155
- const seed = optionalNumber(raw, "seed", { minimum: 0, maximum: 4_294_967_295, integer: true });
156
- if (seed !== undefined)
157
- options.seed = seed;
158
- return options;
159
- }
160
- function optionalEntry(key, value) {
161
- return value === undefined ? {} : { [key]: value };
162
- }
163
81
  function createReclusterTool(runtime, ctx) {
164
82
  const active = getContext(ctx);
165
83
  if (!active)
@@ -170,7 +88,7 @@ function createReclusterTool(runtime, ctx) {
170
88
  description: "Rebuild memory clusters from existing QMD vectors. Call only when memory_list_clusters reports missing or stale analysis.",
171
89
  parameters: reclusterParameters,
172
90
  async execute(_toolCallId, params, signal) {
173
- const options = parseReclusterOptions(params);
91
+ const options = Value.Parse(reclusterParameters, params);
174
92
  const { manager, error } = await runtime.getMemorySearchManager(active);
175
93
  if (!manager)
176
94
  return jsonResult({ status: "unavailable", error: error ?? "memory unavailable" });
@@ -186,6 +104,9 @@ function createReclusterTool(runtime, ctx) {
186
104
  },
187
105
  };
188
106
  }
107
+ const listClustersParameters = Type.Object({
108
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
109
+ }, { additionalProperties: false });
189
110
  function createListClustersTool(runtime, ctx) {
190
111
  const active = getContext(ctx);
191
112
  if (!active)
@@ -194,13 +115,9 @@ function createListClustersTool(runtime, ctx) {
194
115
  name: "memory_list_clusters",
195
116
  label: "List Memory Clusters",
196
117
  description: "List current memory clusters and freshness. Call this before memory_recluster or memory_fetch_cluster.",
197
- parameters: Type.Object({
198
- limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
199
- }, { additionalProperties: false }),
118
+ parameters: listClustersParameters,
200
119
  async execute(_toolCallId, params) {
201
- const raw = toolParams(params);
202
- requireOnlyKeys(raw, ["limit"]);
203
- const limit = optionalNumber(raw, "limit", { minimum: 1, maximum: 50, integer: true });
120
+ const { limit } = Value.Parse(listClustersParameters, params);
204
121
  const { manager, error } = await runtime.getMemorySearchManager(active);
205
122
  if (!manager)
206
123
  return jsonResult({ status: "unavailable", error: error ?? "memory unavailable" });
@@ -208,6 +125,10 @@ function createListClustersTool(runtime, ctx) {
208
125
  },
209
126
  };
210
127
  }
128
+ const fetchClusterParameters = Type.Object({
129
+ clusterId: Type.String({ pattern: "^[0-9a-f]{10}$" }),
130
+ topK: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
131
+ }, { additionalProperties: false });
211
132
  function createFetchClusterTool(runtime, ctx) {
212
133
  const active = getContext(ctx);
213
134
  if (!active)
@@ -216,17 +137,9 @@ function createFetchClusterTool(runtime, ctx) {
216
137
  name: "memory_fetch_cluster",
217
138
  label: "Fetch Memory Cluster",
218
139
  description: "Fetch the top representative QMD chunks for a clusterId returned by memory_list_clusters.",
219
- parameters: Type.Object({
220
- clusterId: Type.String({ pattern: "^[0-9a-f]{10}$" }),
221
- topK: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
222
- }, { additionalProperties: false }),
140
+ parameters: fetchClusterParameters,
223
141
  async execute(_toolCallId, params) {
224
- const raw = toolParams(params);
225
- requireOnlyKeys(raw, ["clusterId", "topK"]);
226
- const clusterId = readStringParam(raw, "clusterId");
227
- if (!clusterId || !/^[0-9a-f]{10}$/.test(clusterId))
228
- throw new Error("clusterId is invalid");
229
- const topK = optionalNumber(raw, "topK", { minimum: 1, maximum: 50, integer: true });
142
+ const { clusterId, topK } = Value.Parse(fetchClusterParameters, params);
230
143
  const { manager, error } = await runtime.getMemorySearchManager(active);
231
144
  if (!manager)
232
145
  return jsonResult({ status: "unavailable", error: error ?? "memory unavailable" });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "unblock-memory",
3
3
  "name": "Unblock Memory",
4
- "version": "0.1.1",
4
+ "version": "0.1.2",
5
5
  "description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
6
6
  "kind": "memory",
7
7
  "activation": { "onStartup": false },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unblocklabs/unblock-memory",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Workspace-native memory for OpenClaw, powered by QMD",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,11 +22,12 @@
22
22
  "scripts": {
23
23
  "build": "tsc -p tsconfig.build.json",
24
24
  "typecheck": "tsc -p tsconfig.json --noEmit",
25
+ "knip": "knip --reporter compact",
25
26
  "test": "node --import tsx --test --test-concurrency=1 tests/**/*.test.ts",
26
27
  "plugin:inspect": "plugin-inspector check --config plugin-inspector.config.json --no-openclaw",
27
28
  "plugin:inspect:runtime": "plugin-inspector check --config plugin-inspector.config.json --no-openclaw --runtime --mock-sdk --allow-execute",
28
29
  "release:check": "node scripts/check-release-version.mjs",
29
- "preflight": "npm run build && npm run typecheck && npm test && npm run plugin:inspect && npm run plugin:inspect:runtime && npm pack --dry-run"
30
+ "preflight": "npm run knip && npm run build && npm run typecheck && npm test && npm run plugin:inspect && npm run plugin:inspect:runtime && npm pack --dry-run"
30
31
  },
31
32
  "dependencies": {
32
33
  "@unblocklabs/qmd": "github:unblocklabs-ai/qmd#0a6b15d",
@@ -38,6 +39,7 @@
38
39
  "@openclaw/plugin-inspector": "^0.3.10",
39
40
  "@types/node": "^24.6.0",
40
41
  "@types/picomatch": "^4.0.2",
42
+ "knip": "^6.32.2",
41
43
  "openclaw": "2026.8.1-beta.3",
42
44
  "tsx": "^4.20.6",
43
45
  "typescript": "^5.9.3"