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.
- package/dist/commands/check-in.js +2 -1
- package/dist/commands/command-claim.js +2 -1
- package/dist/commands/init-agent.js +1 -1
- package/dist/commands/init-project.js +162 -47
- package/dist/commands/policy-audit-submit.js +9 -8
- package/dist/commands/policy-audit-template.js +2 -2
- package/dist/commands/policy-controls.js +3 -3
- package/dist/index.js +6 -0
- package/dist/lib/mcp.js +33 -4
- package/package.json +1 -1
|
@@ -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: `
|
|
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:
|
|
97
|
-
case "gitlab": return "global:
|
|
98
|
-
case "bitbucket": return "global:
|
|
99
|
-
case "azure-repos": return "global:
|
|
100
|
-
case "codecommit": return "global:
|
|
101
|
-
case "gitea": return "global:
|
|
102
|
-
case "forgejo": return "global:
|
|
103
|
-
case "sourcehut": return "global:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1398
|
-
source:
|
|
1399
|
-
registryVersion:
|
|
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-
|
|
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-
|
|
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
|
|
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 (!
|
|
144
|
-
console.error("error: --application-
|
|
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
|
-
|
|
152
|
-
|
|
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 ${
|
|
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 =
|
|
5
|
-
|
|
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(
|
|
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(
|
|
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);
|