nexarch 0.8.27 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 });
@@ -76,10 +76,16 @@ function renderChecksTable(checks) {
76
76
  const lines = [hr, formatRow(headers), hr, ...rows.map(formatRow), hr];
77
77
  return lines.join("\n");
78
78
  }
79
+ function normalizeRefSegment(value) {
80
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
81
+ }
82
+ function buildEntityRef(entityTypeCode, ...segments) {
83
+ return [normalizeRefSegment(entityTypeCode), ...segments.map(normalizeRefSegment).filter(Boolean)].join(":");
84
+ }
79
85
  function getDefaultAgentId() {
80
86
  const osUser = process.env.USERNAME || process.env.USER || userInfo().username || "unknown";
81
87
  const host = hostname() || "unknown-host";
82
- return `nexarch-cli:${osUser}@${host}`;
88
+ return `nexarch_cli_${normalizeRefSegment(osUser)}_${normalizeRefSegment(host)}`;
83
89
  }
84
90
  function getRuntimeMode() {
85
91
  if (process.env.CI)
@@ -519,6 +525,7 @@ export async function initAgent(args) {
519
525
  const capabilitiesArg = parseCsv(parseOptionValue(args, "--capabilities"));
520
526
  const notesArg = parseOptionValue(args, "--notes");
521
527
  const agentId = explicitAgentId ?? getDefaultAgentId();
528
+ const agentExternalKey = buildEntityRef("agent", agentId);
522
529
  const runtime = {
523
530
  osPlatform: platform(),
524
531
  osType: osType(),
@@ -635,7 +642,6 @@ export async function initAgent(args) {
635
642
  if (preflightPassed && policies.policyBundleHash && relForTechBinding) {
636
643
  const nowIso = new Date().toISOString();
637
644
  const agentRunId = `init-agent-${Date.now()}`;
638
- const agentExternalKey = `agent:${agentId}`;
639
645
  const upsertRaw = await callMcpTool("nexarch_upsert_entities", {
640
646
  entities: [
641
647
  {
@@ -713,9 +719,9 @@ export async function initAgent(args) {
713
719
  }
714
720
  if (registration.ok) {
715
721
  techComponents.attempted = true;
716
- const hostExternalKey = `tech:host:${runtime.hostname}:${runtime.arch}`;
717
- const osExternalKey = `tech:os:${runtime.osPlatform}:${runtime.osRelease}:${runtime.arch}`;
718
- const nodeExternalKey = `tech:runtime:nodejs:${runtime.nodeVersion}`;
722
+ const hostExternalKey = buildEntityRef("technology_component", "host", runtime.hostname, runtime.arch);
723
+ const osExternalKey = buildEntityRef("technology_component", "os", runtime.osPlatform, runtime.osRelease, runtime.arch);
724
+ const nodeExternalKey = buildEntityRef("technology_component", "runtime", "nodejs", runtime.nodeVersion);
719
725
  const techUpsertRaw = await callMcpTool("nexarch_upsert_entities", {
720
726
  entities: [
721
727
  {
@@ -886,7 +892,7 @@ export async function initAgent(args) {
886
892
  relationships: [
887
893
  {
888
894
  relationshipTypeCode: "accountable_for",
889
- fromEntityExternalKey: `org:${creds.companyId}`,
895
+ fromEntityExternalKey: buildEntityRef("organisation", creds.companyId),
890
896
  toEntityExternalKey: agentExternalKey,
891
897
  confidence: 1,
892
898
  attributes: { source: "nexarch-cli-init-agent", kind: "org_agent", createdAt: nowIso },
@@ -1065,7 +1071,7 @@ export async function initAgent(args) {
1065
1071
  // Save identity so check-in can find the agent key
1066
1072
  const identityDir = join(homedir(), ".nexarch");
1067
1073
  mkdirSync(identityDir, { recursive: true });
1068
- writeFileSync(join(identityDir, "identity.json"), JSON.stringify({ agentKey: `agent:${agentId}`, companyId: creds.companyId }, null, 2), { mode: 0o600 });
1074
+ writeFileSync(join(identityDir, "identity.json"), JSON.stringify({ agentKey: agentExternalKey, companyId: creds.companyId }, null, 2), { mode: 0o600 });
1069
1075
  }
1070
1076
  catch {
1071
1077
  // non-fatal
@@ -1,4 +1,5 @@
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";
@@ -23,7 +24,7 @@ function parseToolText(result) {
23
24
  return JSON.parse(text);
24
25
  }
25
26
  function slugify(name) {
26
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
27
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
27
28
  }
28
29
  function safeExec(command, args, cwd) {
29
30
  try {
@@ -89,14 +90,14 @@ function inferProvider(ref) {
89
90
  }
90
91
  function providerCanonicalRepoRef(provider) {
91
92
  switch (provider) {
92
- case "github": return "global:repo:github";
93
- case "gitlab": return "global:repo:gitlab";
94
- case "bitbucket": return "global:repo:bitbucket";
95
- case "azure-repos": return "global:repo:azure-repos";
96
- case "codecommit": return "global:repo:codecommit";
97
- case "gitea": return "global:repo:gitea";
98
- case "forgejo": return "global:repo:forgejo";
99
- 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";
100
101
  default: return null;
101
102
  }
102
103
  }
@@ -864,6 +865,88 @@ function pickRelationshipType(toEntityTypeCode, _fromEntityTypeCode = "applicati
864
865
  return "depends_on";
865
866
  }
866
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
+ }
867
950
  // ─── Main command ─────────────────────────────────────────────────────────────
868
951
  export async function initProject(args) {
869
952
  const asJson = parseFlag(args, "--json");
@@ -875,6 +958,10 @@ export async function initProject(args) {
875
958
  const repoRefOverride = parseOptionValue(args, "--repo-ref");
876
959
  const repoUrlOverride = parseOptionValue(args, "--repo-url");
877
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");
878
965
  const creds = requireCredentials();
879
966
  const mcpOpts = { companyId: creds.companyId };
880
967
  if (!asJson)
@@ -883,7 +970,7 @@ export async function initProject(args) {
883
970
  const detectedRepo = detectSourceRepository(dir);
884
971
  const displayName = nameOverride ?? projectName;
885
972
  const projectSlug = slugify(displayName);
886
- const projectExternalKey = `${entityTypeOverride}:${projectSlug}`;
973
+ let projectExternalKey = `${entityTypeOverride}:${projectSlug}`;
887
974
  // Compute sub-package external keys now that projectSlug is known.
888
975
  // Unscoped names (no "/") get prefixed with the project slug to avoid ambiguous keys
889
976
  // like "application:crawler" — they become "application:whatsontap-crawler" instead,
@@ -987,6 +1074,57 @@ export async function initProject(args) {
987
1074
  const repoUrl = repoUrlOverride ?? detectedRepo?.url ?? null;
988
1075
  const sourceVcsType = detectedRepo?.vcsType ?? "unknown";
989
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
+ }
990
1128
  const agentContext = {
991
1129
  agentId: "nexarch-cli:init-project",
992
1130
  agentRunId: `init-project-${Date.now()}`,
@@ -1131,7 +1269,17 @@ export async function initProject(args) {
1131
1269
  }
1132
1270
  }
1133
1271
  // Company org is accountable_for the top-level project entity
1134
- 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
+ }
1135
1283
  addRel("accountable_for", orgExternalKey, projectExternalKey, 1);
1136
1284
  // Also accountable_for any sub-package applications
1137
1285
  for (const sp of subPackages) {
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.8.27",
3
+ "version": "0.9.1",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",