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 +15 -0
- package/dist/commands/push.js +136 -12
- package/dist/utils/ast-extractor.js +24 -1
- package/package.json +2 -1
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
|
|
package/dist/commands/push.js
CHANGED
|
@@ -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
|
|
176
|
+
let changedFiles = [];
|
|
72
177
|
try {
|
|
73
178
|
const diffSummary = await git.diffSummary(['HEAD~1', 'HEAD']);
|
|
74
|
-
|
|
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
|
-
|
|
182
|
+
changedFiles = [];
|
|
81
183
|
}
|
|
82
|
-
|
|
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(` -
|
|
189
|
-
console.log(chalk.dim(` - total
|
|
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.
|
|
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",
|