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.
@@ -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
- return {
251
- key,
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: 10)' },
291
- threshold: { type: 'number', description: 'Minimum similarity threshold 0-1 (default: 0.3)' },
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
- const limit = input.limit || 10;
301
- // Falsiness check would coerce a caller-supplied 0 to 0.3 and silently
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.3;
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
- similarity: r.score,
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
- * Mirrors Claude Code's path derivation from git root.
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
- // Claude Code normalizes to forward slashes then replaces with dashes
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
- const [id, key, ns, content, embeddingJson] = row;
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
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.9.35';
5
+ export const VERSION = '4.9.37';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.9.35",
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.34",
100
+ "moflo": "^4.9.36",
101
101
  "tsx": "^4.21.0",
102
102
  "typescript": "^5.9.3",
103
103
  "vitest": "^4.0.0"