nexarch 0.9.3 → 0.9.5

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.
@@ -59,12 +59,12 @@ async function promptForValue(promptText, required = false) {
59
59
  rl.close();
60
60
  }
61
61
  }
62
- async function confirmInstructionWrite() {
62
+ async function confirmInstructionWrite(promptText = "Allow nexarch init-agent to write/update AGENTS.md/CLAUDE.md registration instructions? [y/N]: ") {
63
63
  if (!process.stdin.isTTY || !process.stdout.isTTY)
64
64
  return false;
65
65
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
66
66
  try {
67
- const answer = (await rl.question("Allow nexarch init-agent to write/update AGENTS.md/CLAUDE.md registration instructions? [y/N]: ")).trim().toLowerCase();
67
+ const answer = (await rl.question(promptText)).trim().toLowerCase();
68
68
  return answer === "y" || answer === "yes";
69
69
  }
70
70
  finally {
@@ -1084,20 +1084,25 @@ export async function initAgent(args) {
1084
1084
  catch {
1085
1085
  // non-fatal
1086
1086
  }
1087
+ let existingInstructionTargets = injectAgentConfigs(registry);
1088
+ if (existingInstructionTargets.length === 0) {
1089
+ existingInstructionTargets = injectGenericAgentConfig(registry);
1090
+ }
1091
+ const alreadyConfigured = existingInstructionTargets.length > 0 && existingInstructionTargets.every((r) => r.status === "already_present");
1087
1092
  if (denyInstructionWriteFlag) {
1088
1093
  instructionsWriteAllowed = false;
1089
1094
  }
1090
1095
  else if (allowInstructionWriteFlag) {
1091
1096
  instructionsWriteAllowed = true;
1092
1097
  }
1098
+ else if (alreadyConfigured) {
1099
+ instructionsWriteAllowed = false;
1100
+ }
1093
1101
  else if (!asJson) {
1094
1102
  instructionsWriteAllowed = await confirmInstructionWrite();
1095
1103
  }
1096
1104
  if (instructionsWriteAllowed) {
1097
- agentConfigResults = injectAgentConfigs(registry);
1098
- if (agentConfigResults.length === 0) {
1099
- agentConfigResults = injectGenericAgentConfig(registry);
1100
- }
1105
+ agentConfigResults = existingInstructionTargets;
1101
1106
  if (agentConfigResults.length > 0) {
1102
1107
  trustAttestationAttempted = true;
1103
1108
  trustAttestation = await requestTrustAttestation(agentId);
@@ -1116,6 +1121,9 @@ export async function initAgent(args) {
1116
1121
  }
1117
1122
  }
1118
1123
  }
1124
+ else if (alreadyConfigured) {
1125
+ agentConfigResults = existingInstructionTargets;
1126
+ }
1119
1127
  }
1120
1128
  checks.push({
1121
1129
  name: "agent.registration",
@@ -1132,29 +1140,34 @@ export async function initAgent(args) {
1132
1140
  ? `awaiting identity input (${identityCapture.missingRequired.join(", ")})`
1133
1141
  : identityCapture.detail,
1134
1142
  });
1143
+ const instructionsAlreadyConfigured = agentConfigResults.length > 0 && agentConfigResults.every((r) => r.status === "already_present");
1135
1144
  checks.push({
1136
1145
  name: "agent.instructions.injection",
1137
1146
  ok: !registration.ok || !instructionsWriteAllowed || agentConfigResults.length > 0,
1138
1147
  detail: !registration.ok
1139
1148
  ? "skipped (registration failed)"
1140
- : !instructionsWriteAllowed
1141
- ? "skipped (consent not granted)"
1142
- : agentConfigResults.length > 0
1143
- ? `updated ${agentConfigResults.length} instruction target file(s)`
1144
- : "no runtime instruction target matched this repository (non-fatal; create AGENTS.md/CLAUDE.md or configure a generic target)",
1149
+ : instructionsAlreadyConfigured
1150
+ ? "already configured (no write needed)"
1151
+ : !instructionsWriteAllowed
1152
+ ? "skipped (consent not granted)"
1153
+ : agentConfigResults.length > 0
1154
+ ? `updated ${agentConfigResults.length} instruction target file(s)`
1155
+ : "no runtime instruction target matched this repository (non-fatal; create AGENTS.md/CLAUDE.md or configure a generic target)",
1145
1156
  });
1146
1157
  checks.push({
1147
1158
  name: "agent.trust.attestation",
1148
1159
  ok: !registration.ok || !instructionsWriteAllowed || !trustAttestationAttempted || Boolean(trustAttestation?.ok),
1149
1160
  detail: !registration.ok
1150
1161
  ? "skipped (registration failed)"
1151
- : !instructionsWriteAllowed
1152
- ? "skipped (consent not granted)"
1153
- : !trustAttestationAttempted
1154
- ? "skipped (no instruction target written)"
1155
- : trustAttestation?.ok
1156
- ? "minted and injected into instruction file(s)"
1157
- : `unavailable (${trustAttestation?.reason ?? "unknown"})`,
1162
+ : instructionsAlreadyConfigured && !instructionsWriteAllowed
1163
+ ? "skipped (already configured)"
1164
+ : !instructionsWriteAllowed
1165
+ ? "skipped (consent not granted)"
1166
+ : !trustAttestationAttempted
1167
+ ? "skipped (no instruction target written)"
1168
+ : trustAttestation?.ok
1169
+ ? "minted and injected into instruction file(s)"
1170
+ : `unavailable (${trustAttestation?.reason ?? "unknown"})`,
1158
1171
  });
1159
1172
  checks.push({
1160
1173
  name: "technology.components",
@@ -28,6 +28,14 @@ function formatMs(ms) {
28
28
  return `${ms}ms`;
29
29
  return `${(ms / 1000).toFixed(2)}s`;
30
30
  }
31
+ function chunkArray(items, size) {
32
+ if (size <= 0)
33
+ return [items];
34
+ const chunks = [];
35
+ for (let i = 0; i < items.length; i += size)
36
+ chunks.push(items.slice(i, i + size));
37
+ return chunks;
38
+ }
31
39
  function slugify(name) {
32
40
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
33
41
  }
@@ -956,12 +964,34 @@ async function promptApplicationChoice(matches, allApps, suggested) {
956
964
  export async function initProject(args) {
957
965
  const asJson = parseFlag(args, "--json");
958
966
  const dryRun = parseFlag(args, "--dry-run");
967
+ const profileEnabled = parseFlag(args, "--profile");
959
968
  const commandStartedAt = Date.now();
969
+ const profile = {
970
+ enabled: profileEnabled,
971
+ startedAt: new Date(commandStartedAt).toISOString(),
972
+ events: [],
973
+ mcpCalls: [],
974
+ upsertBatches: {
975
+ entities: [],
976
+ relationships: [],
977
+ },
978
+ };
960
979
  const logProgress = (phase, details) => {
961
- const elapsed = formatMs(Date.now() - commandStartedAt);
980
+ const atMs = Date.now() - commandStartedAt;
981
+ if (profileEnabled)
982
+ profile.events.push({ phase, atMs, details });
983
+ const elapsed = formatMs(atMs);
962
984
  const line = details ? `[init-project +${elapsed}] ${phase} — ${details}` : `[init-project +${elapsed}] ${phase}`;
963
985
  process.stderr.write(`${line}\n`);
964
986
  };
987
+ const callMcpProfiled = async (toolName, input, meta) => {
988
+ const startedAt = Date.now();
989
+ const result = await callMcpTool(toolName, input, mcpOpts);
990
+ if (profileEnabled) {
991
+ profile.mcpCalls.push({ tool: toolName, durationMs: Date.now() - startedAt, meta });
992
+ }
993
+ return result;
994
+ };
965
995
  const dirArg = parseOptionValue(args, "--dir") ?? process.cwd();
966
996
  const dir = resolvePath(dirArg);
967
997
  const nameOverride = parseOptionValue(args, "--name");
@@ -973,6 +1003,7 @@ export async function initProject(args) {
973
1003
  const forceCreateApplication = parseFlag(args, "--create-application");
974
1004
  const autoMapApplication = parseFlag(args, "--auto-map-application");
975
1005
  const nonInteractive = parseFlag(args, "--non-interactive");
1006
+ const upsertBatchSize = Number(parseOptionValue(args, "--batch-size") ?? "10") || 10;
976
1007
  const creds = requireCredentials();
977
1008
  const mcpOpts = { companyId: creds.companyId };
978
1009
  if (!asJson)
@@ -1021,7 +1052,7 @@ export async function initProject(args) {
1021
1052
  for (let i = 0; i < detectedNames.length; i += BATCH_SIZE) {
1022
1053
  const batch = detectedNames.slice(i, i + BATCH_SIZE);
1023
1054
  logProgress("resolve.batch", `${Math.floor(i / BATCH_SIZE) + 1}/${Math.ceil(detectedNames.length / BATCH_SIZE)} size=${batch.length}`);
1024
- const raw = await callMcpTool("nexarch_resolve_reference", { names: batch, companyId: creds.companyId }, mcpOpts);
1055
+ const raw = await callMcpProfiled("nexarch_resolve_reference", { names: batch, companyId: creds.companyId }, { batchSize: batch.length });
1025
1056
  const data = parseToolText(raw);
1026
1057
  allResolveResults.push(...data.results);
1027
1058
  }
@@ -1061,7 +1092,7 @@ export async function initProject(args) {
1061
1092
  return;
1062
1093
  }
1063
1094
  logProgress("preflight.onboarding.check");
1064
- const onboardingRaw = await callMcpTool("nexarch_get_company_onboarding", {}, mcpOpts);
1095
+ const onboardingRaw = await callMcpProfiled("nexarch_get_company_onboarding", {});
1065
1096
  const onboarding = parseToolText(onboardingRaw);
1066
1097
  if (onboarding.isComplete !== true) {
1067
1098
  const message = "Company onboarding is incomplete. Complete onboarding before running init-project.";
@@ -1074,7 +1105,7 @@ export async function initProject(args) {
1074
1105
  }
1075
1106
  // Policy bootstrap
1076
1107
  logProgress("preflight.policies.check");
1077
- const policiesRaw = await callMcpTool("nexarch_get_applied_policies", {}, mcpOpts);
1108
+ const policiesRaw = await callMcpProfiled("nexarch_get_applied_policies", {});
1078
1109
  const policies = parseToolText(policiesRaw);
1079
1110
  const policyBundleHash = policies.policyBundleHash ?? null;
1080
1111
  if (!policyBundleHash) {
@@ -1099,7 +1130,7 @@ export async function initProject(args) {
1099
1130
  console.log(`\nUsing --application-ref target: ${projectExternalKey}`);
1100
1131
  }
1101
1132
  else {
1102
- const appsRaw = await callMcpTool("nexarch_list_entities", { entityTypeCode: "application", status: "active", limit: 500, companyId: creds.companyId }, mcpOpts);
1133
+ const appsRaw = await callMcpProfiled("nexarch_list_entities", { entityTypeCode: "application", status: "active", limit: 500, companyId: creds.companyId }, { entityTypeCode: "application", limit: 500 });
1103
1134
  const appsData = parseToolText(appsRaw);
1104
1135
  const apps = (appsData.entities ?? []).filter((e) => (e.entityRef ?? e.externalKey));
1105
1136
  if (apps.length > 0) {
@@ -1116,18 +1147,40 @@ export async function initProject(args) {
1116
1147
  console.log(`\nAuto-mapped to existing application: ${suggested.name} (${projectExternalKey})`);
1117
1148
  }
1118
1149
  else if (!interactiveAllowed) {
1119
- if (highConfidence) {
1120
- projectExternalKey = suggested.entityRef;
1150
+ const message = "Application mapping requires explicit choice in non-interactive mode. Pass --application-ref <entityRef> or --create-application (or run interactively).";
1151
+ const existingApplications = apps.slice(0, 25).map((a) => ({
1152
+ entityRef: a.entityRef ?? a.externalKey,
1153
+ name: a.name,
1154
+ entityTypeCode: a.entityTypeCode,
1155
+ }));
1156
+ if (asJson) {
1157
+ process.stdout.write(`${JSON.stringify({
1158
+ ok: true,
1159
+ status: "input_required",
1160
+ code: "APPLICATION_MAPPING_REQUIRED",
1161
+ message,
1162
+ suggested: suggested ?? null,
1163
+ candidates: matches.slice(0, 10),
1164
+ existingApplications,
1165
+ requiredInput: {
1166
+ applicationRefOption: "--application-ref <entityRef>",
1167
+ createOption: "--create-application",
1168
+ },
1169
+ }, null, 2)}\n`);
1170
+ return;
1171
+ }
1172
+ console.log("\nInput required before continuing:");
1173
+ console.log(` ${message}`);
1174
+ if (suggested) {
1175
+ console.log(` Suggested: ${suggested.name} (${suggested.entityRef}) score=${suggested.score.toFixed(2)}`);
1121
1176
  }
1122
- else if (autoMapApplication) {
1123
- const message = "Ambiguous application mapping in non-interactive mode. Pass --application-ref or --create-application.";
1124
- if (asJson) {
1125
- process.stdout.write(`${JSON.stringify({ ok: false, error: "APPLICATION_MAPPING_AMBIGUOUS", message, candidates: matches.slice(0, 5) }, null, 2)}\n`);
1126
- process.exitCode = 1;
1127
- return;
1177
+ if (existingApplications.length > 0) {
1178
+ console.log(" Existing applications:");
1179
+ for (const app of existingApplications) {
1180
+ console.log(` - ${app.name} (${app.entityRef})`);
1128
1181
  }
1129
- throw new Error(message);
1130
1182
  }
1183
+ return;
1131
1184
  }
1132
1185
  else {
1133
1186
  const chosen = await promptApplicationChoice(matches, apps, highConfidence ? suggested : null);
@@ -1290,7 +1343,7 @@ export async function initProject(args) {
1290
1343
  // Company org is accountable_for the top-level project entity
1291
1344
  let orgExternalKey = `organisation:${normalizeToken(creds.companyId)}`;
1292
1345
  try {
1293
- const orgRaw = await callMcpTool("nexarch_list_entities", { entityTypeCode: "organisation", status: "active", limit: 1, companyId: creds.companyId }, mcpOpts);
1346
+ const orgRaw = await callMcpProfiled("nexarch_list_entities", { entityTypeCode: "organisation", status: "active", limit: 1, companyId: creds.companyId }, { entityTypeCode: "organisation", limit: 1 });
1294
1347
  const orgData = parseToolText(orgRaw);
1295
1348
  const org = (orgData.entities ?? [])[0];
1296
1349
  if (org?.entityRef || org?.externalKey)
@@ -1312,17 +1365,64 @@ export async function initProject(args) {
1312
1365
  }
1313
1366
  if (!asJson)
1314
1367
  console.log(`\nWriting to graph…`);
1315
- // Upsert entities
1316
- logProgress("upsert.entities.start", `count=${entities.length}`);
1317
- const entitiesRaw = await callMcpTool("nexarch_upsert_entities", { entities, agentContext, policyContext }, mcpOpts);
1318
- const entitiesResult = parseToolText(entitiesRaw);
1368
+ // Upsert entities (chunked for progressive feedback)
1369
+ const entityChunks = chunkArray(entities, Math.max(1, upsertBatchSize));
1370
+ const entitiesResult = {
1371
+ summary: { requested: 0, succeeded: 0, failed: 0 },
1372
+ errors: [],
1373
+ };
1374
+ logProgress("upsert.entities.start", `count=${entities.length}, batches=${entityChunks.length}, batchSize=${Math.max(1, upsertBatchSize)}`);
1375
+ for (let i = 0; i < entityChunks.length; i += 1) {
1376
+ const chunk = entityChunks[i];
1377
+ logProgress("upsert.entities.batch.start", `${i + 1}/${entityChunks.length} size=${chunk.length}`);
1378
+ const entityBatchStartedAt = Date.now();
1379
+ const entitiesRaw = await callMcpProfiled("nexarch_upsert_entities", { entities: chunk, agentContext, policyContext }, { batchIndex: i + 1, totalBatches: entityChunks.length, batchSize: chunk.length });
1380
+ const chunkResult = parseToolText(entitiesRaw);
1381
+ if (profileEnabled) {
1382
+ profile.upsertBatches.entities.push({
1383
+ index: i + 1,
1384
+ size: chunk.length,
1385
+ durationMs: Date.now() - entityBatchStartedAt,
1386
+ succeeded: Number(chunkResult.summary?.succeeded ?? 0),
1387
+ failed: Number(chunkResult.summary?.failed ?? 0),
1388
+ });
1389
+ }
1390
+ entitiesResult.summary.requested = Number(entitiesResult.summary.requested ?? 0) + Number(chunkResult.summary?.requested ?? chunk.length);
1391
+ entitiesResult.summary.succeeded = Number(entitiesResult.summary.succeeded ?? 0) + Number(chunkResult.summary?.succeeded ?? 0);
1392
+ entitiesResult.summary.failed = Number(entitiesResult.summary.failed ?? 0) + Number(chunkResult.summary?.failed ?? 0);
1393
+ if (chunkResult.errors?.length)
1394
+ entitiesResult.errors.push(...chunkResult.errors);
1395
+ logProgress("upsert.entities.batch.done", `${i + 1}/${entityChunks.length} succeeded=${chunkResult.summary?.succeeded ?? 0}, failed=${chunkResult.summary?.failed ?? 0}`);
1396
+ }
1319
1397
  logProgress("upsert.entities.done", `succeeded=${entitiesResult.summary?.succeeded ?? 0}, failed=${entitiesResult.summary?.failed ?? 0}`);
1320
- // Upsert relationships
1398
+ // Upsert relationships (chunked)
1321
1399
  let relsResult = null;
1322
1400
  if (relationships.length > 0) {
1323
- logProgress("upsert.relationships.start", `count=${relationships.length}`);
1324
- const relsRaw = await callMcpTool("nexarch_upsert_relationships", { relationships, agentContext, policyContext }, mcpOpts);
1325
- relsResult = parseToolText(relsRaw);
1401
+ const relationshipChunks = chunkArray(relationships, Math.max(1, upsertBatchSize));
1402
+ relsResult = { summary: { requested: 0, succeeded: 0, failed: 0 }, errors: [] };
1403
+ logProgress("upsert.relationships.start", `count=${relationships.length}, batches=${relationshipChunks.length}, batchSize=${Math.max(1, upsertBatchSize)}`);
1404
+ for (let i = 0; i < relationshipChunks.length; i += 1) {
1405
+ const chunk = relationshipChunks[i];
1406
+ logProgress("upsert.relationships.batch.start", `${i + 1}/${relationshipChunks.length} size=${chunk.length}`);
1407
+ const relBatchStartedAt = Date.now();
1408
+ const relsRaw = await callMcpProfiled("nexarch_upsert_relationships", { relationships: chunk, agentContext, policyContext }, { batchIndex: i + 1, totalBatches: relationshipChunks.length, batchSize: chunk.length });
1409
+ const chunkResult = parseToolText(relsRaw);
1410
+ if (profileEnabled) {
1411
+ profile.upsertBatches.relationships.push({
1412
+ index: i + 1,
1413
+ size: chunk.length,
1414
+ durationMs: Date.now() - relBatchStartedAt,
1415
+ succeeded: Number(chunkResult.summary?.succeeded ?? 0),
1416
+ failed: Number(chunkResult.summary?.failed ?? 0),
1417
+ });
1418
+ }
1419
+ relsResult.summary.requested = Number(relsResult.summary.requested ?? 0) + Number(chunkResult.summary?.requested ?? chunk.length);
1420
+ relsResult.summary.succeeded = Number(relsResult.summary.succeeded ?? 0) + Number(chunkResult.summary?.succeeded ?? 0);
1421
+ relsResult.summary.failed = Number(relsResult.summary.failed ?? 0) + Number(chunkResult.summary?.failed ?? 0);
1422
+ if (chunkResult.errors?.length)
1423
+ relsResult.errors.push(...chunkResult.errors);
1424
+ logProgress("upsert.relationships.batch.done", `${i + 1}/${relationshipChunks.length} succeeded=${chunkResult.summary?.succeeded ?? 0}, failed=${chunkResult.summary?.failed ?? 0}`);
1425
+ }
1326
1426
  logProgress("upsert.relationships.done", `succeeded=${relsResult.summary?.succeeded ?? 0}, failed=${relsResult.summary?.failed ?? 0}`);
1327
1427
  }
1328
1428
  // Build structured enrichment task (included in JSON output and printed in human mode)
@@ -1573,6 +1673,15 @@ ${subPkgSection}${adrSection}${gapCheckSection}`;
1573
1673
  entityErrors: entitiesResult.errors ?? [],
1574
1674
  relationshipErrors: relsResult?.errors ?? [],
1575
1675
  enrichmentTask,
1676
+ profile: profileEnabled
1677
+ ? {
1678
+ startedAt: profile.startedAt,
1679
+ totalDurationMs: Date.now() - commandStartedAt,
1680
+ events: profile.events,
1681
+ mcpCalls: profile.mcpCalls,
1682
+ upsertBatches: profile.upsertBatches,
1683
+ }
1684
+ : undefined,
1576
1685
  };
1577
1686
  if (asJson) {
1578
1687
  logProgress("complete", `ok=${output.ok}`);
package/dist/index.js CHANGED
@@ -94,6 +94,8 @@ Usage:
94
94
  --create-application force new application entity
95
95
  --auto-map-application auto-map only when high confidence
96
96
  --non-interactive fail on ambiguous mapping
97
+ --batch-size <n> upsert batch size (default: 10)
98
+ --profile include timing/profile data in JSON output
97
99
  --dry-run preview without writing
98
100
  --json
99
101
  nexarch update-entity
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",