openclaw-openviking-plugin 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.
@@ -0,0 +1,18 @@
1
+ {
2
+ "id": "openclaw-openviking-plugin",
3
+ "configSchema": {
4
+ "type": "object",
5
+ "additionalProperties": false,
6
+ "properties": {
7
+ "baseUrl": { "type": "string" },
8
+ "apiKey": { "type": "string" },
9
+ "autoRecall": { "type": "boolean" },
10
+ "autoCapture": { "type": "boolean" },
11
+ "recallLimit": { "type": "number" },
12
+ "recallScoreThreshold": { "type": "number" },
13
+ "recallTokenBudget": { "type": "number" },
14
+ "recallMaxContentChars": { "type": "number" },
15
+ "commitTokenThreshold": { "type": "number" }
16
+ }
17
+ }
18
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "openclaw-openviking-plugin",
3
+ "version": "0.1.0",
4
+ "description": "OpenClaw plugin for long-term memory via OpenViking. Hook-only design, works alongside LCM without conflict.",
5
+ "type": "module",
6
+ "main": "./index.ts",
7
+ "author": "liushuangls",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/liushuangls/openclaw-openviking-plugin.git"
12
+ },
13
+ "homepage": "https://github.com/liushuangls/openclaw-openviking-plugin#readme",
14
+ "keywords": [
15
+ "openclaw",
16
+ "openviking",
17
+ "plugin",
18
+ "memory",
19
+ "ai-agent",
20
+ "long-term-memory"
21
+ ],
22
+ "files": [
23
+ "client.ts",
24
+ "index.ts",
25
+ "src/",
26
+ "openclaw.plugin.json",
27
+ "README.md",
28
+ "README_CN.md"
29
+ ],
30
+ "scripts": {
31
+ "test": "vitest run",
32
+ "test:unit": "vitest run tests/unit",
33
+ "test:integration": "vitest run tests/integration"
34
+ },
35
+ "dependencies": {},
36
+ "devDependencies": {
37
+ "@vitest/coverage-v8": "^3.2.4",
38
+ "vitest": "^3.2.4"
39
+ },
40
+ "openclaw": {
41
+ "extensions": [
42
+ "./index.ts"
43
+ ],
44
+ "compat": {
45
+ "pluginApi": ">=2026.3.24-beta.2",
46
+ "minGatewayVersion": "2026.3.24-beta.2"
47
+ }
48
+ }
49
+ }
package/src/helpers.ts ADDED
@@ -0,0 +1,195 @@
1
+ import type { FindResultItem } from "../client.js";
2
+
3
+ export type BuildMemoryLinesOptions = {
4
+ recallPreferAbstract: boolean;
5
+ recallMaxContentChars: number;
6
+ recallTokenBudget: number;
7
+ };
8
+
9
+ export type PrePromptCountEntry = {
10
+ count: number;
11
+ updatedAt: number;
12
+ };
13
+
14
+ export function rememberPrePromptCount(
15
+ store: Map<string, PrePromptCountEntry>,
16
+ sessionId: string,
17
+ count: number,
18
+ now = Date.now(),
19
+ ): void {
20
+ if (!sessionId || store.has(sessionId)) {
21
+ return;
22
+ }
23
+
24
+ store.set(sessionId, {
25
+ count,
26
+ updatedAt: now,
27
+ });
28
+ }
29
+
30
+ export function consumePrePromptCount(
31
+ store: Map<string, PrePromptCountEntry>,
32
+ sessionId: string,
33
+ ): number | undefined {
34
+ const entry = store.get(sessionId);
35
+ if (!entry) {
36
+ return undefined;
37
+ }
38
+
39
+ store.delete(sessionId);
40
+ return entry.count;
41
+ }
42
+
43
+ export function cleanupExpiredPrePromptCounts(
44
+ store: Map<string, PrePromptCountEntry>,
45
+ ttlMs: number,
46
+ now = Date.now(),
47
+ ): number {
48
+ let removed = 0;
49
+
50
+ for (const [sessionId, entry] of store.entries()) {
51
+ if (now - entry.updatedAt < ttlMs) {
52
+ continue;
53
+ }
54
+
55
+ store.delete(sessionId);
56
+ removed += 1;
57
+ }
58
+
59
+ return removed;
60
+ }
61
+
62
+ export function dedupeMemoriesByUri(items: FindResultItem[]): FindResultItem[] {
63
+ const deduped: FindResultItem[] = [];
64
+ const seen = new Set<string>();
65
+
66
+ for (const item of items) {
67
+ if (!item?.uri || seen.has(item.uri)) {
68
+ continue;
69
+ }
70
+
71
+ seen.add(item.uri);
72
+ deduped.push(item);
73
+ }
74
+
75
+ return deduped;
76
+ }
77
+
78
+ export function clampScore(value: number | undefined): number {
79
+ if (typeof value !== "number" || Number.isNaN(value)) {
80
+ return 0;
81
+ }
82
+
83
+ return Math.max(0, Math.min(1, value));
84
+ }
85
+
86
+ export function selectToolMemories(
87
+ items: FindResultItem[],
88
+ options: {
89
+ limit: number;
90
+ scoreThreshold: number;
91
+ },
92
+ ): FindResultItem[] {
93
+ const selected: FindResultItem[] = [];
94
+
95
+ for (const item of [...dedupeMemoriesByUri(items)].sort(
96
+ (a, b) => clampScore(b.score) - clampScore(a.score),
97
+ )) {
98
+ if (clampScore(item.score) < options.scoreThreshold) {
99
+ continue;
100
+ }
101
+
102
+ selected.push(item);
103
+ if (selected.length >= options.limit) {
104
+ break;
105
+ }
106
+ }
107
+
108
+ return selected;
109
+ }
110
+
111
+ export function formatMemoryLines(memories: FindResultItem[]): string {
112
+ return memories
113
+ .map((item) => {
114
+ const summary = normalizeMemorySummary(item);
115
+ const score = Math.round(clampScore(item.score) * 100);
116
+
117
+ if (summary) {
118
+ return `- [${score}%] ${item.uri}: ${summary}`;
119
+ }
120
+
121
+ return `- [${score}%] ${item.uri}`;
122
+ })
123
+ .join("\n");
124
+ }
125
+
126
+ export function estimateTokenCount(text: string): number {
127
+ if (!text) {
128
+ return 0;
129
+ }
130
+
131
+ return Math.ceil(text.length / 4);
132
+ }
133
+
134
+ export async function buildMemoryLinesWithBudget(
135
+ memories: FindResultItem[],
136
+ readFn: (uri: string) => Promise<string>,
137
+ options: BuildMemoryLinesOptions,
138
+ ): Promise<{ lines: string[]; estimatedTokens: number }> {
139
+ let budgetRemaining = options.recallTokenBudget;
140
+ const lines: string[] = [];
141
+ let totalTokens = 0;
142
+
143
+ for (const item of memories) {
144
+ if (budgetRemaining <= 0) {
145
+ break;
146
+ }
147
+
148
+ const content = await resolveMemoryContent(item, readFn, options);
149
+ const line = `- [${item.category ?? "memory"}] ${content}`;
150
+ const lineTokens = estimateTokenCount(line);
151
+ if (lineTokens > budgetRemaining && lines.length > 0) {
152
+ break;
153
+ }
154
+
155
+ lines.push(line);
156
+ totalTokens += lineTokens;
157
+ budgetRemaining -= lineTokens;
158
+ }
159
+
160
+ return { lines, estimatedTokens: totalTokens };
161
+ }
162
+
163
+ function normalizeMemorySummary(item: FindResultItem): string {
164
+ return (item.abstract ?? item.overview ?? "").trim();
165
+ }
166
+
167
+ async function resolveMemoryContent(
168
+ item: FindResultItem,
169
+ readFn: (uri: string) => Promise<string>,
170
+ options: BuildMemoryLinesOptions,
171
+ ): Promise<string> {
172
+ let content: string;
173
+
174
+ if (options.recallPreferAbstract && item.abstract?.trim()) {
175
+ content = item.abstract.trim();
176
+ } else if (item.level === 2) {
177
+ try {
178
+ const fullContent = await readFn(item.uri);
179
+ content =
180
+ fullContent && typeof fullContent === "string" && fullContent.trim()
181
+ ? fullContent.trim()
182
+ : item.abstract?.trim() || item.uri;
183
+ } catch {
184
+ content = item.abstract?.trim() || item.uri;
185
+ }
186
+ } else {
187
+ content = item.abstract?.trim() || item.uri;
188
+ }
189
+
190
+ if (content.length > options.recallMaxContentChars) {
191
+ content = `${content.slice(0, options.recallMaxContentChars)}...`;
192
+ }
193
+
194
+ return content;
195
+ }