moflo 4.9.35 → 4.9.37
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/.claude/guidance/shipped/moflo-agent-rules.md +12 -0
- package/.claude/guidance/shipped/moflo-memory-protocol.md +30 -0
- package/.claude/guidance/shipped/moflo-subagents.md +4 -0
- package/.claude/helpers/gate.cjs +3 -3
- package/.claude/helpers/subagent-bootstrap.json +1 -1
- package/.claude/helpers/subagent-start.cjs +1 -1
- package/.claude/skills/eldar/SKILL.md +8 -0
- package/bin/gate.cjs +3 -3
- package/bin/index-guidance.mjs +41 -52
- package/bin/migrations/purge-doc-entries.mjs +53 -0
- package/bin/migrations/strip-context-preambles.mjs +97 -0
- package/bin/semantic-search.mjs +4 -1
- package/bin/session-start-launcher.mjs +2 -0
- package/dist/src/cli/commands/doctor-checks-memory-access.js +179 -0
- package/dist/src/cli/commands/memory.js +41 -52
- package/dist/src/cli/init/claudemd-generator.js +4 -0
- package/dist/src/cli/init/moflo-init.js +13 -5
- package/dist/src/cli/mcp-tools/memory-tools.js +169 -31
- package/dist/src/cli/memory/auto-memory-bridge.js +8 -11
- package/dist/src/cli/memory/bridge-entries.js +6 -2
- package/dist/src/cli/memory/memory-initializer.js +17 -11
- package/dist/src/cli/services/claude-stats.js +2 -16
- package/dist/src/cli/services/subagent-bootstrap.js +1 -1
- package/dist/src/cli/shared/utils/claude-projects-path.js +32 -0
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -54,6 +54,63 @@ function notifyMemoryGate() {
|
|
|
54
54
|
const MAX_KEY_LENGTH = 1024;
|
|
55
55
|
const MAX_VALUE_SIZE = 1024 * 1024; // 1MB
|
|
56
56
|
const MAX_QUERY_LENGTH = 4096;
|
|
57
|
+
function parseNavigation(metadataJson, mode) {
|
|
58
|
+
if (!metadataJson)
|
|
59
|
+
return null;
|
|
60
|
+
let meta;
|
|
61
|
+
try {
|
|
62
|
+
meta = JSON.parse(metadataJson);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (!meta || typeof meta !== 'object')
|
|
68
|
+
return null;
|
|
69
|
+
// Discriminator: only `type === 'chunk'` entries carry the nav fields.
|
|
70
|
+
if (meta.type !== 'chunk')
|
|
71
|
+
return null;
|
|
72
|
+
if (mode === 'compact') {
|
|
73
|
+
return {
|
|
74
|
+
parentDoc: meta.parentDoc,
|
|
75
|
+
prevChunk: meta.prevChunk ?? null,
|
|
76
|
+
nextChunk: meta.nextChunk ?? null,
|
|
77
|
+
chunkTitle: meta.chunkTitle,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
parentDoc: meta.parentDoc,
|
|
82
|
+
parentPath: meta.parentPath,
|
|
83
|
+
prevChunk: meta.prevChunk ?? null,
|
|
84
|
+
nextChunk: meta.nextChunk ?? null,
|
|
85
|
+
siblings: meta.siblings,
|
|
86
|
+
chunkIndex: meta.chunkIndex,
|
|
87
|
+
totalChunks: meta.totalChunks,
|
|
88
|
+
hierarchicalParent: meta.hierarchicalParent ?? null,
|
|
89
|
+
hierarchicalChildren: meta.hierarchicalChildren ?? null,
|
|
90
|
+
chunkTitle: meta.chunkTitle,
|
|
91
|
+
headerLevel: meta.headerLevel,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function shapeRetrievedEntry(entry) {
|
|
95
|
+
let value = entry.content;
|
|
96
|
+
try {
|
|
97
|
+
value = JSON.parse(entry.content);
|
|
98
|
+
}
|
|
99
|
+
catch { /* keep string */ }
|
|
100
|
+
return {
|
|
101
|
+
key: entry.key,
|
|
102
|
+
namespace: entry.namespace,
|
|
103
|
+
value,
|
|
104
|
+
tags: entry.tags,
|
|
105
|
+
storedAt: entry.createdAt,
|
|
106
|
+
updatedAt: entry.updatedAt,
|
|
107
|
+
accessCount: entry.accessCount,
|
|
108
|
+
hasEmbedding: entry.hasEmbedding,
|
|
109
|
+
navigation: parseNavigation(entry.metadata, 'full'),
|
|
110
|
+
found: true,
|
|
111
|
+
backend: 'sql.js + HNSW',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
57
114
|
function validateMemoryInput(key, value, query) {
|
|
58
115
|
if (key && key.length > MAX_KEY_LENGTH) {
|
|
59
116
|
throw new Error(`Key exceeds maximum length of ${MAX_KEY_LENGTH} characters`);
|
|
@@ -220,7 +277,7 @@ export const memoryTools = [
|
|
|
220
277
|
},
|
|
221
278
|
{
|
|
222
279
|
name: 'memory_retrieve',
|
|
223
|
-
description: 'Retrieve a value from memory by key',
|
|
280
|
+
description: 'Retrieve a value from memory by key. Chunk entries also return a full `navigation` object — use it with memory_get_neighbors for traversal instead of bulk-retrieving every search hit.',
|
|
224
281
|
category: 'memory',
|
|
225
282
|
inputSchema: {
|
|
226
283
|
type: 'object',
|
|
@@ -238,27 +295,9 @@ export const memoryTools = [
|
|
|
238
295
|
try {
|
|
239
296
|
const result = await getEntry({ key, namespace });
|
|
240
297
|
if (result.found && result.entry) {
|
|
241
|
-
// Try to parse JSON value
|
|
242
|
-
let value = result.entry.content;
|
|
243
|
-
try {
|
|
244
|
-
value = JSON.parse(result.entry.content);
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
// Keep as string
|
|
248
|
-
}
|
|
249
298
|
notifyMemoryGate();
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
namespace,
|
|
253
|
-
value,
|
|
254
|
-
tags: result.entry.tags,
|
|
255
|
-
storedAt: result.entry.createdAt,
|
|
256
|
-
updatedAt: result.entry.updatedAt,
|
|
257
|
-
accessCount: result.entry.accessCount,
|
|
258
|
-
hasEmbedding: result.entry.hasEmbedding,
|
|
259
|
-
found: true,
|
|
260
|
-
backend: 'sql.js + HNSW',
|
|
261
|
-
};
|
|
299
|
+
// #1053 S1: surface RAG navigation for chunked guidance entries.
|
|
300
|
+
return shapeRetrievedEntry(result.entry);
|
|
262
301
|
}
|
|
263
302
|
return {
|
|
264
303
|
key,
|
|
@@ -280,15 +319,15 @@ export const memoryTools = [
|
|
|
280
319
|
},
|
|
281
320
|
{
|
|
282
321
|
name: 'memory_search',
|
|
283
|
-
description: 'Semantic vector search using HNSW index (150x-12,500x faster than keyword search)',
|
|
322
|
+
description: 'Semantic vector search using HNSW index (150x-12,500x faster than keyword search). Results include a compact `navigation` crumb on chunk hits — traverse via memory_get_neighbors rather than retrieving every hit.',
|
|
284
323
|
category: 'memory',
|
|
285
324
|
inputSchema: {
|
|
286
325
|
type: 'object',
|
|
287
326
|
properties: {
|
|
288
327
|
query: { type: 'string', description: 'Search query (semantic similarity)' },
|
|
289
328
|
namespace: { type: 'string', description: 'Namespace to search (default: all namespaces)' },
|
|
290
|
-
limit: { type: 'number', description: 'Maximum results (default:
|
|
291
|
-
threshold: { type: 'number', description: 'Minimum similarity threshold 0-1 (default: 0.
|
|
329
|
+
limit: { type: 'number', description: 'Maximum results (default: 8)' },
|
|
330
|
+
threshold: { type: 'number', description: 'Minimum similarity threshold 0-1 (default: 0.5)' },
|
|
292
331
|
},
|
|
293
332
|
required: ['query'],
|
|
294
333
|
},
|
|
@@ -297,13 +336,13 @@ export const memoryTools = [
|
|
|
297
336
|
const { searchEntries } = await getMemoryFunctions();
|
|
298
337
|
const query = input.query;
|
|
299
338
|
const namespace = input.namespace || 'all';
|
|
300
|
-
|
|
301
|
-
|
|
339
|
+
// #1053 S6: tighter defaults — fewer hits, higher relevance bar.
|
|
340
|
+
const limit = input.limit || 8;
|
|
341
|
+
// Falsiness check would coerce a caller-supplied 0 to default and silently
|
|
302
342
|
// filter low-similarity matches; use a typeof guard so explicit zero
|
|
303
343
|
// means "no threshold" (#837).
|
|
304
|
-
const threshold = typeof input.threshold === 'number' ? input.threshold : 0.
|
|
344
|
+
const threshold = typeof input.threshold === 'number' ? input.threshold : 0.5;
|
|
305
345
|
validateMemoryInput(undefined, undefined, query);
|
|
306
|
-
const startTime = performance.now();
|
|
307
346
|
try {
|
|
308
347
|
const result = await searchEntries({
|
|
309
348
|
query,
|
|
@@ -311,7 +350,6 @@ export const memoryTools = [
|
|
|
311
350
|
limit,
|
|
312
351
|
threshold,
|
|
313
352
|
});
|
|
314
|
-
const duration = performance.now() - startTime;
|
|
315
353
|
// Parse JSON values in results
|
|
316
354
|
const results = result.results.map(r => {
|
|
317
355
|
let value = r.content;
|
|
@@ -321,19 +359,27 @@ export const memoryTools = [
|
|
|
321
359
|
catch {
|
|
322
360
|
// Keep as string
|
|
323
361
|
}
|
|
362
|
+
// #1053 S1: compact RAG navigation crumb per result.
|
|
363
|
+
// Compact subset is small enough to always include — keeps the
|
|
364
|
+
// result envelope navigable without ballooning per-hit size.
|
|
365
|
+
const navigation = parseNavigation(r.metadata, 'compact');
|
|
324
366
|
return {
|
|
325
367
|
key: r.key,
|
|
326
368
|
namespace: r.namespace,
|
|
327
369
|
value,
|
|
328
|
-
|
|
370
|
+
// #1053 S6: 2dp keeps signal, drops noise (8-decimal floats add ~6
|
|
371
|
+
// bytes per hit and don't help any caller).
|
|
372
|
+
similarity: Math.round(r.score * 100) / 100,
|
|
373
|
+
navigation,
|
|
329
374
|
};
|
|
330
375
|
});
|
|
331
376
|
notifyMemoryGate();
|
|
377
|
+
// #1053 S6: searchTime dropped from MCP envelope (CLI keeps it for
|
|
378
|
+
// human reading); `backend` retained — doctor reads it (#1053 epic).
|
|
332
379
|
return {
|
|
333
380
|
query,
|
|
334
381
|
results,
|
|
335
382
|
total: results.length,
|
|
336
|
-
searchTime: `${duration.toFixed(2)}ms`,
|
|
337
383
|
backend: 'HNSW + sql.js',
|
|
338
384
|
};
|
|
339
385
|
}
|
|
@@ -347,6 +393,98 @@ export const memoryTools = [
|
|
|
347
393
|
}
|
|
348
394
|
},
|
|
349
395
|
},
|
|
396
|
+
{
|
|
397
|
+
name: 'memory_get_neighbors',
|
|
398
|
+
description: 'Traverse the chunk graph in one call: fetch the requested neighbors (prev/next/siblings/parent/children) of a chunk key. Returns success:false if the source is not a chunk.',
|
|
399
|
+
category: 'memory',
|
|
400
|
+
inputSchema: {
|
|
401
|
+
type: 'object',
|
|
402
|
+
properties: {
|
|
403
|
+
key: { type: 'string', description: 'Source chunk key (must be a chunk-* entry)' },
|
|
404
|
+
namespace: { type: 'string', description: 'Namespace (default: "default")' },
|
|
405
|
+
include: {
|
|
406
|
+
type: 'array',
|
|
407
|
+
items: { type: 'string', enum: ['prev', 'next', 'siblings', 'parent', 'children'] },
|
|
408
|
+
description: "Which neighbors to fetch. Default: ['prev','next']. parent/children = hierarchical (h2→h3) chunk neighbors; siblings = same-doc chunk peers.",
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
required: ['key'],
|
|
412
|
+
},
|
|
413
|
+
handler: async (input) => {
|
|
414
|
+
await ensureInitialized();
|
|
415
|
+
const { getEntry } = await getMemoryFunctions();
|
|
416
|
+
const key = input.key;
|
|
417
|
+
const namespace = input.namespace || 'default';
|
|
418
|
+
const includeRaw = input.include;
|
|
419
|
+
const include = Array.isArray(includeRaw) && includeRaw.length > 0 ? includeRaw : ['prev', 'next'];
|
|
420
|
+
validateMemoryInput(key);
|
|
421
|
+
try {
|
|
422
|
+
const sourceResult = await getEntry({ key, namespace });
|
|
423
|
+
if (!sourceResult.found || !sourceResult.entry) {
|
|
424
|
+
return {
|
|
425
|
+
success: false,
|
|
426
|
+
key,
|
|
427
|
+
namespace,
|
|
428
|
+
error: `Source key '${key}' not found in namespace '${namespace}'`,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
const sourceMeta = sourceResult.entry.metadata;
|
|
432
|
+
const nav = parseNavigation(sourceMeta, 'full');
|
|
433
|
+
if (!nav) {
|
|
434
|
+
return {
|
|
435
|
+
success: false,
|
|
436
|
+
key,
|
|
437
|
+
namespace,
|
|
438
|
+
error: `Source key '${key}' has no chunk metadata; only chunk-* entries are navigable`,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
// Resolve requested neighbor keys, dedup, exclude the source key itself.
|
|
442
|
+
const neighborKeys = new Set();
|
|
443
|
+
const addIfChunkKey = (k) => {
|
|
444
|
+
if (k && k !== key)
|
|
445
|
+
neighborKeys.add(k);
|
|
446
|
+
};
|
|
447
|
+
for (const inc of include) {
|
|
448
|
+
if (inc === 'prev')
|
|
449
|
+
addIfChunkKey(nav.prevChunk);
|
|
450
|
+
else if (inc === 'next')
|
|
451
|
+
addIfChunkKey(nav.nextChunk);
|
|
452
|
+
else if (inc === 'siblings')
|
|
453
|
+
(nav.siblings ?? []).forEach(addIfChunkKey);
|
|
454
|
+
else if (inc === 'parent')
|
|
455
|
+
addIfChunkKey(nav.hierarchicalParent);
|
|
456
|
+
else if (inc === 'children')
|
|
457
|
+
(nav.hierarchicalChildren ?? []).forEach(addIfChunkKey);
|
|
458
|
+
}
|
|
459
|
+
// Parallel fetch — one round-trip from the caller's perspective.
|
|
460
|
+
// Missing neighbors (deleted/renamed) are silently skipped rather
|
|
461
|
+
// than failing the whole call; the response.total reflects what
|
|
462
|
+
// we actually returned.
|
|
463
|
+
const fetched = await Promise.all(Array.from(neighborKeys).map(async (k) => {
|
|
464
|
+
const res = await getEntry({ key: k, namespace });
|
|
465
|
+
return res.found && res.entry ? shapeRetrievedEntry(res.entry) : null;
|
|
466
|
+
}));
|
|
467
|
+
notifyMemoryGate();
|
|
468
|
+
const neighbors = fetched.filter((e) => e !== null);
|
|
469
|
+
return {
|
|
470
|
+
success: true,
|
|
471
|
+
source: { key, namespace },
|
|
472
|
+
include,
|
|
473
|
+
neighbors,
|
|
474
|
+
total: neighbors.length,
|
|
475
|
+
backend: 'sql.js + HNSW',
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
return {
|
|
480
|
+
success: false,
|
|
481
|
+
key,
|
|
482
|
+
namespace,
|
|
483
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
},
|
|
350
488
|
{
|
|
351
489
|
name: 'memory_delete',
|
|
352
490
|
description: 'Delete a memory entry by key',
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
* @module moflo/cli/memory/auto-memory-bridge
|
|
13
13
|
*/
|
|
14
14
|
import { createHash } from 'node:crypto';
|
|
15
|
-
import { homedir } from 'node:os';
|
|
16
15
|
import { EventEmitter } from 'node:events';
|
|
17
16
|
import * as fs from 'node:fs/promises';
|
|
18
17
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
19
18
|
import * as path from 'node:path';
|
|
19
|
+
import { claudeProjectDirFor } from '../shared/utils/claude-projects-path.js';
|
|
20
20
|
import { createDefaultEntry, } from './types.js';
|
|
21
21
|
import { LearningBridge } from './learning-bridge.js';
|
|
22
22
|
import { MemoryGraph } from './memory-graph.js';
|
|
@@ -539,20 +539,17 @@ export class AutoMemoryBridge extends EventEmitter {
|
|
|
539
539
|
// ===== Utility Functions =====
|
|
540
540
|
/**
|
|
541
541
|
* Resolve the auto memory directory for a given working directory.
|
|
542
|
-
*
|
|
542
|
+
*
|
|
543
|
+
* The git root is preferred over the raw cwd so a session in
|
|
544
|
+
* `<repo>/packages/foo` resolves to the repo's auto-memory store, matching
|
|
545
|
+
* Claude Code's own behaviour. The encoded suffix is produced by the shared
|
|
546
|
+
* `claudeProjectDirFor` helper so this bridge cannot drift away from the
|
|
547
|
+
* Claude Code directory naming convention (issue #1048).
|
|
543
548
|
*/
|
|
544
549
|
export function resolveAutoMemoryDir(workingDir) {
|
|
545
550
|
const gitRoot = findGitRoot(workingDir);
|
|
546
551
|
const basePath = gitRoot || workingDir;
|
|
547
|
-
|
|
548
|
-
// The leading dash IS preserved (e.g. /workspaces/foo -> -workspaces-foo)
|
|
549
|
-
// On Windows, strip drive letter prefix (C:) for cleaner keys
|
|
550
|
-
let normalized = basePath.split(path.sep).join('/');
|
|
551
|
-
if (process.platform === 'win32') {
|
|
552
|
-
normalized = normalized.replace(/^[A-Za-z]:/, '');
|
|
553
|
-
}
|
|
554
|
-
const projectKey = normalized.replace(/\//g, '-');
|
|
555
|
-
return path.join(homedir(), '.claude', 'projects', projectKey, 'memory');
|
|
552
|
+
return path.join(claudeProjectDirFor(basePath), 'memory');
|
|
556
553
|
}
|
|
557
554
|
/**
|
|
558
555
|
* Find the git root directory by walking up from workingDir.
|
|
@@ -403,7 +403,7 @@ export async function bridgeSearchEntries(options) {
|
|
|
403
403
|
let rows;
|
|
404
404
|
try {
|
|
405
405
|
const sql = `
|
|
406
|
-
SELECT id, key, namespace, content, embedding
|
|
406
|
+
SELECT id, key, namespace, content, metadata, embedding
|
|
407
407
|
FROM memory_entries
|
|
408
408
|
WHERE status = 'active' ${nsFilter}
|
|
409
409
|
LIMIT 1000
|
|
@@ -459,6 +459,7 @@ export async function bridgeSearchEntries(options) {
|
|
|
459
459
|
const provenance = usedSemantic
|
|
460
460
|
? `semantic:${semanticScore.toFixed(3)}+bm25:${bm25ScoreVal.toFixed(3)}`
|
|
461
461
|
: `bm25:${bm25ScoreVal.toFixed(3)}`;
|
|
462
|
+
const metadataStr = row.metadata != null ? String(row.metadata) : undefined;
|
|
462
463
|
results.push({
|
|
463
464
|
id: String(row.id).substring(0, 12),
|
|
464
465
|
// The substring is a fallback id-prefix when key is missing —
|
|
@@ -468,6 +469,7 @@ export async function bridgeSearchEntries(options) {
|
|
|
468
469
|
score,
|
|
469
470
|
namespace: String(row.namespace || 'default'),
|
|
470
471
|
provenance,
|
|
472
|
+
metadata: metadataStr,
|
|
471
473
|
});
|
|
472
474
|
}
|
|
473
475
|
}
|
|
@@ -542,13 +544,14 @@ export async function bridgeGetEntry(options) {
|
|
|
542
544
|
updatedAt: cached.updatedAt || new Date().toISOString(),
|
|
543
545
|
hasEmbedding: !!cached.embedding,
|
|
544
546
|
tags: cached.tags || [],
|
|
547
|
+
metadata: cached.metadata || undefined,
|
|
545
548
|
},
|
|
546
549
|
};
|
|
547
550
|
}
|
|
548
551
|
let row;
|
|
549
552
|
try {
|
|
550
553
|
const stmt = ctx.db.prepare(`
|
|
551
|
-
SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags
|
|
554
|
+
SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags, metadata
|
|
552
555
|
FROM memory_entries
|
|
553
556
|
WHERE status = 'active' AND key = ? AND namespace = ?
|
|
554
557
|
LIMIT 1
|
|
@@ -591,6 +594,7 @@ export async function bridgeGetEntry(options) {
|
|
|
591
594
|
updatedAt: row.updated_at || new Date().toISOString(),
|
|
592
595
|
hasEmbedding: !!(row.embedding && String(row.embedding).length > 10),
|
|
593
596
|
tags,
|
|
597
|
+
metadata: row.metadata != null ? String(row.metadata) : undefined,
|
|
594
598
|
};
|
|
595
599
|
await cacheSet(registry, cacheKey, entry);
|
|
596
600
|
return { success: true, found: true, cacheHit: false, entry };
|
|
@@ -424,8 +424,8 @@ export async function getHNSWIndex(options) {
|
|
|
424
424
|
// adjacency. When the sidecar IS loaded we skip the per-row JSON.parse
|
|
425
425
|
// of the embedding column, which is the expensive part on a populated
|
|
426
426
|
// consumer DB.
|
|
427
|
-
const SELECT_WITH_EMBEDDING = `id, key, namespace, content, embedding`;
|
|
428
|
-
const SELECT_METADATA_ONLY = `id, key, namespace, content`;
|
|
427
|
+
const SELECT_WITH_EMBEDDING = `id, key, namespace, content, metadata, embedding`;
|
|
428
|
+
const SELECT_METADATA_ONLY = `id, key, namespace, content, metadata`;
|
|
429
429
|
if (fs.existsSync(dbPath)) {
|
|
430
430
|
try {
|
|
431
431
|
const initSqlJs = (await mofloImport('sql.js')).default;
|
|
@@ -442,7 +442,9 @@ export async function getHNSWIndex(options) {
|
|
|
442
442
|
let parseSkipped = 0;
|
|
443
443
|
if (result[0]?.values) {
|
|
444
444
|
for (const row of result[0].values) {
|
|
445
|
-
|
|
445
|
+
// Column order matches SELECT_WITH_EMBEDDING / SELECT_METADATA_ONLY.
|
|
446
|
+
// When sidecar is loaded, embeddingJson is undefined (column absent).
|
|
447
|
+
const [id, key, ns, content, metadataJson, embeddingJson] = row;
|
|
446
448
|
if (!sidecarLoaded) {
|
|
447
449
|
const vec = parseEmbeddingJson(embeddingJson);
|
|
448
450
|
if (!vec) {
|
|
@@ -455,7 +457,8 @@ export async function getHNSWIndex(options) {
|
|
|
455
457
|
id: String(id),
|
|
456
458
|
key: key || String(id),
|
|
457
459
|
namespace: ns || 'default',
|
|
458
|
-
content: content || ''
|
|
460
|
+
content: content || '',
|
|
461
|
+
metadata: metadataJson || undefined
|
|
459
462
|
});
|
|
460
463
|
}
|
|
461
464
|
}
|
|
@@ -545,7 +548,8 @@ export async function searchHNSWIndex(queryEmbedding, options) {
|
|
|
545
548
|
key: entry.key || entry.id.substring(0, 15),
|
|
546
549
|
content: entry.content.substring(0, 60) + (entry.content.length > 60 ? '...' : ''),
|
|
547
550
|
score,
|
|
548
|
-
namespace: entry.namespace
|
|
551
|
+
namespace: entry.namespace,
|
|
552
|
+
metadata: entry.metadata
|
|
549
553
|
});
|
|
550
554
|
if (filtered.length >= k)
|
|
551
555
|
break;
|
|
@@ -1817,7 +1821,7 @@ export async function searchEntries(options) {
|
|
|
1817
1821
|
const db = new SQL.Database(fileBuffer);
|
|
1818
1822
|
// Get entries with embeddings
|
|
1819
1823
|
const entries = db.exec(`
|
|
1820
|
-
SELECT id, key, namespace, content, embedding
|
|
1824
|
+
SELECT id, key, namespace, content, metadata, embedding
|
|
1821
1825
|
FROM memory_entries
|
|
1822
1826
|
WHERE status = 'active'
|
|
1823
1827
|
${namespace !== 'all' ? `AND namespace = '${namespace.replace(/'/g, "''")}'` : ''}
|
|
@@ -1826,7 +1830,7 @@ export async function searchEntries(options) {
|
|
|
1826
1830
|
const results = [];
|
|
1827
1831
|
if (entries[0]?.values) {
|
|
1828
1832
|
for (const row of entries[0].values) {
|
|
1829
|
-
const [id, key, ns, content, embeddingJson] = row;
|
|
1833
|
+
const [id, key, ns, content, metadataJson, embeddingJson] = row;
|
|
1830
1834
|
let score = 0;
|
|
1831
1835
|
if (embeddingJson) {
|
|
1832
1836
|
try {
|
|
@@ -1849,7 +1853,8 @@ export async function searchEntries(options) {
|
|
|
1849
1853
|
key: key || id.substring(0, 15),
|
|
1850
1854
|
content: (content || '').substring(0, 60) + ((content || '').length > 60 ? '...' : ''),
|
|
1851
1855
|
score,
|
|
1852
|
-
namespace: ns || 'default'
|
|
1856
|
+
namespace: ns || 'default',
|
|
1857
|
+
metadata: metadataJson || undefined
|
|
1853
1858
|
});
|
|
1854
1859
|
}
|
|
1855
1860
|
}
|
|
@@ -1987,7 +1992,7 @@ export async function getEntry(options) {
|
|
|
1987
1992
|
const db = new SQL.Database(fileBuffer);
|
|
1988
1993
|
// Find entry by key
|
|
1989
1994
|
const result = db.exec(`
|
|
1990
|
-
SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags
|
|
1995
|
+
SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags, metadata
|
|
1991
1996
|
FROM memory_entries
|
|
1992
1997
|
WHERE status = 'active'
|
|
1993
1998
|
AND key = '${key.replace(/'/g, "''")}'
|
|
@@ -1998,7 +2003,7 @@ export async function getEntry(options) {
|
|
|
1998
2003
|
db.close();
|
|
1999
2004
|
return { success: true, found: false };
|
|
2000
2005
|
}
|
|
2001
|
-
const [id, entryKey, ns, content, embedding, accessCount, createdAt, updatedAt, tagsJson] = result[0].values[0];
|
|
2006
|
+
const [id, entryKey, ns, content, embedding, accessCount, createdAt, updatedAt, tagsJson, metadataJson] = result[0].values[0];
|
|
2002
2007
|
// Update access count
|
|
2003
2008
|
db.run(`
|
|
2004
2009
|
UPDATE memory_entries
|
|
@@ -2028,7 +2033,8 @@ export async function getEntry(options) {
|
|
|
2028
2033
|
createdAt: createdAt || new Date().toISOString(),
|
|
2029
2034
|
updatedAt: updatedAt || new Date().toISOString(),
|
|
2030
2035
|
hasEmbedding: !!embedding && embedding.length > 10,
|
|
2031
|
-
tags
|
|
2036
|
+
tags,
|
|
2037
|
+
metadata: metadataJson || undefined
|
|
2032
2038
|
}
|
|
2033
2039
|
};
|
|
2034
2040
|
}
|
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { createReadStream } from 'node:fs';
|
|
24
24
|
import { stat, readdir } from 'node:fs/promises';
|
|
25
|
-
import { homedir } from 'node:os';
|
|
26
25
|
import { join } from 'node:path';
|
|
27
26
|
import { createInterface } from 'node:readline';
|
|
27
|
+
import { claudeProjectDirFor, encodeCwdForClaudeProjects, } from '../shared/utils/claude-projects-path.js';
|
|
28
|
+
export { claudeProjectDirFor, encodeCwdForClaudeProjects };
|
|
28
29
|
/**
|
|
29
30
|
* Map a transcript model name to a stable display key. Recognises bare family
|
|
30
31
|
* names ("opus", "sonnet", "haiku"), dated/dotted variants
|
|
@@ -43,21 +44,6 @@ export function canonicalModelKey(model) {
|
|
|
43
44
|
return 'haiku';
|
|
44
45
|
return 'unknown';
|
|
45
46
|
}
|
|
46
|
-
// ============================================================================
|
|
47
|
-
// Path resolution
|
|
48
|
-
// ============================================================================
|
|
49
|
-
/**
|
|
50
|
-
* Encode a CWD the same way Claude Code does for `~/.claude/projects/<dir>`.
|
|
51
|
-
* Replaces `\`, `/`, and `:` with `-` so `C:\Users\eric\Projects\moflo` →
|
|
52
|
-
* `C--Users-eric-Projects-moflo`.
|
|
53
|
-
*/
|
|
54
|
-
export function encodeCwdForClaudeProjects(cwd) {
|
|
55
|
-
return cwd.replace(/[\\/:]/g, '-');
|
|
56
|
-
}
|
|
57
|
-
/** Absolute path to `~/.claude/projects/<encoded-cwd>` for the given CWD. */
|
|
58
|
-
export function claudeProjectDirFor(cwd) {
|
|
59
|
-
return join(homedir(), '.claude', 'projects', encodeCwdForClaudeProjects(cwd));
|
|
60
|
-
}
|
|
61
47
|
const CACHE_TTL_MS = 30_000;
|
|
62
48
|
let cache = null;
|
|
63
49
|
/** Test-only — drop the in-memory cache so the next call re-aggregates. */
|
|
@@ -21,7 +21,7 @@ const BOOTSTRAP_JSON_REL = '.claude/helpers/subagent-bootstrap.json';
|
|
|
21
21
|
// Defense-in-depth copy of the canonical directive in subagent-bootstrap.json.
|
|
22
22
|
// Kept as a single-line literal so the parity test can verify it matches the
|
|
23
23
|
// JSON via plain substring containment.
|
|
24
|
-
const FALLBACK_DIRECTIVE = 'MANDATORY FIRST ACTION: Your very first tool call MUST be mcp__moflo__memory_search (any query, any namespace). The memory-first gate WILL BLOCK all Glob, Grep, and Read calls until you do this. After memory search, follow `.claude/guidance/moflo-subagents.md` protocol.';
|
|
24
|
+
const FALLBACK_DIRECTIVE = 'MANDATORY FIRST ACTION: Your very first tool call MUST be mcp__moflo__memory_search (any query, any namespace). The memory-first gate WILL BLOCK all Glob, Grep, and Read calls until you do this. After memory search, follow `.claude/guidance/moflo-subagents.md` protocol. When search returns chunk hits, traverse via mcp__moflo__memory_get_neighbors before retrieving — see `.claude/guidance/moflo-memory-protocol.md`.';
|
|
25
25
|
function loadDirective() {
|
|
26
26
|
const jsonPath = locateMofloRootPath(BOOTSTRAP_JSON_REL);
|
|
27
27
|
if (!jsonPath) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encode a working directory the same way Claude Code does for its
|
|
3
|
+
* `~/.claude/projects/<dir>/` transcript & memory store.
|
|
4
|
+
*
|
|
5
|
+
* Claude Code replaces *every* non-alphanumeric character in the absolute
|
|
6
|
+
* path with `-`. Earlier moflo versions used a narrower class (`/[\\/:]/g`,
|
|
7
|
+
* or split-and-rejoin variants) which agreed with Claude Code only for
|
|
8
|
+
* paths whose every other character was alphanumeric — issue #1048.
|
|
9
|
+
*
|
|
10
|
+
* Centralised here so the stats aggregator and the auto-memory bridge
|
|
11
|
+
* cannot drift apart.
|
|
12
|
+
*/
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
/**
|
|
16
|
+
* Encode a CWD to the form Claude Code uses as a `~/.claude/projects/`
|
|
17
|
+
* subdirectory name. Replaces every non-alphanumeric character with `-`.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* encodeCwdForClaudeProjects('C:\\Users\\me\\some_project')
|
|
21
|
+
* → 'C--Users-me-some-project'
|
|
22
|
+
* encodeCwdForClaudeProjects('/Users/me/dev/some_project')
|
|
23
|
+
* → '-Users-me-dev-some-project'
|
|
24
|
+
*/
|
|
25
|
+
export function encodeCwdForClaudeProjects(cwd) {
|
|
26
|
+
return cwd.replace(/[^A-Za-z0-9]/g, '-');
|
|
27
|
+
}
|
|
28
|
+
/** Absolute path to `~/.claude/projects/<encoded-cwd>` for the given CWD. */
|
|
29
|
+
export function claudeProjectDirFor(cwd) {
|
|
30
|
+
return join(homedir(), '.claude', 'projects', encodeCwdForClaudeProjects(cwd));
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=claude-projects-path.js.map
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.9.
|
|
3
|
+
"version": "4.9.37",
|
|
4
4
|
"description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
|
|
5
5
|
"main": "dist/src/cli/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
98
98
|
"@typescript-eslint/parser": "^7.18.0",
|
|
99
99
|
"eslint": "^8.0.0",
|
|
100
|
-
"moflo": "^4.9.
|
|
100
|
+
"moflo": "^4.9.36",
|
|
101
101
|
"tsx": "^4.21.0",
|
|
102
102
|
"typescript": "^5.9.3",
|
|
103
103
|
"vitest": "^4.0.0"
|