@jungjaehoon/mama-core 1.0.2 → 1.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.
- package/package.json +33 -21
- package/src/config-loader.js +0 -218
- package/src/db-adapter/README.md +0 -110
- package/src/db-adapter/base-adapter.js +0 -91
- package/src/db-adapter/index.js +0 -31
- package/src/db-adapter/sqlite-adapter.js +0 -364
- package/src/db-adapter/statement.js +0 -127
- package/src/db-manager.js +0 -671
- package/src/debug-logger.js +0 -86
- package/src/decision-formatter.js +0 -1276
- package/src/decision-tracker.js +0 -621
- package/src/embedding-cache.js +0 -222
- package/src/embedding-client.js +0 -141
- package/src/embedding-server/index.js +0 -424
- package/src/embedding-server/mobile/auth.js +0 -160
- package/src/embedding-server/mobile/daemon.js +0 -313
- package/src/embedding-server/mobile/output-parser.js +0 -281
- package/src/embedding-server/mobile/session-api.js +0 -279
- package/src/embedding-server/mobile/session-manager.js +0 -377
- package/src/embedding-server/mobile/websocket-handler.js +0 -443
- package/src/embeddings.js +0 -305
- package/src/errors.js +0 -326
- package/src/index.js +0 -41
- package/src/mama-api.js +0 -2614
- package/src/memory-inject.js +0 -174
- package/src/memory-store.js +0 -89
- package/src/notification-manager.js +0 -3
- package/src/ollama-client.js +0 -391
- package/src/outcome-tracker.js +0 -351
- package/src/progress-indicator.js +0 -110
- package/src/query-intent.js +0 -237
- package/src/relevance-scorer.js +0 -286
- package/src/tier-validator.js +0 -269
- package/src/time-formatter.js +0 -98
package/src/relevance-scorer.js
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MAMA (Memory-Augmented MCP Architecture) - Relevance Scorer
|
|
3
|
-
*
|
|
4
|
-
* Relevance scoring formula for decision ranking and top-N selection
|
|
5
|
-
* Tasks: 1.1-1.4, 2.1-2.7 (Relevance scoring and top-N selection)
|
|
6
|
-
* AC #1, #4, #5: Decision relevance, failure priority boost, top-N selection
|
|
7
|
-
*
|
|
8
|
-
* @module relevance-scorer
|
|
9
|
-
* @version 1.0
|
|
10
|
-
* @date 2025-11-14
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const { cosineSimilarity } = require('./embeddings');
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Calculate relevance score for a single decision
|
|
17
|
-
*
|
|
18
|
-
* Task 1.2: Implement calculateRelevance(decision, queryContext) function
|
|
19
|
-
* AC #1, #4: Relevance scoring with failure priority boost
|
|
20
|
-
*
|
|
21
|
-
* Formula:
|
|
22
|
-
* Relevance = (Recency × 0.2) + (Importance × 0.5) + (Semantic × 0.3)
|
|
23
|
-
*
|
|
24
|
-
* Where:
|
|
25
|
-
* - Recency: exp(-days_since / 30) [30-day half-life]
|
|
26
|
-
* - Importance: OUTCOME_WEIGHTS[outcome]
|
|
27
|
-
* - FAILED: 1.0 (highest - failures are most valuable)
|
|
28
|
-
* - PARTIAL: 0.7
|
|
29
|
-
* - SUCCESS: 0.5
|
|
30
|
-
* - null: 0.3 (ongoing, lowest)
|
|
31
|
-
* - Semantic: cosineSimilarity(decision.embedding, query.embedding)
|
|
32
|
-
*
|
|
33
|
-
* @param {Object} decision - Decision object
|
|
34
|
-
* @param {number} decision.created_at - Created timestamp
|
|
35
|
-
* @param {string} decision.outcome - Outcome type
|
|
36
|
-
* @param {Float32Array} decision.embedding - Decision embedding (384-dim)
|
|
37
|
-
* @param {Object} queryContext - Query context
|
|
38
|
-
* @param {Float32Array} queryContext.embedding - Query embedding (384-dim)
|
|
39
|
-
* @returns {number} Relevance score (0.0-1.0)
|
|
40
|
-
*/
|
|
41
|
-
function calculateRelevance(decision, queryContext) {
|
|
42
|
-
// ═══════════════════════════════════════════════════════════
|
|
43
|
-
// Recency Score (20%)
|
|
44
|
-
// ═══════════════════════════════════════════════════════════
|
|
45
|
-
// Exponential decay with 30-day half-life
|
|
46
|
-
const daysSince = (Date.now() - decision.created_at) / (1000 * 60 * 60 * 24);
|
|
47
|
-
const recencyScore = Math.exp(-daysSince / 30);
|
|
48
|
-
|
|
49
|
-
// Decay curve:
|
|
50
|
-
// 0 days = 1.0
|
|
51
|
-
// 30 days = 0.5
|
|
52
|
-
// 60 days = 0.25
|
|
53
|
-
// 90 days = 0.125
|
|
54
|
-
|
|
55
|
-
// ═══════════════════════════════════════════════════════════
|
|
56
|
-
// Importance Score (50%) - AC #4: Failure Priority Boost
|
|
57
|
-
// ═══════════════════════════════════════════════════════════
|
|
58
|
-
const OUTCOME_WEIGHTS = {
|
|
59
|
-
FAILED: 1.0, // Highest - failures are most valuable (AC #4)
|
|
60
|
-
PARTIAL: 0.7,
|
|
61
|
-
SUCCESS: 0.5,
|
|
62
|
-
pending: 0.3, // Ongoing/pending, lowest
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// Use explicit null check to avoid confusion with object key access
|
|
66
|
-
const outcomeKey = decision.outcome ?? 'pending';
|
|
67
|
-
const importanceScore = OUTCOME_WEIGHTS[outcomeKey] ?? OUTCOME_WEIGHTS.pending;
|
|
68
|
-
|
|
69
|
-
// ═══════════════════════════════════════════════════════════
|
|
70
|
-
// Semantic Score (30%)
|
|
71
|
-
// ═══════════════════════════════════════════════════════════
|
|
72
|
-
let semanticScore = 0;
|
|
73
|
-
|
|
74
|
-
if (decision.embedding && queryContext.embedding) {
|
|
75
|
-
// Task 1.3: Use cosine similarity function
|
|
76
|
-
semanticScore = cosineSimilarity(decision.embedding, queryContext.embedding);
|
|
77
|
-
} else {
|
|
78
|
-
// Fallback: no semantic match if embeddings missing
|
|
79
|
-
semanticScore = 0;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// ═══════════════════════════════════════════════════════════
|
|
83
|
-
// Weighted Sum (Total: 100%)
|
|
84
|
-
// ═══════════════════════════════════════════════════════════
|
|
85
|
-
const relevance = recencyScore * 0.2 + importanceScore * 0.5 + semanticScore * 0.3;
|
|
86
|
-
|
|
87
|
-
return relevance;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Select top N most relevant decisions
|
|
92
|
-
*
|
|
93
|
-
* Task 2.1: Add selectTopDecisions(decisions, queryContext, n=3) function
|
|
94
|
-
* AC #1, #5: Top-N selection with threshold filtering
|
|
95
|
-
*
|
|
96
|
-
* @param {Array<Object>} decisions - Array of decision objects
|
|
97
|
-
* @param {Object} queryContext - Query context with embedding
|
|
98
|
-
* @param {number} n - Number of top decisions to return (default: 3)
|
|
99
|
-
* @returns {Array<Object>} Top N decisions with relevance scores
|
|
100
|
-
*/
|
|
101
|
-
function selectTopDecisions(decisions, queryContext, n = 3) {
|
|
102
|
-
if (!Array.isArray(decisions) || decisions.length === 0) {
|
|
103
|
-
return [];
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Task 2.3: Score all results by relevance
|
|
107
|
-
const decisionsWithScores = decisions.map((decision) => ({
|
|
108
|
-
...decision,
|
|
109
|
-
relevanceScore: calculateRelevance(decision, queryContext),
|
|
110
|
-
}));
|
|
111
|
-
|
|
112
|
-
// Task 2.4: Sort descending (highest relevance first)
|
|
113
|
-
decisionsWithScores.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
114
|
-
|
|
115
|
-
// Task 2.6: Filter out < 0.5 relevance (AC #1)
|
|
116
|
-
const filtered = decisionsWithScores.filter((d) => d.relevanceScore >= 0.5);
|
|
117
|
-
|
|
118
|
-
// Task 2.5: Return top 3 (or top N)
|
|
119
|
-
const topN = filtered.slice(0, n);
|
|
120
|
-
|
|
121
|
-
return topN;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Cosine similarity helper (re-exported from embeddings.js)
|
|
126
|
-
*
|
|
127
|
-
* Task 1.3: Implement cosine similarity function
|
|
128
|
-
* AC #1: Semantic similarity calculation
|
|
129
|
-
*
|
|
130
|
-
* Note: This is re-exported from embeddings.js for convenience
|
|
131
|
-
*
|
|
132
|
-
* @param {Float32Array} vec1 - First embedding vector
|
|
133
|
-
* @param {Float32Array} vec2 - Second embedding vector
|
|
134
|
-
* @returns {number} Cosine similarity (0.0-1.0)
|
|
135
|
-
*/
|
|
136
|
-
// Already available from embeddings.js - no need to reimplement
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Format decisions with top-N selection and summary
|
|
140
|
-
*
|
|
141
|
-
* Task 8.2-8.3: Format top 3 in full detail, rest as summary
|
|
142
|
-
* AC #5: Top-N selection with summary
|
|
143
|
-
*
|
|
144
|
-
* @param {Array<Object>} decisions - All decisions (sorted by relevance)
|
|
145
|
-
* @param {number} topN - Number of decisions to show in full detail (default: 3)
|
|
146
|
-
* @returns {Object} Formatted context {full: Array, summary: Object}
|
|
147
|
-
*/
|
|
148
|
-
function formatTopNContext(decisions, topN = 3) {
|
|
149
|
-
if (!Array.isArray(decisions) || decisions.length === 0) {
|
|
150
|
-
return { full: [], summary: null };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Split into top N and rest
|
|
154
|
-
const fullDetailDecisions = decisions.slice(0, topN);
|
|
155
|
-
const summaryDecisions = decisions.slice(topN);
|
|
156
|
-
|
|
157
|
-
// Full detail for top N
|
|
158
|
-
const full = fullDetailDecisions.map((d) => ({
|
|
159
|
-
decision_id: d.id,
|
|
160
|
-
topic: d.topic,
|
|
161
|
-
decision: d.decision,
|
|
162
|
-
reasoning: d.reasoning,
|
|
163
|
-
outcome: d.outcome,
|
|
164
|
-
failure_reason: d.failure_reason,
|
|
165
|
-
user_involvement: d.user_involvement,
|
|
166
|
-
confidence: d.confidence,
|
|
167
|
-
relevanceScore: d.relevanceScore,
|
|
168
|
-
created_at: d.created_at,
|
|
169
|
-
}));
|
|
170
|
-
|
|
171
|
-
// Summary for rest (count, duration, key failures only)
|
|
172
|
-
let summary = null;
|
|
173
|
-
|
|
174
|
-
if (summaryDecisions.length > 0) {
|
|
175
|
-
// Calculate duration (oldest to newest)
|
|
176
|
-
const oldestTimestamp = Math.min(...summaryDecisions.map((d) => d.created_at));
|
|
177
|
-
const newestTimestamp = Math.max(...summaryDecisions.map((d) => d.created_at));
|
|
178
|
-
const durationDays = Math.floor((newestTimestamp - oldestTimestamp) / (1000 * 60 * 60 * 24));
|
|
179
|
-
|
|
180
|
-
// Extract key failures
|
|
181
|
-
const failures = summaryDecisions
|
|
182
|
-
.filter((d) => d.outcome === 'FAILED')
|
|
183
|
-
.map((d) => ({ decision: d.decision, reason: d.failure_reason }));
|
|
184
|
-
|
|
185
|
-
summary = {
|
|
186
|
-
count: summaryDecisions.length,
|
|
187
|
-
duration_days: durationDays,
|
|
188
|
-
failures: failures.slice(0, 3), // Show max 3 failures
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return { full, summary };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Test relevance scoring with sample decisions
|
|
197
|
-
*
|
|
198
|
-
* Task 1.4: Test relevance scoring with sample decisions
|
|
199
|
-
* AC #1, #4: Verify scoring formula and failure priority
|
|
200
|
-
*
|
|
201
|
-
* @returns {Object} Test results
|
|
202
|
-
*/
|
|
203
|
-
function testRelevanceScoring() {
|
|
204
|
-
const now = Date.now();
|
|
205
|
-
|
|
206
|
-
// Mock embeddings (dummy for testing)
|
|
207
|
-
const queryEmbedding = new Float32Array(384).fill(0.5);
|
|
208
|
-
const decisionEmbedding1 = new Float32Array(384).fill(0.5); // Identical (similarity = 1.0)
|
|
209
|
-
// eslint-disable-next-line no-unused-vars
|
|
210
|
-
const decisionEmbedding2 = new Float32Array(384).fill(0.3); // Different (similarity < 1.0)
|
|
211
|
-
|
|
212
|
-
const scenarios = [
|
|
213
|
-
// Scenario 1: Recent FAILED decision (should have highest relevance)
|
|
214
|
-
{
|
|
215
|
-
name: 'Recent FAILED decision',
|
|
216
|
-
decision: {
|
|
217
|
-
created_at: now - 5 * 24 * 60 * 60 * 1000, // 5 days ago
|
|
218
|
-
outcome: 'FAILED',
|
|
219
|
-
embedding: decisionEmbedding1,
|
|
220
|
-
},
|
|
221
|
-
queryContext: { embedding: queryEmbedding },
|
|
222
|
-
expected: {
|
|
223
|
-
recency: 0.85, // exp(-5/30) ≈ 0.85
|
|
224
|
-
importance: 1.0, // FAILED = 1.0 (AC #4)
|
|
225
|
-
semantic: 1.0, // Identical embeddings
|
|
226
|
-
relevance: 0.87, // (0.85×0.2) + (1.0×0.5) + (1.0×0.3)
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
|
|
230
|
-
// Scenario 2: Recent SUCCESS decision (lower importance)
|
|
231
|
-
{
|
|
232
|
-
name: 'Recent SUCCESS decision',
|
|
233
|
-
decision: {
|
|
234
|
-
created_at: now - 5 * 24 * 60 * 60 * 1000, // 5 days ago
|
|
235
|
-
outcome: 'SUCCESS',
|
|
236
|
-
embedding: decisionEmbedding1,
|
|
237
|
-
},
|
|
238
|
-
queryContext: { embedding: queryEmbedding },
|
|
239
|
-
expected: {
|
|
240
|
-
recency: 0.85,
|
|
241
|
-
importance: 0.5, // SUCCESS = 0.5
|
|
242
|
-
semantic: 1.0,
|
|
243
|
-
relevance: 0.62, // (0.85×0.2) + (0.5×0.5) + (1.0×0.3)
|
|
244
|
-
},
|
|
245
|
-
},
|
|
246
|
-
|
|
247
|
-
// Scenario 3: Old FAILED decision (recency decay)
|
|
248
|
-
{
|
|
249
|
-
name: 'Old FAILED decision',
|
|
250
|
-
decision: {
|
|
251
|
-
created_at: now - 60 * 24 * 60 * 60 * 1000, // 60 days ago
|
|
252
|
-
outcome: 'FAILED',
|
|
253
|
-
embedding: decisionEmbedding1,
|
|
254
|
-
},
|
|
255
|
-
queryContext: { embedding: queryEmbedding },
|
|
256
|
-
expected: {
|
|
257
|
-
recency: 0.25, // exp(-60/30) ≈ 0.25
|
|
258
|
-
importance: 1.0,
|
|
259
|
-
semantic: 1.0,
|
|
260
|
-
relevance: 0.85, // (0.25×0.2) + (1.0×0.5) + (1.0×0.3)
|
|
261
|
-
},
|
|
262
|
-
},
|
|
263
|
-
];
|
|
264
|
-
|
|
265
|
-
const results = scenarios.map((scenario) => {
|
|
266
|
-
const calculated = calculateRelevance(scenario.decision, scenario.queryContext);
|
|
267
|
-
const pass = Math.abs(calculated - scenario.expected.relevance) < 0.05;
|
|
268
|
-
|
|
269
|
-
return {
|
|
270
|
-
name: scenario.name,
|
|
271
|
-
expected: scenario.expected.relevance.toFixed(2),
|
|
272
|
-
calculated: calculated.toFixed(2),
|
|
273
|
-
pass,
|
|
274
|
-
};
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
return results;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Export API
|
|
281
|
-
module.exports = {
|
|
282
|
-
calculateRelevance,
|
|
283
|
-
selectTopDecisions,
|
|
284
|
-
formatTopNContext,
|
|
285
|
-
testRelevanceScoring,
|
|
286
|
-
};
|
package/src/tier-validator.js
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MAMA Tier Validator
|
|
3
|
-
*
|
|
4
|
-
* Centralized tier validation module for MAMA.
|
|
5
|
-
* Validates system requirements and determines tier status (1 or 2).
|
|
6
|
-
*
|
|
7
|
-
* Tier 1: Full features (Node.js 18+, SQLite, Embeddings, Database)
|
|
8
|
-
* Tier 2: Degraded mode (missing one or more requirements)
|
|
9
|
-
*
|
|
10
|
-
* @module tier-validator
|
|
11
|
-
* @version 1.0
|
|
12
|
-
* @date 2026-01-30
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const fs = require('fs');
|
|
16
|
-
const path = require('path');
|
|
17
|
-
const os = require('os');
|
|
18
|
-
const { info: _info, warn: _warn, error: _logError } = require('./debug-logger');
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Validates Node.js version requirement
|
|
22
|
-
*
|
|
23
|
-
* @returns {Object} Check result { status: 'pass'|'fail', details: string }
|
|
24
|
-
*/
|
|
25
|
-
function checkNodeVersion() {
|
|
26
|
-
try {
|
|
27
|
-
const nodeVersion = process.versions.node;
|
|
28
|
-
const majorVersion = parseInt(nodeVersion.split('.')[0], 10);
|
|
29
|
-
|
|
30
|
-
if (majorVersion >= 18) {
|
|
31
|
-
return {
|
|
32
|
-
status: 'pass',
|
|
33
|
-
details: `v${nodeVersion}`,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
status: 'fail',
|
|
39
|
-
details: `v${nodeVersion} (requires 18+)`,
|
|
40
|
-
};
|
|
41
|
-
} catch (error) {
|
|
42
|
-
return {
|
|
43
|
-
status: 'fail',
|
|
44
|
-
details: `Error checking version: ${error.message}`,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Validates SQLite (better-sqlite3) availability
|
|
51
|
-
*
|
|
52
|
-
* Reuses logic from packages/mcp-server/scripts/postinstall.js
|
|
53
|
-
*
|
|
54
|
-
* @returns {Object} Check result { status: 'pass'|'fail', details: string }
|
|
55
|
-
*/
|
|
56
|
-
function checkSQLite() {
|
|
57
|
-
try {
|
|
58
|
-
// Try to require better-sqlite3
|
|
59
|
-
const Database = require('better-sqlite3');
|
|
60
|
-
|
|
61
|
-
// Test instantiation with in-memory database
|
|
62
|
-
const testDb = new Database(':memory:');
|
|
63
|
-
testDb.close();
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
status: 'pass',
|
|
67
|
-
details: 'better-sqlite3 native module ready',
|
|
68
|
-
};
|
|
69
|
-
} catch (error) {
|
|
70
|
-
return {
|
|
71
|
-
status: 'fail',
|
|
72
|
-
details: `better-sqlite3 not available: ${error.message}`,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Validates embedding model availability
|
|
79
|
-
*
|
|
80
|
-
* Checks if embedding model has been downloaded to cache directory.
|
|
81
|
-
* Reuses logic from packages/mama-core/src/embeddings.js
|
|
82
|
-
*
|
|
83
|
-
* @returns {Object} Check result { status: 'pass'|'fail', details: string }
|
|
84
|
-
*/
|
|
85
|
-
function checkEmbeddings() {
|
|
86
|
-
try {
|
|
87
|
-
const { getModelName } = require('./embeddings');
|
|
88
|
-
const modelName = getModelName();
|
|
89
|
-
|
|
90
|
-
// Check if model is cached
|
|
91
|
-
const cacheDir =
|
|
92
|
-
process.env.HF_HOME ||
|
|
93
|
-
process.env.TRANSFORMERS_CACHE ||
|
|
94
|
-
path.join(os.homedir(), '.cache', 'huggingface', 'transformers');
|
|
95
|
-
|
|
96
|
-
// Model cache structure: cache_dir/models--org--model/snapshots/hash/
|
|
97
|
-
const modelPath = path.join(cacheDir, `models--${modelName.replace('/', '--')}`);
|
|
98
|
-
|
|
99
|
-
if (fs.existsSync(modelPath)) {
|
|
100
|
-
return {
|
|
101
|
-
status: 'pass',
|
|
102
|
-
details: `${modelName} (cached)`,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
status: 'fail',
|
|
108
|
-
details: `${modelName} not cached (will download on first use)`,
|
|
109
|
-
};
|
|
110
|
-
} catch (error) {
|
|
111
|
-
return {
|
|
112
|
-
status: 'fail',
|
|
113
|
-
details: `Error checking embeddings: ${error.message}`,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Validates database file accessibility
|
|
120
|
-
*
|
|
121
|
-
* Tests write access to database location (~/.claude/mama-memory.db)
|
|
122
|
-
*
|
|
123
|
-
* @returns {Object} Check result { status: 'pass'|'fail', details: string }
|
|
124
|
-
*/
|
|
125
|
-
function checkDatabase() {
|
|
126
|
-
try {
|
|
127
|
-
const dbPath = process.env.MAMA_DB_PATH || path.join(os.homedir(), '.claude', 'mama-memory.db');
|
|
128
|
-
|
|
129
|
-
const dbDir = path.dirname(dbPath);
|
|
130
|
-
|
|
131
|
-
// Check if directory exists or can be created
|
|
132
|
-
if (!fs.existsSync(dbDir)) {
|
|
133
|
-
try {
|
|
134
|
-
fs.mkdirSync(dbDir, { recursive: true });
|
|
135
|
-
} catch (mkdirErr) {
|
|
136
|
-
return {
|
|
137
|
-
status: 'fail',
|
|
138
|
-
details: `Cannot create database directory: ${mkdirErr.message}`,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Check write access
|
|
144
|
-
try {
|
|
145
|
-
fs.accessSync(dbDir, fs.constants.W_OK);
|
|
146
|
-
} catch (accessErr) {
|
|
147
|
-
return {
|
|
148
|
-
status: 'fail',
|
|
149
|
-
details: `No write access to ${dbDir}`,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
status: 'pass',
|
|
155
|
-
details: dbPath,
|
|
156
|
-
};
|
|
157
|
-
} catch (error) {
|
|
158
|
-
return {
|
|
159
|
-
status: 'fail',
|
|
160
|
-
details: `Error checking database: ${error.message}`,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Validates MAMA tier status
|
|
167
|
-
*
|
|
168
|
-
* Performs all system checks and determines tier:
|
|
169
|
-
* - Tier 1: All checks pass (full features)
|
|
170
|
-
* - Tier 2: One or more checks fail (degraded mode)
|
|
171
|
-
*
|
|
172
|
-
* @returns {Promise<Object>} Validation result
|
|
173
|
-
* @returns {number} result.tier - 1 (full) or 2 (degraded)
|
|
174
|
-
* @returns {Array} result.checks - Array of check results
|
|
175
|
-
* @example
|
|
176
|
-
* const { tier, checks } = await validateTier();
|
|
177
|
-
* console.log(`MAMA Tier: ${tier}`);
|
|
178
|
-
* checks.forEach(check => {
|
|
179
|
-
* console.log(`${check.name}: ${check.status} (${check.details})`);
|
|
180
|
-
* });
|
|
181
|
-
*/
|
|
182
|
-
async function validateTier() {
|
|
183
|
-
const checks = [
|
|
184
|
-
{
|
|
185
|
-
name: 'Node.js',
|
|
186
|
-
...checkNodeVersion(),
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
name: 'SQLite',
|
|
190
|
-
...checkSQLite(),
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
name: 'Embeddings',
|
|
194
|
-
...checkEmbeddings(),
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
name: 'Database',
|
|
198
|
-
...checkDatabase(),
|
|
199
|
-
},
|
|
200
|
-
];
|
|
201
|
-
|
|
202
|
-
// Determine tier: all pass = tier 1, any fail = tier 2
|
|
203
|
-
const tier = checks.every((c) => c.status === 'pass') ? 1 : 2;
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
tier,
|
|
207
|
-
checks,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Get user-friendly tier description
|
|
213
|
-
*
|
|
214
|
-
* @param {number} tier - Tier number (1 or 2)
|
|
215
|
-
* @returns {string} Human-readable tier description
|
|
216
|
-
* @example
|
|
217
|
-
* const desc = getTierDescription(1);
|
|
218
|
-
* console.log(desc); // "Full Features"
|
|
219
|
-
*/
|
|
220
|
-
function getTierDescription(tier) {
|
|
221
|
-
const descriptions = {
|
|
222
|
-
1: 'Full Features - All systems operational',
|
|
223
|
-
2: 'Degraded Mode - Some features unavailable',
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
return descriptions[tier] || 'Unknown Tier';
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Get tier status banner
|
|
231
|
-
*
|
|
232
|
-
* Returns formatted banner showing tier and failed checks
|
|
233
|
-
*
|
|
234
|
-
* @param {Object} validation - Result from validateTier()
|
|
235
|
-
* @returns {string} Formatted banner text
|
|
236
|
-
*/
|
|
237
|
-
function getTierBanner(validation) {
|
|
238
|
-
const { tier, checks } = validation;
|
|
239
|
-
const failedChecks = checks.filter((c) => c.status === 'fail');
|
|
240
|
-
|
|
241
|
-
let banner = `\n┌─────────────────────────────────────────┐\n`;
|
|
242
|
-
banner += `│ MAMA Tier ${tier}: ${getTierDescription(tier).split(' - ')[0]}\n`;
|
|
243
|
-
|
|
244
|
-
if (failedChecks.length > 0) {
|
|
245
|
-
banner += `│\n`;
|
|
246
|
-
banner += `│ ⚠️ Issues detected:\n`;
|
|
247
|
-
failedChecks.forEach((check) => {
|
|
248
|
-
banner += `│ • ${check.name}: ${check.details}\n`;
|
|
249
|
-
});
|
|
250
|
-
} else {
|
|
251
|
-
banner += `│ ✅ All systems operational\n`;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
banner += `└─────────────────────────────────────────┘\n`;
|
|
255
|
-
|
|
256
|
-
return banner;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Export API
|
|
260
|
-
module.exports = {
|
|
261
|
-
validateTier,
|
|
262
|
-
getTierDescription,
|
|
263
|
-
getTierBanner,
|
|
264
|
-
// Internal checks (for testing)
|
|
265
|
-
checkNodeVersion,
|
|
266
|
-
checkSQLite,
|
|
267
|
-
checkEmbeddings,
|
|
268
|
-
checkDatabase,
|
|
269
|
-
};
|
package/src/time-formatter.js
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Time Formatter - Human-Readable Time Formatting
|
|
3
|
-
*
|
|
4
|
-
* Converts Unix timestamps to human-readable relative time format
|
|
5
|
-
* Examples: "2d ago", "3h ago", "just now"
|
|
6
|
-
*
|
|
7
|
-
* Used by list_decisions and recall_decision tools
|
|
8
|
-
*
|
|
9
|
-
* @module time-formatter
|
|
10
|
-
* @date 2025-11-20
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const { warn } = require('./debug-logger');
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Format Unix timestamp (milliseconds) to human-readable relative time
|
|
17
|
-
*
|
|
18
|
-
* AC #2: Format created_at as human-readable ("2d ago", "3h ago", etc.)
|
|
19
|
-
*
|
|
20
|
-
* @param {number|string} timestamp - Unix timestamp in milliseconds OR ISO 8601 string
|
|
21
|
-
* @returns {string} Human-readable time string
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* formatTimeAgo(Date.now() - 3600000) // "1h ago"
|
|
25
|
-
* formatTimeAgo(Date.now() - 172800000) // "2d ago"
|
|
26
|
-
* formatTimeAgo("2025-11-20T10:30:00Z") // "2d ago" (if today is 2025-11-22)
|
|
27
|
-
*/
|
|
28
|
-
function formatTimeAgo(timestamp) {
|
|
29
|
-
try {
|
|
30
|
-
// Handle null/undefined (but allow 0 as valid Unix epoch)
|
|
31
|
-
if (timestamp === null || timestamp === undefined) {
|
|
32
|
-
warn('[time-formatter] Timestamp is null or undefined, returning "unknown"');
|
|
33
|
-
return 'unknown';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Parse ISO 8601 string to timestamp (if string provided)
|
|
37
|
-
let timestampMs;
|
|
38
|
-
if (typeof timestamp === 'string') {
|
|
39
|
-
timestampMs = new Date(timestamp).getTime();
|
|
40
|
-
if (isNaN(timestampMs)) {
|
|
41
|
-
warn(`[time-formatter] Invalid ISO 8601 string: ${timestamp}`);
|
|
42
|
-
return 'unknown';
|
|
43
|
-
}
|
|
44
|
-
} else {
|
|
45
|
-
timestampMs = timestamp;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const now = Date.now();
|
|
49
|
-
const diff = now - timestampMs;
|
|
50
|
-
|
|
51
|
-
// Handle future timestamps (shouldn't happen, but be defensive)
|
|
52
|
-
if (diff < 0) {
|
|
53
|
-
warn(`[time-formatter] Future timestamp detected: ${timestamp}`);
|
|
54
|
-
return 'just now';
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Calculate time units
|
|
58
|
-
const seconds = Math.floor(diff / 1000);
|
|
59
|
-
const minutes = Math.floor(seconds / 60);
|
|
60
|
-
const hours = Math.floor(minutes / 60);
|
|
61
|
-
const days = Math.floor(hours / 24);
|
|
62
|
-
const weeks = Math.floor(days / 7);
|
|
63
|
-
const months = Math.floor(days / 30);
|
|
64
|
-
const years = Math.floor(days / 365);
|
|
65
|
-
|
|
66
|
-
// Return human-readable format
|
|
67
|
-
if (seconds < 60) {
|
|
68
|
-
return 'just now';
|
|
69
|
-
}
|
|
70
|
-
if (minutes < 60) {
|
|
71
|
-
return `${minutes}m ago`;
|
|
72
|
-
}
|
|
73
|
-
if (hours < 24) {
|
|
74
|
-
return `${hours}h ago`;
|
|
75
|
-
}
|
|
76
|
-
if (days < 7) {
|
|
77
|
-
return `${days}d ago`;
|
|
78
|
-
}
|
|
79
|
-
if (weeks < 4) {
|
|
80
|
-
return `${weeks}w ago`;
|
|
81
|
-
}
|
|
82
|
-
// Guard against "0mo ago" when days are 28-29 (months = Math.floor(days/30) = 0)
|
|
83
|
-
if (months < 1) {
|
|
84
|
-
return `${weeks}w ago`;
|
|
85
|
-
}
|
|
86
|
-
if (months < 12) {
|
|
87
|
-
return `${months}mo ago`;
|
|
88
|
-
}
|
|
89
|
-
return `${years}y ago`;
|
|
90
|
-
} catch (error) {
|
|
91
|
-
warn(`[time-formatter] Error formatting timestamp ${timestamp}: ${error.message}`);
|
|
92
|
-
return 'unknown';
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
module.exports = {
|
|
97
|
-
formatTimeAgo,
|
|
98
|
-
};
|