nexarch 0.13.3 → 0.13.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.
@@ -2343,7 +2343,7 @@ export async function initProject(args) {
2343
2343
  pendingSteps.push({
2344
2344
  step: stepNum++,
2345
2345
  action: "enrich_entity",
2346
- instruction: `Enrich the application entity with a meaningful name, description, subtype, and icon.`,
2346
+ instruction: `Enrich the application entity with a meaningful name, description, subtype, and icon. Find a real icon name with \`nexarch search-icons "<what the application is>"\`; a guessed name that doesn't exist is dropped without an error.`,
2347
2347
  command: `nexarch update-entity --key "${projectExternalKey}" --entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? ' --subtype "<subtype>" --icon "<lucide-icon>"' : ""} --name "..." --description "..."`,
2348
2348
  ...(entityTypeOverride === "application"
2349
2349
  ? { notes: [APPLICATION_SUBTYPE_HINT, "Choose app_custom_built as the default if none of the others clearly apply."] }
@@ -2384,9 +2384,11 @@ export async function initProject(args) {
2384
2384
  instruction: `Review the codebase to identify discrete application functions (what the application does). ` +
2385
2385
  `Examine named modules, route layout, service boundaries, and any architecture documentation. ` +
2386
2386
  `Register functions as application_function entities with subtype core_function (primary business function), supporting_function (auxiliary/enablement), integration_function (external connectivity), or data_function (data processing). ` +
2387
- `Only register functions clearly evidenced by the codebase — do not invent them. ${DESCRIPTION_GUIDANCE}`,
2387
+ `Only register functions clearly evidenced by the codebase — do not invent them. ${DESCRIPTION_GUIDANCE} ` +
2388
+ `Give each function an icon for what it does: find a real name with \`nexarch search-icons "<what it does>"\` (or nexarch_search_icons) and pass it as --icon. Don't guess a name; one that doesn't exist is dropped without an error. If two searches find nothing fitting, leave the icon off.`,
2388
2389
  commandTemplates: {
2389
- updateEntity: `nexarch update-entity --key "application_function:${projectSlug}_<function_slug>" --entity-type application_function --subtype core_function --name "..." --description "..."`,
2390
+ searchIcons: `nexarch search-icons "<what the function does>"`,
2391
+ updateEntity: `nexarch update-entity --key "application_function:${projectSlug}_<function_slug>" --entity-type application_function --subtype core_function --icon "<name from search-icons>" --name "..." --description "..."`,
2390
2392
  addRelationship: `nexarch add-relationship --from "application_function:${projectSlug}_<function_slug>" --to ${projectConstruct && isMonorepo ? '"<owning application key from classifyPackages>"' : `"${projectExternalKey}"`} --type part_of`,
2391
2393
  note: "Functions are structural decomposition only: do not add runtime dependencies from an application_function. Model independently deployable services as applications or application components, then relate those boundaries with depends_on or integrates_with.",
2392
2394
  },
@@ -2405,7 +2407,7 @@ export async function initProject(args) {
2405
2407
  pendingSteps.push({
2406
2408
  step: stepNum++,
2407
2409
  action: "register_decision_records",
2408
- instruction: `Look for ADRs (docs/adr/, decisions/, ADR-*.md) and register each as a decision_record entity.`,
2410
+ instruction: `Look for ADRs (docs/adr/, decisions/, ADR-*.md) and register the architectural ones as decision_record entities: a decision that crosses a boundary between applications, services or teams; changes an integration, data ownership, security or deployment; adopts, replaces or retires a technology others depend on; or is expensive to reverse. Skip design decisions inside one application, even when they are written up as ADRs.`,
2409
2411
  commandTemplates: {
2410
2412
  updateEntity: `nexarch update-entity --key "decision_record:${projectSlug}_<adr_slug>" --entity-type decision_record --subtype decision_architecture --name "..." --attributes-json '{"decision":{"summary":"...","detail":"..."}}'`,
2411
2413
  addRelationship: `nexarch add-relationship --from "decision_record:${projectSlug}_<adr_slug>" --to ${projectConstruct && isMonorepo ? '"<the relevant application key>"' : `"${projectExternalKey}"`} --type decides`,
@@ -2519,7 +2521,7 @@ export async function initProject(args) {
2519
2521
  unresolvedNames: unresolvedItems.map((r) => r.input),
2520
2522
  iconHints: {
2521
2523
  provider: "lucide",
2522
- note: "Use any Lucide icon name (kebab-case); omit when confidence is low.",
2524
+ note: "Use a name returned by `nexarch search-icons` (or nexarch_search_icons); a guessed name that doesn't exist is dropped without an error. Omit when nothing fits.",
2523
2525
  },
2524
2526
  };
2525
2527
  }
@@ -2643,15 +2645,19 @@ export async function initProject(args) {
2643
2645
  lines.push(` Use subtype core_function (primary business function), supporting_function (auxiliary/enablement),`);
2644
2646
  lines.push(` integration_function (external connectivity), or data_function (data processing).`);
2645
2647
  lines.push(` Only register functions clearly evidenced by the codebase — do not invent them. ${DESCRIPTION_GUIDANCE}`);
2648
+ lines.push(` Give each function an icon for what it does. Find a real name first; a guessed one is dropped without an error:`);
2649
+ lines.push(` nexarch search-icons "<what the function does>"`);
2646
2650
  lines.push(` For each one found:`);
2647
2651
  const fnOwnerTarget = projectConstruct && isMonorepo ? '"<owning application key from CLASSIFY_THESE>"' : `"${projectExternalKey}"`;
2648
- lines.push(` nexarch update-entity --key "application_function:${projectExternalKey.split(":")[1] ?? "project"}_<function_slug>" --entity-type application_function --subtype core_function --name "..." --description "..."`);
2652
+ lines.push(` nexarch update-entity --key "application_function:${projectExternalKey.split(":")[1] ?? "project"}_<function_slug>" --entity-type application_function --subtype core_function --icon "<name from search-icons>" --name "..." --description "..."`);
2649
2653
  lines.push(` nexarch add-relationship --from "application_function:${projectExternalKey.split(":")[1] ?? "project"}_<function_slug>" --to ${fnOwnerTarget} --type part_of`);
2650
2654
  lines.push(" Functions are structural only: model runtime/service dependencies between application or application_component boundaries, never from an application_function.");
2651
2655
  }
2652
2656
  lines.push(` ${step++}. Scan the READMEs for platforms/SaaS not auto-detected (Vercel, Neon, Stripe, etc.).`);
2653
2657
  lines.push(` For each found: nexarch resolve-names --names "..." --json → nexarch update-entity → nexarch add-relationship`);
2654
- lines.push(` ${step++}. Look for ADRs (docs/adr/, decisions/, ADR-*.md) and register decision_record entities.`);
2658
+ lines.push(` ${step++}. Look for ADRs (docs/adr/, decisions/, ADR-*.md) and register the architectural ones as decision_record entities.`);
2659
+ lines.push(` Architectural means it crosses a boundary between applications, services or teams; changes an integration, data ownership, security or deployment;`);
2660
+ lines.push(` adopts or retires a technology others depend on; or is expensive to reverse. Skip design decisions inside one application.`);
2655
2661
  lines.push(` nexarch update-entity --key "decision_record:${projectExternalKey.split(":")[1] ?? "project"}_<adr_slug>" --entity-type decision_record --subtype decision_architecture --name "..." --attributes-json '{"decision":{"summary":"...","detail":"..."}}'`);
2656
2662
  lines.push(` nexarch add-relationship --from "decision_record:..." --to ${projectConstruct && isMonorepo ? '"<the relevant application key>"' : `"${projectExternalKey}"`} --type decides`);
2657
2663
  lines.push("");
@@ -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({
@@ -0,0 +1,64 @@
1
+ import process from "process";
2
+ import { requireCredentials } from "../lib/credentials.js";
3
+ import { callMcpTool } from "../lib/mcp.js";
4
+ function parseOptionValue(args, option) {
5
+ const idx = args.indexOf(option);
6
+ if (idx === -1)
7
+ return null;
8
+ const value = args[idx + 1];
9
+ if (!value || value.startsWith("--"))
10
+ return null;
11
+ return value;
12
+ }
13
+ /**
14
+ * The words to search for: everything that isn't an option or an option's
15
+ * value, so both `search-icons database` and `search-icons "event stream"` work.
16
+ */
17
+ export function searchIconsQuery(args) {
18
+ const valued = new Set(["--limit"]);
19
+ const words = [];
20
+ for (let i = 0; i < args.length; i++) {
21
+ if (valued.has(args[i])) {
22
+ i++;
23
+ continue;
24
+ }
25
+ if (args[i].startsWith("--"))
26
+ continue;
27
+ words.push(args[i]);
28
+ }
29
+ return words.join(" ").trim();
30
+ }
31
+ /**
32
+ * Finds real icon names for `update-entity --icon`. The gateway silently drops
33
+ * an icon name that doesn't exist, so an agent guessing from memory ends up
34
+ * with no icon and no error; this searches the same validated set it checks.
35
+ */
36
+ export async function searchIcons(args) {
37
+ const asJson = args.includes("--json");
38
+ const query = searchIconsQuery(args);
39
+ if (!query) {
40
+ console.error('error: say what the icon should show, e.g. nexarch search-icons "credit card"');
41
+ process.exit(1);
42
+ }
43
+ const limitRaw = parseOptionValue(args, "--limit");
44
+ const limit = limitRaw ? Number(limitRaw) : undefined;
45
+ if (limitRaw && (!Number.isInteger(limit) || limit < 1)) {
46
+ console.error("error: --limit must be a whole number of 1 or more");
47
+ process.exit(1);
48
+ }
49
+ const creds = requireCredentials();
50
+ const raw = await callMcpTool("nexarch_search_icons", { query, ...(limit ? { limit } : {}) }, { companyId: creds.companyId });
51
+ const result = JSON.parse(raw.content?.[0]?.text ?? "{}");
52
+ if (asJson) {
53
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
54
+ return;
55
+ }
56
+ if (!result.matches?.length) {
57
+ console.log(`No icons match "${query}".`);
58
+ console.log("Icon names are literal objects and actions (database, file, box). Try one more concrete word; if that also finds nothing, leave the icon unset.");
59
+ return;
60
+ }
61
+ for (const name of result.matches)
62
+ console.log(name);
63
+ console.log(`\n${result.matches.length} of ${result.totalMatches} match${result.totalMatches === 1 ? "" : "es"}. Use one with: nexarch update-entity --key "<key>" --icon "${result.matches[0]}"`);
64
+ }
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { updateEntity } from "./commands/update-entity.js";
13
13
  import { addRelationship } from "./commands/add-relationship.js";
14
14
  import { registerAlias } from "./commands/register-alias.js";
15
15
  import { resolveNames } from "./commands/resolve-names.js";
16
+ import { searchIcons } from "./commands/search-icons.js";
16
17
  import { feedback } from "./commands/feedback.js";
17
18
  import { listEntities } from "./commands/list-entities.js";
18
19
  import { listRelationships } from "./commands/list-relationships.js";
@@ -57,6 +58,7 @@ const commands = {
57
58
  "add-relationship": addRelationship,
58
59
  "register-alias": registerAlias,
59
60
  "resolve-names": resolveNames,
61
+ "search-icons": searchIcons,
60
62
  feedback,
61
63
  "list-entities": listEntities,
62
64
  "list-relationships": listRelationships,
@@ -105,7 +107,12 @@ nexarch — Your architecture workspace for AI delivery.
105
107
 
106
108
  Usage:
107
109
  nexarch login Authenticate in browser and store company-scoped credentials
108
- 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
109
116
  nexarch --version Print the version and the path this copy runs from
110
117
  nexarch enroll Bootstrap a headless (browserless) agent with a one-time
111
118
  enrollment code — no login required. Writes the resulting
@@ -133,6 +140,7 @@ Usage:
133
140
  registering anything; answer 'n' to pick a different one.
134
141
  Options: --company <id|code> target a workspace directly
135
142
  --yes accept the stored workspace without asking
143
+ --no-browser log in by pasting a one-time code (see login)
136
144
  nexarch mcp-config Print MCP server config block for manual setup
137
145
  Client list is registry-managed (see 'nexarch mcp-config --client <code>')
138
146
  nexarch mcp-proxy Run as stdio MCP proxy (used by MCP clients)
@@ -232,6 +240,12 @@ Usage:
232
240
  results before calling add-relationship.
233
241
  Options: --names <csv> (required, e.g. "vercel,neon")
234
242
  --json
243
+ nexarch search-icons <words>
244
+ Find real icon names for update-entity --icon. An icon
245
+ name that doesn't exist is dropped without an error, so
246
+ look one up rather than guessing.
247
+ Options: --limit <n> (default 20, max 50)
248
+ --json
235
249
  nexarch feedback
236
250
  Report a Nexarch usability problem or tool error.
237
251
  Options: --kind <usability|error> (required)
@@ -240,7 +240,7 @@ evidence — never invent a compliance status that isn't stored.
240
240
  `;
241
241
  const DECISION_RECORDS_SKILL_BODY = `---
242
242
  name: nexarch-decision-records
243
- description: Record an architectural decision in Nexarch, or check what's already been decided about something. Use when a decision is being made (a technology, pattern, or approach chosen over alternatives), when ADR/RFC documents are found in a repository, when asked what's already been decided about a topic, or when a new decision replaces an older one. Requires the Nexarch MCP tools (nexarch_*).
243
+ description: Record an architectural decision in Nexarch, or check what's already been decided about something. Use when a decision crosses a boundary between applications, services or teams, changes an integration, data ownership, security or deployment, adopts a technology others depend on, or is expensive to reverse; when ADR/RFC documents are found in a repository; when asked what's already been decided about a topic; or when a new decision replaces an older one. Not for design decisions inside one application. Requires the Nexarch MCP tools (nexarch_*).
244
244
  ---
245
245
 
246
246
  # Nexarch Decision Records
@@ -252,6 +252,23 @@ skill is for recording one as it happens — mining a whole repository's ADR
252
252
  history systematically is a separate, human-triggered Decision Review
253
253
  command from the application's page in the workspace.
254
254
 
255
+ ## Architectural, not design
256
+
257
+ Record only architectural decisions. A decision is architectural when it does
258
+ at least one of these:
259
+
260
+ - crosses a boundary between applications, services or teams
261
+ - changes an integration, data ownership, security or authentication, or how
262
+ the system is deployed
263
+ - adopts, replaces or retires a technology that other systems or teams depend on
264
+ - is expensive to reverse once other work builds on it
265
+
266
+ Anything else is a design decision — screen layouts, interaction patterns,
267
+ component library choices, internal module structure, naming. It belongs in
268
+ the repository's design documentation, not in the graph. When unsure, ask:
269
+ if this decision changed, would another team or system have to change too? If
270
+ not, do not record it, even when it is written up as an ADR.
271
+
255
272
  ## Before recording
256
273
 
257
274
  \`nexarch_resolve_reference\` / \`nexarch_list_entities\` for an existing
package/package.json CHANGED
@@ -1,39 +1,39 @@
1
- {
2
- "name": "nexarch",
3
- "version": "0.13.3",
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"
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.5",
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
+ }