nexarch 0.13.4 → 0.13.7

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.
@@ -507,6 +507,36 @@ export function instructionInjectionDetail(registrationOk, staleAttestationTarge
507
507
  return `updated ${agentConfigResults.length} instruction target file(s)`;
508
508
  return "no runtime instruction target matched this repository (non-fatal; create AGENTS.md/CLAUDE.md or configure a generic target)";
509
509
  }
510
+ /**
511
+ * Summarises the per-target attestation writes into the check's ok/detail.
512
+ *
513
+ * This used to be `trustAttestation?.ok`, which was the *first* target's mint
514
+ * result, reported under plural wording ("instruction file(s)") as if it
515
+ * covered them all. On Paul's 20 September setup run that printed a green
516
+ * "minted and injected into instruction file(s)" while only AGENTS.md was
517
+ * refreshed: CLAUDE.md and .cursorrules kept an attestation that had expired
518
+ * 22 minutes earlier, under the pre-ADR-0112 `instruction_injection` scope.
519
+ * The two agents that then read CLAUDE.md cold reported it as an injection
520
+ * attempt, which is the correct reading of what was in front of them.
521
+ *
522
+ * Green here now requires every target to have been verified on disk after
523
+ * writing, and a failure names the files rather than hiding behind a count.
524
+ */
525
+ export function summariseAttestationOutcomes(outcomes) {
526
+ if (outcomes.length === 0)
527
+ return { ok: true, detail: "" };
528
+ const failures = outcomes.filter((outcome) => !outcome.ok);
529
+ if (failures.length === 0) {
530
+ return { ok: true, detail: `minted and injected into ${outcomes.length} instruction file(s)` };
531
+ }
532
+ const named = failures
533
+ .map((failure) => `${basename(failure.path)} (${failure.reason ?? "unknown"})`)
534
+ .join(", ");
535
+ return {
536
+ ok: false,
537
+ detail: `${outcomes.length - failures.length}/${outcomes.length} instruction file(s) attested; failed: ${named}`,
538
+ };
539
+ }
510
540
  export function buildTrustAttestationSection(instructionFile, attestation) {
511
541
  if (!attestation.token || !attestation.payload)
512
542
  return null;
@@ -1292,6 +1322,7 @@ export async function initAgent(args) {
1292
1322
  let unsafeReattestationTargetCount = 0;
1293
1323
  let trustAttestation = null;
1294
1324
  let trustAttestationAttempted = false;
1325
+ const attestationOutcomes = [];
1295
1326
  if (registration.ok) {
1296
1327
  try {
1297
1328
  // Save identity so check-in can find the agent key
@@ -1385,7 +1416,9 @@ export async function initAgent(args) {
1385
1416
  // section can carry different text (different templateCode per
1386
1417
  // runtime), and the attestation has to bind to the exact bytes it
1387
1418
  // covers — see ADR-0112. `trustAttestation` keeps the first result for
1388
- // the JSON summary field; every target still gets its own token.
1419
+ // the JSON summary field; every target still gets its own token, and
1420
+ // every target's write is recorded and verified independently so one
1421
+ // success can no longer stand in for the batch.
1389
1422
  for (const r of attestationTargets) {
1390
1423
  let targetAttestation;
1391
1424
  try {
@@ -1397,18 +1430,38 @@ export async function initAgent(args) {
1397
1430
  }
1398
1431
  if (!trustAttestation)
1399
1432
  trustAttestation = targetAttestation;
1400
- try {
1401
- if (targetAttestation.ok) {
1402
- injectTrustAttestationBlock(r.path, targetAttestation);
1433
+ if (!targetAttestation.ok) {
1434
+ const reason = targetAttestation.reason ?? "unknown";
1435
+ try {
1436
+ injectTrustAttestationUnavailableBlock(r.path, reason);
1403
1437
  }
1404
- else {
1405
- injectTrustAttestationUnavailableBlock(r.path, targetAttestation.reason ?? "unknown");
1438
+ catch {
1439
+ // Best-effort notice only; the recorded outcome below is what the check reports.
1406
1440
  }
1441
+ attestationOutcomes.push({ path: r.path, ok: false, reason: `mint failed (${reason})` });
1442
+ continue;
1443
+ }
1444
+ try {
1445
+ injectTrustAttestationBlock(r.path, targetAttestation);
1407
1446
  injectInitProjectReportingContract(r.path);
1408
1447
  }
1409
- catch {
1410
- // non-fatal
1448
+ catch (error) {
1449
+ // Previously swallowed as "non-fatal", which is how a failed write
1450
+ // reported as a minted-and-injected success.
1451
+ attestationOutcomes.push({
1452
+ path: r.path,
1453
+ ok: false,
1454
+ reason: `write failed (${error instanceof Error ? error.message : "unknown"})`,
1455
+ });
1456
+ continue;
1411
1457
  }
1458
+ // Returning without throwing is not proof the block landed: a replace
1459
+ // that matched nothing writes the file unchanged. Re-read and confirm
1460
+ // the file now carries a current, correctly-scoped attestation.
1461
+ const staleness = trustAttestationStaleness(r.path);
1462
+ attestationOutcomes.push(staleness
1463
+ ? { path: r.path, ok: false, reason: `write did not take (${staleness})` }
1464
+ : { path: r.path, ok: true });
1412
1465
  }
1413
1466
  }
1414
1467
  }
@@ -1428,14 +1481,18 @@ export async function initAgent(args) {
1428
1481
  : identityCapture.detail,
1429
1482
  });
1430
1483
  const instructionsAlreadyConfigured = agentConfigResults.length > 0 && agentConfigResults.every((r) => r.status === "already_present");
1484
+ const attestationSummary = summariseAttestationOutcomes(attestationOutcomes);
1485
+ /** Renewed means every attested target verified on disk, not that one mint succeeded. */
1486
+ const attestationRenewed = attestationOutcomes.length > 0 && attestationSummary.ok;
1431
1487
  checks.push({
1432
1488
  name: "agent.instructions.injection",
1433
1489
  ok: !registration.ok || unsafeReattestationTargetCount === 0 && (!instructionsWriteAllowed || agentConfigResults.length > 0),
1434
- detail: instructionInjectionDetail(registration.ok, staleAttestationTargetCount, mismatchedAttestationTargetCount, unsafeReattestationTargetCount, instructionsWriteAllowed, Boolean(trustAttestation?.ok), instructionsAlreadyConfigured, agentConfigResults),
1490
+ detail: instructionInjectionDetail(registration.ok, staleAttestationTargetCount, mismatchedAttestationTargetCount, unsafeReattestationTargetCount, instructionsWriteAllowed, attestationRenewed, instructionsAlreadyConfigured, agentConfigResults),
1435
1491
  });
1436
1492
  checks.push({
1437
1493
  name: "agent.trust.attestation",
1438
1494
  ok: !registration.ok || (unsafeReattestationTargetCount === 0
1495
+ && attestationSummary.ok
1439
1496
  && ((staleAttestationTargetCount === 0 && !trustAttestationAttempted) || Boolean(trustAttestation?.ok))),
1440
1497
  detail: !registration.ok
1441
1498
  ? "skipped (registration failed)"
@@ -1451,9 +1508,7 @@ export async function initAgent(args) {
1451
1508
  ? agentConfigResults.length > 0
1452
1509
  ? "already current (no refresh needed)"
1453
1510
  : "skipped (no instruction target written)"
1454
- : trustAttestation?.ok
1455
- ? "minted and injected into instruction file(s)"
1456
- : `unavailable (${trustAttestation?.reason ?? "unknown"})`,
1511
+ : attestationSummary.detail,
1457
1512
  });
1458
1513
  checks.push({
1459
1514
  name: "technology.components",
@@ -1537,6 +1592,10 @@ export async function initAgent(args) {
1537
1592
  }
1538
1593
  : null,
1539
1594
  trustAttestation,
1595
+ // Per-target truth. `trustAttestation` above is only the first target's
1596
+ // mint result, kept for back-compat; this is what says whether each
1597
+ // instruction file actually ends up carrying a current attestation.
1598
+ trustAttestationTargets: attestationOutcomes,
1540
1599
  companyId: creds.companyId,
1541
1600
  registry: { version: registry.release.version, registryVersion: registry.registryVersion, publishedAt: registry.release.publishedAt },
1542
1601
  agentConfigs: agentConfigResults,
@@ -1,6 +1,7 @@
1
1
  import { createServer } from "http";
2
2
  import { createServer as createNetServer } from "net";
3
- import { randomBytes } from "crypto";
3
+ import { createHash, randomBytes } from "crypto";
4
+ import * as readline from "node:readline/promises";
4
5
  import { execFile } from "child_process";
5
6
  import { saveCredentials } from "../lib/credentials.js";
6
7
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -136,6 +137,68 @@ function waitForCallback(port, expectedState) {
136
137
  });
137
138
  });
138
139
  }
140
+ /**
141
+ * Whether the browser that approves this login can reach the CLI at
142
+ * localhost. It can't over SSH (the browser is on the person's own machine,
143
+ * the CLI on the remote one) or on a Linux machine with no display; the
144
+ * loopback callback then lands on the wrong machine and the login hangs.
145
+ * `--no-browser` forces paste mode, `--browser` forces the loopback flow.
146
+ */
147
+ export function shouldUsePasteLogin(args, env = process.env, platform = process.platform) {
148
+ if (args.includes("--browser"))
149
+ return false;
150
+ if (args.includes("--no-browser"))
151
+ return true;
152
+ if (env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY)
153
+ return true;
154
+ return platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY;
155
+ }
156
+ /** A random verifier the CLI keeps, and the challenge it sends: base64url(SHA-256(verifier)). */
157
+ export function createPasteChallenge() {
158
+ const verifier = randomBytes(32).toString("base64url");
159
+ return { verifier, challenge: createHash("sha256").update(verifier).digest("base64url") };
160
+ }
161
+ /** Mistakes worth letting someone paste again rather than starting over. */
162
+ const RETRYABLE_EXCHANGE_ERRORS = new Set(["malformed", "bad_signature"]);
163
+ async function pasteLogin(qp, verifier) {
164
+ const authUrl = `${NEXARCH_URL}/auth/cli?${qp.toString()}`;
165
+ console.log("\nThis terminal can't receive the browser's reply directly (for example over SSH),");
166
+ console.log("so you'll paste a one-time code instead.\n");
167
+ console.log(" 1. Open this link in a browser on any machine:\n");
168
+ console.log(` ${authUrl}\n`);
169
+ console.log(" 2. Sign in and authorize.");
170
+ console.log(" 3. Copy the code the page shows and paste it here.\n");
171
+ if (!process.stdin.isTTY) {
172
+ throw new Error("Paste login needs an interactive terminal to paste the code into. Run this command directly in a terminal.");
173
+ }
174
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
175
+ try {
176
+ for (let attempt = 1; attempt <= 3; attempt++) {
177
+ const code = (await rl.question("Code: ")).trim();
178
+ if (!code)
179
+ continue;
180
+ const response = await fetch(`${NEXARCH_URL}/api/auth/cli/exchange`, {
181
+ method: "POST",
182
+ headers: { "content-type": "application/json" },
183
+ body: JSON.stringify({ code, verifier }),
184
+ });
185
+ const body = (await response.json().catch(() => ({})));
186
+ if (response.ok && body.token && body.companyId) {
187
+ return { token: body.token, companyId: body.companyId, companyName: body.companyName, companyCode: body.companyCode };
188
+ }
189
+ const message = body.message ?? `Login failed (${response.status}).`;
190
+ if (body.error && RETRYABLE_EXCHANGE_ERRORS.has(body.error) && attempt < 3) {
191
+ console.log(`\n${message}\n`);
192
+ continue;
193
+ }
194
+ throw new Error(message);
195
+ }
196
+ throw new Error("No code was entered. Run the command again to get a new link.");
197
+ }
198
+ finally {
199
+ rl.close();
200
+ }
201
+ }
139
202
  function getArgValue(args, flag) {
140
203
  const idx = args.indexOf(flag);
141
204
  if (idx === -1)
@@ -148,21 +211,33 @@ function getArgValue(args, flag) {
148
211
  export async function login(args) {
149
212
  printLoginBanner();
150
213
  const state = generateState();
151
- const port = await findFreePort();
152
214
  const requestedCompany = getArgValue(args, "--company");
153
215
  const requestedProfile = getArgValue(args, "--profile");
154
216
  const isCapabilityProfile = (value) => PROFILE_CHOICES.some((c) => c.value === value);
155
217
  const profile = isCapabilityProfile(requestedProfile) ? requestedProfile : await promptCapabilityProfile();
156
- const qp = new URLSearchParams({ port: String(port), state, profile });
157
- if (requestedCompany)
158
- qp.set("company", requestedCompany);
159
- const authUrl = `${NEXARCH_URL}/auth/cli?${qp.toString()}`;
160
218
  console.log(`\nCapability profile: ${profile}`);
161
- console.log("Opening Nexarch in your browser…");
162
- console.log(`\n ${authUrl}\n`);
163
- console.log("If the browser did not open, copy the URL above and paste it in manually.\n");
164
- openBrowser(authUrl);
165
- const { token, companyId, companyName, companyCode } = await waitForCallback(port, state);
219
+ let result;
220
+ if (shouldUsePasteLogin(args)) {
221
+ const { verifier, challenge } = createPasteChallenge();
222
+ const qp = new URLSearchParams({ mode: "paste", challenge, state, profile });
223
+ if (requestedCompany)
224
+ qp.set("company", requestedCompany);
225
+ result = await pasteLogin(qp, verifier);
226
+ }
227
+ else {
228
+ const port = await findFreePort();
229
+ const qp = new URLSearchParams({ port: String(port), state, profile });
230
+ if (requestedCompany)
231
+ qp.set("company", requestedCompany);
232
+ const authUrl = `${NEXARCH_URL}/auth/cli?${qp.toString()}`;
233
+ console.log("Opening Nexarch in your browser…");
234
+ console.log(`\n ${authUrl}\n`);
235
+ console.log("If the browser did not open, copy the URL above and paste it in manually.");
236
+ console.log("If your browser is on a different machine (for example over SSH), stop this and run it again with --no-browser.\n");
237
+ openBrowser(authUrl);
238
+ result = await waitForCallback(port, state);
239
+ }
240
+ const { token, companyId, companyName, companyCode } = result;
166
241
  const expiresAt = new Date();
167
242
  expiresAt.setDate(expiresAt.getDate() + 90);
168
243
  saveCredentials({
@@ -91,13 +91,20 @@ Options:
91
91
  console.log(` State: ${run.auditState} — required rules ${requiredPct} pass` +
92
92
  (required ? ` (${required.pass ?? 0} of ${required.total ?? 0}, ${required.partial ?? 0} partial, ${required.fail ?? 0} fail)` : "") +
93
93
  `; advisory findings: ${run.advisoryIssueCount ?? 0} (do not affect state)`);
94
+ if ((run.criticalIssueCount ?? 0) > 0 || (run.highIssueCount ?? 0) > 0) {
95
+ console.log(` Severity: ${run.criticalIssueCount ?? 0} critical, ${run.highIssueCount ?? 0} high open finding(s)`);
96
+ }
97
+ if ((run.unscoredSeverityCount ?? 0) > 0) {
98
+ console.log(` Unrated: ${run.unscoredSeverityCount} finding(s) have no severity assigned`);
99
+ }
94
100
  }
95
101
  for (const control of run.controls ?? []) {
96
102
  console.log(`\n - ${control.controlName} (${control.controlId})`);
97
103
  for (const rule of control.rules ?? []) {
98
104
  const qualifier = rule.requirementQualifier ? ` ${rule.requirementQualifier}` : "";
99
105
  const level = rule.requirementLevel ? ` [${rule.requirementLevel}${qualifier}]` : "";
100
- console.log(` • [${rule.result}] ${rule.ruleName}${level} (${rule.ruleId})`);
106
+ const ranking = [rule.severity, rule.effort, rule.exploitability].filter(Boolean).join(", ");
107
+ console.log(` • [${rule.result}] ${rule.ruleName}${level} (${rule.ruleId})${ranking ? ` {${ranking}}` : ""}`);
101
108
  if (rule.rationale)
102
109
  console.log(` ${rule.rationale}`);
103
110
  if (Array.isArray(rule.missingRequirements) && rule.missingRequirements.length > 0) {
@@ -105,6 +112,12 @@ Options:
105
112
  console.log(` - missing: ${String(m)}`);
106
113
  }
107
114
  }
115
+ if (Array.isArray(rule.fileRefs) && rule.fileRefs.length > 0) {
116
+ for (const ref of rule.fileRefs) {
117
+ const loc = ref.line !== undefined && ref.line !== null ? `${ref.path}:${ref.line}` : ref.path;
118
+ console.log(` - ${loc}${ref.note ? ` — ${ref.note}` : ""}`);
119
+ }
120
+ }
108
121
  }
109
122
  }
110
123
  }
@@ -44,21 +44,36 @@ function loadIdentityAgentKey() {
44
44
  return null;
45
45
  }
46
46
  }
47
+ function isSeverity(value) {
48
+ return value === "low" || value === "medium" || value === "high" || value === "critical";
49
+ }
50
+ function isEffort(value) {
51
+ return value === "hours" || value === "days" || value === "weeks";
52
+ }
53
+ function isExploitability(value) {
54
+ return value === "theoretical" || value === "plausible" || value === "confirmed";
55
+ }
47
56
  function parseFindingToken(token) {
48
- const [policyControlId, policyRuleId, resultRaw, rationaleRaw, missingRaw] = token.split("|");
57
+ const [policyControlId, policyRuleId, resultRaw, rationaleRaw, missingRaw, severityRaw] = token.split("|");
49
58
  const result = (resultRaw ?? "").trim().toLowerCase();
50
59
  if (!policyControlId?.trim() || !policyRuleId?.trim() || !result) {
51
- throw new Error("Each --finding must be: <policyControlId>|<policyRuleId>|<pass|partial|fail>|<rationale>|<missing1;missing2>");
60
+ throw new Error("Each --finding must be: <policyControlId>|<policyRuleId>|<pass|partial|fail>|<rationale>|<missing1;missing2>|<severity>");
52
61
  }
53
62
  if (result !== "pass" && result !== "partial" && result !== "fail") {
54
63
  throw new Error(`Invalid finding result '${resultRaw}'. Use pass|partial|fail.`);
55
64
  }
65
+ const severityInput = severityRaw?.trim().toLowerCase();
66
+ if (severityInput && !isSeverity(severityInput)) {
67
+ throw new Error(`Invalid severity '${severityRaw}'. Use low|medium|high|critical.`);
68
+ }
69
+ const severity = severityInput && isSeverity(severityInput) ? severityInput : undefined;
56
70
  return {
57
71
  policyControlId: policyControlId.trim(),
58
72
  policyRuleId: policyRuleId.trim(),
59
73
  result,
60
74
  ...(rationaleRaw?.trim() ? { rationale: rationaleRaw.trim() } : {}),
61
75
  ...(missingRaw?.trim() ? { missingRequirements: missingRaw.split(";").map((m) => m.trim()).filter(Boolean) } : {}),
76
+ ...(severity ? { severity } : {}),
62
77
  };
63
78
  }
64
79
  function parseFindings(args) {
@@ -100,12 +115,26 @@ function parseFindings(args) {
100
115
  : Array.isArray(value.missing_requirements)
101
116
  ? value.missing_requirements.map(String)
102
117
  : undefined;
118
+ const severity = isSeverity(value.severity) ? value.severity : undefined;
119
+ const effort = isEffort(value.effort) ? value.effort : undefined;
120
+ const exploitability = isExploitability(value.exploitability) ? value.exploitability : undefined;
121
+ const confidence = typeof value.confidence === "number" ? value.confidence : undefined;
122
+ const fileRefs = Array.isArray(value.fileRefs)
123
+ ? value.fileRefs
124
+ : Array.isArray(value.file_refs)
125
+ ? value.file_refs
126
+ : undefined;
103
127
  return {
104
128
  policyControlId,
105
129
  policyRuleId,
106
130
  result,
107
131
  ...(rationale ? { rationale } : {}),
108
132
  ...(missingRequirements ? { missingRequirements } : {}),
133
+ ...(severity ? { severity } : {}),
134
+ ...(effort ? { effort } : {}),
135
+ ...(exploitability ? { exploitability } : {}),
136
+ ...(confidence !== undefined ? { confidence } : {}),
137
+ ...(fileRefs ? { fileRefs } : {}),
109
138
  };
110
139
  });
111
140
  }
@@ -126,7 +155,7 @@ Options:
126
155
  --command-id <id> Required command id
127
156
  --application-ref <key> Required application reference key (e.g. application:bad-driving)
128
157
  --agent-key <key> Optional agent key (defaults from identity)
129
- --finding <controlId|ruleId|result|rationale|missing1;missing2> Repeatable
158
+ --finding <controlId|ruleId|result|rationale|missing1;missing2|severity> Repeatable
130
159
  --findings-json <json> JSON array of findings
131
160
  --findings-file <path> Path to JSON array of findings
132
161
  --json Print JSON response
@@ -135,6 +164,12 @@ Notes:
135
164
  - Findings are rule-level (policyRuleId is required).
136
165
  - You can submit partial findings multiple times for the same command.
137
166
  - Get valid rule ids with: nexarch policy-controls --entity <application:key> --json
167
+ - Rate severity (low|medium|high|critical), effort (hours|days|weeks) and
168
+ exploitability (theoretical|plausible|confirmed) per finding where you can —
169
+ these decide how findings get ranked. --finding's severity is its 6th field;
170
+ --findings-json/--findings-file also accept effort, exploitability,
171
+ confidence (0-1) and fileRefs ([{path, line, note}]) per finding.
172
+ Omit rather than guess.
138
173
  `);
139
174
  return;
140
175
  }
@@ -180,6 +215,12 @@ Notes:
180
215
  console.log(`Required rules: ${pct} pass, ${result.summary.requiredIssueCount} issue(s) — these decide the audit state`);
181
216
  console.log(`Advisory findings: ${result.summary.advisoryIssueCount ?? 0} (recommended/informational; recorded, not gating)`);
182
217
  }
218
+ if ((result.summary.criticalIssueCount ?? 0) > 0 || (result.summary.highIssueCount ?? 0) > 0) {
219
+ console.log(`Severity: ${result.summary.criticalIssueCount ?? 0} critical, ${result.summary.highIssueCount ?? 0} high (open findings rated by the submitting agent)`);
220
+ }
221
+ if ((result.summary.unscoredSeverityCount ?? 0) > 0) {
222
+ console.log(`Unrated for severity: ${result.summary.unscoredSeverityCount} finding(s) — consider adding severity so this run can be ranked`);
223
+ }
183
224
  if ((result.summary.remainingRules ?? 0) > 0) {
184
225
  console.log(`Remaining rules to submit: ${result.summary.remainingRules}`);
185
226
  }
@@ -50,6 +50,11 @@ Options:
50
50
 
51
51
  Output shape is ready for:
52
52
  nexarch policy-audit-submit --findings-file <path.json>
53
+
54
+ Each entry includes empty severity/effort/exploitability/confidence/fileRefs
55
+ placeholders. Fill them in for every partial/fail finding -- they decide how
56
+ findings get ranked and reported. Leave a field blank/empty rather than
57
+ guess; policy-audit-submit drops anything that isn't a valid value.
53
58
  `);
54
59
  return;
55
60
  }
@@ -76,6 +81,13 @@ Output shape is ready for:
76
81
  result: defaultResult,
77
82
  rationale: "",
78
83
  missingRequirements: [],
84
+ // Fill in for partial/fail findings -- see --help. Left blank rather
85
+ // than defaulted, so an unrated finding stays honestly unrated.
86
+ severity: "",
87
+ effort: "",
88
+ exploitability: "",
89
+ confidence: null,
90
+ fileRefs: [],
79
91
  })));
