@unblocklabs/unblock-memory 0.3.1 → 0.3.3
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 +3 -1
- package/dist/src/contracts.d.ts +3 -0
- package/dist/src/manager.d.ts +2 -1
- package/dist/src/manager.js +65 -25
- package/dist/src/plugin.js +14 -1
- package/dist/src/sources.js +3 -9
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -127,7 +127,9 @@ The `skills` corpus shares the existing QMD store and warm embedding model but
|
|
|
127
127
|
is private to Skill Whisperer: it is excluded from ordinary `memory_search`
|
|
128
128
|
(including `corpora: ["all"]`), `memory_get`, clustering, and memory-maintenance
|
|
129
129
|
tasks. Paths are explicit by design; the plugin does not reconstruct
|
|
130
|
-
OpenClaw's effective skill inventory from `openclaw.json`.
|
|
130
|
+
OpenClaw's effective skill inventory from `openclaw.json`. Configured skill
|
|
131
|
+
globs follow symlinked directories, including OpenClaw's `plugin-skills`
|
|
132
|
+
directory.
|
|
131
133
|
|
|
132
134
|
Use `sessionFilter` to restrict session results by metadata while leaving file
|
|
133
135
|
corpora searchable. Supported fields are `startedFrom` and `startedTo`
|
package/dist/src/contracts.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MemoryPluginCapability } from "openclaw/plugin-sdk/memory-host-core";
|
|
2
|
+
import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
3
|
import type { SessionMetadata } from "./session-projector.js";
|
|
3
4
|
import type { ChatType } from "./config.js";
|
|
4
5
|
export type MemoryPluginRuntimeContract = NonNullable<MemoryPluginCapability["runtime"]>;
|
|
@@ -18,9 +19,11 @@ export type SessionSearchFilter = {
|
|
|
18
19
|
accountId?: string;
|
|
19
20
|
conversationId?: string;
|
|
20
21
|
};
|
|
22
|
+
export type MemoryRequestContext = Pick<OpenClawPluginToolContext, "sessionKey" | "sessionId" | "messageChannel" | "agentAccountId" | "nativeChannelId" | "deliveryContext">;
|
|
21
23
|
export type CorpusSearchOptions = NonNullable<Parameters<MemorySearchManagerContract["search"]>[1]> & {
|
|
22
24
|
corpora?: readonly string[];
|
|
23
25
|
sessionFilter?: SessionSearchFilter;
|
|
26
|
+
requestContext?: MemoryRequestContext;
|
|
24
27
|
};
|
|
25
28
|
export type MemoryEmbeddingProbeResult = Awaited<ReturnType<MemorySearchManagerContract["probeEmbeddingAvailability"]>>;
|
|
26
29
|
export type MemorySyncParams = Parameters<NonNullable<MemorySearchManagerContract["sync"]>>[0];
|
package/dist/src/manager.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QMDStore } from "@unblocklabs/qmd";
|
|
2
2
|
import { type AnalysisRunner, type MemoryAnalysisSummary, type MemoryClusterDetail, type MemoryClusterList, type MemoryClusterSort, type MemoryReclusterOptions } from "./analysis.js";
|
|
3
|
-
import type { CorpusMemorySearchResult, CorpusSearchOptions, MemoryEmbeddingProbeResult, MemoryProviderStatus, MemoryReadResult, MemorySearchManagerContract, MemorySyncParams } from "./contracts.js";
|
|
3
|
+
import type { CorpusMemorySearchResult, CorpusSearchOptions, MemoryEmbeddingProbeResult, MemoryProviderStatus, MemoryReadResult, MemoryRequestContext, MemorySearchManagerContract, MemorySyncParams } from "./contracts.js";
|
|
4
4
|
import type { ChatType } from "./config.js";
|
|
5
5
|
import { type MaintenanceStatus, type TemporalBasis } from "./curation.js";
|
|
6
6
|
import { type SessionSyncResult } from "./session-sync.js";
|
|
@@ -75,6 +75,7 @@ export declare class QmdMemoryManager implements MemorySearchManagerContract {
|
|
|
75
75
|
relPath: string;
|
|
76
76
|
from?: number;
|
|
77
77
|
lines?: number;
|
|
78
|
+
requestContext?: MemoryRequestContext;
|
|
78
79
|
}): Promise<MemoryReadResult>;
|
|
79
80
|
status(): MemoryProviderStatus;
|
|
80
81
|
probeEmbeddingAvailability(): Promise<MemoryEmbeddingProbeResult>;
|
package/dist/src/manager.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { realpathSync } from "node:fs";
|
|
1
|
+
import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { mkdir, stat } from "node:fs/promises";
|
|
3
|
-
import { basename, dirname, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, relative, resolve, sep } from "node:path";
|
|
4
4
|
import chokidar from "chokidar";
|
|
5
|
+
import picomatch from "picomatch";
|
|
5
6
|
import { ensureMemoryAnalysisSchema, latestAnalysisCollections, latestAnalysisRunId, markMemoryAnalysisStale, readAnalysisSummary, readCluster, readClusters, runAnalysisWorker, } from "./analysis.js";
|
|
6
7
|
import { CurationStore, chunkFingerprint, } from "./curation.js";
|
|
7
8
|
import { readSessionManifest, sessionMetadataByPath, syncSessionProjections, } from "./session-sync.js";
|
|
@@ -10,6 +11,50 @@ const DEFAULT_READ_LINES = 120;
|
|
|
10
11
|
const MAX_READ_CHARS = 12_000;
|
|
11
12
|
const WATCH_DEBOUNCE_MS = 250;
|
|
12
13
|
const qmdModule = import("@unblocklabs/qmd");
|
|
14
|
+
function readSkillDocuments(source) {
|
|
15
|
+
const documents = [];
|
|
16
|
+
const visitedDirectories = new Set();
|
|
17
|
+
const visit = (directory) => {
|
|
18
|
+
let canonicalDirectory;
|
|
19
|
+
try {
|
|
20
|
+
canonicalDirectory = realpathSync(directory);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (visitedDirectories.has(canonicalDirectory))
|
|
26
|
+
return;
|
|
27
|
+
visitedDirectories.add(canonicalDirectory);
|
|
28
|
+
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
29
|
+
const path = resolve(directory, entry.name);
|
|
30
|
+
let kind = entry.isDirectory()
|
|
31
|
+
? "directory"
|
|
32
|
+
: entry.isFile()
|
|
33
|
+
? "file"
|
|
34
|
+
: undefined;
|
|
35
|
+
if (entry.isSymbolicLink()) {
|
|
36
|
+
try {
|
|
37
|
+
const target = statSync(path);
|
|
38
|
+
kind = target.isDirectory() ? "directory" : target.isFile() ? "file" : undefined;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (kind === "directory") {
|
|
45
|
+
visit(path);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const relativePath = relative(source.root, path).split(sep).join("/");
|
|
49
|
+
if (kind !== "file" || basename(path).toLowerCase() !== "skill.md" ||
|
|
50
|
+
!picomatch.isMatch(relativePath, source.pattern, { dot: true }))
|
|
51
|
+
continue;
|
|
52
|
+
documents.push({ path, body: readFileSync(path, "utf8") });
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
visit(source.root);
|
|
56
|
+
return documents;
|
|
57
|
+
}
|
|
13
58
|
function frontmatterValue(body, key) {
|
|
14
59
|
const frontmatter = /^---\s*\n([\s\S]*?)\n---(?:\n|$)/u.exec(body)?.[1];
|
|
15
60
|
const raw = frontmatter?.split("\n")
|
|
@@ -264,9 +309,11 @@ export class QmdMemoryManager {
|
|
|
264
309
|
this.#watcher.on("error", (error) => {
|
|
265
310
|
this.#watchError = error instanceof Error ? error.message : String(error);
|
|
266
311
|
});
|
|
267
|
-
this.#watcher.on("all", () => {
|
|
312
|
+
this.#watcher.on("all", (_event, path) => {
|
|
268
313
|
if (this.#closed)
|
|
269
314
|
return;
|
|
315
|
+
if (basename(path).toLowerCase() === "skill.md")
|
|
316
|
+
this.#skillIndex = undefined;
|
|
270
317
|
this.#dirty = true;
|
|
271
318
|
if (this.#watchTimer)
|
|
272
319
|
clearTimeout(this.#watchTimer);
|
|
@@ -690,31 +737,24 @@ export class QmdMemoryManager {
|
|
|
690
737
|
throw new Error("Skill Whisperer requires the QMD embedding model");
|
|
691
738
|
const llm = store.internal.llm;
|
|
692
739
|
this.#skillIndex ??= (async () => {
|
|
693
|
-
const placeholders = collections.map(() => "?").join(", ");
|
|
694
|
-
const rows = store.internal.db.prepare(`
|
|
695
|
-
SELECT document.collection, document.path, content.doc AS body
|
|
696
|
-
FROM documents document
|
|
697
|
-
JOIN content ON content.hash = document.hash
|
|
698
|
-
WHERE document.active = 1 AND document.collection IN (${placeholders})
|
|
699
|
-
ORDER BY document.collection, document.path
|
|
700
|
-
`).all(...collections);
|
|
701
740
|
const sourceOrder = new Map([...this.#sources.keys()].map((collection, index) => [collection, index]));
|
|
702
741
|
const metadata = new Map();
|
|
703
|
-
for (const
|
|
704
|
-
|
|
705
|
-
const safe = parseSafeVirtualPath(file, this.#sources);
|
|
706
|
-
if (!safe || safe.source.kind !== "skills")
|
|
742
|
+
for (const source of this.#sources.values()) {
|
|
743
|
+
if (source.kind !== "skills")
|
|
707
744
|
continue;
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
745
|
+
for (const document of readSkillDocuments(source)) {
|
|
746
|
+
const name = frontmatterValue(document.body, "name") || basename(dirname(document.path));
|
|
747
|
+
const description = frontmatterValue(document.body, "description") ?? "";
|
|
748
|
+
const key = name.toLowerCase();
|
|
749
|
+
const order = sourceOrder.get(source.collection) ?? Number.MAX_SAFE_INTEGER;
|
|
750
|
+
const current = metadata.get(key);
|
|
751
|
+
if (!current || order < current.sourceOrder) {
|
|
752
|
+
metadata.set(key, {
|
|
753
|
+
candidate: { name, path: document.path },
|
|
754
|
+
description,
|
|
755
|
+
sourceOrder: order,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
718
758
|
}
|
|
719
759
|
}
|
|
720
760
|
const skills = [...metadata.values()];
|
package/dist/src/plugin.js
CHANGED
|
@@ -8,7 +8,18 @@ function getContext(ctx) {
|
|
|
8
8
|
const cfg = ctx.getRuntimeConfig?.() ?? ctx.runtimeConfig ?? ctx.config;
|
|
9
9
|
if (!cfg || !ctx.agentId)
|
|
10
10
|
return undefined;
|
|
11
|
-
return {
|
|
11
|
+
return {
|
|
12
|
+
cfg,
|
|
13
|
+
agentId: ctx.agentId,
|
|
14
|
+
requestContext: {
|
|
15
|
+
sessionKey: ctx.sessionKey,
|
|
16
|
+
sessionId: ctx.sessionId,
|
|
17
|
+
messageChannel: ctx.messageChannel,
|
|
18
|
+
agentAccountId: ctx.agentAccountId,
|
|
19
|
+
nativeChannelId: ctx.nativeChannelId,
|
|
20
|
+
deliveryContext: ctx.deliveryContext,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
12
23
|
}
|
|
13
24
|
const searchParameters = Type.Object({
|
|
14
25
|
query: Type.String({ pattern: "\\S" }),
|
|
@@ -58,6 +69,7 @@ function createSearchTool(runtime, ctx) {
|
|
|
58
69
|
maxResults,
|
|
59
70
|
minScore,
|
|
60
71
|
signal,
|
|
72
|
+
requestContext: active.requestContext,
|
|
61
73
|
});
|
|
62
74
|
return jsonResult({
|
|
63
75
|
results: results.map((result) => result.session
|
|
@@ -93,6 +105,7 @@ function createGetTool(runtime, ctx) {
|
|
|
93
105
|
relPath: path,
|
|
94
106
|
from,
|
|
95
107
|
lines,
|
|
108
|
+
requestContext: active.requestContext,
|
|
96
109
|
}));
|
|
97
110
|
},
|
|
98
111
|
};
|
package/dist/src/sources.js
CHANGED
|
@@ -113,16 +113,10 @@ export function resolveConfiguredSkillPath(workspaceDir, inputPath, sources) {
|
|
|
113
113
|
for (const source of sources) {
|
|
114
114
|
if (source.kind !== "skills")
|
|
115
115
|
continue;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
canonicalRoot = realpathSync(source.root);
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
116
|
+
const relativePath = relative(source.root, target);
|
|
117
|
+
if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath))
|
|
121
118
|
continue;
|
|
122
|
-
}
|
|
123
|
-
const relativePath = relative(canonicalRoot, canonicalTarget);
|
|
124
|
-
const safe = parseSafeVirtualPath(`qmd://${source.collection}/${relativePath.split(sep).join("/")}`, new Map([[source.collection, source]]));
|
|
125
|
-
if (safe)
|
|
119
|
+
if (picomatch.isMatch(relativePath.split(sep).join("/"), source.pattern, { dot: true }))
|
|
126
120
|
return canonicalTarget;
|
|
127
121
|
}
|
|
128
122
|
return undefined;
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "unblock-memory",
|
|
3
3
|
"name": "Unblock Memory",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.3",
|
|
5
5
|
"description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"activation": { "onStartup": false },
|