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.
@@ -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.1.33";
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 "<run claude --version to get this, e.g. 1.2.3>"`,
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 a Claude Code agent to complete its Nexarch registration.
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 you are running inside; run \`claude --version\` (or equivalent) to get it and use the version number only (e.g. \`1.2.3\`)
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 will tell 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.
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 Claude: \"run npx nexarch agent prompt and follow the instructions\"");
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)$|^NEXT_PUBLIC_(APP|ADMIN|API|SITE|BASE|WEB)_URL$|(_URL|_SECRET|_TOKEN|_KEY|_PASSWORD|_CREDENTIAL|_DSN|_URI)$/;
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(["node_modules", "dist", ".next", ".turbo", "build", "coverage", ".git"]);
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
- for (const pkgPath of pkgPaths) {
240
- const pkg = readRootPackage(pkgPath);
241
- const deps = collectPackageDeps(pkgPath);
242
- if (pkgPath === rootPkgPath) {
243
- if (pkg.name)
244
- projectName = pkg.name;
245
- rootDepNames = new Set(deps);
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
- else {
248
- // Record as a sub-package for enrichment task
249
- const relativePath = pkgPath.replace(dir + "/", "").replace("/package.json", "");
250
- const packageName = pkg.name ?? relativePath;
251
- const { entityType, subtype } = classifySubPackage(pkgPath, relativePath);
252
- subPackages.push({
253
- name: packageName,
254
- relativePath,
255
- packageJsonPath: pkgPath,
256
- depNames: deps,
257
- entityType,
258
- subtype,
259
- externalKey: "", // computed after scan once projectSlug is known
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
- for (const dep of deps)
263
- names.add(dep);
505
+ catch { }
264
506
  }
265
- // .env files at root
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 { projectName, packageJsonCount: pkgPaths.length, detectedNames: Array.from(names), rootDepNames, subPackages };
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 npm packages, env var names, and a few config files.
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));
@@ -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();
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.1.33",
3
+ "version": "0.2.0",
4
4
  "description": "Connect AI coding tools to your Nexarch architecture workspace",
5
5
  "keywords": [
6
6
  "nexarch",