genexus-mcp 1.2.2 → 1.3.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.
- package/cli/commands/axi.js +5 -1
- package/cli/index.js +10 -1
- package/cli/lib/config.js +3 -1
- package/cli/lib/update-check.js +214 -0
- package/cli/run.test.js +19 -0
- package/package.json +1 -1
package/cli/commands/axi.js
CHANGED
|
@@ -871,6 +871,10 @@ function commandHelpMap() {
|
|
|
871
871
|
usage: 'genexus-mcp llm help [--full] [--fields f1,f2] [--format toon|json|text]',
|
|
872
872
|
examples: ['genexus-mcp llm help --format json', 'genexus-mcp llm help --full --format json']
|
|
873
873
|
},
|
|
874
|
+
update: {
|
|
875
|
+
usage: 'genexus-mcp update [--format toon|json|text]',
|
|
876
|
+
examples: ['genexus-mcp update', 'genexus-mcp update --format json']
|
|
877
|
+
},
|
|
874
878
|
layout: {
|
|
875
879
|
usage: 'genexus-mcp layout status [--title "GeneXus"] [--format ...] OR genexus-mcp layout run --action <focus|activate-layout|activate-tab|send-keys|type-text|click> [--tab "Layout"] [--keys "..."] [--text "..."] [--x N --y N] [--title "..."] [--format ...] OR genexus-mcp layout inspect [--tab "Layout"] [--limit N] [--full] [--title "..."] [--format ...]',
|
|
876
880
|
examples: ['genexus-mcp layout status --format json', 'genexus-mcp layout run --action activate-tab --tab "Layout" --format json', 'genexus-mcp layout inspect --tab Layout --format json']
|
|
@@ -935,7 +939,7 @@ async function handleHelp(targetCommand, ctx) {
|
|
|
935
939
|
bin: binPath,
|
|
936
940
|
command: 'genexus-mcp',
|
|
937
941
|
description: 'GeneXus MCP launcher and AXI-oriented utility CLI',
|
|
938
|
-
commands: ['home', 'axi home', 'status', 'doctor', 'tools list', 'config show', 'layout status', 'layout run', 'layout inspect', 'init', 'llm help', 'help'],
|
|
942
|
+
commands: ['home', 'axi home', 'status', 'doctor', 'tools list', 'config show', 'layout status', 'layout run', 'layout inspect', 'init', 'llm help', 'update', 'help'],
|
|
939
943
|
defaults: { format: 'toon', limit: 100 }
|
|
940
944
|
},
|
|
941
945
|
help: [
|
package/cli/index.js
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
usageEnvelope,
|
|
23
23
|
commandHelpMap
|
|
24
24
|
} = require('./commands/axi');
|
|
25
|
+
const { startBackgroundUpdateCheck, handleUpdate } = require('./lib/update-check');
|
|
25
26
|
|
|
26
27
|
const EXIT_CODES = {
|
|
27
28
|
OK: 0,
|
|
@@ -43,7 +44,7 @@ const GLOBAL_DEFAULTS = {
|
|
|
43
44
|
help: false
|
|
44
45
|
};
|
|
45
46
|
|
|
46
|
-
const KNOWN_COMMANDS = new Set(['status', 'doctor', 'tools', 'config', 'init', 'setup', 'help', 'home', 'axi', 'llm', 'layout']);
|
|
47
|
+
const KNOWN_COMMANDS = new Set(['status', 'doctor', 'tools', 'config', 'init', 'setup', 'help', 'home', 'axi', 'llm', 'layout', 'update']);
|
|
47
48
|
|
|
48
49
|
function parseArgs(argv) {
|
|
49
50
|
const result = {
|
|
@@ -332,12 +333,17 @@ function resolveMetaCommand(parsed, targetHelp) {
|
|
|
332
333
|
if (parsed.subcommand === 'inspect') return 'layout.inspect';
|
|
333
334
|
return 'layout.status';
|
|
334
335
|
}
|
|
336
|
+
if (parsed.command === 'update') return 'update';
|
|
335
337
|
return parsed.command || 'unknown';
|
|
336
338
|
}
|
|
337
339
|
|
|
338
340
|
async function main(argv) {
|
|
339
341
|
const parsed = parseArgs(argv);
|
|
340
342
|
|
|
343
|
+
if (parsed.command !== 'update') {
|
|
344
|
+
startBackgroundUpdateCheck({ quiet: parsed.options.quiet });
|
|
345
|
+
}
|
|
346
|
+
|
|
341
347
|
if (!parsed.command) {
|
|
342
348
|
return launchGateway(argv, parsed.options);
|
|
343
349
|
}
|
|
@@ -438,6 +444,9 @@ async function main(argv) {
|
|
|
438
444
|
case 'init':
|
|
439
445
|
result = await handleInit(parsed.options, ctx);
|
|
440
446
|
break;
|
|
447
|
+
case 'update':
|
|
448
|
+
result = await handleUpdate(parsed.options, ctx);
|
|
449
|
+
break;
|
|
441
450
|
default:
|
|
442
451
|
writeStructured(
|
|
443
452
|
process.stdout,
|
package/cli/lib/config.js
CHANGED
|
@@ -100,11 +100,13 @@ function patchClientConfig(targetConfigPath) {
|
|
|
100
100
|
const claudeWin = path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
|
|
101
101
|
const claudeMac = path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
102
102
|
const antigravityCfg = path.join(os.homedir(), '.gemini', 'antigravity', 'mcp_config.json');
|
|
103
|
+
const claudeCodeCfg = path.join(os.homedir(), '.claude.json');
|
|
103
104
|
|
|
104
105
|
const clients = [
|
|
105
106
|
{ path: claudeWin, name: 'Claude Desktop (Windows)' },
|
|
106
107
|
{ path: claudeMac, name: 'Claude Desktop (macOS)' },
|
|
107
|
-
{ path: antigravityCfg, name: 'Antigravity' }
|
|
108
|
+
{ path: antigravityCfg, name: 'Antigravity' },
|
|
109
|
+
{ path: claudeCodeCfg, name: 'Claude Code' }
|
|
108
110
|
];
|
|
109
111
|
|
|
110
112
|
const patched = [];
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
|
|
6
|
+
const REPO = 'lennix1337/Genexus18MCP';
|
|
7
|
+
const NPM_PACKAGE = 'genexus-mcp';
|
|
8
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
const FETCH_TIMEOUT_MS = 2000;
|
|
10
|
+
|
|
11
|
+
function getPackageVersion() {
|
|
12
|
+
try {
|
|
13
|
+
const pkg = require('../../package.json');
|
|
14
|
+
return typeof pkg.version === 'string' ? pkg.version : null;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getCacheFile() {
|
|
21
|
+
return path.join(os.homedir(), '.genexus-mcp', 'update-check.json');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readCache() {
|
|
25
|
+
try {
|
|
26
|
+
const raw = fs.readFileSync(getCacheFile(), 'utf8');
|
|
27
|
+
const data = JSON.parse(raw);
|
|
28
|
+
if (data && typeof data === 'object') return data;
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeCache(data) {
|
|
35
|
+
try {
|
|
36
|
+
const file = getCacheFile();
|
|
37
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
38
|
+
fs.writeFileSync(file, JSON.stringify(data), 'utf8');
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stripV(v) {
|
|
44
|
+
return typeof v === 'string' ? v.replace(/^v/i, '').trim() : '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseSemver(v) {
|
|
48
|
+
const s = stripV(v);
|
|
49
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(s);
|
|
50
|
+
if (!m) return null;
|
|
51
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function compareSemver(a, b) {
|
|
55
|
+
const pa = parseSemver(a);
|
|
56
|
+
const pb = parseSemver(b);
|
|
57
|
+
if (!pa || !pb) return 0;
|
|
58
|
+
for (let i = 0; i < 3; i += 1) {
|
|
59
|
+
if (pa[i] > pb[i]) return 1;
|
|
60
|
+
if (pa[i] < pb[i]) return -1;
|
|
61
|
+
}
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function fetchLatestRelease() {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const options = {
|
|
68
|
+
hostname: 'api.github.com',
|
|
69
|
+
path: `/repos/${REPO}/releases/latest`,
|
|
70
|
+
method: 'GET',
|
|
71
|
+
headers: {
|
|
72
|
+
'User-Agent': `${NPM_PACKAGE}-cli`,
|
|
73
|
+
'Accept': 'application/vnd.github+json'
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const req = https.request(options, (res) => {
|
|
78
|
+
if (res.statusCode !== 200) {
|
|
79
|
+
res.resume();
|
|
80
|
+
resolve(null);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
let body = '';
|
|
84
|
+
res.setEncoding('utf8');
|
|
85
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
86
|
+
res.on('end', () => {
|
|
87
|
+
try {
|
|
88
|
+
const json = JSON.parse(body);
|
|
89
|
+
const tag = stripV(json.tag_name || '');
|
|
90
|
+
const url = typeof json.html_url === 'string' ? json.html_url : null;
|
|
91
|
+
if (!tag) { resolve(null); return; }
|
|
92
|
+
resolve({ latestVersion: tag, releaseUrl: url });
|
|
93
|
+
} catch {
|
|
94
|
+
resolve(null);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
req.on('error', () => resolve(null));
|
|
100
|
+
req.setTimeout(FETCH_TIMEOUT_MS, () => {
|
|
101
|
+
req.destroy();
|
|
102
|
+
resolve(null);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
req.end();
|
|
106
|
+
if (typeof req.unref === 'function') req.unref();
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function formatBanner(current, latest, releaseUrl) {
|
|
111
|
+
const lines = [
|
|
112
|
+
`[genexus-mcp] update available: v${current} -> v${latest}`,
|
|
113
|
+
`[genexus-mcp] run: npm install -g ${NPM_PACKAGE}@latest`
|
|
114
|
+
];
|
|
115
|
+
if (releaseUrl) lines.push(`[genexus-mcp] release: ${releaseUrl}`);
|
|
116
|
+
return lines.join('\n') + '\n';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isDisabled(opts) {
|
|
120
|
+
if (process.env.GENEXUS_MCP_NO_UPDATE_CHECK === '1') return true;
|
|
121
|
+
if (opts && opts.quiet) return true;
|
|
122
|
+
if (!process.stderr || !process.stderr.isTTY) return true;
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function maybePrintCachedBanner(opts) {
|
|
127
|
+
const current = getPackageVersion();
|
|
128
|
+
if (!current) return;
|
|
129
|
+
const cache = readCache();
|
|
130
|
+
if (!cache || !cache.latestVersion) return;
|
|
131
|
+
if (compareSemver(cache.latestVersion, current) > 0) {
|
|
132
|
+
try {
|
|
133
|
+
process.stderr.write(formatBanner(current, cache.latestVersion, cache.releaseUrl || null));
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function scheduleBackgroundFetch() {
|
|
140
|
+
const cache = readCache();
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
if (cache && typeof cache.checkedAt === 'number' && (now - cache.checkedAt) < CACHE_TTL_MS) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fetchLatestRelease().then((result) => {
|
|
147
|
+
if (!result) return;
|
|
148
|
+
writeCache({
|
|
149
|
+
checkedAt: Date.now(),
|
|
150
|
+
latestVersion: result.latestVersion,
|
|
151
|
+
releaseUrl: result.releaseUrl
|
|
152
|
+
});
|
|
153
|
+
}).catch(() => {});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function startBackgroundUpdateCheck(opts) {
|
|
157
|
+
if (isDisabled(opts)) return;
|
|
158
|
+
maybePrintCachedBanner(opts);
|
|
159
|
+
scheduleBackgroundFetch();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function handleUpdate(_options, ctx) {
|
|
163
|
+
const current = getPackageVersion();
|
|
164
|
+
const result = await fetchLatestRelease();
|
|
165
|
+
|
|
166
|
+
if (!result) {
|
|
167
|
+
return {
|
|
168
|
+
exitCode: ctx.EXIT_CODES.OK,
|
|
169
|
+
envelope: {
|
|
170
|
+
ok: {
|
|
171
|
+
current,
|
|
172
|
+
latest: null,
|
|
173
|
+
updateAvailable: false,
|
|
174
|
+
fetched: false
|
|
175
|
+
},
|
|
176
|
+
help: ['Could not reach GitHub releases API. Check connectivity or retry later.']
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
writeCache({
|
|
182
|
+
checkedAt: Date.now(),
|
|
183
|
+
latestVersion: result.latestVersion,
|
|
184
|
+
releaseUrl: result.releaseUrl
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const updateAvailable = compareSemver(result.latestVersion, current || '0.0.0') > 0;
|
|
188
|
+
const help = updateAvailable
|
|
189
|
+
? [`Run: npm install -g ${NPM_PACKAGE}@latest`, result.releaseUrl ? `Release: ${result.releaseUrl}` : null].filter(Boolean)
|
|
190
|
+
: ['Already on latest version.'];
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
exitCode: ctx.EXIT_CODES.OK,
|
|
194
|
+
envelope: {
|
|
195
|
+
ok: {
|
|
196
|
+
current,
|
|
197
|
+
latest: result.latestVersion,
|
|
198
|
+
releaseUrl: result.releaseUrl,
|
|
199
|
+
updateAvailable,
|
|
200
|
+
installCommand: `npm install -g ${NPM_PACKAGE}@latest`,
|
|
201
|
+
fetched: true
|
|
202
|
+
},
|
|
203
|
+
help
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = {
|
|
209
|
+
startBackgroundUpdateCheck,
|
|
210
|
+
handleUpdate,
|
|
211
|
+
compareSemver,
|
|
212
|
+
parseSemver,
|
|
213
|
+
getPackageVersion
|
|
214
|
+
};
|
package/cli/run.test.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('node:path');
|
|
|
5
5
|
const os = require('node:os');
|
|
6
6
|
const fs = require('node:fs');
|
|
7
7
|
const { renderOutput } = require('./lib/output');
|
|
8
|
+
const { compareSemver } = require('./lib/update-check');
|
|
8
9
|
|
|
9
10
|
const cliPath = path.join(__dirname, 'run.js');
|
|
10
11
|
|
|
@@ -304,6 +305,24 @@ test('quiet flag suppresses launcher stderr noise', () => {
|
|
|
304
305
|
assert.equal(result.stderr.trim(), '');
|
|
305
306
|
});
|
|
306
307
|
|
|
308
|
+
test('update --help returns usage entry', () => {
|
|
309
|
+
const result = runCli(['update', '--help', '--format', 'json']);
|
|
310
|
+
assert.equal(result.status, 0);
|
|
311
|
+
|
|
312
|
+
const parsed = JSON.parse(result.stdout);
|
|
313
|
+
assert.equal(parsed.meta.command, 'help');
|
|
314
|
+
assert.equal(parsed.ok.command, 'update');
|
|
315
|
+
assert.ok(parsed.ok.usage.includes('genexus-mcp update'));
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('compareSemver detects newer, older, equal versions', () => {
|
|
319
|
+
assert.equal(compareSemver('1.3.1', '1.3.0'), 1);
|
|
320
|
+
assert.equal(compareSemver('v1.4.0', '1.3.9'), 1);
|
|
321
|
+
assert.equal(compareSemver('1.3.0', '1.3.0'), 0);
|
|
322
|
+
assert.equal(compareSemver('1.2.9', '1.3.0'), -1);
|
|
323
|
+
assert.equal(compareSemver('garbage', '1.0.0'), 0);
|
|
324
|
+
});
|
|
325
|
+
|
|
307
326
|
test('gateway passthrough remains intact when no AXI subcommand is used', () => {
|
|
308
327
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-test-'));
|
|
309
328
|
const fakeGateway = path.join(tempRoot, 'fake-gateway.js');
|