nexarch 0.8.24 → 0.9.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.
@@ -34,7 +34,7 @@ function loadIdentity() {
34
34
  }
35
35
  export async function checkIn(args) {
36
36
  const asJson = parseFlag(args, "--json");
37
- const agentKeyArg = parseOptionValue(args, "--agent-key");
37
+ const agentKeyArg = parseOptionValue(args, "--agent-ref") ?? parseOptionValue(args, "--agent-key");
38
38
  const creds = requireCredentials();
39
39
  const identity = loadIdentity();
40
40
  const agentKey = agentKeyArg ?? identity.agentKey;
@@ -48,6 +48,7 @@ export async function checkIn(args) {
48
48
  return;
49
49
  }
50
50
  const raw = await callMcpTool("nexarch_claim_command", {
51
+ agentRef: agentKey,
51
52
  agentKey,
52
53
  companyId: creds.companyId,
53
54
  }, { companyId: creds.companyId });
@@ -35,7 +35,7 @@ function loadIdentity() {
35
35
  export async function commandClaim(args) {
36
36
  const asJson = parseFlag(args, "--json");
37
37
  const id = parseOptionValue(args, "--id");
38
- const agentKeyArg = parseOptionValue(args, "--agent-key");
38
+ const agentKeyArg = parseOptionValue(args, "--agent-ref") ?? parseOptionValue(args, "--agent-key");
39
39
  if (!id) {
40
40
  console.error("error: --id <commandId> is required");
41
41
  process.exit(1);
@@ -49,6 +49,7 @@ export async function commandClaim(args) {
49
49
  }
50
50
  const raw = await callMcpTool("nexarch_claim_command_by_id", {
51
51
  commandId: id,
52
+ agentRef: agentKey,
52
53
  agentKey,
53
54
  companyId: creds.companyId,
54
55
  }, { companyId: creds.companyId });
@@ -886,7 +886,7 @@ export async function initAgent(args) {
886
886
  relationships: [
887
887
  {
888
888
  relationshipTypeCode: "accountable_for",
889
- fromEntityExternalKey: `org:${creds.companyId}`,
889
+ fromEntityExternalKey: `organisation:${creds.companyId}`,
890
890
  toEntityExternalKey: agentExternalKey,
891
891
  confidence: 1,
892
892
  attributes: { source: "nexarch-cli-init-agent", kind: "org_agent", createdAt: nowIso },
@@ -1,10 +1,10 @@
1
1
  import process from "process";
2
+ import * as readline from "node:readline/promises";
2
3
  import { execFileSync } from "node:child_process";
3
4
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
4
5
  import { basename, join, relative, resolve as resolvePath } from "node:path";
5
6
  import { requireCredentials } from "../lib/credentials.js";
6
7
  import { callMcpTool } from "../lib/mcp.js";
7
- import { fetchAgentRegistryOrThrow } from "../lib/agent-registry.js";
8
8
  import { buildVersionAttributes } from "../lib/version-normalization.js";
9
9
  // ─── Helpers ─────────────────────────────────────────────────────────────────
10
10
  function parseFlag(args, flag) {
@@ -23,9 +23,6 @@ function parseToolText(result) {
23
23
  const text = result.content?.[0]?.text ?? "{}";
24
24
  return JSON.parse(text);
25
25
  }
26
- function renderTemplate(template, values) {
27
- return template.replace(/{{\s*([A-Z0-9_]+)\s*}}/g, (_match, key) => values[key] ?? "");
28
- }
29
26
  function slugify(name) {
30
27
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
31
28
  }
@@ -93,14 +90,14 @@ function inferProvider(ref) {
93
90
  }
94
91
  function providerCanonicalRepoRef(provider) {
95
92
  switch (provider) {
96
- case "github": return "global:repo:github";
97
- case "gitlab": return "global:repo:gitlab";
98
- case "bitbucket": return "global:repo:bitbucket";
99
- case "azure-repos": return "global:repo:azure-repos";
100
- case "codecommit": return "global:repo:codecommit";
101
- case "gitea": return "global:repo:gitea";
102
- case "forgejo": return "global:repo:forgejo";
103
- case "sourcehut": return "global:repo:sourcehut";
93
+ case "github": return "global:technology_component:github";
94
+ case "gitlab": return "global:technology_component:gitlab";
95
+ case "bitbucket": return "global:technology_component:bitbucket";
96
+ case "azure-repos": return "global:technology_component:azure_repos";
97
+ case "codecommit": return "global:technology_component:codecommit";
98
+ case "gitea": return "global:technology_component:gitea";
99
+ case "forgejo": return "global:technology_component:forgejo";
100
+ case "sourcehut": return "global:technology_component:sourcehut";
104
101
  default: return null;
105
102
  }
106
103
  }
@@ -868,6 +865,88 @@ function pickRelationshipType(toEntityTypeCode, _fromEntityTypeCode = "applicati
868
865
  return "depends_on";
869
866
  }
870
867
  }
868
+ function normalizeToken(value) {
869
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
870
+ }
871
+ function extractHost(input) {
872
+ if (!input)
873
+ return null;
874
+ try {
875
+ return new URL(input).hostname.replace(/^www\./, "").toLowerCase();
876
+ }
877
+ catch {
878
+ return null;
879
+ }
880
+ }
881
+ function scoreApplicationCandidate(app, projectName, repoUrl) {
882
+ const entityRef = app.entityRef ?? app.externalKey ?? null;
883
+ if (!entityRef)
884
+ return null;
885
+ let score = 0;
886
+ const reasons = [];
887
+ const appNameNorm = normalizeToken(app.name);
888
+ const projectNorm = normalizeToken(projectName);
889
+ const refNorm = normalizeToken(entityRef);
890
+ if (appNameNorm === projectNorm) {
891
+ score += 0.45;
892
+ reasons.push("name_exact");
893
+ }
894
+ else if (appNameNorm.includes(projectNorm) || projectNorm.includes(appNameNorm)) {
895
+ score += 0.25;
896
+ reasons.push("name_partial");
897
+ }
898
+ if (refNorm.includes(projectNorm)) {
899
+ score += 0.25;
900
+ reasons.push("ref_matches_project");
901
+ }
902
+ const repoHost = extractHost(repoUrl);
903
+ const appWebsite = typeof app.attributes?.website_url === "string" ? app.attributes.website_url : null;
904
+ const appWebsiteHost = extractHost(appWebsite);
905
+ if (repoHost && appWebsiteHost && repoHost === appWebsiteHost) {
906
+ score += 0.5;
907
+ reasons.push("website_host_exact");
908
+ }
909
+ if (score <= 0)
910
+ return null;
911
+ return { entityRef, name: app.name, score: Math.min(1, score), reasons };
912
+ }
913
+ async function promptApplicationChoice(matches, allApps, suggested) {
914
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
915
+ try {
916
+ console.log("\nExisting application entities found.");
917
+ if (suggested) {
918
+ console.log(`Suggested match: ${suggested.name} (${suggested.entityRef}) score=${suggested.score.toFixed(2)} [${suggested.reasons.join(", ")}]`);
919
+ }
920
+ console.log("\nChoose target application:");
921
+ const options = [];
922
+ let index = 1;
923
+ for (const m of matches.slice(0, 5)) {
924
+ options.push({
925
+ key: String(index++),
926
+ label: `${m.name} (${m.entityRef}) score=${m.score.toFixed(2)}`,
927
+ value: m.entityRef,
928
+ });
929
+ }
930
+ options.push({ key: String(index++), label: "Show all applications", value: "__show_all__" });
931
+ options.push({ key: String(index), label: "Create a new application", value: "__create__" });
932
+ for (const o of options)
933
+ console.log(` ${o.key}) ${o.label}`);
934
+ const answer = (await rl.question("Select option: ")).trim();
935
+ const chosen = options.find((o) => o.key === answer)?.value;
936
+ if (chosen === "__show_all__") {
937
+ console.log("\nAll applications:");
938
+ allApps.forEach((a, i) => console.log(` ${i + 1}) ${a.name} (${a.entityRef ?? a.externalKey ?? "n/a"})`));
939
+ const pick = Number((await rl.question("Pick application number or 0 to create new: ")).trim());
940
+ if (!Number.isFinite(pick) || pick <= 0 || pick > allApps.length)
941
+ return "__create__";
942
+ return allApps[pick - 1].entityRef ?? allApps[pick - 1].externalKey ?? "__create__";
943
+ }
944
+ return chosen && chosen !== "__show_all__" ? chosen : "__create__";
945
+ }
946
+ finally {
947
+ rl.close();
948
+ }
949
+ }
871
950
  // ─── Main command ─────────────────────────────────────────────────────────────
872
951
  export async function initProject(args) {
873
952
  const asJson = parseFlag(args, "--json");
@@ -879,6 +958,10 @@ export async function initProject(args) {
879
958
  const repoRefOverride = parseOptionValue(args, "--repo-ref");
880
959
  const repoUrlOverride = parseOptionValue(args, "--repo-url");
881
960
  const repoPathOverride = parseOptionValue(args, "--repo-path");
961
+ const applicationRefOverride = parseOptionValue(args, "--application-ref");
962
+ const forceCreateApplication = parseFlag(args, "--create-application");
963
+ const autoMapApplication = parseFlag(args, "--auto-map-application");
964
+ const nonInteractive = parseFlag(args, "--non-interactive");
882
965
  const creds = requireCredentials();
883
966
  const mcpOpts = { companyId: creds.companyId };
884
967
  if (!asJson)
@@ -887,7 +970,7 @@ export async function initProject(args) {
887
970
  const detectedRepo = detectSourceRepository(dir);
888
971
  const displayName = nameOverride ?? projectName;
889
972
  const projectSlug = slugify(displayName);
890
- const projectExternalKey = `${entityTypeOverride}:${projectSlug}`;
973
+ let projectExternalKey = `${entityTypeOverride}:${projectSlug}`;
891
974
  // Compute sub-package external keys now that projectSlug is known.
892
975
  // Unscoped names (no "/") get prefixed with the project slug to avoid ambiguous keys
893
976
  // like "application:crawler" — they become "application:whatsontap-crawler" instead,
@@ -991,6 +1074,57 @@ export async function initProject(args) {
991
1074
  const repoUrl = repoUrlOverride ?? detectedRepo?.url ?? null;
992
1075
  const sourceVcsType = detectedRepo?.vcsType ?? "unknown";
993
1076
  const sourceProvider = detectedRepo?.provider ?? "unknown";
1077
+ if (entityTypeOverride === "application" && !forceCreateApplication) {
1078
+ if (applicationRefOverride) {
1079
+ projectExternalKey = applicationRefOverride;
1080
+ if (!asJson)
1081
+ console.log(`\nUsing --application-ref target: ${projectExternalKey}`);
1082
+ }
1083
+ else {
1084
+ const appsRaw = await callMcpTool("nexarch_list_entities", { entityTypeCode: "application", status: "active", limit: 500, companyId: creds.companyId }, mcpOpts);
1085
+ const appsData = parseToolText(appsRaw);
1086
+ const apps = (appsData.entities ?? []).filter((e) => (e.entityRef ?? e.externalKey));
1087
+ if (apps.length > 0) {
1088
+ const matches = apps
1089
+ .map((a) => scoreApplicationCandidate(a, displayName, repoUrl))
1090
+ .filter((m) => Boolean(m))
1091
+ .sort((a, b) => b.score - a.score);
1092
+ const suggested = matches.length > 0 ? matches[0] : null;
1093
+ const highConfidence = suggested && suggested.score >= 0.85;
1094
+ const interactiveAllowed = !asJson && !nonInteractive && process.stdin.isTTY;
1095
+ if (autoMapApplication && highConfidence) {
1096
+ projectExternalKey = suggested.entityRef;
1097
+ if (!asJson)
1098
+ console.log(`\nAuto-mapped to existing application: ${suggested.name} (${projectExternalKey})`);
1099
+ }
1100
+ else if (!interactiveAllowed) {
1101
+ if (highConfidence) {
1102
+ projectExternalKey = suggested.entityRef;
1103
+ }
1104
+ else if (autoMapApplication) {
1105
+ const message = "Ambiguous application mapping in non-interactive mode. Pass --application-ref or --create-application.";
1106
+ if (asJson) {
1107
+ process.stdout.write(`${JSON.stringify({ ok: false, error: "APPLICATION_MAPPING_AMBIGUOUS", message, candidates: matches.slice(0, 5) }, null, 2)}\n`);
1108
+ process.exitCode = 1;
1109
+ return;
1110
+ }
1111
+ throw new Error(message);
1112
+ }
1113
+ }
1114
+ else {
1115
+ const chosen = await promptApplicationChoice(matches, apps, highConfidence ? suggested : null);
1116
+ if (chosen !== "__create__") {
1117
+ projectExternalKey = chosen;
1118
+ if (!asJson)
1119
+ console.log(`Mapped to existing application: ${projectExternalKey}`);
1120
+ }
1121
+ else if (!asJson) {
1122
+ console.log("Creating a new application entity for this project.");
1123
+ }
1124
+ }
1125
+ }
1126
+ }
1127
+ }
994
1128
  const agentContext = {
995
1129
  agentId: "nexarch-cli:init-project",
996
1130
  agentRunId: `init-project-${Date.now()}`,
@@ -1135,7 +1269,17 @@ export async function initProject(args) {
1135
1269
  }
1136
1270
  }
1137
1271
  // Company org is accountable_for the top-level project entity
1138
- const orgExternalKey = `org:${creds.companyId}`;
1272
+ let orgExternalKey = `organisation:${normalizeToken(creds.companyId)}`;
1273
+ try {
1274
+ const orgRaw = await callMcpTool("nexarch_list_entities", { entityTypeCode: "organisation", status: "active", limit: 1, companyId: creds.companyId }, mcpOpts);
1275
+ const orgData = parseToolText(orgRaw);
1276
+ const org = (orgData.entities ?? [])[0];
1277
+ if (org?.entityRef || org?.externalKey)
1278
+ orgExternalKey = (org.entityRef ?? org.externalKey);
1279
+ }
1280
+ catch {
1281
+ // keep fallback
1282
+ }
1139
1283
  addRel("accountable_for", orgExternalKey, projectExternalKey, 1);
1140
1284
  // Also accountable_for any sub-package applications
1141
1285
  for (const sp of subPackages) {
@@ -1161,22 +1305,6 @@ export async function initProject(args) {
1161
1305
  // Build structured enrichment task (included in JSON output and printed in human mode)
1162
1306
  const readmeHints = ["README.md", "README.mdx", "docs/README.md", "docs/index.md"]
1163
1307
  .filter((f) => existsSync(join(dir, f)));
1164
- const INIT_PROJECT_TEMPLATE_CODE = "nexarch_init_project_enrichment_v1";
1165
- let enrichmentTemplateBody = null;
1166
- let enrichmentTemplateSource = "fallback";
1167
- let enrichmentTemplateVersion = null;
1168
- try {
1169
- const registry = await fetchAgentRegistryOrThrow();
1170
- const template = registry.instructionTemplates.find((t) => t.code === INIT_PROJECT_TEMPLATE_CODE);
1171
- if (template?.body?.trim()) {
1172
- enrichmentTemplateBody = template.body;
1173
- enrichmentTemplateSource = "registry";
1174
- enrichmentTemplateVersion = registry.release.version;
1175
- }
1176
- }
1177
- catch {
1178
- // non-fatal: init-project can still run with fallback guidance.
1179
- }
1180
1308
  function buildEnrichmentInstructions() {
1181
1309
  const ecosystemLabel = detectedEcosystems.length > 0
1182
1310
  ? detectedEcosystems.join(", ")
@@ -1377,26 +1505,13 @@ STEP 2 — Enrich the project entity. Run this command with the description you'
1377
1505
  --name "<proper product name from README>" \\
1378
1506
  --description "<2–4 sentence summary of what it does and why>"
1379
1507
  ${subPkgSection}${adrSection}${gapCheckSection}`;
1380
- if (!enrichmentTemplateBody)
1381
- return fallbackTemplate;
1382
- return renderTemplate(enrichmentTemplateBody, {
1383
- PROJECT_ENTITY_KEY: projectExternalKey,
1384
- PROJECT_DIR: dir,
1385
- ECOSYSTEM_LABEL: ecosystemLabel,
1386
- MANIFEST_HINT: manifestHint,
1387
- README_HINTS: readmeHints.length > 0 ? readmeHints.join(", ") : "(none found — check docs/)",
1388
- ENTITY_TYPE: entityTypeOverride,
1389
- ENTITY_BASE_COMMAND: `npx nexarch update-entity \\\n --key "${projectExternalKey}" \\\n --entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? " \\\n --subtype \"app_custom_built\" \\\n --icon \"<curated icon>\"" : ""} \\\n --name "<proper product name from README>" \\\n --description "<2–4 sentence summary of what it does and why>"`,
1390
- STEP_SUBPACKAGES: subPkgSection,
1391
- STEP_ADR: adrSection,
1392
- STEP_GAP_CHECK: gapCheckSection,
1393
- });
1508
+ return fallbackTemplate;
1394
1509
  }
1395
1510
  const enrichmentTask = {
1396
1511
  template: {
1397
- code: INIT_PROJECT_TEMPLATE_CODE,
1398
- source: enrichmentTemplateSource,
1399
- registryVersion: enrichmentTemplateVersion,
1512
+ code: "builtin:init-project-enrichment",
1513
+ source: "builtin",
1514
+ registryVersion: null,
1400
1515
  },
1401
1516
  instructions: buildEnrichmentInstructions(),
1402
1517
  iconHints: {
@@ -115,11 +115,11 @@ export async function policyAuditSubmit(args) {
115
115
  if (parseFlag(args, "--help") || parseFlag(args, "-h")) {
116
116
  console.log(`
117
117
  Usage:
118
- nexarch policy-audit-submit --command-id <id> --application-key <key> [options]
118
+ nexarch policy-audit-submit --command-id <id> --application-ref <key> [options]
119
119
 
120
120
  Options:
121
121
  --command-id <id> Required command id
122
- --application-key <key> Required application key (e.g. application:bad-driving)
122
+ --application-ref <key> Required application reference key (e.g. application:bad-driving)
123
123
  --agent-key <key> Optional agent key (defaults from identity)
124
124
  --finding <controlId|ruleId|result|rationale|missing1;missing2> Repeatable
125
125
  --findings-json <json> JSON array of findings
@@ -134,22 +134,23 @@ Notes:
134
134
  return;
135
135
  }
136
136
  const commandId = parseOptionValue(args, "--command-id") ?? parseOptionValue(args, "--id");
137
- const applicationEntityKey = parseOptionValue(args, "--application-key") ?? parseOptionValue(args, "--entity");
137
+ const applicationEntityRef = parseOptionValue(args, "--application-ref") ?? parseOptionValue(args, "--application-key") ?? parseOptionValue(args, "--entity-ref") ?? parseOptionValue(args, "--entity");
138
138
  const agentKey = parseOptionValue(args, "--agent-key") ?? loadIdentityAgentKey();
139
139
  if (!commandId) {
140
140
  console.error("error: --command-id <uuid> is required");
141
141
  process.exit(1);
142
142
  }
143
- if (!applicationEntityKey) {
144
- console.error("error: --application-key <externalKey> is required (e.g. application:bad-driving)");
143
+ if (!applicationEntityRef) {
144
+ console.error("error: --application-ref <entityRef> is required (e.g. application:bad-driving)");
145
145
  process.exit(1);
146
146
  }
147
147
  const findings = parseFindings(args);
148
148
  const creds = requireCredentials();
149
149
  const raw = await callMcpTool("nexarch_submit_policy_audit", {
150
150
  commandId,
151
- applicationEntityKey,
152
- ...(agentKey ? { agentKey } : {}),
151
+ applicationEntityRef,
152
+ applicationEntityKey: applicationEntityRef,
153
+ ...(agentKey ? { agentRef: agentKey, agentKey } : {}),
153
154
  findings,
154
155
  companyId: creds.companyId,
155
156
  }, { companyId: creds.companyId });
@@ -162,7 +163,7 @@ Notes:
162
163
  console.error(`Policy audit submit failed${result.error ? `: ${result.error}` : ""}`);
163
164
  process.exit(1);
164
165
  }
165
- console.log(`Policy audit submitted for ${applicationEntityKey}.`);
166
+ console.log(`Policy audit submitted for ${applicationEntityRef}.`);
166
167
  if (result.runId)
167
168
  console.log(`Run ID: ${result.runId}`);
168
169
  if (result.summary) {
@@ -32,7 +32,7 @@ function parseToolText(result) {
32
32
  }
33
33
  export async function policyAuditTemplate(args) {
34
34
  const asJson = parseFlag(args, "--json");
35
- const entity = parseOptionValue(args, "--entity") ?? parseOptionValue(args, "--application-key");
35
+ const entity = parseOptionValue(args, "--entity-ref") ?? parseOptionValue(args, "--entity") ?? parseOptionValue(args, "--application-key");
36
36
  const outputPath = parseOptionValue(args, "--out") ?? parseOptionValue(args, "--output");
37
37
  const defaultResult = (parseOptionValue(args, "--default-result") ?? "fail").toLowerCase();
38
38
  const selectedControlIds = new Set(parseMultiOptionValues(args, "--control-id"));
@@ -62,7 +62,7 @@ Output shape is ready for:
62
62
  process.exit(1);
63
63
  }
64
64
  const creds = requireCredentials();
65
- const raw = await callMcpTool("nexarch_get_entity_policy_controls", { entityExternalKey: entity, companyId: creds.companyId }, { companyId: creds.companyId });
65
+ const raw = await callMcpTool("nexarch_get_entity_policy_controls", { entityRef: entity, entityExternalKey: entity, companyId: creds.companyId }, { companyId: creds.companyId });
66
66
  const result = parseToolText(raw);
67
67
  if (!result.found) {
68
68
  console.error(`error: entity not found: ${entity}`);
@@ -19,13 +19,13 @@ function parseToolText(result) {
19
19
  }
20
20
  export async function policyControls(args) {
21
21
  const asJson = parseFlag(args, "--json");
22
- const entity = parseOptionValue(args, "--entity") ?? parseOptionValue(args, "--application-key");
22
+ const entity = parseOptionValue(args, "--entity-ref") ?? parseOptionValue(args, "--entity") ?? parseOptionValue(args, "--application-key");
23
23
  if (!entity) {
24
24
  console.error("error: --entity <externalKey> is required (e.g. application:bad-driving)");
25
25
  process.exit(1);
26
26
  }
27
27
  const creds = requireCredentials();
28
- const raw = await callMcpTool("nexarch_get_entity_policy_controls", { entityExternalKey: entity, companyId: creds.companyId }, { companyId: creds.companyId });
28
+ const raw = await callMcpTool("nexarch_get_entity_policy_controls", { entityRef: entity, entityExternalKey: entity, companyId: creds.companyId }, { companyId: creds.companyId });
29
29
  const result = parseToolText(raw);
30
30
  if (asJson) {
31
31
  process.stdout.write(JSON.stringify(result) + "\n");
@@ -35,7 +35,7 @@ export async function policyControls(args) {
35
35
  console.log(`Entity not found: ${entity}`);
36
36
  return;
37
37
  }
38
- console.log(`Policy controls for ${result.entityExternalKey ?? entity}`);
38
+ console.log(`Policy controls for ${result.entityRef ?? result.entityExternalKey ?? entity}`);
39
39
  console.log(`Controls: ${result.controlCount ?? 0}`);
40
40
  for (const control of result.controls ?? []) {
41
41
  const rules = control.rules ?? [];
package/dist/index.js CHANGED
@@ -85,9 +85,15 @@ Usage:
85
85
  config files against the reference library, write entities and
86
86
  relationships to the architecture graph, and log unresolved
87
87
  names as reference candidates.
88
+ When entity-type=application and existing applications are present,
89
+ prompts to map to an existing app or create a new one.
88
90
  Options: --dir <path> (default: cwd)
89
91
  --name <name> override project name
90
92
  --entity-type <code> (default: application)
93
+ --application-ref <entityRef> force mapping target
94
+ --create-application force new application entity
95
+ --auto-map-application auto-map only when high confidence
96
+ --non-interactive fail on ambiguous mapping
91
97
  --dry-run preview without writing
92
98
  --json
93
99
  nexarch update-entity
package/dist/lib/mcp.js CHANGED
@@ -1,8 +1,18 @@
1
1
  import https from "https";
2
2
  import { requireCredentials } from "./credentials.js";
3
3
  const MCP_GATEWAY_URL = "https://mcp.nexarch.ai";
4
- const REQUEST_TIMEOUT_MS = 90 * 1000;
5
- async function callMcpRpc(method, params = {}, options = {}) {
4
+ const REQUEST_TIMEOUT_MS = Number.parseInt(process.env.NEXARCH_MCP_TIMEOUT_MS ?? "90000", 10) || 90_000;
5
+ const REQUEST_RETRIES = Math.max(0, Number.parseInt(process.env.NEXARCH_MCP_RETRIES ?? "2", 10) || 2);
6
+ function sleep(ms) {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+ function isRetryableNetworkError(error) {
10
+ const message = error instanceof Error ? error.message : String(error);
11
+ const code = error && typeof error === "object" && "code" in error ? String(error.code ?? "") : "";
12
+ return /timed out|etimedout|econnreset|eai_again|socket hang up|request timeout/i.test(message)
13
+ || ["ETIMEDOUT", "ECONNRESET", "EAI_AGAIN"].includes(code);
14
+ }
15
+ async function requestOnce(method, params, options) {
6
16
  const creds = requireCredentials();
7
17
  const companyId = options.companyId ?? creds.companyId;
8
18
  const body = JSON.stringify({
@@ -54,13 +64,32 @@ async function callMcpRpc(method, params = {}, options = {}) {
54
64
  });
55
65
  });
56
66
  req.on("timeout", () => {
57
- req.destroy(new Error("Request timed out. Check your connection and try again."));
67
+ req.destroy(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms. Check your connection and try again.`));
58
68
  });
59
69
  req.on("error", reject);
60
70
  req.write(body);
61
71
  req.end();
62
72
  });
63
73
  }
74
+ async function callMcpRpc(method, params = {}, options = {}) {
75
+ let lastError = null;
76
+ for (let attempt = 0; attempt <= REQUEST_RETRIES; attempt += 1) {
77
+ try {
78
+ return await requestOnce(method, params, options);
79
+ }
80
+ catch (error) {
81
+ lastError = error;
82
+ const isRetryable = isRetryableNetworkError(error);
83
+ const isLastAttempt = attempt >= REQUEST_RETRIES;
84
+ if (!isRetryable || isLastAttempt)
85
+ break;
86
+ // 300ms, 900ms, 2100ms...
87
+ const backoffMs = 300 * (2 ** attempt) + (attempt * 300);
88
+ await sleep(backoffMs);
89
+ }
90
+ }
91
+ throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "MCP call failed"));
92
+ }
64
93
  export async function callMcpTool(toolName, toolArgs = {}, options = {}) {
65
94
  return callMcpRpc("tools/call", { name: toolName, arguments: toolArgs }, options);
66
95
  }
@@ -101,7 +130,7 @@ export async function forwardToGateway(token, companyId, body) {
101
130
  });
102
131
  });
103
132
  req.on("timeout", () => {
104
- req.destroy(new Error("Request timed out."));
133
+ req.destroy(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms.`));
105
134
  });
106
135
  req.on("error", reject);
107
136
  req.write(body);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.8.24",
3
+ "version": "0.9.0",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",