monoai 0.2.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.
- package/dist/commands/git-commit.js +73 -0
- package/dist/commands/login.js +65 -0
- package/dist/commands/sync.js +96 -0
- package/dist/index.js +15 -0
- package/dist/utils/ast-extractor.js +112 -0
- package/dist/utils/config.js +19 -0
- package/package.json +29 -0
- package/src/commands/git-commit.ts +87 -0
- package/src/commands/login.ts +73 -0
- package/src/commands/sync.ts +111 -0
- package/src/index.ts +20 -0
- package/src/utils/ast-extractor.ts +131 -0
- package/src/utils/config.ts +28 -0
- package/test-redaction.js +5 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { simpleGit } from 'simple-git';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
const git = simpleGit();
|
|
7
|
+
export const gitCommitCommand = new Command('git-commit')
|
|
8
|
+
.description('Sync local codebase with MonoAI with git metadata')
|
|
9
|
+
.action(async () => {
|
|
10
|
+
try {
|
|
11
|
+
console.log(chalk.blue('š Analyzing codebase and git status...'));
|
|
12
|
+
const isRepo = await git.checkIsRepo();
|
|
13
|
+
if (!isRepo) {
|
|
14
|
+
console.error(chalk.red('ā Not a git repository. Please run this inside a git project.'));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
// 1. Get Git Metadata
|
|
18
|
+
const log = await git.log({ maxCount: 1 });
|
|
19
|
+
const lastCommit = log.latest;
|
|
20
|
+
const branch = await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
21
|
+
if (!lastCommit) {
|
|
22
|
+
console.error(chalk.red('ā No git commits found. Please commit your changes first.'));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
console.log(chalk.green(`ā
Found commit: ${lastCommit.hash.substring(0, 7)} on branch ${branch}`));
|
|
26
|
+
// 2. Scan Directory Structure (Simplified for demo)
|
|
27
|
+
// In real implementation, this would be a deep traversal respecting .gitignore
|
|
28
|
+
const structure = {
|
|
29
|
+
name: path.basename(process.cwd()),
|
|
30
|
+
totalFiles: 0,
|
|
31
|
+
files: []
|
|
32
|
+
};
|
|
33
|
+
const scanDir = (dir, base = '') => {
|
|
34
|
+
const items = fs.readdirSync(dir);
|
|
35
|
+
for (const item of items) {
|
|
36
|
+
if (item === 'node_modules' || item === '.git' || item === 'dist')
|
|
37
|
+
continue;
|
|
38
|
+
const fullPath = path.join(dir, item);
|
|
39
|
+
const relativePath = path.join(base, item);
|
|
40
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
41
|
+
scanDir(fullPath, relativePath);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
structure.totalFiles++;
|
|
45
|
+
structure.files.push(relativePath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
scanDir(process.cwd());
|
|
50
|
+
// 3. Send to MonoAI (Convex)
|
|
51
|
+
console.log(chalk.blue('š Syncing with MonoAI...'));
|
|
52
|
+
const payload = {
|
|
53
|
+
commitId: lastCommit.hash.substring(0, 7),
|
|
54
|
+
commitMessage: lastCommit.message,
|
|
55
|
+
branch: branch,
|
|
56
|
+
structure: JSON.stringify(structure),
|
|
57
|
+
syncStatus: 'success',
|
|
58
|
+
lastSyncedAt: Date.now()
|
|
59
|
+
};
|
|
60
|
+
// TODO: Get actual CONVEX_SITE_URL and user token from config
|
|
61
|
+
const CONVEX_SITE_URL = 'http://localhost:5173'; // Placeholder
|
|
62
|
+
console.log(chalk.yellow(`š” Sending data to MonoAI (${CONVEX_SITE_URL}/cli/git-commit)...`));
|
|
63
|
+
// Simulating successful response for demo verification
|
|
64
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
65
|
+
console.log(chalk.green('⨠[Simulation] Successfully synced to MonoAI!'));
|
|
66
|
+
console.log(chalk.dim(` Branch: ${branch}`));
|
|
67
|
+
console.log(chalk.dim(` Commit: ${lastCommit.hash.substring(0, 7)}`));
|
|
68
|
+
console.log(chalk.dim(` Message: ${lastCommit.message}`));
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
console.error(chalk.red('ā Error during sync:'), error.message);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import Conf from 'conf';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import open from 'open';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
const config = new Conf({ projectName: 'monoai' });
|
|
8
|
+
const CONVEX_SITE_URL = 'https://majestic-crane-609.convex.site';
|
|
9
|
+
export const loginCommand = new Command('login')
|
|
10
|
+
.description('Authenticate with MonoAI')
|
|
11
|
+
.action(async () => {
|
|
12
|
+
console.log(chalk.blue('š Starting MonoAI Login...'));
|
|
13
|
+
try {
|
|
14
|
+
// 1. Init Session
|
|
15
|
+
const initSpinner = ora('Initializing auth session...').start();
|
|
16
|
+
const initRes = await axios.post(`${CONVEX_SITE_URL}/cli/auth/init`, {
|
|
17
|
+
deviceDescription: process.platform
|
|
18
|
+
});
|
|
19
|
+
initSpinner.succeed();
|
|
20
|
+
const { tempCode, loginUrl } = initRes.data;
|
|
21
|
+
console.log(chalk.yellow(`\nš Verification Code: ${chalk.bold(tempCode)}`));
|
|
22
|
+
console.log(chalk.dim(` Opening browser... if it doesn't open, visit:`));
|
|
23
|
+
console.log(chalk.underline(loginUrl));
|
|
24
|
+
console.log('\n');
|
|
25
|
+
await open(loginUrl);
|
|
26
|
+
// 2. Poll Status
|
|
27
|
+
const pollSpinner = ora('Waiting for approval in browser...').start();
|
|
28
|
+
let attempts = 0;
|
|
29
|
+
const maxAttempts = 60; // 2 minutes (2s * 60)
|
|
30
|
+
const pollInterval = setInterval(async () => {
|
|
31
|
+
try {
|
|
32
|
+
const pollRes = await axios.post(`${CONVEX_SITE_URL}/cli/auth/poll`, { tempCode });
|
|
33
|
+
const { status, token, userId } = pollRes.data;
|
|
34
|
+
if (status === 'approved' && token) {
|
|
35
|
+
clearInterval(pollInterval);
|
|
36
|
+
config.set('auth_token', token);
|
|
37
|
+
config.set('user_id', userId);
|
|
38
|
+
config.set('convex_url', CONVEX_SITE_URL); // Store for future use
|
|
39
|
+
pollSpinner.succeed(chalk.green('ā
Login Successful!'));
|
|
40
|
+
console.log(chalk.dim(` Token saved to ${config.path}`));
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
else if (status === 'expired' || status === 'rejected') {
|
|
44
|
+
clearInterval(pollInterval);
|
|
45
|
+
pollSpinner.fail(chalk.red(`ā Session ${status}. Please try again.`));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
attempts++;
|
|
50
|
+
if (attempts >= maxAttempts) {
|
|
51
|
+
clearInterval(pollInterval);
|
|
52
|
+
pollSpinner.fail(chalk.red('ā Timed out.'));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
// Ignore poll errors (network blips)
|
|
59
|
+
}
|
|
60
|
+
}, 2000);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.error(chalk.red('\nā Login failed:'), error.message);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { simpleGit } from 'simple-git';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import ignore from 'ignore';
|
|
8
|
+
import Conf from 'conf';
|
|
9
|
+
import { extractSkeleton } from '../utils/ast-extractor.js';
|
|
10
|
+
const git = simpleGit();
|
|
11
|
+
const config = new Conf({ projectName: 'monoai' });
|
|
12
|
+
export const syncCommand = new Command('push')
|
|
13
|
+
.description('Push codebase integrity and AST skeleton to MonoAI')
|
|
14
|
+
.action(async () => {
|
|
15
|
+
try {
|
|
16
|
+
console.log(chalk.blue('šļø Starting MonoAI Strategic Push...'));
|
|
17
|
+
// 0. Auth Check
|
|
18
|
+
const token = config.get('auth_token');
|
|
19
|
+
if (!token) {
|
|
20
|
+
console.error(chalk.red('ā Not Authenticated. Please run:'));
|
|
21
|
+
console.error(chalk.white(' npx monoai login'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const isRepo = await git.checkIsRepo();
|
|
25
|
+
if (!isRepo) {
|
|
26
|
+
console.error(chalk.red('ā Not a git repository.'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// 1. Git Metadata (Zero-HITL Intent)
|
|
30
|
+
const log = await git.log({ maxCount: 1 });
|
|
31
|
+
const lastCommit = log.latest;
|
|
32
|
+
const branch = await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
33
|
+
if (!lastCommit) {
|
|
34
|
+
console.error(chalk.red('ā No commits found.'));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
console.log(chalk.dim(` Branch: ${chalk.white(branch)}`));
|
|
38
|
+
console.log(chalk.dim(` Commit: ${chalk.white(lastCommit.hash.substring(0, 7))}`));
|
|
39
|
+
// 2. Scan & Extract AST Skeleton
|
|
40
|
+
console.log(chalk.blue('š Analyzing structural integrity (AST)...'));
|
|
41
|
+
const ig = ignore();
|
|
42
|
+
if (fs.existsSync('.gitignore')) {
|
|
43
|
+
ig.add(fs.readFileSync('.gitignore').toString());
|
|
44
|
+
}
|
|
45
|
+
// Hardcoded safety
|
|
46
|
+
ig.add(['node_modules', '.git', 'dist', '.env', 'build']);
|
|
47
|
+
const filesToAnalyze = [];
|
|
48
|
+
const scanDir = (dir) => {
|
|
49
|
+
const items = fs.readdirSync(dir);
|
|
50
|
+
for (const item of items) {
|
|
51
|
+
const fullPath = path.join(dir, item);
|
|
52
|
+
const relativePath = path.relative(process.cwd(), fullPath);
|
|
53
|
+
if (ig.ignores(relativePath))
|
|
54
|
+
continue;
|
|
55
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
56
|
+
scanDir(fullPath);
|
|
57
|
+
}
|
|
58
|
+
else if (/\.(ts|tsx|js|jsx)$/.test(item)) {
|
|
59
|
+
filesToAnalyze.push(fullPath);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
scanDir(process.cwd());
|
|
64
|
+
const skeleton = extractSkeleton(filesToAnalyze);
|
|
65
|
+
// 3. Payload Construction
|
|
66
|
+
const payload = {
|
|
67
|
+
name: path.basename(process.cwd()),
|
|
68
|
+
branch: branch,
|
|
69
|
+
commitId: lastCommit.hash.substring(0, 7),
|
|
70
|
+
commitMessage: lastCommit.message,
|
|
71
|
+
structure: JSON.stringify(skeleton), // Now structured AST
|
|
72
|
+
syncStatus: 'success',
|
|
73
|
+
};
|
|
74
|
+
// 4. Send to Navigator (Convex)
|
|
75
|
+
console.log(chalk.blue('š” Transmitting to Value Engine...'));
|
|
76
|
+
const CONVEX_SITE_URL = config.get('convex_url') || 'https://majestic-crane-609.convex.site';
|
|
77
|
+
await axios.post(`${CONVEX_SITE_URL}/cli/git-commit`, {
|
|
78
|
+
codebaseData: payload
|
|
79
|
+
}, {
|
|
80
|
+
headers: {
|
|
81
|
+
'Authorization': `Bearer ${token}`
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
console.log(chalk.green('⨠[Navigator] Push complete! Check your dashboard for Navigator alignment.'));
|
|
85
|
+
console.log(chalk.dim(` Message: ${lastCommit.message.split('\n')[0]}`));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error.response?.status === 401) {
|
|
89
|
+
console.error(chalk.red('ā Authentication Expired. Please run:'));
|
|
90
|
+
console.error(chalk.white(' npx monoai login'));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
console.error(chalk.red('ā Sync failed:'), error.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { syncCommand } from './commands/sync.js';
|
|
4
|
+
import { loginCommand } from './commands/login.js';
|
|
5
|
+
const program = new Command();
|
|
6
|
+
program
|
|
7
|
+
.name('monoai')
|
|
8
|
+
.description('MonoAI CLI - Strategic Navigator')
|
|
9
|
+
.version('0.1.2');
|
|
10
|
+
// Git sub-command group
|
|
11
|
+
const git = new Command('git').description('Git related operations');
|
|
12
|
+
git.addCommand(syncCommand.name('push'));
|
|
13
|
+
program.addCommand(loginCommand);
|
|
14
|
+
program.addCommand(git);
|
|
15
|
+
program.parse();
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Project, SyntaxKind } from 'ts-morph';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
// š”ļø Security: Redaction Patterns
|
|
4
|
+
const SECRET_PATTERNS = [
|
|
5
|
+
/sk-[a-zA-Z0-9-_]{20,}/g, // OpenAI / Stripe style
|
|
6
|
+
/eyJ[a-zA-Z0-9-_]{20,}/g, // JWT style
|
|
7
|
+
/AIza[0-9A-Za-z-_]{35}/g, // Google Cloud style
|
|
8
|
+
/ghp_[a-zA-Z0-9]{36}/g // GitHub Personal Access Token
|
|
9
|
+
];
|
|
10
|
+
export function extractSkeleton(filePaths) {
|
|
11
|
+
const project = new Project();
|
|
12
|
+
// š”ļø Security: File Filter
|
|
13
|
+
const safePaths = filePaths.filter(p => {
|
|
14
|
+
const base = path.basename(p);
|
|
15
|
+
if (base.startsWith('.env'))
|
|
16
|
+
return false;
|
|
17
|
+
if (base === '.DS_Store')
|
|
18
|
+
return false;
|
|
19
|
+
if (p.includes('node_modules'))
|
|
20
|
+
return false;
|
|
21
|
+
if (p.includes('.git/'))
|
|
22
|
+
return false;
|
|
23
|
+
return true;
|
|
24
|
+
});
|
|
25
|
+
project.addSourceFilesAtPaths(safePaths);
|
|
26
|
+
const result = {};
|
|
27
|
+
let totalPayloadSize = 0;
|
|
28
|
+
project.getSourceFiles().forEach(sourceFile => {
|
|
29
|
+
// š”ļø Security: Secret Redaction (Active Scanning)
|
|
30
|
+
sourceFile.forEachDescendant((node) => {
|
|
31
|
+
if (node.getKind() === SyntaxKind.StringLiteral) {
|
|
32
|
+
const sl = node;
|
|
33
|
+
const text = sl.getLiteralText();
|
|
34
|
+
let redacted = text;
|
|
35
|
+
let found = false;
|
|
36
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
37
|
+
if (pattern.test(text)) {
|
|
38
|
+
redacted = '[REDACTED_SECRET]';
|
|
39
|
+
found = true;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (found) {
|
|
44
|
+
// AST Rewrite (Memory only, does not save to disk)
|
|
45
|
+
sl.setLiteralValue(redacted);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const filePath = sourceFile.getFilePath();
|
|
50
|
+
const skeleton = {
|
|
51
|
+
functions: [],
|
|
52
|
+
classes: [],
|
|
53
|
+
interfaces: [],
|
|
54
|
+
types: []
|
|
55
|
+
};
|
|
56
|
+
// Extract Functions
|
|
57
|
+
sourceFile.getFunctions().forEach(f => {
|
|
58
|
+
if (f.isExported()) {
|
|
59
|
+
skeleton.functions.push({
|
|
60
|
+
name: f.getName(),
|
|
61
|
+
parameters: f.getParameters().map(p => ({
|
|
62
|
+
name: p.getName(),
|
|
63
|
+
type: p.getType().getText()
|
|
64
|
+
})),
|
|
65
|
+
returnType: f.getReturnType().getText(),
|
|
66
|
+
jsDoc: f.getJsDocs().map(d => d.getCommentText()).join('\n')
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// Extract Classes
|
|
71
|
+
sourceFile.getClasses().forEach(c => {
|
|
72
|
+
if (c.isExported()) {
|
|
73
|
+
skeleton.classes.push({
|
|
74
|
+
name: c.getName(),
|
|
75
|
+
methods: c.getMethods().map(m => ({
|
|
76
|
+
name: m.getName(),
|
|
77
|
+
parameters: m.getParameters().map(p => ({
|
|
78
|
+
name: p.getName(),
|
|
79
|
+
type: p.getType().getText()
|
|
80
|
+
})),
|
|
81
|
+
returnType: m.getReturnType().getText()
|
|
82
|
+
})),
|
|
83
|
+
jsDoc: c.getJsDocs().map(d => d.getCommentText()).join('\n')
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
// Extract Interfaces
|
|
88
|
+
sourceFile.getInterfaces().forEach(i => {
|
|
89
|
+
if (i.isExported()) {
|
|
90
|
+
skeleton.interfaces.push({
|
|
91
|
+
name: i.getName(),
|
|
92
|
+
jsDoc: i.getJsDocs().map(d => d.getCommentText()).join('\n')
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
// Extract Types
|
|
97
|
+
sourceFile.getTypeAliases().forEach(t => {
|
|
98
|
+
if (t.isExported()) {
|
|
99
|
+
skeleton.types.push({
|
|
100
|
+
name: t.getName(),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
result[filePath] = skeleton;
|
|
105
|
+
});
|
|
106
|
+
// š”ļø Security: Payload Size Limit (DoS Prevention)
|
|
107
|
+
const payloadString = JSON.stringify(result);
|
|
108
|
+
if (payloadString.length > 5 * 1024 * 1024) { // 5MB
|
|
109
|
+
throw new Error("Payload too large. Security limit exceeded (5MB).");
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
const config = new Conf({
|
|
3
|
+
projectName: 'monoai',
|
|
4
|
+
projectSuffix: 'cli'
|
|
5
|
+
});
|
|
6
|
+
export const saveCredentials = (token, url) => {
|
|
7
|
+
config.set('authToken', token);
|
|
8
|
+
config.set('convexUrl', url);
|
|
9
|
+
};
|
|
10
|
+
export const getCredentials = () => {
|
|
11
|
+
return {
|
|
12
|
+
authToken: config.get('authToken'),
|
|
13
|
+
convexUrl: config.get('convexUrl')
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export const clearCredentials = () => {
|
|
17
|
+
config.delete('authToken');
|
|
18
|
+
config.delete('convexUrl');
|
|
19
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "monoai",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"description": "MonoAI CLI for syncing codebase history",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"monoai": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"axios": "^1.6.2",
|
|
16
|
+
"chalk": "^4.1.2",
|
|
17
|
+
"commander": "^11.1.0",
|
|
18
|
+
"conf": "^15.1.0",
|
|
19
|
+
"ignore": "^7.0.5",
|
|
20
|
+
"open": "^9.1.0",
|
|
21
|
+
"ora": "^7.0.1",
|
|
22
|
+
"simple-git": "^3.21.0",
|
|
23
|
+
"ts-morph": "^27.0.2"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^20.19.31",
|
|
27
|
+
"typescript": "^5.3.2"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { simpleGit } from 'simple-git';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
const git = simpleGit();
|
|
9
|
+
|
|
10
|
+
export const gitCommitCommand = new Command('git-commit')
|
|
11
|
+
.description('Sync local codebase with MonoAI with git metadata')
|
|
12
|
+
.action(async () => {
|
|
13
|
+
try {
|
|
14
|
+
console.log(chalk.blue('š Analyzing codebase and git status...'));
|
|
15
|
+
|
|
16
|
+
const isRepo = await git.checkIsRepo();
|
|
17
|
+
if (!isRepo) {
|
|
18
|
+
console.error(chalk.red('ā Not a git repository. Please run this inside a git project.'));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 1. Get Git Metadata
|
|
23
|
+
const log = await git.log({ maxCount: 1 });
|
|
24
|
+
const lastCommit = log.latest;
|
|
25
|
+
const branch = await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
26
|
+
|
|
27
|
+
if (!lastCommit) {
|
|
28
|
+
console.error(chalk.red('ā No git commits found. Please commit your changes first.'));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(chalk.green(`ā
Found commit: ${lastCommit.hash.substring(0, 7)} on branch ${branch}`));
|
|
33
|
+
|
|
34
|
+
// 2. Scan Directory Structure (Simplified for demo)
|
|
35
|
+
// In real implementation, this would be a deep traversal respecting .gitignore
|
|
36
|
+
const structure = {
|
|
37
|
+
name: path.basename(process.cwd()),
|
|
38
|
+
totalFiles: 0,
|
|
39
|
+
files: [] as string[]
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const scanDir = (dir: string, base: string = '') => {
|
|
43
|
+
const items = fs.readdirSync(dir);
|
|
44
|
+
for (const item of items) {
|
|
45
|
+
if (item === 'node_modules' || item === '.git' || item === 'dist') continue;
|
|
46
|
+
const fullPath = path.join(dir, item);
|
|
47
|
+
const relativePath = path.join(base, item);
|
|
48
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
49
|
+
scanDir(fullPath, relativePath);
|
|
50
|
+
} else {
|
|
51
|
+
structure.totalFiles++;
|
|
52
|
+
structure.files.push(relativePath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
scanDir(process.cwd());
|
|
58
|
+
|
|
59
|
+
// 3. Send to MonoAI (Convex)
|
|
60
|
+
console.log(chalk.blue('š Syncing with MonoAI...'));
|
|
61
|
+
|
|
62
|
+
const payload = {
|
|
63
|
+
commitId: lastCommit.hash.substring(0, 7),
|
|
64
|
+
commitMessage: lastCommit.message,
|
|
65
|
+
branch: branch,
|
|
66
|
+
structure: JSON.stringify(structure),
|
|
67
|
+
syncStatus: 'success',
|
|
68
|
+
lastSyncedAt: Date.now()
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// TODO: Get actual CONVEX_SITE_URL and user token from config
|
|
72
|
+
const CONVEX_SITE_URL = 'http://localhost:5173'; // Placeholder
|
|
73
|
+
|
|
74
|
+
console.log(chalk.yellow(`š” Sending data to MonoAI (${CONVEX_SITE_URL}/cli/git-commit)...`));
|
|
75
|
+
|
|
76
|
+
// Simulating successful response for demo verification
|
|
77
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
78
|
+
|
|
79
|
+
console.log(chalk.green('⨠[Simulation] Successfully synced to MonoAI!'));
|
|
80
|
+
console.log(chalk.dim(` Branch: ${branch}`));
|
|
81
|
+
console.log(chalk.dim(` Commit: ${lastCommit.hash.substring(0, 7)}`));
|
|
82
|
+
console.log(chalk.dim(` Message: ${lastCommit.message}`));
|
|
83
|
+
|
|
84
|
+
} catch (error: any) {
|
|
85
|
+
console.error(chalk.red('ā Error during sync:'), error.message);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import Conf from 'conf';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import open from 'open';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
|
|
8
|
+
const config = new Conf({ projectName: 'monoai' });
|
|
9
|
+
const CONVEX_SITE_URL = 'https://majestic-crane-609.convex.site';
|
|
10
|
+
|
|
11
|
+
export const loginCommand = new Command('login')
|
|
12
|
+
.description('Authenticate with MonoAI')
|
|
13
|
+
.action(async () => {
|
|
14
|
+
console.log(chalk.blue('š Starting MonoAI Login...'));
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// 1. Init Session
|
|
18
|
+
const initSpinner = ora('Initializing auth session...').start();
|
|
19
|
+
const initRes = await axios.post(`${CONVEX_SITE_URL}/cli/auth/init`, {
|
|
20
|
+
deviceDescription: process.platform
|
|
21
|
+
});
|
|
22
|
+
initSpinner.succeed();
|
|
23
|
+
|
|
24
|
+
const { tempCode, loginUrl } = initRes.data;
|
|
25
|
+
|
|
26
|
+
console.log(chalk.yellow(`\nš Verification Code: ${chalk.bold(tempCode)}`));
|
|
27
|
+
console.log(chalk.dim(` Opening browser... if it doesn't open, visit:`));
|
|
28
|
+
console.log(chalk.underline(loginUrl));
|
|
29
|
+
console.log('\n');
|
|
30
|
+
|
|
31
|
+
await open(loginUrl);
|
|
32
|
+
|
|
33
|
+
// 2. Poll Status
|
|
34
|
+
const pollSpinner = ora('Waiting for approval in browser...').start();
|
|
35
|
+
|
|
36
|
+
let attempts = 0;
|
|
37
|
+
const maxAttempts = 60; // 2 minutes (2s * 60)
|
|
38
|
+
|
|
39
|
+
const pollInterval = setInterval(async () => {
|
|
40
|
+
try {
|
|
41
|
+
const pollRes = await axios.post(`${CONVEX_SITE_URL}/cli/auth/poll`, { tempCode });
|
|
42
|
+
const { status, token, userId } = pollRes.data;
|
|
43
|
+
|
|
44
|
+
if (status === 'approved' && token) {
|
|
45
|
+
clearInterval(pollInterval);
|
|
46
|
+
config.set('auth_token', token);
|
|
47
|
+
config.set('user_id', userId);
|
|
48
|
+
config.set('convex_url', CONVEX_SITE_URL); // Store for future use
|
|
49
|
+
|
|
50
|
+
pollSpinner.succeed(chalk.green('ā
Login Successful!'));
|
|
51
|
+
console.log(chalk.dim(` Token saved to ${config.path}`));
|
|
52
|
+
process.exit(0);
|
|
53
|
+
} else if (status === 'expired' || status === 'rejected') {
|
|
54
|
+
clearInterval(pollInterval);
|
|
55
|
+
pollSpinner.fail(chalk.red(`ā Session ${status}. Please try again.`));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
} else {
|
|
58
|
+
attempts++;
|
|
59
|
+
if (attempts >= maxAttempts) {
|
|
60
|
+
clearInterval(pollInterval);
|
|
61
|
+
pollSpinner.fail(chalk.red('ā Timed out.'));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
// Ignore poll errors (network blips)
|
|
67
|
+
}
|
|
68
|
+
}, 2000);
|
|
69
|
+
|
|
70
|
+
} catch (error: any) {
|
|
71
|
+
console.error(chalk.red('\nā Login failed:'), error.message);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { simpleGit } from 'simple-git';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import ignore from 'ignore';
|
|
8
|
+
import Conf from 'conf';
|
|
9
|
+
import { extractSkeleton } from '../utils/ast-extractor.js';
|
|
10
|
+
|
|
11
|
+
const git = simpleGit();
|
|
12
|
+
const config = new Conf({ projectName: 'monoai' });
|
|
13
|
+
|
|
14
|
+
export const syncCommand = new Command('push')
|
|
15
|
+
.description('Push codebase integrity and AST skeleton to MonoAI')
|
|
16
|
+
.action(async () => {
|
|
17
|
+
try {
|
|
18
|
+
console.log(chalk.blue('šļø Starting MonoAI Strategic Push...'));
|
|
19
|
+
|
|
20
|
+
// 0. Auth Check
|
|
21
|
+
const token = config.get('auth_token');
|
|
22
|
+
if (!token) {
|
|
23
|
+
console.error(chalk.red('ā Not Authenticated. Please run:'));
|
|
24
|
+
console.error(chalk.white(' npx monoai login'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const isRepo = await git.checkIsRepo();
|
|
29
|
+
if (!isRepo) {
|
|
30
|
+
console.error(chalk.red('ā Not a git repository.'));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 1. Git Metadata (Zero-HITL Intent)
|
|
35
|
+
const log = await git.log({ maxCount: 1 });
|
|
36
|
+
const lastCommit = log.latest;
|
|
37
|
+
const branch = await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
38
|
+
|
|
39
|
+
if (!lastCommit) {
|
|
40
|
+
console.error(chalk.red('ā No commits found.'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(chalk.dim(` Branch: ${chalk.white(branch)}`));
|
|
45
|
+
console.log(chalk.dim(` Commit: ${chalk.white(lastCommit.hash.substring(0, 7))}`));
|
|
46
|
+
|
|
47
|
+
// 2. Scan & Extract AST Skeleton
|
|
48
|
+
console.log(chalk.blue('š Analyzing structural integrity (AST)...'));
|
|
49
|
+
|
|
50
|
+
const ig = ignore();
|
|
51
|
+
if (fs.existsSync('.gitignore')) {
|
|
52
|
+
ig.add(fs.readFileSync('.gitignore').toString());
|
|
53
|
+
}
|
|
54
|
+
// Hardcoded safety
|
|
55
|
+
ig.add(['node_modules', '.git', 'dist', '.env', 'build']);
|
|
56
|
+
|
|
57
|
+
const filesToAnalyze: string[] = [];
|
|
58
|
+
const scanDir = (dir: string) => {
|
|
59
|
+
const items = fs.readdirSync(dir);
|
|
60
|
+
for (const item of items) {
|
|
61
|
+
const fullPath = path.join(dir, item);
|
|
62
|
+
const relativePath = path.relative(process.cwd(), fullPath);
|
|
63
|
+
|
|
64
|
+
if (ig.ignores(relativePath)) continue;
|
|
65
|
+
|
|
66
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
67
|
+
scanDir(fullPath);
|
|
68
|
+
} else if (/\.(ts|tsx|js|jsx)$/.test(item)) {
|
|
69
|
+
filesToAnalyze.push(fullPath);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
scanDir(process.cwd());
|
|
75
|
+
const skeleton = extractSkeleton(filesToAnalyze);
|
|
76
|
+
|
|
77
|
+
// 3. Payload Construction
|
|
78
|
+
const payload = {
|
|
79
|
+
name: path.basename(process.cwd()),
|
|
80
|
+
branch: branch,
|
|
81
|
+
commitId: lastCommit.hash.substring(0, 7),
|
|
82
|
+
commitMessage: lastCommit.message,
|
|
83
|
+
structure: JSON.stringify(skeleton), // Now structured AST
|
|
84
|
+
syncStatus: 'success' as const,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// 4. Send to Navigator (Convex)
|
|
88
|
+
console.log(chalk.blue('š” Transmitting to Value Engine...'));
|
|
89
|
+
|
|
90
|
+
const CONVEX_SITE_URL = config.get('convex_url') as string || 'https://majestic-crane-609.convex.site';
|
|
91
|
+
|
|
92
|
+
await axios.post(`${CONVEX_SITE_URL}/cli/git-commit`, {
|
|
93
|
+
codebaseData: payload
|
|
94
|
+
}, {
|
|
95
|
+
headers: {
|
|
96
|
+
'Authorization': `Bearer ${token}`
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
console.log(chalk.green('⨠[Navigator] Push complete! Check your dashboard for Navigator alignment.'));
|
|
101
|
+
console.log(chalk.dim(` Message: ${lastCommit.message.split('\n')[0]}`));
|
|
102
|
+
|
|
103
|
+
} catch (error: any) {
|
|
104
|
+
if (error.response?.status === 401) {
|
|
105
|
+
console.error(chalk.red('ā Authentication Expired. Please run:'));
|
|
106
|
+
console.error(chalk.white(' npx monoai login'));
|
|
107
|
+
} else {
|
|
108
|
+
console.error(chalk.red('ā Sync failed:'), error.message);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { syncCommand } from './commands/sync.js';
|
|
4
|
+
import { loginCommand } from './commands/login.js';
|
|
5
|
+
|
|
6
|
+
const program = new Command();
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('monoai')
|
|
10
|
+
.description('MonoAI CLI - Strategic Navigator')
|
|
11
|
+
.version('0.1.2');
|
|
12
|
+
|
|
13
|
+
// Git sub-command group
|
|
14
|
+
const git = new Command('git').description('Git related operations');
|
|
15
|
+
git.addCommand(syncCommand.name('push'));
|
|
16
|
+
|
|
17
|
+
program.addCommand(loginCommand);
|
|
18
|
+
program.addCommand(git);
|
|
19
|
+
|
|
20
|
+
program.parse();
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Project, SyntaxKind, StringLiteral } from 'ts-morph';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface CodeSkeleton {
|
|
5
|
+
functions: any[];
|
|
6
|
+
classes: any[];
|
|
7
|
+
interfaces: any[];
|
|
8
|
+
types: any[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// š”ļø Security: Redaction Patterns
|
|
12
|
+
const SECRET_PATTERNS = [
|
|
13
|
+
/sk-[a-zA-Z0-9-_]{20,}/g, // OpenAI / Stripe style
|
|
14
|
+
/eyJ[a-zA-Z0-9-_]{20,}/g, // JWT style
|
|
15
|
+
/AIza[0-9A-Za-z-_]{35}/g, // Google Cloud style
|
|
16
|
+
/ghp_[a-zA-Z0-9]{36}/g // GitHub Personal Access Token
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function extractSkeleton(filePaths: string[]): Record<string, CodeSkeleton> {
|
|
20
|
+
const project = new Project();
|
|
21
|
+
|
|
22
|
+
// š”ļø Security: File Filter
|
|
23
|
+
const safePaths = filePaths.filter(p => {
|
|
24
|
+
const base = path.basename(p);
|
|
25
|
+
if (base.startsWith('.env')) return false;
|
|
26
|
+
if (base === '.DS_Store') return false;
|
|
27
|
+
if (p.includes('node_modules')) return false;
|
|
28
|
+
if (p.includes('.git/')) return false;
|
|
29
|
+
return true;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
project.addSourceFilesAtPaths(safePaths);
|
|
33
|
+
|
|
34
|
+
const result: Record<string, CodeSkeleton> = {};
|
|
35
|
+
let totalPayloadSize = 0;
|
|
36
|
+
|
|
37
|
+
project.getSourceFiles().forEach(sourceFile => {
|
|
38
|
+
// š”ļø Security: Secret Redaction (Active Scanning)
|
|
39
|
+
sourceFile.forEachDescendant((node) => {
|
|
40
|
+
if (node.getKind() === SyntaxKind.StringLiteral) {
|
|
41
|
+
const sl = node as StringLiteral;
|
|
42
|
+
const text = sl.getLiteralText();
|
|
43
|
+
let redacted = text;
|
|
44
|
+
let found = false;
|
|
45
|
+
|
|
46
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
47
|
+
if (pattern.test(text)) {
|
|
48
|
+
redacted = '[REDACTED_SECRET]';
|
|
49
|
+
found = true;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (found) {
|
|
55
|
+
// AST Rewrite (Memory only, does not save to disk)
|
|
56
|
+
sl.setLiteralValue(redacted);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const filePath = sourceFile.getFilePath();
|
|
62
|
+
const skeleton: CodeSkeleton = {
|
|
63
|
+
functions: [],
|
|
64
|
+
classes: [],
|
|
65
|
+
interfaces: [],
|
|
66
|
+
types: []
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Extract Functions
|
|
70
|
+
sourceFile.getFunctions().forEach(f => {
|
|
71
|
+
if (f.isExported()) {
|
|
72
|
+
skeleton.functions.push({
|
|
73
|
+
name: f.getName(),
|
|
74
|
+
parameters: f.getParameters().map(p => ({
|
|
75
|
+
name: p.getName(),
|
|
76
|
+
type: p.getType().getText()
|
|
77
|
+
})),
|
|
78
|
+
returnType: f.getReturnType().getText(),
|
|
79
|
+
jsDoc: f.getJsDocs().map(d => d.getCommentText()).join('\n')
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Extract Classes
|
|
85
|
+
sourceFile.getClasses().forEach(c => {
|
|
86
|
+
if (c.isExported()) {
|
|
87
|
+
skeleton.classes.push({
|
|
88
|
+
name: c.getName(),
|
|
89
|
+
methods: c.getMethods().map(m => ({
|
|
90
|
+
name: m.getName(),
|
|
91
|
+
parameters: m.getParameters().map(p => ({
|
|
92
|
+
name: p.getName(),
|
|
93
|
+
type: p.getType().getText()
|
|
94
|
+
})),
|
|
95
|
+
returnType: m.getReturnType().getText()
|
|
96
|
+
})),
|
|
97
|
+
jsDoc: c.getJsDocs().map(d => d.getCommentText()).join('\n')
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Extract Interfaces
|
|
103
|
+
sourceFile.getInterfaces().forEach(i => {
|
|
104
|
+
if (i.isExported()) {
|
|
105
|
+
skeleton.interfaces.push({
|
|
106
|
+
name: i.getName(),
|
|
107
|
+
jsDoc: i.getJsDocs().map(d => d.getCommentText()).join('\n')
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Extract Types
|
|
113
|
+
sourceFile.getTypeAliases().forEach(t => {
|
|
114
|
+
if (t.isExported()) {
|
|
115
|
+
skeleton.types.push({
|
|
116
|
+
name: t.getName(),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
result[filePath] = skeleton;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// š”ļø Security: Payload Size Limit (DoS Prevention)
|
|
125
|
+
const payloadString = JSON.stringify(result);
|
|
126
|
+
if (payloadString.length > 5 * 1024 * 1024) { // 5MB
|
|
127
|
+
throw new Error("Payload too large. Security limit exceeded (5MB).");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
|
|
3
|
+
interface CliConfig {
|
|
4
|
+
authToken?: string;
|
|
5
|
+
convexUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const config = new Conf<CliConfig>({
|
|
9
|
+
projectName: 'monoai',
|
|
10
|
+
projectSuffix: 'cli'
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const saveCredentials = (token: string, url: string) => {
|
|
14
|
+
config.set('authToken', token);
|
|
15
|
+
config.set('convexUrl', url);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const getCredentials = () => {
|
|
19
|
+
return {
|
|
20
|
+
authToken: config.get('authToken'),
|
|
21
|
+
convexUrl: config.get('convexUrl')
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const clearCredentials = () => {
|
|
26
|
+
config.delete('authToken');
|
|
27
|
+
config.delete('convexUrl');
|
|
28
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"moduleResolution": "node16",
|
|
12
|
+
"resolveJsonModule": true
|
|
13
|
+
},
|
|
14
|
+
"include": [
|
|
15
|
+
"src/**/*"
|
|
16
|
+
],
|
|
17
|
+
"exclude": [
|
|
18
|
+
"node_modules",
|
|
19
|
+
"dist"
|
|
20
|
+
]
|
|
21
|
+
}
|