80
92
  if (findings.length === 0) {
81
93
  console.error("error: no rules found for selected controls/entity");
package/dist/index.js CHANGED
@@ -107,7 +107,12 @@ nexarch — Your architecture workspace for AI delivery.
107
107
 
108
108
  Usage:
109
109
  nexarch login Authenticate in browser and store company-scoped credentials
110
- Option: --company <id>
110
+ Options: --company <id>
111
+ --no-browser paste a one-time code instead of
112
+ receiving the browser's reply
113
+ locally; automatic over SSH or on
114
+ Linux with no display
115
+ --browser always use the local browser reply
111
116
  nexarch --version Print the version and the path this copy runs from
112
117
  nexarch enroll Bootstrap a headless (browserless) agent with a one-time
113
118
  enrollment code — no login required. Writes the resulting
@@ -135,6 +140,7 @@ Usage:
135
140
  registering anything; answer 'n' to pick a different one.
136
141
  Options: --company <id|code> target a workspace directly
137
142
  --yes accept the stored workspace without asking
143
+ --no-browser log in by pasting a one-time code (see login)
138
144
  nexarch mcp-config Print MCP server config block for manual setup
139
145
  Client list is registry-managed (see 'nexarch mcp-config --client <code>')
140
146
  nexarch mcp-proxy Run as stdio MCP proxy (used by MCP clients)
package/package.json CHANGED
@@ -1,39 +1,39 @@
1
- {
2
- "name": "nexarch",
3
- "version": "0.13.4",
4
- "description": "Your architecture workspace for AI delivery.",
5
- "keywords": [
6
- "nexarch",
7
- "mcp",
8
- "architecture",
9
- "ai"
10
- ],
11
- "license": "MIT",
12
- "author": "Nexarch <hello@nexarch.ai>",
13
- "homepage": "https://nexarch.ai",
14
- "engines": {
15
- "node": ">=18"
16
- },
17
- "type": "module",
18
- "bin": {
19
- "nexarch": "dist/index.js"
20
- },
21
- "files": [
22
- "dist"
23
- ],
24
- "scripts": {
25
- "build": "tsc",
26
- "prepublishOnly": "tsc",
27
- "dev": "tsx src/index.ts",
28
- "typecheck": "tsc --noEmit",
29
- "test": "tsx scripts/test-mcp-proxy-response.ts && tsx scripts/test-trust-reattest.ts && tsx scripts/test-similar-applications.ts && tsx scripts/test-iac-tool.ts && tsx scripts/test-ansible.ts && tsx scripts/test-reference-sightings.ts && tsx scripts/test-search-icons.ts"
30
- },
31
- "devDependencies": {
32
- "@types/node": "^22",
33
- "tsx": "^4",
34
- "typescript": "^5"
35
- },
36
- "dependencies": {
37
- "yaml": "^2.9.0"
38
- }
39
- }
1
+ {
2
+ "name": "nexarch",
3
+ "version": "0.13.7",
4
+ "description": "Your architecture workspace for AI delivery.",
5
+ "keywords": [
6
+ "nexarch",
7
+ "mcp",
8
+ "architecture",
9
+ "ai"
10
+ ],
11
+ "license": "MIT",
12
+ "author": "Nexarch <hello@nexarch.ai>",
13
+ "homepage": "https://nexarch.ai",
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "type": "module",
18
+ "bin": {
19
+ "nexarch": "dist/index.js"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "prepublishOnly": "tsc",
27
+ "dev": "tsx src/index.ts",
28
+ "typecheck": "tsc --noEmit",
29
+ "test": "tsx scripts/test-mcp-proxy-response.ts && tsx scripts/test-trust-reattest.ts && tsx scripts/test-similar-applications.ts && tsx scripts/test-iac-tool.ts && tsx scripts/test-ansible.ts && tsx scripts/test-reference-sightings.ts && tsx scripts/test-search-icons.ts && tsx scripts/test-paste-login.ts"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^22",
33
+ "tsx": "^4",
34
+ "typescript": "^5"
35
+ },
36
+ "dependencies": {
37
+ "yaml": "^2.9.0"
38
+ }
39
+ }