ultra-dex 2.2.0 → 2.2.1

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.
@@ -3,6 +3,7 @@ import fs from 'fs/promises';
3
3
  import path from 'path';
4
4
  import { ASSETS_ROOT, ROOT_FALLBACK } from '../config/paths.js';
5
5
  import { githubBlobUrl } from '../config/urls.js';
6
+ import { readWithFallback } from '../utils/fallback.js';
6
7
 
7
8
  export const AGENTS = [
8
9
  { name: 'cto', description: 'Architecture & tech decisions', file: '1-leadership/cto.md', tier: 'Leadership' },
@@ -24,12 +25,8 @@ export const AGENTS = [
24
25
 
25
26
  async function readAgentPrompt(agent) {
26
27
  const agentPath = path.join(ASSETS_ROOT, 'agents', agent.file);
27
- try {
28
- return await fs.readFile(agentPath, 'utf-8');
29
- } catch (err) {
30
- const fallbackPath = path.join(ROOT_FALLBACK, 'agents', agent.file);
31
- return await fs.readFile(fallbackPath, 'utf-8');
32
- }
28
+ const fallbackPath = path.join(ROOT_FALLBACK, 'agents', agent.file);
29
+ return readWithFallback(agentPath, fallbackPath, 'utf-8');
33
30
  }
34
31
 
35
32
  export function registerAgentsCommand(program) {
@@ -207,12 +207,10 @@ ${answers.ideaWhat} for ${answers.ideaFor}.
207
207
  const cursorRulesPath = path.join(ASSETS_ROOT, 'cursor-rules');
208
208
  const fallbackRulesPath = path.join(ROOT_FALLBACK, 'cursor-rules');
209
209
  try {
210
- const ruleFiles = await listWithFallback(cursorRulesPath, fallbackRulesPath);
211
- const sourcePath = ruleFiles ? cursorRulesPath : fallbackRulesPath;
210
+ const { files: ruleFiles, sourcePath } = await listWithFallback(cursorRulesPath, fallbackRulesPath);
212
211
  for (const file of ruleFiles.filter(f => f.endsWith('.mdc'))) {
213
- await copyWithFallback(
212
+ await fs.copyFile(
214
213
  path.join(sourcePath, file),
215
- null,
216
214
  path.join(rulesDir, file)
217
215
  );
218
216
  }
@@ -330,7 +328,7 @@ ${answers.ideaWhat} for ${answers.ideaFor}.
330
328
  console.log(chalk.blue(` ${githubWebUrl()}`));
331
329
  } catch (error) {
332
330
  spinner.fail(chalk.red('Failed to create project'));
333
- console.error(error);
331
+ console.error(`[init] ${error?.message ?? error}`);
334
332
  process.exit(1);
335
333
  }
336
334
  });
@@ -1,17 +1,69 @@
1
1
  import chalk from 'chalk';
2
- import fs from 'fs/promises';
3
2
  import http from 'http';
4
- import { validateSafePath } from '../utils/validation.js';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import { readFileSafe } from '../utils/files.js';
5
6
 
6
- async function readFileSafe(filePath, label) {
7
+ // State management helpers
8
+ async function loadState() {
7
9
  try {
8
- const content = await fs.readFile(filePath, 'utf-8');
9
- return { label, content };
10
+ const content = await fs.readFile(path.resolve(process.cwd(), '.ultra/state.json'), 'utf8');
11
+ return JSON.parse(content);
10
12
  } catch {
11
- return { label, content: '' };
13
+ return null;
14
+ }
15
+ }
16
+
17
+ async function computeState() {
18
+ const state = {
19
+ version: '2.1.0',
20
+ updatedAt: new Date().toISOString(),
21
+ project: { name: path.basename(process.cwd()) },
22
+ files: {},
23
+ sections: { total: 34, completed: 0, list: [] },
24
+ score: 0
25
+ };
26
+
27
+ const coreFiles = ['CONTEXT.md', 'IMPLEMENTATION-PLAN.md', 'CHECKLIST.md', 'QUICK-START.md'];
28
+ for (const file of coreFiles) {
29
+ try {
30
+ const stat = await fs.stat(path.resolve(process.cwd(), file));
31
+ state.files[file] = { exists: true, size: stat.size };
32
+ } catch {
33
+ state.files[file] = { exists: false };
34
+ }
12
35
  }
36
+
37
+ try {
38
+ const plan = await fs.readFile(path.resolve(process.cwd(), 'IMPLEMENTATION-PLAN.md'), 'utf8');
39
+ const sectionRegex = /^##\s+(\d+)\.\s+(.+)$/gm;
40
+ let match;
41
+ while ((match = sectionRegex.exec(plan)) !== null) {
42
+ state.sections.list.push({ number: parseInt(match[1]), title: match[2].trim() });
43
+ }
44
+ state.sections.completed = state.sections.list.length;
45
+ } catch { /* no plan */ }
46
+
47
+ const fileScore = Object.values(state.files).filter(f => f.exists).length / coreFiles.length * 40;
48
+ const sectionScore = state.sections.completed / state.sections.total * 60;
49
+ state.score = Math.round(fileScore + sectionScore);
50
+
51
+ return state;
13
52
  }
14
53
 
54
+ const BUILD_AGENTS = [
55
+ { name: 'planner', tier: 'architect', task: 'Break down requirements into tasks' },
56
+ { name: 'cto', tier: 'architect', task: 'Technical decisions & architecture' },
57
+ { name: 'backend', tier: 'core', task: 'API, business logic, services' },
58
+ { name: 'frontend', tier: 'core', task: 'UI components, pages, styling' },
59
+ { name: 'database', tier: 'core', task: 'Schema design, migrations, queries' },
60
+ { name: 'auth', tier: 'specialist', task: 'Authentication & authorization' },
61
+ { name: 'security', tier: 'specialist', task: 'Security audit & hardening' },
62
+ { name: 'testing', tier: 'specialist', task: 'Test strategy & implementation' },
63
+ { name: 'reviewer', tier: 'quality', task: 'Code review & best practices' },
64
+ { name: 'devops', tier: 'quality', task: 'CI/CD, deployment, infrastructure' }
65
+ ];
66
+
15
67
  export function registerServeCommand(program) {
16
68
  program
17
69
  .command('serve')
@@ -25,13 +77,24 @@ export function registerServeCommand(program) {
25
77
  }
26
78
 
27
79
  const server = http.createServer(async (req, res) => {
80
+ res.setHeader('Access-Control-Allow-Origin', '*');
81
+
28
82
  if (!req.url || req.url === '/') {
29
- res.writeHead(200, { 'Content-Type': 'text/plain' });
30
- res.end('Ultra-Dex MCP Server\n');
83
+ res.writeHead(200, { 'Content-Type': 'application/json' });
84
+ res.end(JSON.stringify({
85
+ name: 'Ultra-Dex MCP Server',
86
+ version: '2.1.0',
87
+ endpoints: ['/context', '/state', '/score', '/agents', '/agent/:name', '/refresh']
88
+ }));
31
89
  return;
32
90
  }
33
91
 
34
92
  if (req.url === '/context') {
93
+ const meta = {
94
+ protocol: 'mcp-lite',
95
+ version: '0.1',
96
+ generatedAt: new Date().toISOString(),
97
+ };
35
98
  const [context, plan, quickStart] = await Promise.all([
36
99
  readFileSafe('CONTEXT.md', 'CONTEXT.md'),
37
100
  readFileSafe('IMPLEMENTATION-PLAN.md', 'IMPLEMENTATION-PLAN.md'),
@@ -39,7 +102,54 @@ export function registerServeCommand(program) {
39
102
  ]);
40
103
 
41
104
  res.writeHead(200, { 'Content-Type': 'application/json' });
42
- res.end(JSON.stringify({ files: [context, plan, quickStart] }));
105
+ res.end(JSON.stringify({ meta, files: [context, plan, quickStart] }));
106
+ return;
107
+ }
108
+
109
+ // /state - returns .ultra/state.json
110
+ if (req.url === '/state') {
111
+ let state = await loadState();
112
+ if (!state) state = await computeState();
113
+ res.writeHead(200, { 'Content-Type': 'application/json' });
114
+ res.end(JSON.stringify(state));
115
+ return;
116
+ }
117
+
118
+ // /score - quick alignment score
119
+ if (req.url === '/score') {
120
+ const state = await computeState();
121
+ res.writeHead(200, { 'Content-Type': 'application/json' });
122
+ res.end(JSON.stringify({ score: state.score, sections: state.sections.completed, total: 34 }));
123
+ return;
124
+ }
125
+
126
+ // /agents - list available agents
127
+ if (req.url === '/agents') {
128
+ res.writeHead(200, { 'Content-Type': 'application/json' });
129
+ res.end(JSON.stringify({ agents: BUILD_AGENTS }));
130
+ return;
131
+ }
132
+
133
+ // /agent/:name - get specific agent prompt
134
+ if (req.url.startsWith('/agent/')) {
135
+ const agentName = req.url.replace('/agent/', '');
136
+ try {
137
+ const agentPath = path.resolve(process.cwd(), `agents/${agentName}.md`);
138
+ const content = await fs.readFile(agentPath, 'utf8');
139
+ res.writeHead(200, { 'Content-Type': 'application/json' });
140
+ res.end(JSON.stringify({ agent: agentName, prompt: content }));
141
+ } catch {
142
+ res.writeHead(404, { 'Content-Type': 'application/json' });
143
+ res.end(JSON.stringify({ error: `Agent ${agentName} not found` }));
144
+ }
145
+ return;
146
+ }
147
+
148
+ // /refresh - force state refresh
149
+ if (req.url === '/refresh') {
150
+ const state = await computeState();
151
+ res.writeHead(200, { 'Content-Type': 'application/json' });
152
+ res.end(JSON.stringify({ refreshed: true, score: state.score }));
43
153
  return;
44
154
  }
45
155
 
@@ -49,8 +159,15 @@ export function registerServeCommand(program) {
49
159
 
50
160
  server.listen(port, () => {
51
161
  console.log(chalk.green(`\n✅ Ultra-Dex MCP server running on http://localhost:${port}`));
52
- console.log(chalk.gray(' GET /context -> CONTEXT.md, IMPLEMENTATION-PLAN.md, QUICK-START.md'));
53
- console.log(chalk.gray(' GET / -> health check\n'));
162
+ console.log(chalk.bold('\n📡 Endpoints:'));
163
+ console.log(chalk.gray(' GET / Server info & endpoint list'));
164
+ console.log(chalk.gray(' GET /context → All context files'));
165
+ console.log(chalk.gray(' GET /state → Full project state'));
166
+ console.log(chalk.gray(' GET /score → Quick alignment score'));
167
+ console.log(chalk.gray(' GET /agents → List available agents'));
168
+ console.log(chalk.gray(' GET /agent/:n → Get specific agent prompt'));
169
+ console.log(chalk.gray(' GET /refresh → Force state refresh'));
170
+ console.log(chalk.cyan('\n💡 Connect your AI tool to this server for live context.\n'));
54
171
  });
55
172
  });
56
173
  }
@@ -0,0 +1,35 @@
1
+ import chalk from 'chalk';
2
+ import path from 'path';
3
+ import { snapshotContext } from '../utils/sync.js';
4
+
5
+ export function registerSyncCommand(program) {
6
+ program
7
+ .command('sync')
8
+ .description('Auto-sync CONTEXT.md with current codebase')
9
+ .option('-d, --dir <directory>', 'Project directory', '.')
10
+ .action(async (options) => {
11
+ const rootDir = path.resolve(options.dir);
12
+ console.log(chalk.cyan('\n🔁 Ultra-Dex Context Sync\n'));
13
+
14
+ try {
15
+ const result = await snapshotContext(rootDir);
16
+ if (result.missingContext) {
17
+ console.log(chalk.red('❌ CONTEXT.md not found. Run `ultra-dex init` first.'));
18
+ process.exit(1);
19
+ }
20
+
21
+ if (result.updated) {
22
+ console.log(chalk.green('✅ CONTEXT.md updated with latest snapshot.'));
23
+ } else {
24
+ console.log(chalk.yellow('⚠️ CONTEXT.md already up to date.'));
25
+ }
26
+ console.log(chalk.gray(`Files scanned: ${result.summary.fileCount}`));
27
+ console.log(chalk.gray(`Stack guess: ${result.summary.stack}`));
28
+ console.log(chalk.gray(`Changes since last sync: +${result.diff.added} / -${result.diff.removed}\n`));
29
+ } catch (error) {
30
+ console.log(chalk.red('❌ Sync failed.'));
31
+ console.error(error);
32
+ process.exit(1);
33
+ }
34
+ });
35
+ }
@@ -1,4 +1,4 @@
1
- import { githubBlobUrl, githubTreeUrl } from '../config/urls.js';
1
+ import { githubBlobUrl, githubWebUrl } from '../config/urls.js';
2
2
 
3
3
  export const CONTEXT_TEMPLATE = `# {{PROJECT_NAME}} - Context
4
4
 
@@ -21,6 +21,6 @@ export const CONTEXT_TEMPLATE = `# {{PROJECT_NAME}} - Context
21
21
  Setting up the implementation plan.
22
22
 
23
23
  ## Resources
24
- - [Ultra-Dex Template](${githubTreeUrl('')})
24
+ - [Ultra-Dex Template](${githubWebUrl()})
25
25
  - [TaskFlow Example](${githubBlobUrl('@%20Ultra%20DeX/Saas%20plan/Examples/TaskFlow-Complete.md')})
26
26
  `;
@@ -26,11 +26,13 @@ export async function copyWithFallback(primaryPath, fallbackPath, destinationPat
26
26
 
27
27
  export async function listWithFallback(primaryPath, fallbackPath) {
28
28
  try {
29
- return await fs.readdir(primaryPath);
29
+ const files = await fs.readdir(primaryPath);
30
+ return { files, sourcePath: primaryPath };
30
31
  } catch (primaryError) {
31
32
  if (!fallbackPath) {
32
33
  throw primaryError;
33
34
  }
34
- return await fs.readdir(fallbackPath);
35
+ const files = await fs.readdir(fallbackPath);
36
+ return { files, sourcePath: fallbackPath };
35
37
  }
36
38
  }
@@ -21,47 +21,6 @@ export async function pathExists(targetPath, type = 'file') {
21
21
  }
22
22
  }
23
23
 
24
- export async function copyWithFallback({ primary, fallback, destination, onPrimaryMissing }) {
25
- try {
26
- await fs.copyFile(primary, destination);
27
- return 'primary';
28
- } catch (primaryError) {
29
- if (onPrimaryMissing) {
30
- onPrimaryMissing(primaryError);
31
- }
32
- if (!fallback) {
33
- throw primaryError;
34
- }
35
- await fs.copyFile(fallback, destination);
36
- return 'fallback';
37
- }
38
- }
39
-
40
- export async function readWithFallback({ primary, fallback, encoding = 'utf-8' }) {
41
- try {
42
- return await fs.readFile(primary, encoding);
43
- } catch (primaryError) {
44
- if (!fallback) {
45
- throw primaryError;
46
- }
47
- return await fs.readFile(fallback, encoding);
48
- }
49
- }
50
-
51
24
  export function resolveAssetPath(basePath, relativePath) {
52
25
  return path.join(basePath, relativePath);
53
26
  }
54
-
55
- export async function copyDirectory(srcDir, destDir) {
56
- await fs.mkdir(destDir, { recursive: true });
57
- const entries = await fs.readdir(srcDir, { withFileTypes: true });
58
- for (const entry of entries) {
59
- const srcPath = path.join(srcDir, entry.name);
60
- const destPath = path.join(destDir, entry.name);
61
- if (entry.isDirectory()) {
62
- await copyDirectory(srcPath, destPath);
63
- } else if (entry.isFile()) {
64
- await fs.copyFile(srcPath, destPath);
65
- }
66
- }
67
- }
@@ -0,0 +1,216 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ const IGNORED_DIRS = new Set([
5
+ '.git',
6
+ 'node_modules',
7
+ '.next',
8
+ 'dist',
9
+ 'build',
10
+ 'out',
11
+ '.turbo',
12
+ '.cache',
13
+ '.ultra-dex',
14
+ '.cursor',
15
+ '.agents',
16
+ 'coverage',
17
+ '.idea',
18
+ '.vscode',
19
+ ]);
20
+ const IGNORED_FILES = new Set(['CONTEXT.md', '.DS_Store']);
21
+ const SNAPSHOT_DIR = '.ultra-dex';
22
+ const SNAPSHOT_FILE = 'context-snapshot.json';
23
+ const AUTO_SYNC_HEADER = '## Auto-Sync Snapshot';
24
+ const SCHEMA_PATTERNS = [
25
+ /schema\.prisma$/i,
26
+ /drizzle\/schema/i,
27
+ /supabase\/migrations/i,
28
+ /migrations\/.*\.(sql|ts|js)$/i,
29
+ /db\/schema/i,
30
+ ];
31
+
32
+ async function listFilesRecursive(rootDir, baseDir = rootDir) {
33
+ let results = [];
34
+ const entries = await fs.readdir(rootDir, { withFileTypes: true });
35
+ for (const entry of entries) {
36
+ if (IGNORED_DIRS.has(entry.name)) continue;
37
+ const fullPath = path.join(rootDir, entry.name);
38
+ const relativePath = path.relative(baseDir, fullPath);
39
+ if (entry.isDirectory()) {
40
+ results = results.concat(await listFilesRecursive(fullPath, baseDir));
41
+ } else if (entry.isFile()) {
42
+ if (IGNORED_FILES.has(entry.name)) continue;
43
+ results.push(relativePath);
44
+ }
45
+ }
46
+ return results;
47
+ }
48
+
49
+ function inferStackFromFiles(fileList) {
50
+ if (fileList.some((file) => file.includes('package.json'))) {
51
+ if (fileList.some((file) => file.includes('next.config'))) return 'Next.js';
52
+ if (fileList.some((file) => file.includes('remix.config'))) return 'Remix';
53
+ if (fileList.some((file) => file.includes('svelte.config'))) return 'SvelteKit';
54
+ return 'Node.js';
55
+ }
56
+ if (fileList.some((file) => file.includes('pyproject.toml') || file.includes('requirements.txt'))) {
57
+ return 'Python';
58
+ }
59
+ return 'Unknown';
60
+ }
61
+
62
+ function classifyFilePaths(files) {
63
+ const appFiles = [];
64
+ const apiFiles = [];
65
+ const schemaFiles = [];
66
+ const configFiles = [];
67
+
68
+ for (const file of files) {
69
+ if (SCHEMA_PATTERNS.some((pattern) => pattern.test(file))) {
70
+ schemaFiles.push(file);
71
+ continue;
72
+ }
73
+ if (/^app\/api\//i.test(file) || /api\/.*\.(ts|js)$/i.test(file)) {
74
+ apiFiles.push(file);
75
+ continue;
76
+ }
77
+ if (/\.(tsx|jsx|svelte|vue)$/i.test(file) || /^app\//i.test(file)) {
78
+ appFiles.push(file);
79
+ continue;
80
+ }
81
+ if (/(config|\.config)\.(js|ts|json)$/i.test(file) || /\.(env|toml|yaml|yml)$/i.test(file)) {
82
+ configFiles.push(file);
83
+ }
84
+ }
85
+
86
+ return { appFiles, apiFiles, schemaFiles, configFiles };
87
+ }
88
+
89
+ function buildAutoSyncSection(summary) {
90
+ const lines = [];
91
+ lines.push(AUTO_SYNC_HEADER);
92
+ lines.push('');
93
+ lines.push(`- Last synced: ${summary.generatedAt}`);
94
+ lines.push(`- Project root: ${summary.root}`);
95
+ lines.push(`- Stack guess: ${summary.stack}`);
96
+ lines.push(`- Total files scanned: ${summary.fileCount}`);
97
+ lines.push(`- App/UI files: ${summary.appCount}`);
98
+ lines.push(`- API files: ${summary.apiCount}`);
99
+ lines.push(`- Schema files: ${summary.schemaCount}`);
100
+ lines.push(`- Config files: ${summary.configCount}`);
101
+ lines.push('');
102
+ if (summary.appFiles.length > 0) {
103
+ lines.push('### App/UI Files');
104
+ lines.push(...summary.appFiles.map((file) => `- ${file}`));
105
+ lines.push('');
106
+ }
107
+ if (summary.apiFiles.length > 0) {
108
+ lines.push('### API Files');
109
+ lines.push(...summary.apiFiles.map((file) => `- ${file}`));
110
+ lines.push('');
111
+ }
112
+ if (summary.schemaFiles.length > 0) {
113
+ lines.push('### Schema Files');
114
+ lines.push(...summary.schemaFiles.map((file) => `- ${file}`));
115
+ lines.push('');
116
+ }
117
+ if (summary.configFiles.length > 0) {
118
+ lines.push('### Config Files');
119
+ lines.push(...summary.configFiles.map((file) => `- ${file}`));
120
+ lines.push('');
121
+ }
122
+
123
+ return lines.join('\n').trim();
124
+ }
125
+
126
+ function summarizeDiff(previous, next) {
127
+ if (!previous) {
128
+ return {
129
+ added: next.fileCount,
130
+ removed: 0,
131
+ };
132
+ }
133
+ const previousSet = new Set(previous.fileList || []);
134
+ const nextSet = new Set(next.fileList || []);
135
+ let added = 0;
136
+ let removed = 0;
137
+ for (const file of nextSet) {
138
+ if (!previousSet.has(file)) added++;
139
+ }
140
+ for (const file of previousSet) {
141
+ if (!nextSet.has(file)) removed++;
142
+ }
143
+ return { added, removed };
144
+ }
145
+
146
+ export async function snapshotContext(rootDir) {
147
+ const files = await listFilesRecursive(rootDir);
148
+ const { appFiles, apiFiles, schemaFiles, configFiles } = classifyFilePaths(files);
149
+ const summary = {
150
+ generatedAt: new Date().toISOString(),
151
+ root: rootDir,
152
+ fileCount: files.length,
153
+ appCount: appFiles.length,
154
+ apiCount: apiFiles.length,
155
+ schemaCount: schemaFiles.length,
156
+ configCount: configFiles.length,
157
+ appFiles: appFiles.slice(0, 25),
158
+ apiFiles: apiFiles.slice(0, 25),
159
+ schemaFiles: schemaFiles.slice(0, 25),
160
+ configFiles: configFiles.slice(0, 25),
161
+ stack: inferStackFromFiles(files),
162
+ fileList: files,
163
+ };
164
+
165
+ const snapshotDir = path.join(rootDir, SNAPSHOT_DIR);
166
+ await fs.mkdir(snapshotDir, { recursive: true });
167
+ const snapshotPath = path.join(snapshotDir, SNAPSHOT_FILE);
168
+ let previous = null;
169
+ try {
170
+ const previousRaw = await fs.readFile(snapshotPath, 'utf-8');
171
+ previous = JSON.parse(previousRaw);
172
+ } catch {
173
+ previous = null;
174
+ }
175
+
176
+ await fs.writeFile(snapshotPath, JSON.stringify(summary, null, 2));
177
+
178
+ const contextPath = path.join(rootDir, 'CONTEXT.md');
179
+ let contextContent = null;
180
+ try {
181
+ contextContent = await fs.readFile(contextPath, 'utf-8');
182
+ } catch {
183
+ return {
184
+ summary,
185
+ updated: false,
186
+ missingContext: true,
187
+ diff: summarizeDiff(previous, summary),
188
+ };
189
+ }
190
+
191
+ const section = buildAutoSyncSection(summary);
192
+ let updatedContext = contextContent;
193
+ if (contextContent.includes(AUTO_SYNC_HEADER)) {
194
+ const pattern = new RegExp(`${AUTO_SYNC_HEADER}[\\s\\S]*?(?=^##\\s|\\n##\\s|$)`, 'm');
195
+ updatedContext = contextContent.replace(pattern, `${section}\n\n`);
196
+ } else {
197
+ updatedContext = `${contextContent.trim()}\n\n${section}\n`;
198
+ }
199
+
200
+ if (updatedContext !== contextContent) {
201
+ await fs.writeFile(contextPath, updatedContext);
202
+ return {
203
+ summary,
204
+ updated: true,
205
+ missingContext: false,
206
+ diff: summarizeDiff(previous, summary),
207
+ };
208
+ }
209
+
210
+ return {
211
+ summary,
212
+ updated: false,
213
+ missingContext: false,
214
+ diff: summarizeDiff(previous, summary),
215
+ };
216
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra-dex",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "CLI for Ultra-Dex SaaS Implementation Framework with AI-Powered Plan Generation, Build Mode, and Code Review",
5
5
  "keywords": [
6
6
  "saas",