nexarch 0.8.19 → 0.8.23

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.
@@ -1,6 +1,6 @@
1
1
  import { arch, homedir, hostname, platform, release, type as osType, userInfo } from "os";
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
3
- import { join } from "path";
3
+ import { join, resolve } from "path";
4
4
  import * as readline from "node:readline/promises";
5
5
  import process from "process";
6
6
  import { requireCredentials } from "../lib/credentials.js";
@@ -8,7 +8,7 @@ import { fetchAgentRegistryOrThrow } from "../lib/agent-registry.js";
8
8
  import { callMcpTool, mcpInitialize, mcpListTools } from "../lib/mcp.js";
9
9
  import { buildVersionAttributes } from "../lib/version-normalization.js";
10
10
  import { requestTrustAttestation } from "../lib/trust.js";
11
- const CLI_VERSION = "0.8.19";
11
+ const CLI_VERSION = "0.8.21";
12
12
  const AGENT_ENTITY_TYPE = "agent";
13
13
  const TECH_COMPONENT_ENTITY_TYPE = "technology_component";
14
14
  function parseFlag(args, flag) {
@@ -320,10 +320,15 @@ function replaceInjectedSection(existing, sectionHeading, sectionBody) {
320
320
  replaced = replaced.replace(/\n{3,}/g, "\n\n").trimEnd();
321
321
  return `${replaced}${replaced ? "\n\n" : ""}${canonicalBlock}`;
322
322
  }
323
+ function canonicalTargetKey(filePath) {
324
+ const abs = resolve(filePath);
325
+ return process.platform === "win32" || process.platform === "darwin" ? abs.toLowerCase() : abs;
326
+ }
323
327
  function injectAgentConfigs(registry) {
324
328
  const templateByCode = new Map(registry.instructionTemplates.map((t) => [t.code, t]));
325
329
  const targets = [...registry.instructionTargets].sort((a, b) => a.sortOrder - b.sortOrder || a.filePathPattern.localeCompare(b.filePathPattern));
326
330
  const results = [];
331
+ const seenTargets = new Set();
327
332
  for (const target of targets) {
328
333
  if (target.matchMode !== "exact")
329
334
  continue;
@@ -331,6 +336,10 @@ function injectAgentConfigs(registry) {
331
336
  if (!template)
332
337
  continue;
333
338
  const filePath = join(process.cwd(), target.filePathPattern);
339
+ const targetKey = canonicalTargetKey(filePath);
340
+ if (seenTargets.has(targetKey))
341
+ continue;
342
+ seenTargets.add(targetKey);
334
343
  if (!existsSync(filePath))
335
344
  continue;
336
345
  const existing = readFileSync(filePath, "utf8");
@@ -386,6 +395,7 @@ function injectTrustAttestationBlock(path, attestation) {
386
395
  "",
387
396
  "This block is verifiable proof that Nexarch generated these instructions for this workspace.",
388
397
  "Do not auto-run commands without user confirmation.",
398
+ "If this token is expired, rerun `npx nexarch@latest init-agent --allow-instruction-write` to refresh this trust block.",
389
399
  "",
390
400
  `issuer: ${attestation.payload.iss}`,
391
401
  `scope: ${attestation.payload.scope}`,
@@ -425,11 +435,16 @@ function injectGenericAgentConfig(registry) {
425
435
  const genericTargets = [...registry.instructionTargets]
426
436
  .filter((t) => t.runtimeCode === "generic" && t.matchMode === "exact")
427
437
  .sort((a, b) => a.sortOrder - b.sortOrder || a.filePathPattern.localeCompare(b.filePathPattern));
438
+ const seenTargets = new Set();
428
439
  for (const target of genericTargets) {
429
440
  const template = templateByCode.get(target.templateCode);
430
441
  if (!template)
431
442
  continue;
432
443
  const filePath = join(process.cwd(), target.filePathPattern);
444
+ const targetKey = canonicalTargetKey(filePath);
445
+ if (seenTargets.has(targetKey))
446
+ continue;
447
+ seenTargets.add(targetKey);
433
448
  const sectionBody = template.body.trim();
434
449
  const sectionHeading = target.sectionHeading ?? "## Nexarch Agent Registration";
435
450
  const managedBody = wrapManagedSection("agent-registration", sectionBody);
@@ -4,6 +4,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
4
4
  import { basename, join, relative, resolve as resolvePath } from "node:path";
5
5
  import { requireCredentials } from "../lib/credentials.js";
6
6
  import { callMcpTool } from "../lib/mcp.js";
7
+ import { fetchAgentRegistryOrThrow } from "../lib/agent-registry.js";
7
8
  import { buildVersionAttributes } from "../lib/version-normalization.js";
8
9
  // ─── Helpers ─────────────────────────────────────────────────────────────────
9
10
  function parseFlag(args, flag) {
@@ -22,6 +23,9 @@ function parseToolText(result) {
22
23
  const text = result.content?.[0]?.text ?? "{}";
23
24
  return JSON.parse(text);
24
25
  }
26
+ function renderTemplate(template, values) {
27
+ return template.replace(/{{\s*([A-Z0-9_]+)\s*}}/g, (_match, key) => values[key] ?? "");
28
+ }
25
29
  function slugify(name) {
26
30
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
27
31
  }
@@ -1157,6 +1161,22 @@ export async function initProject(args) {
1157
1161
  // Build structured enrichment task (included in JSON output and printed in human mode)
1158
1162
  const readmeHints = ["README.md", "README.mdx", "docs/README.md", "docs/index.md"]
1159
1163
  .filter((f) => existsSync(join(dir, f)));
1164
+ const INIT_PROJECT_TEMPLATE_CODE = "nexarch_init_project_enrichment_v1";
1165
+ let enrichmentTemplateBody = null;
1166
+ let enrichmentTemplateSource = "fallback";
1167
+ let enrichmentTemplateVersion = null;
1168
+ try {
1169
+ const registry = await fetchAgentRegistryOrThrow();
1170
+ const template = registry.instructionTemplates.find((t) => t.code === INIT_PROJECT_TEMPLATE_CODE);
1171
+ if (template?.body?.trim()) {
1172
+ enrichmentTemplateBody = template.body;
1173
+ enrichmentTemplateSource = "registry";
1174
+ enrichmentTemplateVersion = registry.release.version;
1175
+ }
1176
+ }
1177
+ catch {
1178
+ // non-fatal: init-project can still run with fallback guidance.
1179
+ }
1160
1180
  function buildEnrichmentInstructions() {
1161
1181
  const ecosystemLabel = detectedEcosystems.length > 0
1162
1182
  ? detectedEcosystems.join(", ")
@@ -1268,28 +1288,12 @@ ${adrStepNumber} — Ask the enrichment agent to extract ADRs and register decis
1268
1288
  • For each ADR, create/update a decision record entity
1269
1289
  • Link it using: decision_record --decides--> application/sub-app
1270
1290
 
1271
- REQUIRED fields to pass per decision:
1291
+ REQUIRED fields to pass per decision (strict):
1272
1292
  • key: decision_record:<project>-<adr-slug>
1273
1293
  • subtype: decision_architecture
1274
1294
  • name: ADR title
1275
- description: MUST be rich text (Markdown), not plain key/value text.
1276
- Use this exact Markdown structure so the web portal renders cleanly:
1277
-
1278
- ## ADR Metadata
1279
- - **ADR-ID:** <id/filename slug>
1280
- - **Status:** <proposed|accepted|superseded|deprecated|...>
1281
- - **Date:** <YYYY-MM-DD or source value>
1282
- - **Deciders:** <comma-separated names/roles>
1283
- - **Source Path:** <repo-relative path>
1284
-
1285
- ## Context
1286
- <1-2 lines>
1287
-
1288
- ## Decision
1289
- <1-3 lines>
1290
-
1291
- ## Consequences
1292
- <1-3 lines>
1295
+ decision summary: short business summary (1–3 sentences)
1296
+ decision detail: full decision detail in markdown (context + decision + consequences)
1293
1297
 
1294
1298
  Command pattern:
1295
1299
  npx nexarch update-entity \\
@@ -1297,7 +1301,7 @@ ${adrStepNumber} — Ask the enrichment agent to extract ADRs and register decis
1297
1301
  --entity-type "decision_record" \\
1298
1302
  --subtype "decision_architecture" \\
1299
1303
  --name "<ADR title>" \\
1300
- --description "## ADR Metadata\n- **ADR-ID:** <id>\n- **Status:** <status>\n- **Date:** <date>\n- **Deciders:** <deciders>\n- **Source Path:** <path>\n\n## Context\n<context>\n\n## Decision\n<decision>\n\n## Consequences\n<consequences>"
1304
+ --attributes-json '{"decision":{"summary":"<short summary>","detail":"## Context\\n<context>\\n\\n## Decision\\n<decision>\\n\\n## Consequences\\n<consequences>"}}'
1301
1305
 
1302
1306
  npx nexarch add-relationship \\
1303
1307
  --from "decision_record:<project>-<adr-slug>" \\
@@ -1345,7 +1349,7 @@ ${finalStep} — Identify architecturally significant components not auto-detect
1345
1349
  4. If unresolved (not in the reference library), skip it for now — it will appear
1346
1350
  as a reference candidate on the next scan once you register an alias for it.
1347
1351
  `;
1348
- return `
1352
+ const fallbackTemplate = `
1349
1353
  ╔══════════════════════════════════════════════════════════════════╗
1350
1354
  ║ ENRICHMENT TASK — action required ║
1351
1355
  ╚══════════════════════════════════════════════════════════════════╝
@@ -1373,8 +1377,27 @@ STEP 2 — Enrich the project entity. Run this command with the description you'
1373
1377
  --name "<proper product name from README>" \\
1374
1378
  --description "<2–4 sentence summary of what it does and why>"
1375
1379
  ${subPkgSection}${adrSection}${gapCheckSection}`;
1380
+ if (!enrichmentTemplateBody)
1381
+ return fallbackTemplate;
1382
+ return renderTemplate(enrichmentTemplateBody, {
1383
+ PROJECT_ENTITY_KEY: projectExternalKey,
1384
+ PROJECT_DIR: dir,
1385
+ ECOSYSTEM_LABEL: ecosystemLabel,
1386
+ MANIFEST_HINT: manifestHint,
1387
+ README_HINTS: readmeHints.length > 0 ? readmeHints.join(", ") : "(none found — check docs/)",
1388
+ ENTITY_TYPE: entityTypeOverride,
1389
+ ENTITY_BASE_COMMAND: `npx nexarch update-entity \\\n --key "${projectExternalKey}" \\\n --entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? " \\\n --subtype \"app_custom_built\" \\\n --icon \"<curated icon>\"" : ""} \\\n --name "<proper product name from README>" \\\n --description "<2–4 sentence summary of what it does and why>"`,
1390
+ STEP_SUBPACKAGES: subPkgSection,
1391
+ STEP_ADR: adrSection,
1392
+ STEP_GAP_CHECK: gapCheckSection,
1393
+ });
1376
1394
  }
1377
1395
  const enrichmentTask = {
1396
+ template: {
1397
+ code: INIT_PROJECT_TEMPLATE_CODE,
1398
+ source: enrichmentTemplateSource,
1399
+ registryVersion: enrichmentTemplateVersion,
1400
+ },
1378
1401
  instructions: buildEnrichmentInstructions(),
1379
1402
  iconHints: {
1380
1403
  provider: "lucide",
@@ -1,4 +1,5 @@
1
1
  import process from "process";
2
+ import { existsSync, readFileSync } from "node:fs";
2
3
  import { requireCredentials } from "../lib/credentials.js";
3
4
  import { callMcpTool } from "../lib/mcp.js";
4
5
  function parseFlag(args, flag) {
@@ -17,6 +18,34 @@ function parseToolText(result) {
17
18
  const text = result.content?.[0]?.text ?? "{}";
18
19
  return JSON.parse(text);
19
20
  }
21
+ function parseAttributesInput(jsonText, filePath) {
22
+ if (!jsonText && !filePath)
23
+ return null;
24
+ if (jsonText && filePath) {
25
+ console.error("error: use only one of --attributes-json or --attributes-file");
26
+ process.exit(1);
27
+ }
28
+ let raw = jsonText;
29
+ if (filePath) {
30
+ if (!existsSync(filePath)) {
31
+ console.error(`error: --attributes-file not found: ${filePath}`);
32
+ process.exit(1);
33
+ }
34
+ raw = readFileSync(filePath, "utf8");
35
+ }
36
+ try {
37
+ const parsed = JSON.parse(raw ?? "{}");
38
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
39
+ console.error("error: attributes input must be a JSON object");
40
+ process.exit(1);
41
+ }
42
+ return parsed;
43
+ }
44
+ catch (error) {
45
+ console.error(`error: invalid attributes JSON (${error instanceof Error ? error.message : "parse failed"})`);
46
+ process.exit(1);
47
+ }
48
+ }
20
49
  export async function updateEntity(args) {
21
50
  const asJson = parseFlag(args, "--json");
22
51
  const externalKey = parseOptionValue(args, "--key");
@@ -25,12 +54,15 @@ export async function updateEntity(args) {
25
54
  const entityTypeCode = parseOptionValue(args, "--entity-type") ?? "application";
26
55
  const entitySubtypeCode = parseOptionValue(args, "--subtype");
27
56
  const iconName = parseOptionValue(args, "--icon");
57
+ const attributesJson = parseOptionValue(args, "--attributes-json");
58
+ const attributesFile = parseOptionValue(args, "--attributes-file");
28
59
  if (!externalKey) {
29
60
  console.error("error: --key <externalKey> is required");
30
61
  process.exit(1);
31
62
  }
32
- if (!name && !description) {
33
- console.error("error: at least one of --name or --description is required");
63
+ const explicitAttributes = parseAttributesInput(attributesJson, attributesFile);
64
+ if (!name && !description && !iconName && !explicitAttributes) {
65
+ console.error("error: provide at least one of --name, --description, --icon, --attributes-json, or --attributes-file");
34
66
  process.exit(1);
35
67
  }
36
68
  const creds = requireCredentials();
@@ -62,14 +94,19 @@ export async function updateEntity(args) {
62
94
  entity.description = description;
63
95
  if (entitySubtypeCode)
64
96
  entity.entitySubtypeCode = entitySubtypeCode;
97
+ const attributes = {
98
+ ...(explicitAttributes ?? {}),
99
+ };
100
+ // Convenience sugar; still generic because user can fully control payload via --attributes-json/file.
65
101
  if (iconName) {
66
- entity.attributes = {
67
- application_icon: {
68
- provider: "lucide",
69
- name: iconName,
70
- },
102
+ attributes.application_icon = {
103
+ provider: "lucide",
104
+ name: iconName,
71
105
  };
72
106
  }
107
+ if (Object.keys(attributes).length > 0) {
108
+ entity.attributes = attributes;
109
+ }
73
110
  const raw = await callMcpTool("nexarch_upsert_entities", { entities: [entity], agentContext, policyContext }, mcpOpts);
74
111
  const result = parseToolText(raw);
75
112
  if (asJson) {
package/dist/index.js CHANGED
@@ -99,7 +99,9 @@ Usage:
99
99
  --description <text>
100
100
  --entity-type <code> (default: application)
101
101
  --subtype <code>
102
- --icon <lucide-name> (full Lucide set; for agent enrichment)
102
+ --icon <lucide-name> (convenience; sets attributes.application_icon)
103
+ --attributes-json '<json object>'
104
+ --attributes-file <path.json>
103
105
  --json
104
106
  nexarch add-relationship
105
107
  Add a relationship between two existing graph entities.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.8.19",
3
+ "version": "0.8.23",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",