@hbarefoot/engram 1.2.0 → 1.3.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.
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" type="image/png" href="/favicon.png" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>Engram Dashboard</title>
8
- <script type="module" crossorigin src="/assets/index-D3bysGhj.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-D0xT6oKC.css">
8
+ <script type="module" crossorigin src="/assets/index-CK-bEXRL.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-CIMIyJGP.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hbarefoot/engram",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Persistent memory for AI agents. SQLite for agent state.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import os from 'os';
3
4
  import { validateContent } from '../../extract/secrets.js';
4
5
 
5
6
  const CLAUDE_LOCATIONS = [
@@ -11,43 +12,94 @@ const CLAUDE_LOCATIONS = [
11
12
 
12
13
  /**
13
14
  * Detect if .claude project files exist
15
+ * @param {Object} [options] - Detection options
16
+ * @param {string} [options.cwd] - Working directory to scan
17
+ * @param {string[]} [options.paths] - Additional directories to scan
18
+ * @returns {{ found: boolean, path: string|null, paths: string[] }}
14
19
  */
15
20
  export function detect(options = {}) {
16
21
  const cwd = options.cwd || process.cwd();
22
+ const foundPaths = [];
23
+ const seen = new Set();
17
24
 
25
+ // Check cwd
18
26
  for (const loc of CLAUDE_LOCATIONS) {
19
27
  const fullPath = path.resolve(cwd, loc);
20
28
  if (fs.existsSync(fullPath)) {
21
- return { found: true, path: path.resolve(cwd) };
29
+ const dir = path.resolve(cwd);
30
+ if (!seen.has(dir)) {
31
+ seen.add(dir);
32
+ foundPaths.push(dir);
33
+ }
34
+ break;
22
35
  }
23
36
  }
24
37
 
25
- return { found: false, path: null };
38
+ // Always check home directory for user-level .claude
39
+ const homeDir = os.homedir();
40
+ for (const loc of CLAUDE_LOCATIONS) {
41
+ const fullPath = path.resolve(homeDir, loc);
42
+ if (fs.existsSync(fullPath)) {
43
+ const dir = path.resolve(homeDir);
44
+ if (!seen.has(dir)) {
45
+ seen.add(dir);
46
+ foundPaths.push(dir);
47
+ }
48
+ break;
49
+ }
50
+ }
51
+
52
+ // Check additional paths
53
+ if (options.paths && Array.isArray(options.paths)) {
54
+ for (const extraDir of options.paths) {
55
+ for (const loc of CLAUDE_LOCATIONS) {
56
+ const fullPath = path.resolve(extraDir, loc);
57
+ if (fs.existsSync(fullPath)) {
58
+ const dir = path.resolve(extraDir);
59
+ if (!seen.has(dir)) {
60
+ seen.add(dir);
61
+ foundPaths.push(dir);
62
+ }
63
+ break;
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ return {
70
+ found: foundPaths.length > 0,
71
+ path: foundPaths[0] || null,
72
+ paths: foundPaths
73
+ };
26
74
  }
27
75
 
28
76
  /**
29
77
  * Parse .claude project files into memory candidates
78
+ * Scans cwd, home directory, and any additional paths
30
79
  */
31
80
  export async function parse(options = {}) {
32
81
  const result = { source: 'claude', memories: [], skipped: [], warnings: [] };
33
- const cwd = options.cwd || process.cwd();
82
+ const detected = detect(options);
83
+ const dirsToScan = detected.paths.length > 0 ? detected.paths : [options.cwd || process.cwd()];
34
84
 
35
- // Parse CLAUDE.md
36
- const claudeMdPath = path.resolve(cwd, 'CLAUDE.md');
37
- if (fs.existsSync(claudeMdPath)) {
38
- parseClaudeMd(claudeMdPath, result);
39
- }
85
+ for (const dir of dirsToScan) {
86
+ // Parse CLAUDE.md
87
+ const claudeMdPath = path.resolve(dir, 'CLAUDE.md');
88
+ if (fs.existsSync(claudeMdPath)) {
89
+ parseClaudeMd(claudeMdPath, result);
90
+ }
40
91
 
41
- // Parse .claude/settings.json
42
- const settingsPath = path.resolve(cwd, '.claude/settings.json');
43
- if (fs.existsSync(settingsPath)) {
44
- parseClaudeSettings(settingsPath, result);
45
- }
92
+ // Parse .claude/settings.json
93
+ const settingsPath = path.resolve(dir, '.claude/settings.json');
94
+ if (fs.existsSync(settingsPath)) {
95
+ parseClaudeSettings(settingsPath, result);
96
+ }
46
97
 
47
- // Parse .claude/commands directory
48
- const commandsPath = path.resolve(cwd, '.claude/commands');
49
- if (fs.existsSync(commandsPath) && fs.statSync(commandsPath).isDirectory()) {
50
- parseClaudeCommands(commandsPath, result);
98
+ // Parse .claude/commands directory
99
+ const commandsPath = path.resolve(dir, '.claude/commands');
100
+ if (fs.existsSync(commandsPath) && fs.statSync(commandsPath).isDirectory()) {
101
+ parseClaudeCommands(commandsPath, result);
102
+ }
51
103
  }
52
104
 
53
105
  if (result.memories.length === 0) {
@@ -15,25 +15,48 @@ const CURSORRULES_LOCATIONS = [
15
15
  * Detect if .cursorrules exists
16
16
  * @param {Object} options
17
17
  * @param {string} [options.cwd] - Working directory to scan
18
- * @returns {Object} Detection result
18
+ * @param {string[]} [options.paths] - Additional directories to scan
19
+ * @returns {{ found: boolean, path: string|null, paths: string[] }}
19
20
  */
20
21
  export function detect(options = {}) {
21
22
  const cwd = options.cwd || process.cwd();
23
+ const foundPaths = [];
24
+ const seen = new Set();
22
25
 
26
+ // Check cwd
23
27
  for (const loc of CURSORRULES_LOCATIONS) {
24
28
  const fullPath = path.resolve(cwd, loc);
25
- if (fs.existsSync(fullPath)) {
26
- return { found: true, path: fullPath };
29
+ if (!seen.has(fullPath) && fs.existsSync(fullPath)) {
30
+ seen.add(fullPath);
31
+ foundPaths.push(fullPath);
27
32
  }
28
33
  }
29
34
 
30
- // Also check home directory
31
- const homePath = path.join(os.homedir(), '.cursorrules');
32
- if (fs.existsSync(homePath)) {
33
- return { found: true, path: homePath };
35
+ // Check home directory
36
+ const homePath = path.resolve(os.homedir(), '.cursorrules');
37
+ if (!seen.has(homePath) && fs.existsSync(homePath)) {
38
+ seen.add(homePath);
39
+ foundPaths.push(homePath);
34
40
  }
35
41
 
36
- return { found: false, path: null };
42
+ // Check additional paths
43
+ if (options.paths && Array.isArray(options.paths)) {
44
+ for (const dir of options.paths) {
45
+ for (const loc of CURSORRULES_LOCATIONS) {
46
+ const fullPath = path.resolve(dir, loc);
47
+ if (!seen.has(fullPath) && fs.existsSync(fullPath)) {
48
+ seen.add(fullPath);
49
+ foundPaths.push(fullPath);
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ return {
56
+ found: foundPaths.length > 0,
57
+ path: foundPaths[0] || null,
58
+ paths: foundPaths
59
+ };
37
60
  }
38
61
 
39
62
  /**
@@ -46,20 +69,38 @@ export function detect(options = {}) {
46
69
  export async function parse(options = {}) {
47
70
  const result = { source: 'cursorrules', memories: [], skipped: [], warnings: [] };
48
71
 
49
- const filePath = options.filePath || (() => {
50
- const detected = detect(options);
51
- return detected.path;
52
- })();
72
+ // If explicit filePath, parse just that one (backward compat)
73
+ if (options.filePath) {
74
+ parseOneCursorrules(options.filePath, result);
75
+ return result;
76
+ }
53
77
 
54
- if (!filePath || !fs.existsSync(filePath)) {
78
+ const detected = detect(options);
79
+ if (!detected.found) {
55
80
  result.warnings.push('No .cursorrules file found');
56
81
  return result;
57
82
  }
58
83
 
84
+ for (const filePath of detected.paths) {
85
+ parseOneCursorrules(filePath, result);
86
+ }
87
+
88
+ return result;
89
+ }
90
+
91
+ /**
92
+ * Parse a single .cursorrules file and accumulate results
93
+ */
94
+ function parseOneCursorrules(filePath, result) {
95
+ if (!filePath || !fs.existsSync(filePath)) {
96
+ result.warnings.push('No .cursorrules file found');
97
+ return;
98
+ }
99
+
59
100
  const content = fs.readFileSync(filePath, 'utf-8').trim();
60
101
  if (!content) {
61
102
  result.warnings.push('.cursorrules file is empty');
62
- return result;
103
+ return;
63
104
  }
64
105
 
65
106
  const lines = content.split('\n');
@@ -109,8 +150,6 @@ export async function parse(options = {}) {
109
150
  if (currentBlock.length > 0) {
110
151
  flushBlock(currentBlock, currentSection, result);
111
152
  }
112
-
113
- return result;
114
153
  }
115
154
 
116
155
  /**
@@ -3,19 +3,44 @@ import path from 'path';
3
3
 
4
4
  /**
5
5
  * Detect if .env.example exists
6
+ * @param {Object} [options] - Detection options
7
+ * @param {string} [options.cwd] - Working directory to scan
8
+ * @param {string[]} [options.paths] - Additional directories to scan
9
+ * @returns {{ found: boolean, path: string|null, paths: string[] }}
6
10
  */
7
11
  export function detect(options = {}) {
8
12
  const candidates = ['.env.example', '.env.sample', '.env.template'];
9
13
  const baseDir = options.cwd || process.cwd();
14
+ const foundPaths = [];
15
+ const seen = new Set();
10
16
 
17
+ // Check cwd
11
18
  for (const name of candidates) {
12
19
  const filePath = path.resolve(baseDir, name);
13
- if (fs.existsSync(filePath)) {
14
- return { found: true, path: filePath };
20
+ if (!seen.has(filePath) && fs.existsSync(filePath)) {
21
+ seen.add(filePath);
22
+ foundPaths.push(filePath);
15
23
  }
16
24
  }
17
25
 
18
- return { found: false, path: null };
26
+ // Check additional paths
27
+ if (options.paths && Array.isArray(options.paths)) {
28
+ for (const dir of options.paths) {
29
+ for (const name of candidates) {
30
+ const filePath = path.resolve(dir, name);
31
+ if (!seen.has(filePath) && fs.existsSync(filePath)) {
32
+ seen.add(filePath);
33
+ foundPaths.push(filePath);
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ return {
40
+ found: foundPaths.length > 0,
41
+ path: foundPaths[0] || null,
42
+ paths: foundPaths
43
+ };
19
44
  }
20
45
 
21
46
  /**
@@ -25,16 +50,34 @@ export function detect(options = {}) {
25
50
  export async function parse(options = {}) {
26
51
  const result = { source: 'env', memories: [], skipped: [], warnings: [] };
27
52
 
28
- const filePath = options.filePath || (() => {
29
- const detected = detect(options);
30
- return detected.path;
31
- })();
53
+ // If explicit filePath, parse just that one (backward compat)
54
+ if (options.filePath) {
55
+ parseOneEnv(options.filePath, result);
56
+ return result;
57
+ }
32
58
 
33
- if (!filePath || !fs.existsSync(filePath)) {
59
+ const detected = detect(options);
60
+ if (!detected.found) {
34
61
  result.warnings.push('No .env.example file found');
35
62
  return result;
36
63
  }
37
64
 
65
+ for (const filePath of detected.paths) {
66
+ parseOneEnv(filePath, result);
67
+ }
68
+
69
+ return result;
70
+ }
71
+
72
+ /**
73
+ * Parse a single .env.example file and accumulate results
74
+ */
75
+ function parseOneEnv(filePath, result) {
76
+ if (!fs.existsSync(filePath)) {
77
+ result.warnings.push(`No .env.example file found at ${filePath}`);
78
+ return;
79
+ }
80
+
38
81
  const content = fs.readFileSync(filePath, 'utf-8');
39
82
  const lines = content.split('\n');
40
83
 
@@ -68,7 +111,7 @@ export async function parse(options = {}) {
68
111
 
69
112
  if (variables.length === 0) {
70
113
  result.warnings.push('.env.example has no variables');
71
- return result;
114
+ return;
72
115
  }
73
116
 
74
117
  // Group variables by prefix for cleaner memories
@@ -116,8 +159,6 @@ export async function parse(options = {}) {
116
159
 
117
160
  // Security warning
118
161
  result.warnings.push('Only variable NAMES were extracted — no values or secrets');
119
-
120
- return result;
121
162
  }
122
163
 
123
164
  export const meta = {
@@ -10,14 +10,41 @@ const GITCONFIG_LOCATIONS = [
10
10
 
11
11
  /**
12
12
  * Detect if git config exists
13
+ * @param {Object} [options] - Detection options
14
+ * @param {string[]} [options.paths] - Additional directories to scan for gitconfig
15
+ * @returns {{ found: boolean, path: string|null, paths: string[] }}
13
16
  */
14
- export function detect() {
17
+ export function detect(options = {}) {
18
+ const foundPaths = [];
19
+ const seen = new Set();
20
+
15
21
  for (const loc of GITCONFIG_LOCATIONS) {
16
- if (fs.existsSync(loc)) {
17
- return { found: true, path: loc };
22
+ const resolved = path.resolve(loc);
23
+ if (!seen.has(resolved) && fs.existsSync(resolved)) {
24
+ seen.add(resolved);
25
+ foundPaths.push(resolved);
26
+ }
27
+ }
28
+
29
+ // Check additional paths for gitconfig files
30
+ if (options.paths && Array.isArray(options.paths)) {
31
+ for (const dir of options.paths) {
32
+ for (const name of ['.gitconfig', '.config/git/config']) {
33
+ const loc = path.join(dir, name);
34
+ const resolved = path.resolve(loc);
35
+ if (!seen.has(resolved) && fs.existsSync(resolved)) {
36
+ seen.add(resolved);
37
+ foundPaths.push(resolved);
38
+ }
39
+ }
18
40
  }
19
41
  }
20
- return { found: false, path: null };
42
+
43
+ return {
44
+ found: foundPaths.length > 0,
45
+ path: foundPaths[0] || null,
46
+ paths: foundPaths
47
+ };
21
48
  }
22
49
 
23
50
  /**
@@ -26,16 +53,34 @@ export function detect() {
26
53
  export async function parse(options = {}) {
27
54
  const result = { source: 'git', memories: [], skipped: [], warnings: [] };
28
55
 
29
- const filePath = options.filePath || (() => {
30
- const detected = detect();
31
- return detected.path;
32
- })();
56
+ // If explicit filePath, parse just that one (backward compat)
57
+ if (options.filePath) {
58
+ parseOneGitconfig(options.filePath, result);
59
+ return result;
60
+ }
33
61
 
34
- if (!filePath || !fs.existsSync(filePath)) {
62
+ const detected = detect(options);
63
+ if (!detected.found) {
35
64
  result.warnings.push('No .gitconfig found');
36
65
  return result;
37
66
  }
38
67
 
68
+ for (const filePath of detected.paths) {
69
+ parseOneGitconfig(filePath, result);
70
+ }
71
+
72
+ return result;
73
+ }
74
+
75
+ /**
76
+ * Parse a single .gitconfig file and accumulate results
77
+ */
78
+ function parseOneGitconfig(filePath, result) {
79
+ if (!filePath || !fs.existsSync(filePath)) {
80
+ result.warnings.push('No .gitconfig found');
81
+ return;
82
+ }
83
+
39
84
  const content = fs.readFileSync(filePath, 'utf-8');
40
85
  const sections = parseIniFile(content);
41
86
 
@@ -146,8 +191,6 @@ export async function parse(options = {}) {
146
191
  source: 'import:git'
147
192
  });
148
193
  }
149
-
150
- return result;
151
194
  }
152
195
 
153
196
  /**
@@ -15,9 +15,24 @@ const VAULT_SEARCH_DIRS = [
15
15
 
16
16
  /**
17
17
  * Detect Obsidian vaults by looking for .obsidian directories
18
+ * @param {Object} [options] - Detection options
19
+ * @param {string[]} [options.vaultPaths] - Explicit vault paths to check
20
+ * @param {string[]} [options.paths] - Additional directories to scan for vaults
21
+ * @returns {{ found: boolean, path: string|null, paths: string[], vaults: string[] }}
18
22
  */
19
23
  export function detect(options = {}) {
20
- const searchDirs = options.vaultPaths || VAULT_SEARCH_DIRS;
24
+ const searchDirs = [...(options.vaultPaths || VAULT_SEARCH_DIRS)];
25
+
26
+ // Merge additional paths into search dirs
27
+ if (options.paths && Array.isArray(options.paths)) {
28
+ for (const dir of options.paths) {
29
+ const resolved = path.resolve(dir);
30
+ if (!searchDirs.includes(resolved)) {
31
+ searchDirs.push(resolved);
32
+ }
33
+ }
34
+ }
35
+
21
36
  const vaults = [];
22
37
 
23
38
  for (const dir of searchDirs) {
@@ -37,7 +52,7 @@ export function detect(options = {}) {
37
52
  }
38
53
  }
39
54
 
40
- return { found: vaults.length > 0, path: vaults[0] || null, vaults };
55
+ return { found: vaults.length > 0, path: vaults[0] || null, paths: vaults, vaults };
41
56
  }
42
57
 
43
58
  /**
@@ -3,11 +3,39 @@ import path from 'path';
3
3
 
4
4
  /**
5
5
  * Detect if package.json exists
6
+ * @param {Object} [options] - Detection options
7
+ * @param {string} [options.cwd] - Working directory to scan
8
+ * @param {string[]} [options.paths] - Additional directories to scan
9
+ * @returns {{ found: boolean, path: string|null, paths: string[] }}
6
10
  */
7
11
  export function detect(options = {}) {
8
12
  const cwd = options.cwd || process.cwd();
13
+ const foundPaths = [];
14
+ const seen = new Set();
15
+
16
+ // Check cwd
9
17
  const filePath = path.resolve(cwd, 'package.json');
10
- return { found: fs.existsSync(filePath), path: fs.existsSync(filePath) ? filePath : null };
18
+ if (fs.existsSync(filePath)) {
19
+ seen.add(filePath);
20
+ foundPaths.push(filePath);
21
+ }
22
+
23
+ // Check additional paths
24
+ if (options.paths && Array.isArray(options.paths)) {
25
+ for (const dir of options.paths) {
26
+ const p = path.resolve(dir, 'package.json');
27
+ if (!seen.has(p) && fs.existsSync(p)) {
28
+ seen.add(p);
29
+ foundPaths.push(p);
30
+ }
31
+ }
32
+ }
33
+
34
+ return {
35
+ found: foundPaths.length > 0,
36
+ path: foundPaths[0] || null,
37
+ paths: foundPaths
38
+ };
11
39
  }
12
40
 
13
41
  /**
@@ -15,20 +43,41 @@ export function detect(options = {}) {
15
43
  */
16
44
  export async function parse(options = {}) {
17
45
  const result = { source: 'package', memories: [], skipped: [], warnings: [] };
18
- const cwd = options.cwd || process.cwd();
19
- const filePath = options.filePath || path.resolve(cwd, 'package.json');
20
46
 
21
- if (!fs.existsSync(filePath)) {
47
+ // If explicit filePath, parse just that one (backward compat)
48
+ if (options.filePath) {
49
+ parseOnePackage(options.filePath, result);
50
+ return result;
51
+ }
52
+
53
+ const detected = detect(options);
54
+ if (!detected.found) {
22
55
  result.warnings.push('No package.json found');
23
56
  return result;
24
57
  }
25
58
 
59
+ for (const filePath of detected.paths) {
60
+ parseOnePackage(filePath, result);
61
+ }
62
+
63
+ return result;
64
+ }
65
+
66
+ /**
67
+ * Parse a single package.json file and accumulate results
68
+ */
69
+ function parseOnePackage(filePath, result) {
70
+ if (!fs.existsSync(filePath)) {
71
+ result.warnings.push(`No package.json found at ${filePath}`);
72
+ return;
73
+ }
74
+
26
75
  let pkg;
27
76
  try {
28
77
  pkg = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
29
78
  } catch {
30
- result.warnings.push('Failed to parse package.json');
31
- return result;
79
+ result.warnings.push(`Failed to parse ${filePath}`);
80
+ return;
32
81
  }
33
82
 
34
83
  // Project name and description
@@ -164,8 +213,6 @@ export async function parse(options = {}) {
164
213
  });
165
214
  }
166
215
  }
167
-
168
- return result;
169
216
  }
170
217
 
171
218
  export const meta = {