@hbarefoot/engram 1.0.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.
- package/LICENSE +21 -0
- package/README.md +535 -0
- package/bin/engram.js +421 -0
- package/dashboard/dist/assets/index-BHkLa5w_.css +1 -0
- package/dashboard/dist/assets/index-D9QR_Cnu.js +45 -0
- package/dashboard/dist/index.html +14 -0
- package/dashboard/package.json +21 -0
- package/package.json +76 -0
- package/src/config/index.js +150 -0
- package/src/embed/index.js +249 -0
- package/src/export/static.js +396 -0
- package/src/extract/rules.js +233 -0
- package/src/extract/secrets.js +114 -0
- package/src/index.js +54 -0
- package/src/memory/consolidate.js +420 -0
- package/src/memory/context.js +346 -0
- package/src/memory/feedback.js +197 -0
- package/src/memory/recall.js +350 -0
- package/src/memory/store.js +626 -0
- package/src/server/mcp.js +668 -0
- package/src/server/rest.js +499 -0
- package/src/utils/id.js +9 -0
- package/src/utils/logger.js +79 -0
- package/src/utils/time.js +296 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Engram Dashboard</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-D9QR_Cnu.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BHkLa5w_.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="root"></div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "engram-dashboard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"build": "vite build",
|
|
8
|
+
"preview": "vite preview"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"react": "^18.3.1",
|
|
12
|
+
"react-dom": "^18.3.1"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
16
|
+
"autoprefixer": "^10.4.20",
|
|
17
|
+
"postcss": "^8.4.49",
|
|
18
|
+
"tailwindcss": "^3.4.17",
|
|
19
|
+
"vite": "^6.0.5"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hbarefoot/engram",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Persistent memory for AI agents. SQLite for agent state.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"engram": "bin/engram.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src",
|
|
13
|
+
"dashboard/dist",
|
|
14
|
+
"dashboard/package.json",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node bin/engram.js start",
|
|
20
|
+
"mcp": "node bin/engram.js start --mcp-only",
|
|
21
|
+
"dev": "concurrently \"node bin/engram.js start\" \"cd dashboard && npm run dev\"",
|
|
22
|
+
"build": "cd dashboard && npm run build",
|
|
23
|
+
"prepublishOnly": "npm run build",
|
|
24
|
+
"test": "vitest",
|
|
25
|
+
"test:run": "vitest run",
|
|
26
|
+
"lint": "eslint src/",
|
|
27
|
+
"pm2:start": "pm2 start ecosystem.config.cjs",
|
|
28
|
+
"pm2:stop": "pm2 stop engram",
|
|
29
|
+
"pm2:restart": "pm2 restart engram",
|
|
30
|
+
"pm2:delete": "pm2 delete engram",
|
|
31
|
+
"pm2:logs": "pm2 logs engram",
|
|
32
|
+
"pm2:status": "pm2 status engram",
|
|
33
|
+
"pm2:monit": "pm2 monit"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"ai",
|
|
37
|
+
"agent",
|
|
38
|
+
"memory",
|
|
39
|
+
"mcp",
|
|
40
|
+
"claude",
|
|
41
|
+
"llm",
|
|
42
|
+
"persistent",
|
|
43
|
+
"knowledge",
|
|
44
|
+
"sqlite",
|
|
45
|
+
"embedding"
|
|
46
|
+
],
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"author": "Engram Contributors",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "https://github.com/HBarefoot/engram.git"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/HBarefoot/engram/issues"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/HBarefoot/engram#readme",
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=20.0.0"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
62
|
+
"better-sqlite3": "^11.0.0",
|
|
63
|
+
"fastify": "^5.0.0",
|
|
64
|
+
"@fastify/static": "^8.0.0",
|
|
65
|
+
"@fastify/multipart": "^9.0.0",
|
|
66
|
+
"@fastify/cors": "^10.0.0",
|
|
67
|
+
"commander": "^12.0.0",
|
|
68
|
+
"@xenova/transformers": "^2.17.0",
|
|
69
|
+
"yaml": "^2.4.0"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"vitest": "^2.0.0",
|
|
73
|
+
"eslint": "^9.0.0",
|
|
74
|
+
"concurrently": "^9.0.0"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default configuration for Engram
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
port: 3838,
|
|
10
|
+
dataDir: path.join(os.homedir(), '.engram'),
|
|
11
|
+
defaults: {
|
|
12
|
+
namespace: 'default',
|
|
13
|
+
recallLimit: 5,
|
|
14
|
+
confidenceThreshold: 0.3,
|
|
15
|
+
tokenBudget: 500,
|
|
16
|
+
maxRecallResults: 20
|
|
17
|
+
},
|
|
18
|
+
embedding: {
|
|
19
|
+
provider: 'local',
|
|
20
|
+
model: 'Xenova/all-MiniLM-L6-v2',
|
|
21
|
+
endpoint: null
|
|
22
|
+
},
|
|
23
|
+
llm: {
|
|
24
|
+
provider: null,
|
|
25
|
+
endpoint: null,
|
|
26
|
+
model: null,
|
|
27
|
+
apiKey: null
|
|
28
|
+
},
|
|
29
|
+
consolidation: {
|
|
30
|
+
enabled: true,
|
|
31
|
+
intervalHours: 24,
|
|
32
|
+
duplicateThreshold: 0.92,
|
|
33
|
+
decayEnabled: true
|
|
34
|
+
},
|
|
35
|
+
security: {
|
|
36
|
+
secretDetection: true,
|
|
37
|
+
auditLog: false
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Deep merge two objects
|
|
43
|
+
* @param {Object} target - Target object
|
|
44
|
+
* @param {Object} source - Source object
|
|
45
|
+
* @returns {Object} Merged object
|
|
46
|
+
*/
|
|
47
|
+
function deepMerge(target, source) {
|
|
48
|
+
const result = { ...target };
|
|
49
|
+
|
|
50
|
+
for (const key in source) {
|
|
51
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
52
|
+
result[key] = deepMerge(target[key] || {}, source[key]);
|
|
53
|
+
} else {
|
|
54
|
+
result[key] = source[key];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Ensure the data directory exists
|
|
63
|
+
* @param {string} dataDir - Path to data directory
|
|
64
|
+
*/
|
|
65
|
+
function ensureDataDir(dataDir) {
|
|
66
|
+
if (!fs.existsSync(dataDir)) {
|
|
67
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Create subdirectories
|
|
71
|
+
const modelsDir = path.join(dataDir, 'models');
|
|
72
|
+
if (!fs.existsSync(modelsDir)) {
|
|
73
|
+
fs.mkdirSync(modelsDir, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Load configuration from file or create default
|
|
79
|
+
* @param {string} [configPath] - Optional custom config path
|
|
80
|
+
* @returns {Object} Configuration object
|
|
81
|
+
*/
|
|
82
|
+
export function loadConfig(configPath) {
|
|
83
|
+
const config = { ...DEFAULT_CONFIG };
|
|
84
|
+
|
|
85
|
+
// Determine config file path
|
|
86
|
+
const actualConfigPath = configPath || path.join(config.dataDir, 'config.json');
|
|
87
|
+
|
|
88
|
+
// Ensure data directory exists
|
|
89
|
+
ensureDataDir(config.dataDir);
|
|
90
|
+
|
|
91
|
+
// Load config from file if it exists
|
|
92
|
+
if (fs.existsSync(actualConfigPath)) {
|
|
93
|
+
try {
|
|
94
|
+
const fileConfig = JSON.parse(fs.readFileSync(actualConfigPath, 'utf-8'));
|
|
95
|
+
const merged = deepMerge(config, fileConfig);
|
|
96
|
+
|
|
97
|
+
// Ensure data directory from loaded config exists
|
|
98
|
+
if (merged.dataDir !== config.dataDir) {
|
|
99
|
+
ensureDataDir(merged.dataDir);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return merged;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.warn(`Failed to load config from ${actualConfigPath}:`, error.message);
|
|
105
|
+
console.warn('Using default configuration');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Save default config to file if it doesn't exist
|
|
110
|
+
try {
|
|
111
|
+
fs.writeFileSync(actualConfigPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.warn(`Failed to save default config to ${actualConfigPath}:`, error.message);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return config;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Save configuration to file
|
|
121
|
+
* @param {Object} config - Configuration object
|
|
122
|
+
* @param {string} [configPath] - Optional custom config path
|
|
123
|
+
*/
|
|
124
|
+
export function saveConfig(config, configPath) {
|
|
125
|
+
const actualConfigPath = configPath || path.join(config.dataDir, 'config.json');
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
fs.writeFileSync(actualConfigPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw new Error(`Failed to save config to ${actualConfigPath}: ${error.message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get the database path from config
|
|
136
|
+
* @param {Object} config - Configuration object
|
|
137
|
+
* @returns {string} Path to SQLite database
|
|
138
|
+
*/
|
|
139
|
+
export function getDatabasePath(config) {
|
|
140
|
+
return path.join(config.dataDir, 'memory.db');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get the models directory from config
|
|
145
|
+
* @param {Object} config - Configuration object
|
|
146
|
+
* @returns {string} Path to models directory
|
|
147
|
+
*/
|
|
148
|
+
export function getModelsPath(config) {
|
|
149
|
+
return path.join(config.dataDir, 'models');
|
|
150
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { pipeline } from '@xenova/transformers';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import * as logger from '../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Cached pipeline instance
|
|
12
|
+
*/
|
|
13
|
+
let cachedPipeline = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Model configuration
|
|
17
|
+
*/
|
|
18
|
+
const MODEL_CONFIG = {
|
|
19
|
+
name: 'Xenova/all-MiniLM-L6-v2',
|
|
20
|
+
task: 'feature-extraction'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initialize the embedding pipeline
|
|
25
|
+
* Downloads model on first use and caches it
|
|
26
|
+
* @param {string} modelsPath - Path to models directory
|
|
27
|
+
* @returns {Promise<Object>} Pipeline instance
|
|
28
|
+
*/
|
|
29
|
+
export async function initializePipeline(modelsPath) {
|
|
30
|
+
if (cachedPipeline) {
|
|
31
|
+
logger.debug('Using cached embedding pipeline');
|
|
32
|
+
return cachedPipeline;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
logger.info('Initializing embedding model', { model: MODEL_CONFIG.name });
|
|
37
|
+
|
|
38
|
+
// Ensure models directory exists
|
|
39
|
+
if (!fs.existsSync(modelsPath)) {
|
|
40
|
+
fs.mkdirSync(modelsPath, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Set cache directory for transformers
|
|
44
|
+
process.env.TRANSFORMERS_CACHE = modelsPath;
|
|
45
|
+
|
|
46
|
+
logger.info('Loading embedding model (this may take a moment on first run)...');
|
|
47
|
+
|
|
48
|
+
// Create pipeline
|
|
49
|
+
cachedPipeline = await pipeline(
|
|
50
|
+
MODEL_CONFIG.task,
|
|
51
|
+
MODEL_CONFIG.name,
|
|
52
|
+
{
|
|
53
|
+
quantized: true,
|
|
54
|
+
progress_callback: (progress) => {
|
|
55
|
+
if (progress.status === 'downloading') {
|
|
56
|
+
const percent = progress.progress ? Math.round(progress.progress) : 0;
|
|
57
|
+
logger.debug(`Downloading model: ${percent}%`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
logger.info('Embedding model loaded successfully');
|
|
64
|
+
|
|
65
|
+
return cachedPipeline;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
logger.error('Failed to initialize embedding pipeline', { error: error.message });
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generate embedding for text
|
|
74
|
+
* @param {string} text - Text to embed
|
|
75
|
+
* @param {string} modelsPath - Path to models directory
|
|
76
|
+
* @returns {Promise<Float32Array>} Embedding vector
|
|
77
|
+
*/
|
|
78
|
+
export async function generateEmbedding(text, modelsPath) {
|
|
79
|
+
try {
|
|
80
|
+
const pipe = await initializePipeline(modelsPath);
|
|
81
|
+
|
|
82
|
+
logger.debug('Generating embedding', { textLength: text.length });
|
|
83
|
+
|
|
84
|
+
// Generate embedding
|
|
85
|
+
const result = await pipe(text, {
|
|
86
|
+
pooling: 'mean',
|
|
87
|
+
normalize: true
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Extract the embedding array
|
|
91
|
+
// The result is a tensor, we need to convert it to Float32Array
|
|
92
|
+
const embedding = new Float32Array(result.data);
|
|
93
|
+
|
|
94
|
+
logger.debug('Embedding generated', { dimensions: embedding.length });
|
|
95
|
+
|
|
96
|
+
return embedding;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
logger.error('Failed to generate embedding', { error: error.message });
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Generate embeddings for multiple texts in batch
|
|
105
|
+
* @param {string[]} texts - Array of texts to embed
|
|
106
|
+
* @param {string} modelsPath - Path to models directory
|
|
107
|
+
* @returns {Promise<Float32Array[]>} Array of embedding vectors
|
|
108
|
+
*/
|
|
109
|
+
export async function generateEmbeddings(texts, modelsPath) {
|
|
110
|
+
try {
|
|
111
|
+
const pipe = await initializePipeline(modelsPath);
|
|
112
|
+
|
|
113
|
+
logger.debug('Generating batch embeddings', { count: texts.length });
|
|
114
|
+
|
|
115
|
+
const embeddings = [];
|
|
116
|
+
|
|
117
|
+
for (const text of texts) {
|
|
118
|
+
const result = await pipe(text, {
|
|
119
|
+
pooling: 'mean',
|
|
120
|
+
normalize: true
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
embeddings.push(new Float32Array(result.data));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
logger.debug('Batch embeddings generated', { count: embeddings.length });
|
|
127
|
+
|
|
128
|
+
return embeddings;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
logger.error('Failed to generate batch embeddings', { error: error.message });
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Calculate cosine similarity between two embeddings
|
|
137
|
+
* @param {Float32Array} a - First embedding
|
|
138
|
+
* @param {Float32Array} b - Second embedding
|
|
139
|
+
* @returns {number} Similarity score (0-1)
|
|
140
|
+
*/
|
|
141
|
+
export function cosineSimilarity(a, b) {
|
|
142
|
+
if (a.length !== b.length) {
|
|
143
|
+
throw new Error('Embeddings must have the same dimensions');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let dotProduct = 0;
|
|
147
|
+
let normA = 0;
|
|
148
|
+
let normB = 0;
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < a.length; i++) {
|
|
151
|
+
dotProduct += a[i] * b[i];
|
|
152
|
+
normA += a[i] * a[i];
|
|
153
|
+
normB += b[i] * b[i];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const similarity = dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
157
|
+
|
|
158
|
+
// Clamp to [0, 1] range (should already be in this range for normalized embeddings)
|
|
159
|
+
return Math.max(0, Math.min(1, similarity));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check if embedding model is available (cached)
|
|
164
|
+
* @param {string} modelsPath - Path to models directory (may be overridden)
|
|
165
|
+
* @returns {Object} Object with available flag and actual path
|
|
166
|
+
*/
|
|
167
|
+
export function isModelAvailable(modelsPath) {
|
|
168
|
+
// First check the provided modelsPath
|
|
169
|
+
if (fs.existsSync(modelsPath) && fs.readdirSync(modelsPath).length > 0) {
|
|
170
|
+
return { available: true, path: modelsPath };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check Xenova transformers cache in node_modules (most common for local dev)
|
|
174
|
+
const possiblePaths = [
|
|
175
|
+
path.resolve(__dirname, '../../node_modules/@xenova/transformers/.cache/Xenova/all-MiniLM-L6-v2'),
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
// Add home directory cache paths
|
|
179
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
180
|
+
if (homeDir) {
|
|
181
|
+
possiblePaths.push(path.join(homeDir, '.cache', 'huggingface', 'hub', 'models--Xenova--all-MiniLM-L6-v2'));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const cachePath of possiblePaths) {
|
|
185
|
+
try {
|
|
186
|
+
if (fs.existsSync(cachePath) && fs.readdirSync(cachePath).length > 0) {
|
|
187
|
+
return { available: true, path: cachePath };
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
// Continue checking other paths
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { available: false, path: modelsPath };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Calculate directory size recursively
|
|
199
|
+
* @param {string} dirPath - Directory path
|
|
200
|
+
* @returns {number} Size in bytes
|
|
201
|
+
*/
|
|
202
|
+
function getDirectorySize(dirPath) {
|
|
203
|
+
let size = 0;
|
|
204
|
+
try {
|
|
205
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
206
|
+
for (const entry of entries) {
|
|
207
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
208
|
+
if (entry.isFile()) {
|
|
209
|
+
try {
|
|
210
|
+
const stats = fs.statSync(fullPath);
|
|
211
|
+
size += stats.size;
|
|
212
|
+
} catch (e) {
|
|
213
|
+
// Skip unreadable files
|
|
214
|
+
}
|
|
215
|
+
} else if (entry.isDirectory()) {
|
|
216
|
+
size += getDirectorySize(fullPath);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch (e) {
|
|
220
|
+
// Return 0 if directory can't be read
|
|
221
|
+
}
|
|
222
|
+
return size;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get model information
|
|
227
|
+
* @param {string} modelsPath - Path to models directory
|
|
228
|
+
* @returns {Object} Model information
|
|
229
|
+
*/
|
|
230
|
+
export function getModelInfo(modelsPath) {
|
|
231
|
+
const modelCheck = isModelAvailable(modelsPath);
|
|
232
|
+
const available = modelCheck.available;
|
|
233
|
+
const actualPath = modelCheck.path;
|
|
234
|
+
|
|
235
|
+
let size = 0;
|
|
236
|
+
if (available) {
|
|
237
|
+
size = getDirectorySize(actualPath);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
name: MODEL_CONFIG.name,
|
|
242
|
+
task: MODEL_CONFIG.task,
|
|
243
|
+
available,
|
|
244
|
+
cached: cachedPipeline !== null,
|
|
245
|
+
sizeBytes: size,
|
|
246
|
+
sizeMB: Math.round(size / (1024 * 1024)),
|
|
247
|
+
path: actualPath
|
|
248
|
+
};
|
|
249
|
+
}
|