@openviking/opencode-plugin 0.2.2 → 0.2.4
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/INSTALL-ZH.md +3 -2
- package/INSTALL.md +4 -2
- package/README.md +6 -2
- package/index.mjs +1 -1
- package/lib/config.mjs +33 -5
- package/lib/memory-recall.mjs +14 -5
- package/lib/memory-session.mjs +45 -38
- package/lib/runtime.mjs +1 -0
- package/lib/shared/async-writer.mjs +78 -0
- package/lib/shared/batch-send.mjs +121 -0
- package/lib/shared/capture-utils.mjs +1 -2
- package/lib/shared/credentials.mjs +21 -0
- package/lib/shared/mcp-proxy-core.mjs +47 -1
- package/lib/shared/pending-queue.mjs +6 -5
- package/lib/shared/plugin-config.mjs +90 -0
- package/lib/shared/recall-compress-core.mjs +195 -0
- package/lib/shared/recall-core.mjs +323 -16
- package/lib/shared/retryable.mjs +9 -0
- package/lib/utils.mjs +1 -0
- package/lib/viking-uri-guard.mjs +1 -1
- package/package.json +1 -1
- package/servers/mcp-proxy.mjs +1 -0
- package/servers/mcp-proxy.test.mjs +2 -0
|
@@ -142,6 +142,7 @@ export function createOpenVikingMcpProxy({
|
|
|
142
142
|
readConfig,
|
|
143
143
|
loggerFactory,
|
|
144
144
|
fetchImpl = globalThis.fetch,
|
|
145
|
+
localToolProvider = null,
|
|
145
146
|
} = {}) {
|
|
146
147
|
if (typeof fetchImpl !== "function") {
|
|
147
148
|
throw new Error("global fetch is required; use Node.js 18 or newer");
|
|
@@ -179,6 +180,43 @@ export function createOpenVikingMcpProxy({
|
|
|
179
180
|
} catch { /* debug logging must never affect protocol IO */ }
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
function localTools() {
|
|
184
|
+
if (!localToolProvider || typeof localToolProvider.listTools !== "function") return [];
|
|
185
|
+
const tools = localToolProvider.listTools();
|
|
186
|
+
return Array.isArray(tools) ? tools : [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function appendLocalTools(message, outbound) {
|
|
190
|
+
if (message.method !== "tools/list" || !Array.isArray(outbound?.result?.tools)) {
|
|
191
|
+
return outbound;
|
|
192
|
+
}
|
|
193
|
+
const additions = localTools().filter((tool) => tool?.name);
|
|
194
|
+
if (additions.length === 0) return outbound;
|
|
195
|
+
const localNames = new Set(additions.map((tool) => tool.name));
|
|
196
|
+
const upstreamTools = outbound.result.tools.filter((tool) => !localNames.has(tool?.name));
|
|
197
|
+
return {
|
|
198
|
+
...outbound,
|
|
199
|
+
result: {
|
|
200
|
+
...outbound.result,
|
|
201
|
+
tools: [...upstreamTools, ...additions],
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function callLocalTool(message) {
|
|
207
|
+
if (
|
|
208
|
+
message.method !== "tools/call"
|
|
209
|
+
|| !localToolProvider
|
|
210
|
+
|| typeof localToolProvider.callTool !== "function"
|
|
211
|
+
) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
const name = message.params?.name;
|
|
215
|
+
if (!localTools().some((tool) => tool?.name === name)) return null;
|
|
216
|
+
reloadIfCredentialFilesChanged("local_tool_call");
|
|
217
|
+
return localToolProvider.callTool(message.params, { config: proxyConfig });
|
|
218
|
+
}
|
|
219
|
+
|
|
182
220
|
function reloadConfig(reason) {
|
|
183
221
|
proxyConfig = readConfig();
|
|
184
222
|
logger = loggerFactory("mcp-proxy", proxyConfig);
|
|
@@ -215,6 +253,7 @@ export function createOpenVikingMcpProxy({
|
|
|
215
253
|
if (proxyConfig.account) headers["X-OpenViking-Account"] = proxyConfig.account;
|
|
216
254
|
if (proxyConfig.user) headers["X-OpenViking-User"] = proxyConfig.user;
|
|
217
255
|
if (proxyConfig.peerId) headers["X-OpenViking-Actor-Peer"] = proxyConfig.peerId;
|
|
256
|
+
if (proxyConfig.userAgent) headers["User-Agent"] = proxyConfig.userAgent;
|
|
218
257
|
return headers;
|
|
219
258
|
}
|
|
220
259
|
|
|
@@ -381,6 +420,13 @@ export function createOpenVikingMcpProxy({
|
|
|
381
420
|
}
|
|
382
421
|
|
|
383
422
|
try {
|
|
423
|
+
const localResult = await callLocalTool(message);
|
|
424
|
+
if (localResult !== null) {
|
|
425
|
+
if (expectsResponse) {
|
|
426
|
+
await writeMessage({ jsonrpc: "2.0", id: message.id, result: localResult });
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
384
430
|
const send = sendWithRetry(message, { expectsResponse });
|
|
385
431
|
if (message.method === "initialize") {
|
|
386
432
|
initializeInFlight = send
|
|
@@ -396,7 +442,7 @@ export function createOpenVikingMcpProxy({
|
|
|
396
442
|
return;
|
|
397
443
|
}
|
|
398
444
|
for (const outbound of result.messages) {
|
|
399
|
-
await writeMessage(outbound);
|
|
445
|
+
await writeMessage(appendLocalTools(message, outbound));
|
|
400
446
|
}
|
|
401
447
|
} catch (err) {
|
|
402
448
|
if (expectsResponse) {
|
|
@@ -24,6 +24,7 @@ import { mkdir, readdir, readFile, rename, writeFile, unlink, stat, chmod } from
|
|
|
24
24
|
import { createHash } from "node:crypto";
|
|
25
25
|
import { join } from "node:path";
|
|
26
26
|
import { homedir } from "node:os";
|
|
27
|
+
import { isRetryableFailure } from "./retryable.mjs";
|
|
27
28
|
|
|
28
29
|
const DEFAULT_MAX_RETRIES = 3;
|
|
29
30
|
const DEFAULT_TTL_DAYS = 7;
|
|
@@ -109,9 +110,7 @@ function pendingFromProcessingFilename(filename) {
|
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
function isRetryableReplayFailure(res) {
|
|
112
|
-
|
|
113
|
-
const status = Number(res.status || 0);
|
|
114
|
-
return !status || status >= 500 || status === 408 || status === 429;
|
|
113
|
+
return isRetryableFailure(res);
|
|
115
114
|
}
|
|
116
115
|
|
|
117
116
|
async function readEntry(dir, filename) {
|
|
@@ -182,10 +181,12 @@ async function recoverStaleProcessing(dir) {
|
|
|
182
181
|
* @param {string} type - "addMessage" or "commitSession"
|
|
183
182
|
* @param {string} sessionId - OV session ID
|
|
184
183
|
* @param {object} payload - the data that failed to send
|
|
184
|
+
* @param {object} options
|
|
185
|
+
* @param {number} options.createdAt - optional queue timestamp override
|
|
185
186
|
*/
|
|
186
|
-
export async function enqueue(type, sessionId, payload) {
|
|
187
|
+
export async function enqueue(type, sessionId, payload, options = {}) {
|
|
187
188
|
const dir = getPendingDir();
|
|
188
|
-
const now = Date.now();
|
|
189
|
+
const now = Number.isFinite(options.createdAt) ? options.createdAt : Date.now();
|
|
189
190
|
const dedupKey = makeDedupKey(type, sessionId, payload);
|
|
190
191
|
const filename = pendingFilename(dedupKey, 0);
|
|
191
192
|
const entry = {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
|
|
2
|
+
/**
|
|
3
|
+
* Per-harness plugin settings from ovcli.conf.
|
|
4
|
+
*
|
|
5
|
+
* ovcli.conf carried connection fields only, so every harness had to keep its
|
|
6
|
+
* tuning knobs in ov.conf's harness section — a server-side file that a
|
|
7
|
+
* client-side plugin has no business editing. The `plugin` section fixes that:
|
|
8
|
+
* shared keys apply to every harness that reads them, and a per-harness object
|
|
9
|
+
* overrides them.
|
|
10
|
+
*
|
|
11
|
+
* {
|
|
12
|
+
* "url": "...", "api_key": "...",
|
|
13
|
+
* "plugin": {
|
|
14
|
+
* "recallQueryExpansion": "off",
|
|
15
|
+
* "recallCompress": "auto"
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* Resolution stays env → ovcli.conf plugin.<harness> → ovcli.conf plugin →
|
|
20
|
+
* ov.conf harness section (legacy) → defaults.
|
|
21
|
+
*
|
|
22
|
+
* Consumers: Claude Code and Codex only. The other harnesses ship this module
|
|
23
|
+
* through `sync.mjs` but still read their knobs from the environment, so a
|
|
24
|
+
* `plugin` entry named after them is inert. `HARNESS_KEYS` lists what a harness
|
|
25
|
+
* loader actually consumes today — add a key here as its loader starts calling
|
|
26
|
+
* `loadPluginSettings`, not before, so the section never promises a knob that
|
|
27
|
+
* silently does nothing.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { readFileSync } from "node:fs";
|
|
31
|
+
import { homedir } from "node:os";
|
|
32
|
+
import { join, resolve as resolvePath } from "node:path";
|
|
33
|
+
|
|
34
|
+
const DEFAULT_OVCLI_CONF_PATH = join(homedir(), ".openviking", "ovcli.conf");
|
|
35
|
+
|
|
36
|
+
export const HARNESS_KEYS = {
|
|
37
|
+
claudeCode: "claude_code",
|
|
38
|
+
codex: "codex",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function tryLoadJson(path) {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Merge the shared plugin section with the harness-specific override.
|
|
51
|
+
* Returns a flat settings object; unknown keys pass through untouched so a
|
|
52
|
+
* harness can add its own knobs without touching this module.
|
|
53
|
+
*/
|
|
54
|
+
export function loadPluginSettings(harness, env = process.env) {
|
|
55
|
+
const path = resolvePath(
|
|
56
|
+
(env.OPENVIKING_CLI_CONFIG_FILE || DEFAULT_OVCLI_CONF_PATH).replace(/^~/, homedir()),
|
|
57
|
+
);
|
|
58
|
+
const file = tryLoadJson(path);
|
|
59
|
+
const plugin = file && typeof file.plugin === "object" && file.plugin ? file.plugin : {};
|
|
60
|
+
|
|
61
|
+
const shared = {};
|
|
62
|
+
for (const [key, value] of Object.entries(plugin)) {
|
|
63
|
+
if (value && typeof value === "object" && !Array.isArray(value)) continue;
|
|
64
|
+
shared[key] = value;
|
|
65
|
+
}
|
|
66
|
+
const scoped = harness && plugin[harness] && typeof plugin[harness] === "object"
|
|
67
|
+
? plugin[harness]
|
|
68
|
+
: {};
|
|
69
|
+
|
|
70
|
+
return { ...shared, ...scoped };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const REWRITE_MODES = new Set(["off", "client", "server", "auto"]);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Normalize the tri-state rewrite knob.
|
|
77
|
+
*
|
|
78
|
+
* off — never compress
|
|
79
|
+
* client — always compress locally through the host CLI
|
|
80
|
+
* server — always ask the server for a digest
|
|
81
|
+
* auto — prefer local (cost lands on the user's own subscription), fall back
|
|
82
|
+
* to the server when no healthy host CLI is available
|
|
83
|
+
*/
|
|
84
|
+
export function normalizeRewriteMode(value, fallback = "off") {
|
|
85
|
+
const raw = String(value ?? "").trim().toLowerCase();
|
|
86
|
+
if (REWRITE_MODES.has(raw)) return raw;
|
|
87
|
+
if (raw === "1" || raw === "true" || raw === "yes") return "auto";
|
|
88
|
+
if (raw === "0" || raw === "false" || raw === "no") return "off";
|
|
89
|
+
return fallback;
|
|
90
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
|
|
6
|
+
export const NO_RELEVANT_MEMORY = "NO_RELEVANT_MEMORY";
|
|
7
|
+
export const DIGEST_HEADER = "OpenViking memory digest:";
|
|
8
|
+
const COMPRESS_OK = "ok";
|
|
9
|
+
const COMPRESS_EMPTY = "empty";
|
|
10
|
+
const COMPRESS_FAILED = "failed";
|
|
11
|
+
|
|
12
|
+
// Medium-constraint prompt: state the goal and two structural floors, but no hard
|
|
13
|
+
// bullet-length contract. Hard per-bullet limits pin the digest to headline
|
|
14
|
+
// density; leaving it unconstrained lets small models rewrite long URIs into dead
|
|
15
|
+
// links, which the URI repair below cleans up.
|
|
16
|
+
export function buildRecallCompressionPrompt({ query, rendered, maxBullets = 6 }) {
|
|
17
|
+
return `You are a memory relevance compressor utility.
|
|
18
|
+
Do not use any tools. Do not investigate. Only transform the given text.
|
|
19
|
+
|
|
20
|
+
User query:
|
|
21
|
+
${query}
|
|
22
|
+
|
|
23
|
+
Retrieved OpenViking context fragments:
|
|
24
|
+
${rendered}
|
|
25
|
+
|
|
26
|
+
Write a memory digest for a coding agent about to answer that query. Keep the
|
|
27
|
+
concrete facts (paths, identifiers, decisions, constraints); drop pleasantries
|
|
28
|
+
and conversational filler.
|
|
29
|
+
|
|
30
|
+
Format rules:
|
|
31
|
+
- Group related facts by topic, one bullet per topic, at most ${maxBullets} bullets.
|
|
32
|
+
- Start every bullet with "- ".
|
|
33
|
+
- End every bullet with its source, copied verbatim from the fragments above:
|
|
34
|
+
"来源:viking://..." or "source: viking://...". Never edit, shorten, or invent a URI.
|
|
35
|
+
- Output the digest body only. No preamble, no closing remark.
|
|
36
|
+
|
|
37
|
+
If nothing above is relevant to the query, output exactly: ${NO_RELEVANT_MEMORY}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function editDistance(a, b) {
|
|
41
|
+
if (a === b) return 0;
|
|
42
|
+
const rows = a.length + 1;
|
|
43
|
+
const cols = b.length + 1;
|
|
44
|
+
let prev = Array.from({ length: cols }, (_, i) => i);
|
|
45
|
+
for (let i = 1; i < rows; i += 1) {
|
|
46
|
+
const cur = [i];
|
|
47
|
+
for (let j = 1; j < cols; j += 1) {
|
|
48
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
49
|
+
cur[j] = Math.min(cur[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
50
|
+
}
|
|
51
|
+
prev = cur;
|
|
52
|
+
}
|
|
53
|
+
return prev[cols - 1];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function nearestUri(candidate, validUris) {
|
|
57
|
+
let best = "";
|
|
58
|
+
let bestDistance = Infinity;
|
|
59
|
+
for (const uri of validUris) {
|
|
60
|
+
const distance = editDistance(candidate, uri);
|
|
61
|
+
if (distance < bestDistance) {
|
|
62
|
+
best = uri;
|
|
63
|
+
bestDistance = distance;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Only repair near-misses; an unrelated hallucination is dropped instead.
|
|
67
|
+
const tolerance = Math.max(4, Math.floor(candidate.length * 0.25));
|
|
68
|
+
return bestDistance <= tolerance ? best : "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Small models occasionally mangle long URIs. Snap every cited URI back onto the
|
|
73
|
+
* set the server actually returned, and drop bullets whose citation cannot be
|
|
74
|
+
* recovered so the digest never carries a dead link.
|
|
75
|
+
*/
|
|
76
|
+
export function repairDigestUris(digest, validUris = []) {
|
|
77
|
+
const text = String(digest || "");
|
|
78
|
+
if (!text) return "";
|
|
79
|
+
const valid = validUris.map((uri) => String(uri || "").trim()).filter(Boolean);
|
|
80
|
+
if (!valid.length) return text;
|
|
81
|
+
const validSet = new Set(valid);
|
|
82
|
+
|
|
83
|
+
const lines = [];
|
|
84
|
+
for (const line of text.split(/\r?\n/)) {
|
|
85
|
+
if (!line.trim().startsWith("- ")) {
|
|
86
|
+
lines.push(line);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
let dropped = false;
|
|
90
|
+
const repaired = line.replace(/viking:\/\/[^\s<>"')\]]+/g, (uri) => {
|
|
91
|
+
if (validSet.has(uri)) return uri;
|
|
92
|
+
const nearest = nearestUri(uri, valid);
|
|
93
|
+
if (nearest) return nearest;
|
|
94
|
+
dropped = true;
|
|
95
|
+
return uri;
|
|
96
|
+
});
|
|
97
|
+
if (!dropped) lines.push(repaired);
|
|
98
|
+
}
|
|
99
|
+
return lines.join("\n").trim();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function normalizeCompressedContext(raw, maxChars = 4000, maxBullets = 6) {
|
|
103
|
+
const text = String(raw || "").trim();
|
|
104
|
+
if (!text) return null;
|
|
105
|
+
if (text.toUpperCase() === NO_RELEVANT_MEMORY) return "";
|
|
106
|
+
const bullets = text.split(/\r?\n/)
|
|
107
|
+
.map((line) => line.trim())
|
|
108
|
+
.filter((line) => /^[-*]\s+/.test(line) && line.includes("viking://"))
|
|
109
|
+
.slice(0, Math.max(1, maxBullets))
|
|
110
|
+
.map((line) => `- ${line.replace(/^[-*]\s+/, "").slice(0, 500).trim()}`);
|
|
111
|
+
if (!bullets.length) return null;
|
|
112
|
+
return (`${DIGEST_HEADER}\n${bullets.join("\n")}`).slice(0, Math.max(100, maxChars));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function recallDigestCacheKey({
|
|
116
|
+
query = "",
|
|
117
|
+
rendered = "",
|
|
118
|
+
entries = [],
|
|
119
|
+
maxInputChars = 18000,
|
|
120
|
+
maxBullets = 6,
|
|
121
|
+
} = {}) {
|
|
122
|
+
const uris = entries.map((entry) => String(entry?.uri || "").trim()).filter(Boolean).sort();
|
|
123
|
+
const source = JSON.stringify({
|
|
124
|
+
version: 2,
|
|
125
|
+
query: String(query),
|
|
126
|
+
rendered: String(rendered).slice(0, maxInputChars),
|
|
127
|
+
uris,
|
|
128
|
+
maxInputChars,
|
|
129
|
+
maxBullets,
|
|
130
|
+
});
|
|
131
|
+
return createHash("sha256").update(source).digest("hex");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function readCache(path) {
|
|
135
|
+
if (!path) return null;
|
|
136
|
+
try { return JSON.parse(await readFile(path, "utf8")); } catch { return null; }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function writeCache(path, value) {
|
|
140
|
+
if (!path) return;
|
|
141
|
+
try {
|
|
142
|
+
await mkdir(dirname(path), { recursive: true });
|
|
143
|
+
const tmp = `${path}.tmp`;
|
|
144
|
+
await writeFile(tmp, JSON.stringify(value));
|
|
145
|
+
await rename(tmp, path);
|
|
146
|
+
} catch { /* best effort */ }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function compressRecallContext({
|
|
150
|
+
query,
|
|
151
|
+
rendered,
|
|
152
|
+
entries = [],
|
|
153
|
+
cfg = {},
|
|
154
|
+
runCompressor,
|
|
155
|
+
cachePath = "",
|
|
156
|
+
now = 0,
|
|
157
|
+
}) {
|
|
158
|
+
const input = String(rendered || "").trim();
|
|
159
|
+
if (!input) return { status: COMPRESS_EMPTY, context: "" };
|
|
160
|
+
const minChars = Math.max(0, Number(cfg.recallCompressMinInputChars ?? 1500));
|
|
161
|
+
if (input.length < minChars) return { status: COMPRESS_OK, context: input };
|
|
162
|
+
|
|
163
|
+
const maxInputChars = Math.max(1000, Number(cfg.recallCompressMaxInputChars || 18000));
|
|
164
|
+
const maxBullets = Math.max(1, Number(cfg.recallCompressMaxBullets || 6));
|
|
165
|
+
const key = recallDigestCacheKey({
|
|
166
|
+
query,
|
|
167
|
+
rendered: input,
|
|
168
|
+
entries,
|
|
169
|
+
maxInputChars,
|
|
170
|
+
maxBullets,
|
|
171
|
+
});
|
|
172
|
+
const cached = await readCache(cachePath);
|
|
173
|
+
if (cached?.key === key && typeof cached.digest === "string") {
|
|
174
|
+
return { status: COMPRESS_OK, context: cached.digest };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const prompt = buildRecallCompressionPrompt({
|
|
178
|
+
query,
|
|
179
|
+
rendered: input.slice(0, maxInputChars),
|
|
180
|
+
maxBullets,
|
|
181
|
+
});
|
|
182
|
+
const raw = await runCompressor(prompt);
|
|
183
|
+
const normalized = normalizeCompressedContext(raw, 4000, maxBullets);
|
|
184
|
+
if (normalized === null) return { status: COMPRESS_FAILED, context: "" };
|
|
185
|
+
if (!normalized) return { status: COMPRESS_EMPTY, context: "" };
|
|
186
|
+
|
|
187
|
+
const validUris = entries.map((entry) => entry?.uri).filter(Boolean);
|
|
188
|
+
const digest = repairDigestUris(normalized, validUris.length
|
|
189
|
+
? validUris
|
|
190
|
+
: (input.match(/viking:\/\/[^\s<>"']+/g) || []));
|
|
191
|
+
if (!digest) return { status: COMPRESS_FAILED, context: "" };
|
|
192
|
+
|
|
193
|
+
await writeCache(cachePath, { key, digest, updatedAt: now || 0 });
|
|
194
|
+
return { status: COMPRESS_OK, context: digest };
|
|
195
|
+
}
|