nexarch 0.1.33 → 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/init-agent.js +16 -12
- package/dist/commands/init-project.js +296 -31
- package/dist/commands/mcp-config.js +42 -0
- package/dist/commands/setup.js +5 -1
- package/dist/lib/clients.js +27 -0
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ import { join } from "path";
|
|
|
4
4
|
import process from "process";
|
|
5
5
|
import { requireCredentials } from "../lib/credentials.js";
|
|
6
6
|
import { callMcpTool, mcpInitialize, mcpListTools } from "../lib/mcp.js";
|
|
7
|
-
const CLI_VERSION = "0.
|
|
7
|
+
const CLI_VERSION = "0.2.0";
|
|
8
8
|
const AGENT_ENTITY_TYPE = "agent";
|
|
9
9
|
const TECH_COMPONENT_ENTITY_TYPE = "technology_component";
|
|
10
10
|
function parseFlag(args, flag) {
|
|
@@ -96,18 +96,18 @@ function writeAgentBootstrap(params) {
|
|
|
96
96
|
`npx nexarch agent identify`,
|
|
97
97
|
`--agent-id "${params.agentId}"`,
|
|
98
98
|
`--provider "<anthropic|openai|google|other>"`,
|
|
99
|
-
`--model "<model-id e.g. claude-sonnet-4-6>"`,
|
|
100
|
-
`--client "<claude-code|cursor|copilot|other>"`,
|
|
99
|
+
`--model "<model-id e.g. gpt-4o|claude-sonnet-4-6|gemini-2-flash>"`,
|
|
100
|
+
`--client "<claude-code|cursor|copilot|continue|codex-cli|other>"`,
|
|
101
101
|
`--framework "<openclaw|n8n|m365-agent-framework|other>"`,
|
|
102
102
|
`--session-id "<your current session id if known, otherwise omit>"`,
|
|
103
|
-
`--tool-version "<
|
|
103
|
+
`--tool-version "<version of the client tool, e.g. 1.2.3>"`,
|
|
104
104
|
`--capabilities "<comma,separated,capabilities>"`,
|
|
105
105
|
`--json`,
|
|
106
106
|
].join(" \\\n ");
|
|
107
107
|
const content = `# Nexarch Agent Bootstrap
|
|
108
108
|
|
|
109
109
|
This file was written by \`nexarch init-agent\` on ${new Date().toISOString()}.
|
|
110
|
-
It contains instructions for
|
|
110
|
+
It contains instructions for an AI agent to complete its Nexarch registration.
|
|
111
111
|
|
|
112
112
|
## What has already been registered
|
|
113
113
|
|
|
@@ -130,12 +130,12 @@ ${identifyCmd}
|
|
|
130
130
|
\`\`\`
|
|
131
131
|
|
|
132
132
|
Fill in:
|
|
133
|
-
- **provider** — your AI provider (e.g. \`anthropic\`)
|
|
134
|
-
- **model** — the exact model ID you are running as (e.g. \`claude-sonnet-4-6\`)
|
|
135
|
-
- **client** — the tool you are running inside (e.g. \`claude-code\`)
|
|
133
|
+
- **provider** — your AI provider (e.g. \`anthropic\`, \`openai\`, \`google\`)
|
|
134
|
+
- **model** — the exact model ID you are running as (e.g. \`gpt-4o\`, \`claude-sonnet-4-6\`, \`gemini-2-flash\`)
|
|
135
|
+
- **client** — the MCP client tool you are running inside (e.g. \`claude-code\`, \`cursor\`, \`continue\`, \`codex-cli\`)
|
|
136
136
|
- **framework** — the agent framework in use (e.g. \`openclaw\`)
|
|
137
137
|
- **session-id** — your current session ID if available
|
|
138
|
-
- **tool-version** — the version of the client tool
|
|
138
|
+
- **tool-version** — the version of the client tool (run the tool's version command and use the version number only, e.g. \`1.2.3\`)
|
|
139
139
|
- **capabilities** — comma-separated list of your capabilities (e.g. \`code,files,bash,web\`)
|
|
140
140
|
|
|
141
141
|
Omit any flag whose value you genuinely do not know rather than guessing.
|
|
@@ -149,14 +149,18 @@ const CLAUDE_MD_SECTION_MARKER = "## Nexarch Agent Registration";
|
|
|
149
149
|
const CLAUDE_MD_SECTION = `
|
|
150
150
|
## Nexarch Agent Registration
|
|
151
151
|
|
|
152
|
-
When a human has run \`nexarch login\` and \`nexarch init-agent\`, they
|
|
152
|
+
When a human has run \`nexarch login\` and \`nexarch init-agent\`, they may ask you to run \`npx nexarch agent prompt\`. Do so, read the output, and follow the instructions — it contains a pre-filled \`npx nexarch agent identify\` command for you to complete with your own provider, model, client, and capability values, then execute. Confirm \`"ok": true\` in the JSON output.
|
|
153
|
+
|
|
154
|
+
This applies to any AI coding assistant with MCP tool access (Claude Code, Cursor, Windsurf, Continue.dev, Codex CLI, etc.).
|
|
153
155
|
`;
|
|
154
156
|
function injectAgentConfigs() {
|
|
155
157
|
const candidateNames = [
|
|
156
158
|
"CLAUDE.md", "claude.md", "Claude.md", // Claude Code
|
|
157
|
-
"AGENTS.md", "agents.md", // OpenAI Codex CLI
|
|
159
|
+
"AGENTS.md", "agents.md", // OpenAI Codex CLI / generic
|
|
158
160
|
".github/copilot-instructions.md", // GitHub Copilot
|
|
159
161
|
".cursorrules", // Cursor (legacy)
|
|
162
|
+
".continue/rules.md", // Continue.dev
|
|
163
|
+
".windsurfrules", // Windsurf
|
|
160
164
|
];
|
|
161
165
|
const results = [];
|
|
162
166
|
for (const name of candidateNames) {
|
|
@@ -690,7 +694,7 @@ export async function initAgent(args) {
|
|
|
690
694
|
console.log(`📝 Nexarch registration instructions added to: ${r.path}`);
|
|
691
695
|
}
|
|
692
696
|
}
|
|
693
|
-
console.log("\n Tell
|
|
697
|
+
console.log("\n Tell your agent: \"run npx nexarch agent prompt and follow the instructions\"");
|
|
694
698
|
}
|
|
695
699
|
else {
|
|
696
700
|
console.log("⚠ Handshake/registration completed with issues.");
|
|
@@ -25,7 +25,7 @@ function slugify(name) {
|
|
|
25
25
|
}
|
|
26
26
|
// ─── Project scanning ─────────────────────────────────────────────────────────
|
|
27
27
|
// Noise patterns for env var keys that are internal config, not external service references
|
|
28
|
-
const ENV_KEY_NOISE = /^(NODE_ENV|PORT|HOST|DEBUG|LOG_LEVEL|TZ|LANG|PATH|HOME|USER|SHELL|TERM)$|(_LOG_LEVEL|_MAX_|_MIN_|_DEFAULT_|_TIMEOUT|_DELAY|_JOBS|_INTERVAL|_LIMIT|_RETRIES|_CONCURREN|_WORKERS)
|
|
28
|
+
const ENV_KEY_NOISE = /^(NODE_ENV|PORT|HOST|DEBUG|LOG_LEVEL|TZ|LANG|PATH|HOME|USER|SHELL|TERM)$|(_LOG_LEVEL|_MAX_|_MIN_|_DEFAULT_|_TIMEOUT|_DELAY|_JOBS|_INTERVAL|_LIMIT|_RETRIES|_CONCURREN|_WORKERS)$|(_URL|_SECRET|_TOKEN|_KEY|_PASSWORD|_CREDENTIAL|_DSN|_URI)$/;
|
|
29
29
|
function parseEnvKeys(content) {
|
|
30
30
|
return content
|
|
31
31
|
.split("\n")
|
|
@@ -93,7 +93,22 @@ function detectFromFilesystem(dir) {
|
|
|
93
93
|
detected.push("pulumi");
|
|
94
94
|
return detected;
|
|
95
95
|
}
|
|
96
|
-
const SKIP_DIRS = new Set([
|
|
96
|
+
const SKIP_DIRS = new Set([
|
|
97
|
+
// JavaScript / Node
|
|
98
|
+
"node_modules", "dist", ".next", ".turbo", "coverage",
|
|
99
|
+
// Python
|
|
100
|
+
"__pycache__", ".venv", "venv", "env", ".env", "site-packages", ".mypy_cache", ".ruff_cache",
|
|
101
|
+
// Go
|
|
102
|
+
"vendor",
|
|
103
|
+
// Rust
|
|
104
|
+
"target",
|
|
105
|
+
// Java / Kotlin
|
|
106
|
+
".gradle", ".mvn",
|
|
107
|
+
// Ruby
|
|
108
|
+
".bundle",
|
|
109
|
+
// Generic
|
|
110
|
+
"build", ".git", ".cache", "tmp",
|
|
111
|
+
]);
|
|
97
112
|
function collectPackageDeps(pkgPath) {
|
|
98
113
|
try {
|
|
99
114
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
@@ -229,40 +244,267 @@ function findPackageJsonPaths(rootDir) {
|
|
|
229
244
|
}
|
|
230
245
|
return paths;
|
|
231
246
|
}
|
|
247
|
+
// ─── Ecosystem scanners ───────────────────────────────────────────────────────
|
|
248
|
+
// Strip Python version specifiers: "requests>=2.28,<3" → "requests", "flask[async]" → "flask"
|
|
249
|
+
function normalisePyDep(raw) {
|
|
250
|
+
const trimmed = raw.trim().replace(/\s*#.*$/, "").trim();
|
|
251
|
+
if (!trimmed || trimmed.startsWith("-") || trimmed.startsWith("."))
|
|
252
|
+
return null;
|
|
253
|
+
// Strip extras [...] then version specifiers
|
|
254
|
+
return trimmed.split(/[>=<!~\[;\s]/)[0].trim().toLowerCase() || null;
|
|
255
|
+
}
|
|
256
|
+
function scanPythonRequirements(filePath) {
|
|
257
|
+
try {
|
|
258
|
+
return readFileSync(filePath, "utf8")
|
|
259
|
+
.split("\n")
|
|
260
|
+
.map(normalisePyDep)
|
|
261
|
+
.filter((n) => n !== null && n.length > 1);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function scanPyprojectToml(filePath) {
|
|
268
|
+
try {
|
|
269
|
+
const content = readFileSync(filePath, "utf8");
|
|
270
|
+
const names = [];
|
|
271
|
+
// PEP 621: [project] dependencies = ["requests>=2.28", ...]
|
|
272
|
+
const pep621 = content.match(/\[project\][\s\S]*?^dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
273
|
+
if (pep621) {
|
|
274
|
+
for (const m of pep621[1].matchAll(/"([^"]+)"|'([^']+)'/g)) {
|
|
275
|
+
const name = normalisePyDep(m[1] ?? m[2] ?? "");
|
|
276
|
+
if (name)
|
|
277
|
+
names.push(name);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// Poetry: [tool.poetry.dependencies] key = value
|
|
281
|
+
const poetryMatch = content.match(/\[tool\.poetry\.dependencies\]([\s\S]*?)(?=^\[|\Z)/m);
|
|
282
|
+
if (poetryMatch) {
|
|
283
|
+
for (const line of poetryMatch[1].split("\n")) {
|
|
284
|
+
const key = line.match(/^([a-zA-Z][a-zA-Z0-9_-]*)\s*=/)?.[1];
|
|
285
|
+
if (key && key.toLowerCase() !== "python")
|
|
286
|
+
names.push(key.toLowerCase());
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return names;
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function scanPipfile(filePath) {
|
|
296
|
+
try {
|
|
297
|
+
const content = readFileSync(filePath, "utf8");
|
|
298
|
+
const names = [];
|
|
299
|
+
let inPackages = false;
|
|
300
|
+
for (const line of content.split("\n")) {
|
|
301
|
+
const trimmed = line.trim();
|
|
302
|
+
if (trimmed === "[packages]") {
|
|
303
|
+
inPackages = true;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (trimmed.startsWith("[")) {
|
|
307
|
+
inPackages = false;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (inPackages) {
|
|
311
|
+
const key = trimmed.match(/^([a-zA-Z][a-zA-Z0-9_-]*)\s*=/)?.[1];
|
|
312
|
+
if (key)
|
|
313
|
+
names.push(key.toLowerCase());
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return names;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function scanGoMod(filePath) {
|
|
323
|
+
try {
|
|
324
|
+
const names = [];
|
|
325
|
+
let inRequire = false;
|
|
326
|
+
for (const line of readFileSync(filePath, "utf8").split("\n")) {
|
|
327
|
+
const trimmed = line.trim();
|
|
328
|
+
if (trimmed === "require (") {
|
|
329
|
+
inRequire = true;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (inRequire && trimmed === ")") {
|
|
333
|
+
inRequire = false;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
// Single-line: require github.com/foo/bar v1.2.3
|
|
337
|
+
const single = trimmed.match(/^require\s+(\S+)\s+v/);
|
|
338
|
+
if (single) {
|
|
339
|
+
names.push(single[1]);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
// Inside require block: \t github.com/foo/bar v1.2.3
|
|
343
|
+
if (inRequire) {
|
|
344
|
+
const m = trimmed.match(/^(\S+)\s+v/);
|
|
345
|
+
if (m && !trimmed.endsWith("// indirect"))
|
|
346
|
+
names.push(m[1]);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return names;
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return [];
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function scanGemfile(filePath) {
|
|
356
|
+
try {
|
|
357
|
+
const names = [];
|
|
358
|
+
for (const line of readFileSync(filePath, "utf8").split("\n")) {
|
|
359
|
+
const m = line.trim().match(/^gem\s+['"]([^'"]+)['"]/);
|
|
360
|
+
if (m)
|
|
361
|
+
names.push(m[1]);
|
|
362
|
+
}
|
|
363
|
+
return names;
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function scanCargoToml(filePath) {
|
|
370
|
+
try {
|
|
371
|
+
const content = readFileSync(filePath, "utf8");
|
|
372
|
+
const names = [];
|
|
373
|
+
let inDeps = false;
|
|
374
|
+
for (const line of content.split("\n")) {
|
|
375
|
+
const trimmed = line.trim();
|
|
376
|
+
if (/^\[(dependencies|workspace\.dependencies)\]/.test(trimmed)) {
|
|
377
|
+
inDeps = true;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (/^\[dev-dependencies\]/.test(trimmed)) {
|
|
381
|
+
inDeps = false;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (trimmed.startsWith("[")) {
|
|
385
|
+
inDeps = false;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (inDeps) {
|
|
389
|
+
const key = trimmed.match(/^([a-zA-Z][a-zA-Z0-9_-]*)\s*=/)?.[1];
|
|
390
|
+
if (key)
|
|
391
|
+
names.push(key.toLowerCase());
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return names;
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// Detect which language ecosystems are present; return names detected from each manifest
|
|
401
|
+
function scanEcosystems(dir) {
|
|
402
|
+
const names = [];
|
|
403
|
+
const ecosystems = [];
|
|
404
|
+
// Python
|
|
405
|
+
if (existsSync(join(dir, "pyproject.toml"))) {
|
|
406
|
+
ecosystems.push("python");
|
|
407
|
+
names.push(...scanPyprojectToml(join(dir, "pyproject.toml")));
|
|
408
|
+
}
|
|
409
|
+
if (existsSync(join(dir, "requirements.txt"))) {
|
|
410
|
+
if (!ecosystems.includes("python"))
|
|
411
|
+
ecosystems.push("python");
|
|
412
|
+
names.push(...scanPythonRequirements(join(dir, "requirements.txt")));
|
|
413
|
+
}
|
|
414
|
+
if (existsSync(join(dir, "requirements-dev.txt"))) {
|
|
415
|
+
names.push(...scanPythonRequirements(join(dir, "requirements-dev.txt")));
|
|
416
|
+
}
|
|
417
|
+
if (existsSync(join(dir, "Pipfile"))) {
|
|
418
|
+
if (!ecosystems.includes("python"))
|
|
419
|
+
ecosystems.push("python");
|
|
420
|
+
names.push(...scanPipfile(join(dir, "Pipfile")));
|
|
421
|
+
}
|
|
422
|
+
// Go
|
|
423
|
+
if (existsSync(join(dir, "go.mod"))) {
|
|
424
|
+
ecosystems.push("go");
|
|
425
|
+
names.push(...scanGoMod(join(dir, "go.mod")));
|
|
426
|
+
}
|
|
427
|
+
// Ruby
|
|
428
|
+
if (existsSync(join(dir, "Gemfile"))) {
|
|
429
|
+
ecosystems.push("ruby");
|
|
430
|
+
names.push(...scanGemfile(join(dir, "Gemfile")));
|
|
431
|
+
}
|
|
432
|
+
// Rust
|
|
433
|
+
if (existsSync(join(dir, "Cargo.toml"))) {
|
|
434
|
+
ecosystems.push("rust");
|
|
435
|
+
names.push(...scanCargoToml(join(dir, "Cargo.toml")));
|
|
436
|
+
}
|
|
437
|
+
return { names, ecosystems };
|
|
438
|
+
}
|
|
232
439
|
function scanProject(dir) {
|
|
233
440
|
const names = new Set();
|
|
234
441
|
let projectName = basename(dir);
|
|
235
442
|
const subPackages = [];
|
|
236
443
|
let rootDepNames = new Set();
|
|
444
|
+
const detectedEcosystems = [];
|
|
445
|
+
// ── npm / Node.js ──────────────────────────────────────────────────────────
|
|
237
446
|
const pkgPaths = findPackageJsonPaths(dir);
|
|
238
447
|
const rootPkgPath = join(dir, "package.json");
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
448
|
+
if (pkgPaths.length > 0) {
|
|
449
|
+
detectedEcosystems.push("nodejs");
|
|
450
|
+
for (const pkgPath of pkgPaths) {
|
|
451
|
+
const pkg = readRootPackage(pkgPath);
|
|
452
|
+
const deps = collectPackageDeps(pkgPath);
|
|
453
|
+
if (pkgPath === rootPkgPath) {
|
|
454
|
+
if (pkg.name)
|
|
455
|
+
projectName = pkg.name;
|
|
456
|
+
rootDepNames = new Set(deps);
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
const relativePath = pkgPath.replace(dir + "/", "").replace("/package.json", "");
|
|
460
|
+
const packageName = pkg.name ?? relativePath;
|
|
461
|
+
const { entityType, subtype } = classifySubPackage(pkgPath, relativePath);
|
|
462
|
+
subPackages.push({
|
|
463
|
+
name: packageName,
|
|
464
|
+
relativePath,
|
|
465
|
+
packageJsonPath: pkgPath,
|
|
466
|
+
depNames: deps,
|
|
467
|
+
entityType,
|
|
468
|
+
subtype,
|
|
469
|
+
externalKey: "",
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
for (const dep of deps)
|
|
473
|
+
names.add(dep);
|
|
246
474
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
475
|
+
}
|
|
476
|
+
// ── Other ecosystems (Python, Go, Ruby, Rust) ──────────────────────────────
|
|
477
|
+
const { names: ecosystemNames, ecosystems } = scanEcosystems(dir);
|
|
478
|
+
for (const n of ecosystemNames)
|
|
479
|
+
names.add(n);
|
|
480
|
+
for (const e of ecosystems) {
|
|
481
|
+
if (!detectedEcosystems.includes(e))
|
|
482
|
+
detectedEcosystems.push(e);
|
|
483
|
+
// Root deps for non-npm ecosystems — treat all detected names as root-level
|
|
484
|
+
for (const n of ecosystemNames)
|
|
485
|
+
rootDepNames.add(n);
|
|
486
|
+
}
|
|
487
|
+
// Try to infer project name from Python pyproject.toml if no package.json name
|
|
488
|
+
if (!detectedEcosystems.includes("nodejs") && existsSync(join(dir, "pyproject.toml"))) {
|
|
489
|
+
try {
|
|
490
|
+
const content = readFileSync(join(dir, "pyproject.toml"), "utf8");
|
|
491
|
+
const nameMatch = content.match(/^\s*name\s*=\s*["']([^"']+)["']/m);
|
|
492
|
+
if (nameMatch)
|
|
493
|
+
projectName = nameMatch[1];
|
|
494
|
+
}
|
|
495
|
+
catch { }
|
|
496
|
+
}
|
|
497
|
+
// Try go.mod module name
|
|
498
|
+
if (!detectedEcosystems.includes("nodejs") && existsSync(join(dir, "go.mod"))) {
|
|
499
|
+
try {
|
|
500
|
+
const content = readFileSync(join(dir, "go.mod"), "utf8");
|
|
501
|
+
const moduleMatch = content.match(/^module\s+(\S+)/m);
|
|
502
|
+
if (moduleMatch)
|
|
503
|
+
projectName = moduleMatch[1].split("/").pop() ?? projectName;
|
|
261
504
|
}
|
|
262
|
-
|
|
263
|
-
names.add(dep);
|
|
505
|
+
catch { }
|
|
264
506
|
}
|
|
265
|
-
// .env files
|
|
507
|
+
// ── .env files ─────────────────────────────────────────────────────────────
|
|
266
508
|
for (const envFile of [".env", ".env.example", ".env.local", ".env.runtime"]) {
|
|
267
509
|
const envPath = join(dir, envFile);
|
|
268
510
|
if (existsSync(envPath)) {
|
|
@@ -273,12 +515,19 @@ function scanProject(dir) {
|
|
|
273
515
|
catch { }
|
|
274
516
|
}
|
|
275
517
|
}
|
|
276
|
-
// Git remote, CI/CD, deployment config
|
|
518
|
+
// ── Git remote, CI/CD, deployment config ───────────────────────────────────
|
|
277
519
|
for (const name of detectFromGitConfig(dir))
|
|
278
520
|
names.add(name);
|
|
279
521
|
for (const name of detectFromFilesystem(dir))
|
|
280
522
|
names.add(name);
|
|
281
|
-
return {
|
|
523
|
+
return {
|
|
524
|
+
projectName,
|
|
525
|
+
packageJsonCount: pkgPaths.length,
|
|
526
|
+
detectedNames: Array.from(names),
|
|
527
|
+
rootDepNames,
|
|
528
|
+
subPackages,
|
|
529
|
+
detectedEcosystems,
|
|
530
|
+
};
|
|
282
531
|
}
|
|
283
532
|
// ─── Relationship type selection ──────────────────────────────────────────────
|
|
284
533
|
function pickRelationshipType(toEntityTypeCode, _fromEntityTypeCode = "application") {
|
|
@@ -309,7 +558,7 @@ export async function initProject(args) {
|
|
|
309
558
|
const mcpOpts = { companyId: creds.companyId };
|
|
310
559
|
if (!asJson)
|
|
311
560
|
console.log(`Scanning ${dir}…`);
|
|
312
|
-
const { projectName, packageJsonCount, detectedNames, rootDepNames, subPackages } = scanProject(dir);
|
|
561
|
+
const { projectName, packageJsonCount, detectedNames, rootDepNames, subPackages, detectedEcosystems } = scanProject(dir);
|
|
313
562
|
const displayName = nameOverride ?? projectName;
|
|
314
563
|
const projectSlug = slugify(displayName);
|
|
315
564
|
const projectExternalKey = `${entityTypeOverride}:${projectSlug}`;
|
|
@@ -484,6 +733,20 @@ export async function initProject(args) {
|
|
|
484
733
|
const readmeHints = ["README.md", "README.mdx", "docs/README.md", "docs/index.md"]
|
|
485
734
|
.filter((f) => existsSync(join(dir, f)));
|
|
486
735
|
function buildEnrichmentInstructions() {
|
|
736
|
+
const ecosystemLabel = detectedEcosystems.length > 0
|
|
737
|
+
? detectedEcosystems.join(", ")
|
|
738
|
+
: "unknown";
|
|
739
|
+
const manifestHint = detectedEcosystems.includes("nodejs")
|
|
740
|
+
? "package.json"
|
|
741
|
+
: detectedEcosystems.includes("python")
|
|
742
|
+
? "pyproject.toml / requirements.txt"
|
|
743
|
+
: detectedEcosystems.includes("go")
|
|
744
|
+
? "go.mod"
|
|
745
|
+
: detectedEcosystems.includes("ruby")
|
|
746
|
+
? "Gemfile"
|
|
747
|
+
: detectedEcosystems.includes("rust")
|
|
748
|
+
? "Cargo.toml"
|
|
749
|
+
: "dependency manifest";
|
|
487
750
|
const subPkgSection = subPackages.length === 0 ? "" : `
|
|
488
751
|
STEP 3 — Classify and register each sub-package as its own entity.
|
|
489
752
|
The scanner found ${subPackages.length} workspace package(s):
|
|
@@ -565,7 +828,7 @@ ${subPackages.map((sp) => {
|
|
|
565
828
|
const gapCheckSection = `
|
|
566
829
|
${finalStep} — Identify architecturally significant components not auto-detected.
|
|
567
830
|
|
|
568
|
-
The mechanical scan only finds
|
|
831
|
+
The mechanical scan only finds declared dependencies, env var names, and a few config files.
|
|
569
832
|
It systematically misses hosting platforms, managed infrastructure, SaaS integrations,
|
|
570
833
|
and external APIs that are only mentioned in README or deployment config.
|
|
571
834
|
|
|
@@ -613,6 +876,8 @@ You now need to enrich it with meaningful content.
|
|
|
613
876
|
|
|
614
877
|
PROJECT ENTITY : ${projectExternalKey}
|
|
615
878
|
PROJECT DIR : ${dir}
|
|
879
|
+
ECOSYSTEM(S) : ${ecosystemLabel}
|
|
880
|
+
MANIFEST : ${manifestHint}
|
|
616
881
|
${readmeHints.length > 0 ? `README FILES : ${readmeHints.join(", ")}` : "README FILES : (none found — check docs/)"}
|
|
617
882
|
|
|
618
883
|
STEP 1 — Read the project README / docs and build your understanding of:
|
|
@@ -655,7 +920,7 @@ ${subPkgSection}${gapCheckSection}`;
|
|
|
655
920
|
};
|
|
656
921
|
const output = {
|
|
657
922
|
ok: Number(entitiesResult.summary?.failed ?? 0) === 0,
|
|
658
|
-
project: { name: displayName, externalKey: projectExternalKey, entityType: entityTypeOverride },
|
|
923
|
+
project: { name: displayName, externalKey: projectExternalKey, entityType: entityTypeOverride, detectedEcosystems },
|
|
659
924
|
entities: entitiesResult.summary ?? {},
|
|
660
925
|
relationships: relsResult?.summary ?? { requested: 0, succeeded: 0, failed: 0 },
|
|
661
926
|
resolved: resolvedItems.length,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadCredentials } from "../lib/credentials.js";
|
|
1
2
|
import { nexarchServerBlock } from "../lib/clients.js";
|
|
2
3
|
export async function mcpConfig(args) {
|
|
3
4
|
const clientFlag = args.find((a) => !a.startsWith("-")) ?? "claude-desktop";
|
|
@@ -22,6 +23,47 @@ Locations:
|
|
|
22
23
|
console.log(JSON.stringify(config, null, 2));
|
|
23
24
|
break;
|
|
24
25
|
}
|
|
26
|
+
case "continue":
|
|
27
|
+
case "continue-dev": {
|
|
28
|
+
// Continue.dev uses an array-style mcpServers with a "name" field per entry
|
|
29
|
+
const config = { mcpServers: [{ name: "nexarch", ...serverBlock }] };
|
|
30
|
+
console.log("Add (or merge) the following into your ~/.continue/config.json:\n");
|
|
31
|
+
console.log(JSON.stringify(config, null, 2));
|
|
32
|
+
console.log(`
|
|
33
|
+
Locations:
|
|
34
|
+
macOS/Linux: ~/.continue/config.json
|
|
35
|
+
Windows: %USERPROFILE%\\.continue\\config.json
|
|
36
|
+
`);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case "http":
|
|
40
|
+
case "direct": {
|
|
41
|
+
// Direct HTTP connection — for MCP clients that support Streamable HTTP transport.
|
|
42
|
+
// The gateway accepts JSON-RPC 2.0 POST requests at the /mcp endpoint.
|
|
43
|
+
// Auth is via Bearer token (your nexarch token from ~/.nexarch/credentials.json).
|
|
44
|
+
const creds = loadCredentials();
|
|
45
|
+
const tokenHint = creds ? creds.token : "<your-nexarch-token>";
|
|
46
|
+
console.log("Direct HTTP MCP connection (Streamable HTTP / SSE transport):\n");
|
|
47
|
+
console.log(` Endpoint : https://mcp.nexarch.ai/mcp`);
|
|
48
|
+
console.log(` Auth : Authorization: Bearer ${tokenHint}`);
|
|
49
|
+
console.log(` Protocol : MCP JSON-RPC 2.0 over HTTP POST`);
|
|
50
|
+
console.log(`
|
|
51
|
+
Use this when your MCP client supports connecting to a remote HTTP MCP server directly
|
|
52
|
+
(no stdio proxy required). Examples: custom agent frameworks, LangChain MCP integration,
|
|
53
|
+
or any client that speaks the MCP Streamable HTTP or SSE transport.
|
|
54
|
+
|
|
55
|
+
Example curl to verify connectivity:
|
|
56
|
+
|
|
57
|
+
curl -s -X POST https://mcp.nexarch.ai/mcp \\
|
|
58
|
+
-H "Authorization: Bearer ${tokenHint}" \\
|
|
59
|
+
-H "Content-Type: application/json" \\
|
|
60
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | jq .
|
|
61
|
+
|
|
62
|
+
If your client requires an MCP server URL instead of command/args, use:
|
|
63
|
+
https://mcp.nexarch.ai/mcp
|
|
64
|
+
`);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
25
67
|
default: {
|
|
26
68
|
const config = { mcpServers: { nexarch: serverBlock } };
|
|
27
69
|
console.log(JSON.stringify(config, null, 2));
|
package/dist/commands/setup.js
CHANGED
|
@@ -6,9 +6,13 @@ export async function setup(_args) {
|
|
|
6
6
|
const clients = detectClients();
|
|
7
7
|
if (clients.length === 0) {
|
|
8
8
|
console.log("No supported MCP clients detected.");
|
|
9
|
-
console.log("\nSupported clients: Claude Desktop, Cursor, Windsurf");
|
|
9
|
+
console.log("\nSupported clients: Claude Desktop, Cursor, Windsurf, Continue.dev");
|
|
10
10
|
console.log("\nTo configure manually, run:");
|
|
11
11
|
console.log(" nexarch mcp-config --client claude-desktop");
|
|
12
|
+
console.log(" nexarch mcp-config --client cursor");
|
|
13
|
+
console.log(" nexarch mcp-config --client continue");
|
|
14
|
+
console.log("\nFor clients that support direct HTTP MCP connections:");
|
|
15
|
+
console.log(" nexarch mcp-config --client http");
|
|
12
16
|
return;
|
|
13
17
|
}
|
|
14
18
|
const serverBlock = nexarchServerBlock();
|
package/dist/lib/clients.js
CHANGED
|
@@ -40,6 +40,10 @@ function windsurfPaths() {
|
|
|
40
40
|
return [join(homedir(), ".windsurf", "mcp.json")];
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
function continueDevPaths() {
|
|
44
|
+
// Continue.dev stores config at ~/.continue/config.json on all platforms
|
|
45
|
+
return [join(homedir(), ".continue", "config.json")];
|
|
46
|
+
}
|
|
43
47
|
function mergeClaudeStyle(existing, serverBlock) {
|
|
44
48
|
let config = {};
|
|
45
49
|
try {
|
|
@@ -53,6 +57,28 @@ function mergeClaudeStyle(existing, serverBlock) {
|
|
|
53
57
|
config.mcpServers = servers;
|
|
54
58
|
return JSON.stringify(config, null, 2);
|
|
55
59
|
}
|
|
60
|
+
// Continue.dev uses an array under mcpServers (each entry has a "name" field).
|
|
61
|
+
// We upsert by name so re-running setup is idempotent.
|
|
62
|
+
function mergeContinueStyle(existing, serverBlock) {
|
|
63
|
+
let config = {};
|
|
64
|
+
try {
|
|
65
|
+
config = JSON.parse(existing);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
config = {};
|
|
69
|
+
}
|
|
70
|
+
const entry = { name: "nexarch", ...serverBlock };
|
|
71
|
+
const servers = config.mcpServers ?? [];
|
|
72
|
+
const idx = servers.findIndex((s) => s["name"] === "nexarch");
|
|
73
|
+
if (idx >= 0) {
|
|
74
|
+
servers[idx] = entry;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
servers.push(entry);
|
|
78
|
+
}
|
|
79
|
+
config.mcpServers = servers;
|
|
80
|
+
return JSON.stringify(config, null, 2);
|
|
81
|
+
}
|
|
56
82
|
function mergeFlatStyle(existing, serverBlock) {
|
|
57
83
|
let config = {};
|
|
58
84
|
try {
|
|
@@ -71,6 +97,7 @@ export function detectClients() {
|
|
|
71
97
|
...claudeDesktopPaths().map((p) => ({ name: "Claude Desktop", configPath: p, merge: mergeClaudeStyle })),
|
|
72
98
|
...cursorPaths().map((p) => ({ name: "Cursor", configPath: p, merge: mergeFlatStyle })),
|
|
73
99
|
...windsurfPaths().map((p) => ({ name: "Windsurf", configPath: p, merge: mergeFlatStyle })),
|
|
100
|
+
...continueDevPaths().map((p) => ({ name: "Continue.dev", configPath: p, merge: mergeContinueStyle })),
|
|
74
101
|
];
|
|
75
102
|
return candidates.filter((c) => existsSync(join(c.configPath, "..")));
|
|
76
103
|
}
|