monoai 0.2.7 → 0.2.8

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/README.md CHANGED
@@ -56,4 +56,19 @@ coverage
56
56
  *.log
57
57
  ```
58
58
 
59
+ ## .monoaiwhitelist (optional, include-only)
60
+
61
+ `monoai push` can also read `.monoaiwhitelist` as an include-only filter.
62
+ When this file exists and has rules, only matched paths are scanned and uploaded.
63
+
64
+ Use this when you run `monoai push` from a monorepo root but want to sync only one app (for example `step4.vite-web-migration`).
65
+
66
+ ```gitignore
67
+ # Example: sync only step4 project
68
+ step4.vite-web-migration/**
69
+ ```
70
+
71
+ Behavior summary:
72
+ - `.monoaiwhitelist` limits scope first (include-only).
73
+ - `.gitignore` and `.monoaiignore` still exclude paths inside that scope.
59
74
 
@@ -10,6 +10,7 @@ import { extractSkeleton } from '../utils/ast-extractor.js';
10
10
  const git = simpleGit();
11
11
  const config = new Conf({ projectName: 'monoai' });
12
12
  const MONOAIIGNORE_FILENAME = '.monoaiignore';
13
+ const MONOAIWHITELIST_FILENAME = '.monoaiwhitelist';
13
14
  const DEFAULT_MONOAIIGNORE = `# MonoAI AST scan ignore rules
14
15
  # Uses .gitignore-style patterns.
15
16
 
@@ -26,11 +27,110 @@ coverage
26
27
  **/.agent/**
27
28
  *.log
28
29
  `;
30
+ function loadMonoaiWhitelist(cwd) {
31
+ const whitelistPath = path.join(cwd, MONOAIWHITELIST_FILENAME);
32
+ if (!fs.existsSync(whitelistPath)) {
33
+ return { matcher: null, ruleCount: 0 };
34
+ }
35
+ const rules = fs.readFileSync(whitelistPath, 'utf8')
36
+ .split(/\r?\n/g)
37
+ .map((line) => line.trim())
38
+ .filter((line) => line.length > 0 && !line.startsWith('#'));
39
+ if (rules.length === 0) {
40
+ return { matcher: null, ruleCount: 0 };
41
+ }
42
+ return {
43
+ matcher: ignore().add(rules),
44
+ ruleCount: rules.length,
45
+ };
46
+ }
47
+ function normalizeGitFilePath(value) {
48
+ return String(value || "")
49
+ .trim()
50
+ .replace(/\\/g, "/")
51
+ .replace(/^\.?\//, "");
52
+ }
53
+ function scopePrefix(filePath, depth) {
54
+ const parts = normalizeGitFilePath(filePath).split("/").filter(Boolean);
55
+ if (parts.length === 0)
56
+ return "";
57
+ return parts.slice(0, Math.min(depth, parts.length)).join("/");
58
+ }
59
+ function asNumber(value) {
60
+ const n = Number(value);
61
+ if (!Number.isFinite(n))
62
+ return 0;
63
+ return Math.max(0, Math.round(n));
64
+ }
65
+ function buildChangedFileSignals(diffFiles) {
66
+ const rows = Array.isArray(diffFiles) ? diffFiles : [];
67
+ return rows
68
+ .map((file) => {
69
+ const normalizedPath = normalizeGitFilePath(String(file?.file || ""));
70
+ if (!normalizedPath)
71
+ return null;
72
+ const insertions = asNumber(file?.insertions);
73
+ const deletions = asNumber(file?.deletions);
74
+ const changes = Math.max(asNumber(file?.changes), insertions + deletions);
75
+ const ext = path.extname(normalizedPath).replace(".", "").toLowerCase() || "none";
76
+ return {
77
+ path: normalizedPath,
78
+ insertions,
79
+ deletions,
80
+ changes,
81
+ scope1: scopePrefix(normalizedPath, 1),
82
+ scope2: scopePrefix(normalizedPath, 2),
83
+ extension: ext,
84
+ };
85
+ })
86
+ .filter(Boolean);
87
+ }
88
+ function buildGraphInsightsFromChanges(files) {
89
+ if (!Array.isArray(files) || files.length === 0)
90
+ return [];
91
+ const byScope = new Map();
92
+ const byExtension = new Map();
93
+ for (const row of files) {
94
+ const scope = row.scope2 || row.scope1 || "root";
95
+ const churn = Math.max(1, row.changes);
96
+ const currentScope = byScope.get(scope) || { files: 0, churn: 0 };
97
+ currentScope.files += 1;
98
+ currentScope.churn += churn;
99
+ byScope.set(scope, currentScope);
100
+ const extCount = byExtension.get(row.extension) || 0;
101
+ byExtension.set(row.extension, extCount + 1);
102
+ }
103
+ const hotFiles = files
104
+ .slice()
105
+ .sort((a, b) => b.changes - a.changes)
106
+ .slice(0, 8)
107
+ .map((row) => `changed_file:${row.path} (+${row.insertions}/-${row.deletions})`);
108
+ const hotScopes = Array.from(byScope.entries())
109
+ .sort((a, b) => {
110
+ if (b[1].churn !== a[1].churn)
111
+ return b[1].churn - a[1].churn;
112
+ return b[1].files - a[1].files;
113
+ })
114
+ .slice(0, 6)
115
+ .map(([scope, stat]) => `changed_scope:${scope} files=${stat.files} churn=${stat.churn}`);
116
+ const extensionMix = Array.from(byExtension.entries())
117
+ .sort((a, b) => b[1] - a[1])
118
+ .slice(0, 4)
119
+ .map(([ext, count]) => `extension_mix:${ext}=${count}`);
120
+ return Array.from(new Set([
121
+ `changed_total_files:${files.length}`,
122
+ ...hotScopes,
123
+ ...hotFiles,
124
+ ...extensionMix,
125
+ ])).slice(0, 24);
126
+ }
29
127
  export const pushCommand = new Command('push')
30
128
  .description('Sync your codebase structure to MonoAI')
31
129
  .option('-v, --verbose', 'Show internal pipeline logs')
130
+ .option('-f, --force', 'Force sync even if the same commit was already uploaded')
32
131
  .action(async (options) => {
33
132
  const verbose = !!options?.verbose;
133
+ const force = !!options?.force;
34
134
  const totalStart = Date.now();
35
135
  const stageTimes = [];
36
136
  const logDetail = (message) => {
@@ -60,26 +160,34 @@ export const pushCommand = new Command('push')
60
160
  console.error(chalk.red('❌ This folder is not a Git repository.'));
61
161
  return;
62
162
  }
163
+ const { matcher: whitelistMatcher, ruleCount: whitelistRuleCount } = loadMonoaiWhitelist(process.cwd());
164
+ const isWhitelisted = (relativePath) => !whitelistMatcher || whitelistMatcher.ignores(normalizeGitFilePath(relativePath));
165
+ if (whitelistMatcher) {
166
+ console.log(chalk.blue(`🎯 Applying ${MONOAIWHITELIST_FILENAME} (${whitelistRuleCount} rule${whitelistRuleCount > 1 ? 's' : ''})`));
167
+ }
63
168
  // 1. Git Metadata (Zero-HITL Intent)
64
- const { lastCommit, branch, changedScopes } = await track('git metadata', async () => {
169
+ const { lastCommit, branch, changedScopes, graphInsights } = await track('git metadata', async () => {
65
170
  const log = await git.log({ maxCount: 1 });
66
171
  const lastCommit = log.latest;
67
172
  const branch = await git.revparse(['--abbrev-ref', 'HEAD']);
68
173
  if (!lastCommit) {
69
174
  throw new Error('No commits found.');
70
175
  }
71
- let changedScopes = [];
176
+ let changedFiles = [];
72
177
  try {
73
178
  const diffSummary = await git.diffSummary(['HEAD~1', 'HEAD']);
74
- changedScopes = Array.from(new Set(diffSummary.files.map((f) => {
75
- const parts = f.file.split('/');
76
- return parts.length > 1 ? parts[0] : f.file;
77
- })));
179
+ changedFiles = buildChangedFileSignals(diffSummary?.files || []);
78
180
  }
79
181
  catch {
80
- changedScopes = [];
182
+ changedFiles = [];
81
183
  }
82
- return { lastCommit, branch, changedScopes };
184
+ changedFiles = changedFiles.filter((row) => isWhitelisted(row.path));
185
+ const changedScopes = Array.from(new Set(changedFiles
186
+ .flatMap((row) => [row.scope2, row.scope1])
187
+ .map((row) => String(row || "").trim())
188
+ .filter(Boolean))).slice(0, 24);
189
+ const graphInsights = buildGraphInsightsFromChanges(changedFiles);
190
+ return { lastCommit, branch, changedScopes, graphInsights };
83
191
  });
84
192
  const shortCommitId = lastCommit.hash.substring(0, 7);
85
193
  const snapshotId = `${branch}@${shortCommitId}`;
@@ -107,13 +215,15 @@ export const pushCommand = new Command('push')
107
215
  const items = fs.readdirSync(dir);
108
216
  for (const item of items) {
109
217
  const fullPath = path.join(dir, item);
110
- const relativePath = path.relative(process.cwd(), fullPath);
218
+ const relativePath = normalizeGitFilePath(path.relative(process.cwd(), fullPath));
111
219
  if (ig.ignores(relativePath))
112
220
  continue;
113
221
  if (fs.statSync(fullPath).isDirectory()) {
114
222
  scanDir(fullPath);
115
223
  }
116
224
  else if (/\.(ts|tsx|js|jsx)$/.test(item)) {
225
+ if (!isWhitelisted(relativePath))
226
+ continue;
117
227
  filesToAnalyze.push(fullPath);
118
228
  }
119
229
  }
@@ -138,13 +248,15 @@ export const pushCommand = new Command('push')
138
248
  commitMessage: lastCommit.message,
139
249
  structure: JSON.stringify(skeleton), // Structured AST
140
250
  changedScopes,
251
+ graphInsights,
141
252
  syncStatus: 'processing',
142
253
  };
143
254
  // 5. Send to Navigator (Convex)
144
255
  console.log(chalk.blue('📡 Uploading to MonoAI...'));
145
256
  const transmitResult = await track('transmit', async () => {
146
257
  const response = await axios.post(`${CONVEX_SITE_URL}/cli/git-commit`, {
147
- codebaseData: payload
258
+ codebaseData: payload,
259
+ force,
148
260
  }, {
149
261
  headers: {
150
262
  'Authorization': `Bearer ${token}`
@@ -152,6 +264,18 @@ export const pushCommand = new Command('push')
152
264
  });
153
265
  return response.data;
154
266
  });
267
+ if (transmitResult?.deduped) {
268
+ console.log(chalk.yellow('⚠ This commit was already synced. No new snapshot was created.'));
269
+ if (transmitResult.message && verbose) {
270
+ console.log(chalk.dim(` ${transmitResult.message}`));
271
+ }
272
+ const totalMs = Date.now() - totalStart;
273
+ console.log(chalk.blue(`⏱ Total time: ${(totalMs / 1000).toFixed(2)}s`));
274
+ return;
275
+ }
276
+ if (force) {
277
+ logDetail(' Force mode enabled: duplicate commit dedupe was bypassed.');
278
+ }
155
279
  if (transmitResult?.graphJobId) {
156
280
  const terminalStatuses = new Set(['done', 'error']);
157
281
  const waitStart = Date.now();
@@ -185,8 +309,8 @@ export const pushCommand = new Command('push')
185
309
  console.log(chalk.dim(` - queue wait: ${fmt(finalJob.queueWaitMs)}`));
186
310
  console.log(chalk.dim(` - graph build: ${fmt(finalJob.cogneeMs)}`));
187
311
  console.log(chalk.dim(` - callback: ${fmt(finalJob.callbackMs)}`));
188
- console.log(chalk.dim(` - worker total: ${fmt(finalJob.workerTotalMs)}`));
189
- console.log(chalk.dim(` - total pipeline: ${fmt(finalJob.totalPipelineMs)}`));
312
+ console.log(chalk.dim(` - service total: ${fmt(finalJob.workerTotalMs)}`));
313
+ console.log(chalk.dim(` - total service time: ${fmt(finalJob.totalPipelineMs)}`));
190
314
  }
191
315
  if (finalJob.status === 'error' && finalJob.error) {
192
316
  console.log(chalk.red(`❌ Could not build Knowledge Graph: ${finalJob.error}`));
@@ -7,6 +7,29 @@ const SECRET_PATTERNS = [
7
7
  /AIza[0-9A-Za-z-_]{35}/g, // Google Cloud style
8
8
  /ghp_[a-zA-Z0-9]{36}/g // GitHub Personal Access Token
9
9
  ];
10
+ function toRepoRelativePath(rawPath) {
11
+ const normalized = String(rawPath || "").replace(/\\/g, "/").trim();
12
+ if (!normalized)
13
+ return "";
14
+ const cwd = process.cwd().replace(/\\/g, "/");
15
+ if (normalized.startsWith(cwd + "/")) {
16
+ return normalized.slice(cwd.length + 1);
17
+ }
18
+ if (normalized === cwd) {
19
+ return ".";
20
+ }
21
+ const relative = path.relative(process.cwd(), rawPath).replace(/\\/g, "/");
22
+ if (relative && !relative.startsWith("../") && relative !== "..") {
23
+ return relative.replace(/^\.?\//, "");
24
+ }
25
+ // Fallback for older absolute paths captured under this mono repo.
26
+ const repoMarker = "/Web_AIChat/";
27
+ const markerIdx = normalized.lastIndexOf(repoMarker);
28
+ if (markerIdx >= 0) {
29
+ return normalized.slice(markerIdx + repoMarker.length).replace(/^\.?\//, "");
30
+ }
31
+ return normalized.replace(/^\.?\//, "");
32
+ }
10
33
  export function extractSkeleton(filePaths) {
11
34
  const project = new Project();
12
35
  // 🛡️ Security: File Filter
@@ -46,7 +69,7 @@ export function extractSkeleton(filePaths) {
46
69
  }
47
70
  }
48
71
  });
49
- const filePath = sourceFile.getFilePath();
72
+ const filePath = toRepoRelativePath(sourceFile.getFilePath());
50
73
  const skeleton = {
51
74
  functions: [],
52
75
  classes: [],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "monoai",
3
3
  "type": "module",
4
- "version": "0.2.7",
4
+ "version": "0.2.8",
5
5
  "description": "MonoAI CLI for syncing codebase history",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -22,6 +22,7 @@
22
22
  "commander": "^11.1.0",
23
23
  "conf": "^15.1.0",
24
24
  "ignore": "^7.0.5",
25
+ "monoai": "^0.2.7",
25
26
  "open": "^9.1.0",
26
27
  "ora": "^7.0.1",
27
28
  "simple-git": "^3.21.0",