@lotargo/memory_plugin 1.1.5 → 1.1.6
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 +243 -234
- package/mcp-server/admin/server.js +228 -228
- package/mcp-server/admin/snapshot.js +303 -303
- package/mcp-server/benchmarks/fetch_real_corpus.js +351 -351
- package/mcp-server/benchmarks/gpu_profile_benchmark.js +170 -0
- package/mcp-server/benchmarks/stress_ingestion.js +195 -193
- package/mcp-server/benchmarks/test_dual_layer.js +140 -140
- package/mcp-server/cli.js +293 -7
- package/mcp-server/config/config_manager.js +11 -7
- package/mcp-server/db/database.js +43 -43
- package/mcp-server/graph/graph_extractor.js +72 -72
- package/mcp-server/graph/knowledge_linker.js +102 -102
- package/mcp-server/index.js +454 -454
- package/mcp-server/ingest/chunker.js +337 -337
- package/mcp-server/ingest/exporter.js +80 -80
- package/mcp-server/ingest/normalizer.js +104 -104
- package/mcp-server/ingest/pipeline.js +22 -7
- package/mcp-server/ingest/sentence_segmenter.js +74 -74
- package/mcp-server/memory.js +72 -72
- package/mcp-server/ml/gpu_monitor.js +166 -0
- package/mcp-server/ml/model_manager.js +321 -17
- package/mcp-server/preinstall.js +44 -44
- package/mcp-server/retrieval/retriever.js +10 -5
- package/mcp-server/setup.js +148 -148
- package/mcp-server/storage/blob_store.js +62 -62
- package/opencode-plugin/index.js +244 -244
- package/package.json +58 -54
- package/skills/using-memory/SKILL.md +122 -122
package/mcp-server/setup.js
CHANGED
|
@@ -1,148 +1,148 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
2
|
-
import { existsSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { homedir } from "os";
|
|
5
|
-
|
|
6
|
-
export async function runSetup() {
|
|
7
|
-
const args = process.argv.slice(2);
|
|
8
|
-
const hasSpecificFlag = args.some((a) =>
|
|
9
|
-
["--opencode", "--claude", "--codex", "--antigravity", "--gemini"].includes(a.toLowerCase())
|
|
10
|
-
);
|
|
11
|
-
|
|
12
|
-
const doOpenCode = !hasSpecificFlag || args.includes("--opencode");
|
|
13
|
-
const doClaude = !hasSpecificFlag || args.includes("--claude");
|
|
14
|
-
const doAntigravity = !hasSpecificFlag || args.includes("--antigravity") || args.includes("--gemini");
|
|
15
|
-
const doCodex = !hasSpecificFlag || args.includes("--codex");
|
|
16
|
-
|
|
17
|
-
console.log("\nSetting up @lotargo/memory_plugin...\n");
|
|
18
|
-
const home = homedir();
|
|
19
|
-
let configuredCount = 0;
|
|
20
|
-
|
|
21
|
-
// 1. OpenCode (~/.config/opencode/opencode.json)
|
|
22
|
-
if (doOpenCode) {
|
|
23
|
-
try {
|
|
24
|
-
const opencodeDir = process.env.OPENCODE_CONFIG_DIR || join(home, ".config", "opencode");
|
|
25
|
-
const opencodeConfigPath = join(opencodeDir, "opencode.json");
|
|
26
|
-
await mkdir(opencodeDir, { recursive: true });
|
|
27
|
-
|
|
28
|
-
let config = {};
|
|
29
|
-
if (existsSync(opencodeConfigPath)) {
|
|
30
|
-
try {
|
|
31
|
-
config = JSON.parse(await readFile(opencodeConfigPath, "utf-8"));
|
|
32
|
-
} catch (e) {}
|
|
33
|
-
}
|
|
34
|
-
if (!Array.isArray(config.plugin)) config.plugin = [];
|
|
35
|
-
// Clean up legacy / incorrect plugin entry names
|
|
36
|
-
const obsoleteNames = ["opencode-memory-plugin", "memory_plugin", "memory-plugin"];
|
|
37
|
-
config.plugin = config.plugin.filter((p) => !obsoleteNames.includes(p));
|
|
38
|
-
if (!config.plugin.includes("@lotargo/memory_plugin")) {
|
|
39
|
-
config.plugin.push("@lotargo/memory_plugin");
|
|
40
|
-
}
|
|
41
|
-
// Clean up legacy mcp-helper.js standalone file plugin if present
|
|
42
|
-
const legacyPluginFile = join(opencodeDir, "plugins", "mcp-helper.js");
|
|
43
|
-
if (existsSync(legacyPluginFile)) {
|
|
44
|
-
try { const { unlink } = await import("fs/promises"); await unlink(legacyPluginFile); } catch (e) {}
|
|
45
|
-
}
|
|
46
|
-
await writeFile(opencodeConfigPath, JSON.stringify(config, null, 2));
|
|
47
|
-
console.log(" [OK] OpenCode: configured plugin in ~/.config/opencode/opencode.json");
|
|
48
|
-
configuredCount++;
|
|
49
|
-
} catch (err) {
|
|
50
|
-
console.log(" [SKIP] OpenCode setup skipped:", err.message);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// 2. Claude Code (~/.claude.json)
|
|
55
|
-
if (doClaude) {
|
|
56
|
-
try {
|
|
57
|
-
const claudePath = join(home, ".claude.json");
|
|
58
|
-
let config = {};
|
|
59
|
-
if (existsSync(claudePath)) {
|
|
60
|
-
try {
|
|
61
|
-
config = JSON.parse(await readFile(claudePath, "utf-8"));
|
|
62
|
-
} catch (e) {}
|
|
63
|
-
}
|
|
64
|
-
if (!config.mcpServers) config.mcpServers = {};
|
|
65
|
-
config.mcpServers["memory-agent"] = {
|
|
66
|
-
command: "npx",
|
|
67
|
-
args: ["-y", "@lotargo/memory_plugin"],
|
|
68
|
-
};
|
|
69
|
-
await writeFile(claudePath, JSON.stringify(config, null, 2));
|
|
70
|
-
console.log(" [OK] Claude Code: configured MCP server in ~/.claude.json");
|
|
71
|
-
configuredCount++;
|
|
72
|
-
} catch (err) {
|
|
73
|
-
console.log(" [SKIP] Claude Code setup skipped:", err.message);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// 3. Antigravity / Gemini CLI (~/.gemini/config/mcp_config.json & .agents/mcp_config.json)
|
|
78
|
-
if (doAntigravity) {
|
|
79
|
-
try {
|
|
80
|
-
// Global Antigravity config
|
|
81
|
-
const geminiConfigDir = join(home, ".gemini", "config");
|
|
82
|
-
await mkdir(geminiConfigDir, { recursive: true });
|
|
83
|
-
const geminiConfigFile = join(geminiConfigDir, "mcp_config.json");
|
|
84
|
-
|
|
85
|
-
let config = {};
|
|
86
|
-
if (existsSync(geminiConfigFile)) {
|
|
87
|
-
try {
|
|
88
|
-
config = JSON.parse(await readFile(geminiConfigFile, "utf-8"));
|
|
89
|
-
} catch (e) {}
|
|
90
|
-
}
|
|
91
|
-
if (!config.mcpServers) config.mcpServers = {};
|
|
92
|
-
config.mcpServers["memory-agent"] = {
|
|
93
|
-
command: "npx",
|
|
94
|
-
args: ["-y", "@lotargo/memory_plugin"],
|
|
95
|
-
};
|
|
96
|
-
await writeFile(geminiConfigFile, JSON.stringify(config, null, 2));
|
|
97
|
-
|
|
98
|
-
// Local workspace config (.agents/mcp_config.json) if in project
|
|
99
|
-
const cwd = process.cwd();
|
|
100
|
-
if (existsSync(join(cwd, ".agents")) || existsSync(join(cwd, "package.json")) || existsSync(join(cwd, ".git"))) {
|
|
101
|
-
const localAgentsDir = join(cwd, ".agents");
|
|
102
|
-
await mkdir(localAgentsDir, { recursive: true });
|
|
103
|
-
const localMcpFile = join(localAgentsDir, "mcp_config.json");
|
|
104
|
-
let localConfig = {};
|
|
105
|
-
if (existsSync(localMcpFile)) {
|
|
106
|
-
try {
|
|
107
|
-
localConfig = JSON.parse(await readFile(localMcpFile, "utf-8"));
|
|
108
|
-
} catch (e) {}
|
|
109
|
-
}
|
|
110
|
-
if (!localConfig.mcpServers) localConfig.mcpServers = {};
|
|
111
|
-
localConfig.mcpServers["memory-agent"] = {
|
|
112
|
-
command: "npx",
|
|
113
|
-
args: ["-y", "@lotargo/memory_plugin"],
|
|
114
|
-
};
|
|
115
|
-
await writeFile(localMcpFile, JSON.stringify(localConfig, null, 2));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
console.log(" [OK] Antigravity: configured MCP server in ~/.gemini/config/mcp_config.json and .agents/mcp_config.json");
|
|
119
|
-
configuredCount++;
|
|
120
|
-
} catch (err) {
|
|
121
|
-
console.log(" [SKIP] Antigravity setup skipped:", err.message);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// 4. Codex (~/.codex/config.toml)
|
|
126
|
-
if (doCodex) {
|
|
127
|
-
try {
|
|
128
|
-
const codexDir = join(home, ".codex");
|
|
129
|
-
const codexConfig = join(codexDir, "config.toml");
|
|
130
|
-
if (existsSync(codexDir)) {
|
|
131
|
-
let content = existsSync(codexConfig) ? await readFile(codexConfig, "utf-8") : "";
|
|
132
|
-
if (!content.includes("memory-agent")) {
|
|
133
|
-
const tomlSnippet = `\n[mcp_servers.memory-agent]\ncommand = "npx"\nargs = ["-y", "@lotargo/memory_plugin"]\n`;
|
|
134
|
-
content += tomlSnippet;
|
|
135
|
-
await writeFile(codexConfig, content);
|
|
136
|
-
console.log(" [OK] Codex: added mcp_servers.memory-agent to ~/.codex/config.toml");
|
|
137
|
-
configuredCount++;
|
|
138
|
-
} else {
|
|
139
|
-
console.log(" [INFO] Codex: already configured");
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
} catch (err) {
|
|
143
|
-
console.log(" [SKIP] Codex setup skipped:", err.message);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
console.log(`\nSetup complete. Configured ${configuredCount} environment(s).\n`);
|
|
148
|
-
}
|
|
1
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
|
|
6
|
+
export async function runSetup() {
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const hasSpecificFlag = args.some((a) =>
|
|
9
|
+
["--opencode", "--claude", "--codex", "--antigravity", "--gemini"].includes(a.toLowerCase())
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const doOpenCode = !hasSpecificFlag || args.includes("--opencode");
|
|
13
|
+
const doClaude = !hasSpecificFlag || args.includes("--claude");
|
|
14
|
+
const doAntigravity = !hasSpecificFlag || args.includes("--antigravity") || args.includes("--gemini");
|
|
15
|
+
const doCodex = !hasSpecificFlag || args.includes("--codex");
|
|
16
|
+
|
|
17
|
+
console.log("\nSetting up @lotargo/memory_plugin...\n");
|
|
18
|
+
const home = homedir();
|
|
19
|
+
let configuredCount = 0;
|
|
20
|
+
|
|
21
|
+
// 1. OpenCode (~/.config/opencode/opencode.json)
|
|
22
|
+
if (doOpenCode) {
|
|
23
|
+
try {
|
|
24
|
+
const opencodeDir = process.env.OPENCODE_CONFIG_DIR || join(home, ".config", "opencode");
|
|
25
|
+
const opencodeConfigPath = join(opencodeDir, "opencode.json");
|
|
26
|
+
await mkdir(opencodeDir, { recursive: true });
|
|
27
|
+
|
|
28
|
+
let config = {};
|
|
29
|
+
if (existsSync(opencodeConfigPath)) {
|
|
30
|
+
try {
|
|
31
|
+
config = JSON.parse(await readFile(opencodeConfigPath, "utf-8"));
|
|
32
|
+
} catch (e) {}
|
|
33
|
+
}
|
|
34
|
+
if (!Array.isArray(config.plugin)) config.plugin = [];
|
|
35
|
+
// Clean up legacy / incorrect plugin entry names
|
|
36
|
+
const obsoleteNames = ["opencode-memory-plugin", "memory_plugin", "memory-plugin"];
|
|
37
|
+
config.plugin = config.plugin.filter((p) => !obsoleteNames.includes(p));
|
|
38
|
+
if (!config.plugin.includes("@lotargo/memory_plugin")) {
|
|
39
|
+
config.plugin.push("@lotargo/memory_plugin");
|
|
40
|
+
}
|
|
41
|
+
// Clean up legacy mcp-helper.js standalone file plugin if present
|
|
42
|
+
const legacyPluginFile = join(opencodeDir, "plugins", "mcp-helper.js");
|
|
43
|
+
if (existsSync(legacyPluginFile)) {
|
|
44
|
+
try { const { unlink } = await import("fs/promises"); await unlink(legacyPluginFile); } catch (e) {}
|
|
45
|
+
}
|
|
46
|
+
await writeFile(opencodeConfigPath, JSON.stringify(config, null, 2));
|
|
47
|
+
console.log(" [OK] OpenCode: configured plugin in ~/.config/opencode/opencode.json");
|
|
48
|
+
configuredCount++;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.log(" [SKIP] OpenCode setup skipped:", err.message);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 2. Claude Code (~/.claude.json)
|
|
55
|
+
if (doClaude) {
|
|
56
|
+
try {
|
|
57
|
+
const claudePath = join(home, ".claude.json");
|
|
58
|
+
let config = {};
|
|
59
|
+
if (existsSync(claudePath)) {
|
|
60
|
+
try {
|
|
61
|
+
config = JSON.parse(await readFile(claudePath, "utf-8"));
|
|
62
|
+
} catch (e) {}
|
|
63
|
+
}
|
|
64
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
65
|
+
config.mcpServers["memory-agent"] = {
|
|
66
|
+
command: "npx",
|
|
67
|
+
args: ["-y", "@lotargo/memory_plugin"],
|
|
68
|
+
};
|
|
69
|
+
await writeFile(claudePath, JSON.stringify(config, null, 2));
|
|
70
|
+
console.log(" [OK] Claude Code: configured MCP server in ~/.claude.json");
|
|
71
|
+
configuredCount++;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.log(" [SKIP] Claude Code setup skipped:", err.message);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 3. Antigravity / Gemini CLI (~/.gemini/config/mcp_config.json & .agents/mcp_config.json)
|
|
78
|
+
if (doAntigravity) {
|
|
79
|
+
try {
|
|
80
|
+
// Global Antigravity config
|
|
81
|
+
const geminiConfigDir = join(home, ".gemini", "config");
|
|
82
|
+
await mkdir(geminiConfigDir, { recursive: true });
|
|
83
|
+
const geminiConfigFile = join(geminiConfigDir, "mcp_config.json");
|
|
84
|
+
|
|
85
|
+
let config = {};
|
|
86
|
+
if (existsSync(geminiConfigFile)) {
|
|
87
|
+
try {
|
|
88
|
+
config = JSON.parse(await readFile(geminiConfigFile, "utf-8"));
|
|
89
|
+
} catch (e) {}
|
|
90
|
+
}
|
|
91
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
92
|
+
config.mcpServers["memory-agent"] = {
|
|
93
|
+
command: "npx",
|
|
94
|
+
args: ["-y", "@lotargo/memory_plugin"],
|
|
95
|
+
};
|
|
96
|
+
await writeFile(geminiConfigFile, JSON.stringify(config, null, 2));
|
|
97
|
+
|
|
98
|
+
// Local workspace config (.agents/mcp_config.json) if in project
|
|
99
|
+
const cwd = process.cwd();
|
|
100
|
+
if (existsSync(join(cwd, ".agents")) || existsSync(join(cwd, "package.json")) || existsSync(join(cwd, ".git"))) {
|
|
101
|
+
const localAgentsDir = join(cwd, ".agents");
|
|
102
|
+
await mkdir(localAgentsDir, { recursive: true });
|
|
103
|
+
const localMcpFile = join(localAgentsDir, "mcp_config.json");
|
|
104
|
+
let localConfig = {};
|
|
105
|
+
if (existsSync(localMcpFile)) {
|
|
106
|
+
try {
|
|
107
|
+
localConfig = JSON.parse(await readFile(localMcpFile, "utf-8"));
|
|
108
|
+
} catch (e) {}
|
|
109
|
+
}
|
|
110
|
+
if (!localConfig.mcpServers) localConfig.mcpServers = {};
|
|
111
|
+
localConfig.mcpServers["memory-agent"] = {
|
|
112
|
+
command: "npx",
|
|
113
|
+
args: ["-y", "@lotargo/memory_plugin"],
|
|
114
|
+
};
|
|
115
|
+
await writeFile(localMcpFile, JSON.stringify(localConfig, null, 2));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(" [OK] Antigravity: configured MCP server in ~/.gemini/config/mcp_config.json and .agents/mcp_config.json");
|
|
119
|
+
configuredCount++;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.log(" [SKIP] Antigravity setup skipped:", err.message);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 4. Codex (~/.codex/config.toml)
|
|
126
|
+
if (doCodex) {
|
|
127
|
+
try {
|
|
128
|
+
const codexDir = join(home, ".codex");
|
|
129
|
+
const codexConfig = join(codexDir, "config.toml");
|
|
130
|
+
if (existsSync(codexDir)) {
|
|
131
|
+
let content = existsSync(codexConfig) ? await readFile(codexConfig, "utf-8") : "";
|
|
132
|
+
if (!content.includes("memory-agent")) {
|
|
133
|
+
const tomlSnippet = `\n[mcp_servers.memory-agent]\ncommand = "npx"\nargs = ["-y", "@lotargo/memory_plugin"]\n`;
|
|
134
|
+
content += tomlSnippet;
|
|
135
|
+
await writeFile(codexConfig, content);
|
|
136
|
+
console.log(" [OK] Codex: added mcp_servers.memory-agent to ~/.codex/config.toml");
|
|
137
|
+
configuredCount++;
|
|
138
|
+
} else {
|
|
139
|
+
console.log(" [INFO] Codex: already configured");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.log(" [SKIP] Codex setup skipped:", err.message);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`\nSetup complete. Configured ${configuredCount} environment(s).\n`);
|
|
148
|
+
}
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { gzipSync, gunzipSync } from "node:zlib";
|
|
3
|
-
import { readFile, writeFile, mkdir, unlink, access } from "node:fs/promises";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { BLOBS_DIR } from "../db/database.js";
|
|
7
|
-
|
|
8
|
-
export function hashContent(data) {
|
|
9
|
-
const hash = createHash("sha256");
|
|
10
|
-
hash.update(data);
|
|
11
|
-
return hash.digest("hex");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function getBlobPath(hash, baseDir = BLOBS_DIR) {
|
|
15
|
-
const prefix = hash.substring(0, 2);
|
|
16
|
-
return join(baseDir, prefix, `${hash}.raw.gz`);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function blobExists(hash, baseDir = BLOBS_DIR) {
|
|
20
|
-
const blobPath = getBlobPath(hash, baseDir);
|
|
21
|
-
return existsSync(blobPath);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function saveBlob(content, baseDir = BLOBS_DIR) {
|
|
25
|
-
const buffer = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
|
|
26
|
-
const hash = hashContent(buffer);
|
|
27
|
-
const blobPath = getBlobPath(hash, baseDir);
|
|
28
|
-
|
|
29
|
-
if (existsSync(blobPath)) {
|
|
30
|
-
return { hash, size: buffer.length, path: blobPath, deduplicated: true };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const parentDir = join(blobPath, "..");
|
|
34
|
-
if (!existsSync(parentDir)) {
|
|
35
|
-
await mkdir(parentDir, { recursive: true });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const compressed = gzipSync(buffer);
|
|
39
|
-
await writeFile(blobPath, compressed);
|
|
40
|
-
|
|
41
|
-
return { hash, size: buffer.length, path: blobPath, deduplicated: false };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function readBlob(hash, baseDir = BLOBS_DIR) {
|
|
45
|
-
const blobPath = getBlobPath(hash, baseDir);
|
|
46
|
-
if (!existsSync(blobPath)) {
|
|
47
|
-
throw new Error(`Blob not found for hash: ${hash}`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const compressed = await readFile(blobPath);
|
|
51
|
-
const decompressed = gunzipSync(compressed);
|
|
52
|
-
return decompressed.toString("utf-8");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function deleteBlob(hash, baseDir = BLOBS_DIR) {
|
|
56
|
-
const blobPath = getBlobPath(hash, baseDir);
|
|
57
|
-
if (existsSync(blobPath)) {
|
|
58
|
-
await unlink(blobPath);
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { gzipSync, gunzipSync } from "node:zlib";
|
|
3
|
+
import { readFile, writeFile, mkdir, unlink, access } from "node:fs/promises";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { BLOBS_DIR } from "../db/database.js";
|
|
7
|
+
|
|
8
|
+
export function hashContent(data) {
|
|
9
|
+
const hash = createHash("sha256");
|
|
10
|
+
hash.update(data);
|
|
11
|
+
return hash.digest("hex");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getBlobPath(hash, baseDir = BLOBS_DIR) {
|
|
15
|
+
const prefix = hash.substring(0, 2);
|
|
16
|
+
return join(baseDir, prefix, `${hash}.raw.gz`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function blobExists(hash, baseDir = BLOBS_DIR) {
|
|
20
|
+
const blobPath = getBlobPath(hash, baseDir);
|
|
21
|
+
return existsSync(blobPath);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function saveBlob(content, baseDir = BLOBS_DIR) {
|
|
25
|
+
const buffer = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
|
|
26
|
+
const hash = hashContent(buffer);
|
|
27
|
+
const blobPath = getBlobPath(hash, baseDir);
|
|
28
|
+
|
|
29
|
+
if (existsSync(blobPath)) {
|
|
30
|
+
return { hash, size: buffer.length, path: blobPath, deduplicated: true };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const parentDir = join(blobPath, "..");
|
|
34
|
+
if (!existsSync(parentDir)) {
|
|
35
|
+
await mkdir(parentDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const compressed = gzipSync(buffer);
|
|
39
|
+
await writeFile(blobPath, compressed);
|
|
40
|
+
|
|
41
|
+
return { hash, size: buffer.length, path: blobPath, deduplicated: false };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function readBlob(hash, baseDir = BLOBS_DIR) {
|
|
45
|
+
const blobPath = getBlobPath(hash, baseDir);
|
|
46
|
+
if (!existsSync(blobPath)) {
|
|
47
|
+
throw new Error(`Blob not found for hash: ${hash}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const compressed = await readFile(blobPath);
|
|
51
|
+
const decompressed = gunzipSync(compressed);
|
|
52
|
+
return decompressed.toString("utf-8");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function deleteBlob(hash, baseDir = BLOBS_DIR) {
|
|
56
|
+
const blobPath = getBlobPath(hash, baseDir);
|
|
57
|
+
if (existsSync(blobPath)) {
|
|
58
|
+
await unlink(blobPath);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|