carto-md 1.0.2 → 1.0.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carto-md",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "The context layer for AI-native development.",
5
5
  "bin": {
6
6
  "carto": "src/cli/index.js"
@@ -9,10 +9,11 @@
9
9
  * 5. Functions (auto)
10
10
  * 6. Database Tables (auto)
11
11
  * 7. Environment Variables (auto)
12
- * 8. Frontend API Calls (auto)
13
- * 9. Frontend Storage Keys (auto)
12
+ * 8. File Relationships (auto)
13
+ * 9. Frontend API Calls (auto)
14
+ * 10. Frontend Storage Keys (auto)
14
15
  */
15
- function formatSections({ routes, models, frontend, structure, warnings, fileMap, functions, dbTables, envVars }) {
16
+ function formatSections({ routes, models, frontend, structure, warnings, fileMap, functions, dbTables, envVars, importGraph }) {
16
17
  const sections = [];
17
18
 
18
19
  // 1. Project Structure
@@ -114,7 +115,21 @@ function formatSections({ routes, models, frontend, structure, warnings, fileMap
114
115
  sections.push('_No environment variables detected._');
115
116
  }
116
117
 
117
- // 8. Frontend API Calls
118
+ // 8. File Relationships
119
+ sections.push('\n## File Relationships (auto)\n');
120
+ if (importGraph && Object.keys(importGraph).length > 0) {
121
+ const sortedFiles = Object.keys(importGraph).sort();
122
+ for (const file of sortedFiles) {
123
+ const deps = importGraph[file];
124
+ if (deps.length > 0) {
125
+ sections.push(`${file} \u2192 ${deps.join(', ')}`);
126
+ }
127
+ }
128
+ } else {
129
+ sections.push('_No file relationships detected._');
130
+ }
131
+
132
+ // 9. Frontend API Calls
118
133
  sections.push('\n## Frontend API Calls (auto)\n');
119
134
  if (frontend.fetches.length > 0) {
120
135
  sections.push('| Method | URL |');
@@ -126,7 +141,7 @@ function formatSections({ routes, models, frontend, structure, warnings, fileMap
126
141
  sections.push('_No fetch calls found._');
127
142
  }
128
143
 
129
- // 9. Frontend Storage Keys
144
+ // 10. Frontend Storage Keys
130
145
  sections.push('\n## Frontend Storage Keys (auto)\n');
131
146
  if (frontend.storageKeys.length > 0) {
132
147
  sections.push('| Operation | Key |');
package/src/cli/init.js CHANGED
@@ -55,11 +55,14 @@ async function run(projectRoot) {
55
55
  'utf-8'
56
56
  );
57
57
 
58
+ // Install pre-commit hook
59
+ installGitHook(projectRoot);
60
+
58
61
  // Run first sync
59
62
  const syncConfig = resolveConfig(projectRoot, config);
60
63
  await runFullSync(syncConfig);
61
64
 
62
- console.log('[CARTO] AGENTS.md generated. Run "carto watch" to keep it live.');
65
+ console.log('[CARTO] AGENTS.md generated. Carto will sync on every git commit.');
63
66
  }
64
67
 
65
68
  /**
@@ -77,4 +80,29 @@ function resolveConfig(projectRoot, config) {
77
80
  };
78
81
  }
79
82
 
83
+ function installGitHook(projectRoot) {
84
+ const gitDir = path.join(projectRoot, '.git');
85
+ if (!fs.existsSync(gitDir)) return;
86
+
87
+ const hooksDir = path.join(gitDir, 'hooks');
88
+ if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true });
89
+
90
+ const hookPath = path.join(hooksDir, 'pre-commit');
91
+ const hookLine = 'carto sync\n';
92
+
93
+ if (fs.existsSync(hookPath)) {
94
+ const existing = fs.readFileSync(hookPath, 'utf-8');
95
+ if (existing.includes('carto sync')) {
96
+ console.log('[CARTO] Git hook already installed.');
97
+ return;
98
+ }
99
+ fs.appendFileSync(hookPath, '\n' + hookLine);
100
+ } else {
101
+ fs.writeFileSync(hookPath, '#!/bin/sh\n' + hookLine);
102
+ }
103
+
104
+ fs.chmodSync(hookPath, '755');
105
+ console.log('[CARTO] Git pre-commit hook installed.');
106
+ }
107
+
80
108
  module.exports = { run, resolveConfig };
@@ -0,0 +1,212 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ /**
5
+ * extractImports(content, filePath, projectRoot) → Array<string>
6
+ *
7
+ * Extracts relative import paths from a source file.
8
+ * Returns resolved relative paths (from project root) of local dependencies.
9
+ *
10
+ * JS/TS patterns:
11
+ * import X from './Y'
12
+ * import { X } from './Y'
13
+ * import './Y'
14
+ * const X = require('./Y')
15
+ * require('./Y')
16
+ *
17
+ * Python patterns:
18
+ * from .module import X (relative)
19
+ * from ..module import X (relative)
20
+ * from app.module import X (local package — resolved if file exists)
21
+ * import .module (relative)
22
+ *
23
+ * Only includes paths that resolve to actual files in the project.
24
+ * Skips: node_modules, non-code files, anything that doesn't resolve.
25
+ */
26
+ function extractImports(content, filePath, projectRoot) {
27
+ const ext = path.extname(filePath).toLowerCase();
28
+ const fileDir = path.dirname(filePath);
29
+
30
+ let rawImports = [];
31
+
32
+ if (['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(ext)) {
33
+ rawImports = extractJSImports(content);
34
+ } else if (ext === '.py') {
35
+ rawImports = extractPythonImports(content, filePath, projectRoot);
36
+ }
37
+
38
+ // Resolve and deduplicate
39
+ const resolved = new Set();
40
+
41
+ for (const imp of rawImports) {
42
+ const resolvedPath = resolveImportPath(imp, fileDir, projectRoot, ext);
43
+ if (resolvedPath) {
44
+ // Store as relative to project root
45
+ const rel = path.relative(projectRoot, resolvedPath);
46
+ resolved.add(rel);
47
+ }
48
+ }
49
+
50
+ return [...resolved].sort();
51
+ }
52
+
53
+ /**
54
+ * Extract import paths from JS/TS content. Relative paths only.
55
+ */
56
+ function extractJSImports(content) {
57
+ const imports = [];
58
+
59
+ // import ... from './path' or import './path'
60
+ const importPattern = /import\s+(?:[\s\S]*?\s+from\s+)?['"](\.[^'"]+)['"]/g;
61
+ let match;
62
+ while ((match = importPattern.exec(content)) !== null) {
63
+ imports.push(match[1]);
64
+ }
65
+
66
+ // require('./path')
67
+ const requirePattern = /require\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
68
+ while ((match = requirePattern.exec(content)) !== null) {
69
+ imports.push(match[1]);
70
+ }
71
+
72
+ return imports;
73
+ }
74
+
75
+ /**
76
+ * Extract import paths from Python content. Relative imports only.
77
+ */
78
+ function extractPythonImports(content, filePath, projectRoot) {
79
+ const imports = [];
80
+ const fileDir = path.dirname(filePath);
81
+
82
+ // from .module import X or from ..module import X
83
+ const fromRelPattern = /^from\s+(\.+\w*(?:\.\w+)*)\s+import/gm;
84
+ let match;
85
+ while ((match = fromRelPattern.exec(content)) !== null) {
86
+ const resolved = resolvePythonRelativeImport(match[1], fileDir);
87
+ if (resolved) imports.push(resolved);
88
+ }
89
+
90
+ // from app.module import X — try to resolve as local file
91
+ const fromAbsPattern = /^from\s+(\w+(?:\.\w+)+)\s+import/gm;
92
+ while ((match = fromAbsPattern.exec(content)) !== null) {
93
+ const modulePath = match[1].replace(/\./g, path.sep);
94
+ // Try from project root
95
+ const resolved = tryResolvePythonModule(modulePath, projectRoot);
96
+ if (resolved) {
97
+ imports.push(resolved);
98
+ continue;
99
+ }
100
+ // Try from file's directory (for cases like `from app.models` when inside aws-risk-agent/)
101
+ const fromFileDir = tryResolvePythonModule(modulePath, fileDir);
102
+ if (fromFileDir) {
103
+ imports.push(fromFileDir);
104
+ continue;
105
+ }
106
+ // Try from parent directories up to project root
107
+ let searchDir = path.dirname(fileDir);
108
+ while (searchDir.startsWith(projectRoot) && searchDir !== projectRoot) {
109
+ const fromParent = tryResolvePythonModule(modulePath, searchDir);
110
+ if (fromParent) {
111
+ imports.push(fromParent);
112
+ break;
113
+ }
114
+ searchDir = path.dirname(searchDir);
115
+ }
116
+ }
117
+
118
+ return imports;
119
+ }
120
+
121
+ /**
122
+ * Try to resolve a dotted Python module path from a base directory.
123
+ */
124
+ function tryResolvePythonModule(modulePath, baseDir) {
125
+ const asFile = path.join(baseDir, modulePath + '.py');
126
+ if (fs.existsSync(asFile)) return asFile;
127
+ const asInit = path.join(baseDir, modulePath, '__init__.py');
128
+ if (fs.existsSync(asInit)) return asInit;
129
+ return null;
130
+ }
131
+
132
+ /**
133
+ * Resolve a Python relative import like '.models' or '..utils' to an absolute path.
134
+ */
135
+ function resolvePythonRelativeImport(importStr, fileDir) {
136
+ // Count leading dots
137
+ let dots = 0;
138
+ while (dots < importStr.length && importStr[dots] === '.') dots++;
139
+
140
+ const modulePart = importStr.substring(dots);
141
+
142
+ // Go up (dots - 1) directories from fileDir
143
+ let baseDir = fileDir;
144
+ for (let i = 1; i < dots; i++) {
145
+ baseDir = path.dirname(baseDir);
146
+ }
147
+
148
+ if (!modulePart) return null;
149
+
150
+ const modulePath = modulePart.replace(/\./g, path.sep);
151
+ const asFile = path.join(baseDir, modulePath + '.py');
152
+ if (fs.existsSync(asFile)) return asFile;
153
+
154
+ const asInit = path.join(baseDir, modulePath, '__init__.py');
155
+ if (fs.existsSync(asInit)) return asInit;
156
+
157
+ return null;
158
+ }
159
+
160
+ /**
161
+ * Resolve a JS/TS import path to an actual file.
162
+ * Tries: exact, .js, .ts, .jsx, .tsx, /index.js, /index.ts
163
+ */
164
+ function resolveImportPath(importPath, fileDir, projectRoot, sourceExt) {
165
+ // For Python, importPath is already absolute
166
+ if (path.isAbsolute(importPath)) {
167
+ return fs.existsSync(importPath) ? importPath : null;
168
+ }
169
+
170
+ const base = path.resolve(fileDir, importPath);
171
+
172
+ // Try exact
173
+ if (fs.existsSync(base) && fs.statSync(base).isFile()) return base;
174
+
175
+ // Try extensions
176
+ const extensions = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
177
+ for (const ext of extensions) {
178
+ const withExt = base + ext;
179
+ if (fs.existsSync(withExt)) return withExt;
180
+ }
181
+
182
+ // Try index files
183
+ for (const ext of extensions) {
184
+ const indexFile = path.join(base, 'index' + ext);
185
+ if (fs.existsSync(indexFile)) return indexFile;
186
+ }
187
+
188
+ return null;
189
+ }
190
+
191
+ /**
192
+ * buildImportGraph(fileContents, projectRoot) → { 'relative/path.js': ['relative/dep.js', ...] }
193
+ *
194
+ * fileContents: Array of { filePath, content } (absolute paths)
195
+ * Returns a map of relative file paths to their relative dependencies.
196
+ * Only includes files that have at least one resolved dependency.
197
+ */
198
+ function buildImportGraph(fileContents, projectRoot) {
199
+ const graph = {};
200
+
201
+ for (const { filePath, content } of fileContents) {
202
+ const deps = extractImports(content, filePath, projectRoot);
203
+ if (deps.length > 0) {
204
+ const relPath = path.relative(projectRoot, filePath);
205
+ graph[relPath] = deps;
206
+ }
207
+ }
208
+
209
+ return graph;
210
+ }
211
+
212
+ module.exports = { extractImports, buildImportGraph };
package/src/sync.js CHANGED
@@ -5,6 +5,7 @@ const { formatSections } = require('./agents/formatter');
5
5
  const { mergeIntoAgentsMd } = require('./agents/merger');
6
6
  const { inferResponsibility } = require('./extractors/filemap');
7
7
  const { validateExtracted } = require('./agents/validator');
8
+ const { buildImportGraph } = require('./extractors/imports');
8
9
 
9
10
  const IGNORE_DIRS = new Set(['node_modules', '.git', '__pycache__', '.venv', 'venv', '.idea', '.vscode', '.carto', 'AGENTS.md']);
10
11
 
@@ -166,6 +167,20 @@ async function runFullSync(config) {
166
167
  return true;
167
168
  });
168
169
 
170
+ // Build import graph from all processed files
171
+ const fileContentsForImports = [];
172
+ const allProcessedPaths = [...new Set([...allCodeFiles, ...allFrontendFiles])];
173
+ // Re-read is avoided — collect during processing. Use a second pass for simplicity.
174
+ for (const filePath of allProcessedPaths) {
175
+ try {
176
+ const content = fs.readFileSync(filePath, 'utf-8');
177
+ fileContentsForImports.push({ filePath, content });
178
+ } catch {
179
+ // skip — already warned during extraction
180
+ }
181
+ }
182
+ const importGraph = buildImportGraph(fileContentsForImports, config.projectRoot);
183
+
169
184
  // Build file map
170
185
  const fileMap = [];
171
186
  for (const filePath of allCodeFiles) {
@@ -204,7 +219,8 @@ async function runFullSync(config) {
204
219
  fileMap,
205
220
  functions: validated.functions,
206
221
  dbTables: validated.dbTables,
207
- envVars: validated.envVars
222
+ envVars: validated.envVars,
223
+ importGraph
208
224
  });
209
225
 
210
226
  mergeIntoAgentsMd(config.output, autoContent);