nexarch 0.12.0 → 0.12.3

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.
@@ -5,6 +5,7 @@ import { homedir } from "os";
5
5
  import { saveCheckInLease } from "../lib/check-in-lease.js";
6
6
  import { requireCredentials } from "../lib/credentials.js";
7
7
  import { callMcpTool } from "../lib/mcp.js";
8
+ import { inferAccessibleApplicationRefs } from "../lib/application-context.js";
8
9
  function parseFlag(args, flag) {
9
10
  return args.includes(flag);
10
11
  }
@@ -36,6 +37,7 @@ function loadIdentity() {
36
37
  export async function checkIn(args) {
37
38
  const asJson = parseFlag(args, "--json");
38
39
  const agentKeyArg = parseOptionValue(args, "--agent-ref") ?? parseOptionValue(args, "--agent-key");
40
+ const applicationRefArg = parseOptionValue(args, "--application-ref");
39
41
  const creds = requireCredentials();
40
42
  const identity = loadIdentity();
41
43
  const agentKey = agentKeyArg ?? identity.agentKey;
@@ -48,9 +50,16 @@ export async function checkIn(args) {
48
50
  }
49
51
  return;
50
52
  }
53
+ const applicationContext = await inferAccessibleApplicationRefs({
54
+ companyId: creds.companyId,
55
+ explicitApplicationRefs: applicationRefArg ? [applicationRefArg] : undefined,
56
+ });
51
57
  const raw = await callMcpTool("nexarch_check_in", {
52
58
  agentRef: agentKey,
53
59
  agentKey,
60
+ applicationContext: applicationContext.accessibleApplicationRefs.length > 0
61
+ ? { accessibleApplicationRefs: applicationContext.accessibleApplicationRefs }
62
+ : undefined,
54
63
  companyId: creds.companyId,
55
64
  }, { companyId: creds.companyId });
56
65
  const result = parseToolText(raw);
@@ -74,6 +83,7 @@ export async function checkIn(args) {
74
83
  if (result.leaseExpiresAt) {
75
84
  console.log(`Check-in lease active until ${new Date(result.leaseExpiresAt).toLocaleString()}.`);
76
85
  }
86
+ console.log(`Application context: ${applicationContext.detail}`);
77
87
  for (const warning of result.warnings ?? []) {
78
88
  if (warning.message)
79
89
  console.log(`Warning: ${warning.message}`);
@@ -91,7 +101,7 @@ export async function checkIn(args) {
91
101
  }
92
102
  for (const cmd of commands) {
93
103
  const claimable = cmd.claimable ? "yes" : "no";
94
- const reason = cmd.claimReason ?? (cmd.claimable ? "application_access_confirmed" : "application_not_found_for_company");
104
+ const reason = cmd.claimReason ?? (cmd.claimable ? "application_access_confirmed" : "application_context_required_for_claim");
95
105
  console.log(`\n- ID : ${cmd.id}`);
96
106
  console.log(` Type : ${cmd.command_type}`);
97
107
  console.log(` Target : ${cmd.target_entity_key ?? "(none)"}`);
@@ -101,12 +111,14 @@ export async function checkIn(args) {
101
111
  console.log(` Type ver : ${cmd.command_type_version}`);
102
112
  if (cmd.resolved_runtime_handler)
103
113
  console.log(` Runtime : ${cmd.resolved_runtime_handler}`);
104
- if (cmd.claimable) {
105
- console.log(` Claim cmd : npx nexarch command-claim --id "${cmd.id}"`);
106
- }
114
+ if (cmd.claimGuidance)
115
+ console.log(` Guidance : ${cmd.claimGuidance}`);
116
+ console.log(` Claim cmd : npx nexarch command-claim --id "${cmd.id}"${cmd.target_entity_key ? ` --application-ref "${cmd.target_entity_key}"` : ""}`);
107
117
  }
108
118
  console.log("\nTo validate an application target is accessible in Nexarch before claiming:");
109
119
  console.log(" npx nexarch policy-controls --entity <application:key> --json");
120
+ console.log("If no application context was inferred, re-run with:");
121
+ console.log(" npx nexarch check-in --application-ref <application:key>");
110
122
  }
111
123
  else {
112
124
  console.log("No pending application-target commands found.");
@@ -37,6 +37,7 @@ export async function commandClaim(args) {
37
37
  const asJson = parseFlag(args, "--json");
38
38
  const id = parseOptionValue(args, "--id");
39
39
  const agentKeyArg = parseOptionValue(args, "--agent-ref") ?? parseOptionValue(args, "--agent-key");
40
+ const applicationEntityRef = parseOptionValue(args, "--application-ref");
40
41
  if (!id) {
41
42
  console.error("error: --id <commandId> is required");
42
43
  process.exit(1);
@@ -54,6 +55,7 @@ export async function commandClaim(args) {
54
55
  agentRef: agentKey,
55
56
  agentKey,
56
57
  leaseToken: lease?.leaseToken,
58
+ applicationEntityRef: applicationEntityRef ?? undefined,
57
59
  companyId: creds.companyId,
58
60
  }, { companyId: creds.companyId });
59
61
  const result = parseToolText(raw);
@@ -72,6 +74,8 @@ export async function commandClaim(args) {
72
74
  console.log(`Command ${id} was not claimed.`);
73
75
  if (result.reason)
74
76
  console.log(`Reason: ${result.reason}`);
77
+ if (!applicationEntityRef)
78
+ console.log("Tip: re-run with --application-ref <application:key>.");
75
79
  return;
76
80
  }
77
81
  const cmd = result.command;
@@ -337,29 +337,28 @@ function canonicalTargetKey(filePath) {
337
337
  const abs = resolve(filePath);
338
338
  return process.platform === "win32" || process.platform === "darwin" ? abs.toLowerCase() : abs;
339
339
  }
340
- function injectAgentConfigs(registry) {
340
+ function injectAgentConfigs(registry, runtimeCode) {
341
341
  const templateByCode = new Map(registry.instructionTemplates.map((t) => [t.code, t]));
342
- const targets = [...registry.instructionTargets].sort((a, b) => a.sortOrder - b.sortOrder || a.filePathPattern.localeCompare(b.filePathPattern));
343
- const results = [];
344
- const seenTargets = new Set();
345
- for (const target of targets) {
346
- if (target.matchMode !== "exact")
347
- continue;
342
+ const sortedTargets = [...registry.instructionTargets]
343
+ .filter((target) => target.matchMode === "exact")
344
+ .sort((a, b) => a.sortOrder - b.sortOrder || a.filePathPattern.localeCompare(b.filePathPattern));
345
+ const candidateTargets = runtimeCode
346
+ ? sortedTargets.filter((target) => target.runtimeCode === runtimeCode)
347
+ : sortedTargets;
348
+ const applyToTarget = (target) => {
348
349
  const template = templateByCode.get(target.templateCode);
349
350
  if (!template)
350
- continue;
351
+ return null;
351
352
  const filePath = join(process.cwd(), target.filePathPattern);
352
- const targetKey = canonicalTargetKey(filePath);
353
- if (seenTargets.has(targetKey))
354
- continue;
355
- seenTargets.add(targetKey);
356
- if (!existsSync(filePath))
357
- continue;
358
- const existing = readFileSync(filePath, "utf8");
359
353
  const sectionBody = template.body.trim();
360
354
  const sectionHeading = target.sectionHeading ?? "## Nexarch Agent Registration";
361
355
  const sectionMarker = target.sectionMarker ?? sectionHeading;
362
356
  const managedBody = wrapManagedSection("agent-registration", sectionBody);
357
+ if (!existsSync(filePath)) {
358
+ writeFileSync(filePath, `${managedBody}\n`, "utf8");
359
+ return { path: filePath, status: "injected" };
360
+ }
361
+ const existing = readFileSync(filePath, "utf8");
363
362
  if (target.insertionMode === "replace_section") {
364
363
  let replaced = replaceManagedSection(existing, "agent-registration", sectionBody);
365
364
  if (replaced === existing) {
@@ -372,29 +371,46 @@ function injectAgentConfigs(registry) {
372
371
  }
373
372
  if (replaced !== existing) {
374
373
  writeFileSync(filePath, replaced, "utf8");
375
- results.push({ path: filePath, status: "updated" });
374
+ return { path: filePath, status: "updated" };
376
375
  }
377
- else if (existing.includes(managedBody) || existing.includes(sectionBody)) {
378
- results.push({ path: filePath, status: "already_present" });
379
- }
380
- else {
381
- const separator = existing.endsWith("\n") ? "" : "\n";
382
- writeFileSync(filePath, existing + separator + managedBody + "\n", "utf8");
383
- results.push({ path: filePath, status: "injected" });
376
+ if (existing.includes(managedBody) || existing.includes(sectionBody)) {
377
+ return { path: filePath, status: "already_present" };
384
378
  }
385
- continue;
379
+ const separator = existing.endsWith("\n") ? "" : "\n";
380
+ writeFileSync(filePath, existing + separator + managedBody + "\n", "utf8");
381
+ return { path: filePath, status: "injected" };
386
382
  }
387
383
  if (existing.includes(managedBody) || existing.includes(sectionBody)) {
388
- results.push({ path: filePath, status: "already_present" });
389
- }
390
- else {
391
- const separator = existing.endsWith("\n") ? "" : "\n";
392
- const next = existing + separator + managedBody + "\n";
393
- writeFileSync(filePath, next, "utf8");
394
- results.push({ path: filePath, status: "injected" });
384
+ return { path: filePath, status: "already_present" };
395
385
  }
386
+ const separator = existing.endsWith("\n") ? "" : "\n";
387
+ const next = existing + separator + managedBody + "\n";
388
+ writeFileSync(filePath, next, "utf8");
389
+ return { path: filePath, status: "injected" };
390
+ };
391
+ const seenTargets = new Set();
392
+ const existingMatches = [];
393
+ for (const target of candidateTargets) {
394
+ const filePath = join(process.cwd(), target.filePathPattern);
395
+ const targetKey = canonicalTargetKey(filePath);
396
+ if (seenTargets.has(targetKey))
397
+ continue;
398
+ seenTargets.add(targetKey);
399
+ if (existsSync(filePath))
400
+ existingMatches.push(target);
401
+ }
402
+ if (existingMatches.length > 0) {
403
+ const results = existingMatches
404
+ .map((target) => applyToTarget(target))
405
+ .filter((result) => Boolean(result));
406
+ if (results.length > 0)
407
+ return results;
408
+ }
409
+ if (runtimeCode && candidateTargets.length > 0) {
410
+ const created = applyToTarget(candidateTargets[0]);
411
+ return created ? [created] : [];
396
412
  }
397
- return results;
413
+ return [];
398
414
  }
399
415
  function injectTrustAttestationBlock(path, attestation) {
400
416
  if (!attestation.token || !attestation.payload)
@@ -1017,6 +1033,7 @@ export async function initAgent(args) {
1017
1033
  detail: "skipped",
1018
1034
  missingRequired: [],
1019
1035
  };
1036
+ let selectedClient = clientArg ?? null;
1020
1037
  if (registration.ok) {
1021
1038
  let provider = providerArg;
1022
1039
  let model = modelArg;
@@ -1026,6 +1043,7 @@ export async function initAgent(args) {
1026
1043
  model = model ?? await promptForValue("Model id (e.g. claude-sonnet-4-6): ", true);
1027
1044
  client = client ?? await promptForValue("Client (e.g. claude-code/cursor/codex-cli): ", true);
1028
1045
  }
1046
+ selectedClient = client ?? null;
1029
1047
  const missingRequired = [
1030
1048
  !provider ? "provider" : null,
1031
1049
  !model ? "model" : null,
@@ -1110,7 +1128,7 @@ export async function initAgent(args) {
1110
1128
  catch {
1111
1129
  // non-fatal
1112
1130
  }
1113
- let existingInstructionTargets = injectAgentConfigs(registry);
1131
+ let existingInstructionTargets = injectAgentConfigs(registry, selectedClient);
1114
1132
  if (existingInstructionTargets.length === 0) {
1115
1133
  existingInstructionTargets = injectGenericAgentConfig(registry);
1116
1134
  }
@@ -221,7 +221,7 @@ function choosePreferredRepositoryRef(candidates) {
221
221
  });
222
222
  return scored[0]?.rawRef ?? null;
223
223
  }
224
- function detectSourceRepository(dir) {
224
+ export function detectSourceRepository(dir) {
225
225
  let rawRef = null;
226
226
  let vcsType = "unknown";
227
227
  // Prefer git when present, and prefer hosted remotes over local-only URLs.
@@ -869,7 +869,7 @@ function structuralRelForSubPackage(sp, projectExternalKey) {
869
869
  return { type: "part_of", from: sp.externalKey, to: projectExternalKey };
870
870
  return null;
871
871
  }
872
- function scanProject(dir) {
872
+ export function scanProject(dir) {
873
873
  const names = new Set();
874
874
  let projectName = basename(dir);
875
875
  const subPackages = [];
@@ -995,7 +995,7 @@ function pickRelationshipType(toEntityTypeCode, toEntitySubtypeCode, _fromEntity
995
995
  return "depends_on";
996
996
  }
997
997
  }
998
- function normalizeToken(value) {
998
+ export function normalizeToken(value) {
999
999
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
1000
1000
  }
1001
1001
  function extractHost(input) {
@@ -1008,7 +1008,7 @@ function extractHost(input) {
1008
1008
  return null;
1009
1009
  }
1010
1010
  }
1011
- function scoreApplicationCandidate(app, projectName, repoUrl) {
1011
+ export function scoreApplicationCandidate(app, projectName, repoUrl) {
1012
1012
  const entityRef = app.entityRef ?? app.externalKey ?? null;
1013
1013
  if (!entityRef)
1014
1014
  return null;
@@ -0,0 +1,77 @@
1
+ import process from "process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { arch, hostname, platform, release, type as osType, homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { requireCredentials } from "../lib/credentials.js";
6
+ import { callMcpTool } from "../lib/mcp.js";
7
+ import { inferAccessibleApplicationRefs } from "../lib/application-context.js";
8
+ function parseFlag(args, flag) {
9
+ return args.includes(flag);
10
+ }
11
+ function parseOptionValue(args, option) {
12
+ const idx = args.indexOf(option);
13
+ if (idx === -1)
14
+ return null;
15
+ const value = args[idx + 1];
16
+ if (!value || value.startsWith("--"))
17
+ return null;
18
+ return value;
19
+ }
20
+ function parseToolText(result) {
21
+ const text = result.content?.[0]?.text ?? "{}";
22
+ return JSON.parse(text);
23
+ }
24
+ function loadIdentity() {
25
+ const identityPath = join(homedir(), ".nexarch", "identity.json");
26
+ if (!existsSync(identityPath))
27
+ return { agentKey: null };
28
+ try {
29
+ const data = JSON.parse(readFileSync(identityPath, "utf8"));
30
+ return { agentKey: data.agentKey ?? null };
31
+ }
32
+ catch {
33
+ return { agentKey: null };
34
+ }
35
+ }
36
+ export async function registerRuntime(args) {
37
+ const asJson = parseFlag(args, "--json");
38
+ const applicationRefArg = parseOptionValue(args, "--application-ref");
39
+ const client = parseOptionValue(args, "--client") ?? "nexarch-cli";
40
+ const version = parseOptionValue(args, "--version") ?? null;
41
+ const mode = process.env.CI ? "ci" : "interactive";
42
+ const creds = requireCredentials();
43
+ const identity = loadIdentity();
44
+ if (!identity.agentKey)
45
+ throw new Error("No agent key found. Run nexarch init-agent first.");
46
+ const applicationContext = await inferAccessibleApplicationRefs({
47
+ companyId: creds.companyId,
48
+ explicitApplicationRefs: applicationRefArg ? [applicationRefArg] : undefined,
49
+ });
50
+ const raw = await callMcpTool("nexarch_register_agent_runtime", {
51
+ agentRef: identity.agentKey,
52
+ runtime: {
53
+ client,
54
+ ...(version ? { version } : {}),
55
+ mode,
56
+ nodeVersion: process.version,
57
+ },
58
+ host: {
59
+ hostname: hostname(),
60
+ osPlatform: platform(),
61
+ osType: osType(),
62
+ osRelease: release(),
63
+ arch: arch(),
64
+ },
65
+ applicationContext: applicationContext.accessibleApplicationRefs.length > 0
66
+ ? { accessibleApplicationRefs: applicationContext.accessibleApplicationRefs }
67
+ : undefined,
68
+ companyId: creds.companyId,
69
+ }, { companyId: creds.companyId });
70
+ const result = parseToolText(raw);
71
+ if (asJson) {
72
+ process.stdout.write(`${JSON.stringify(result)}\n`);
73
+ return;
74
+ }
75
+ console.log(`Registered runtime for ${identity.agentKey}.`);
76
+ console.log(`Application context: ${applicationContext.detail}`);
77
+ }
@@ -67,9 +67,9 @@ export async function setup(args) {
67
67
  if (clients.length > 0) {
68
68
  const names = clients.map((c) => c.name);
69
69
  const listed = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
70
- console.log(`\nSetup complete. Open a new ${listed} session and ask your agent to "register with Nexarch".`);
70
+ console.log(`\nSetup complete. Open a new ${listed} session your Nexarch registration is already in place. If you want the agent to look for pending work, ask it to check in.`);
71
71
  }
72
72
  else {
73
- console.log('\nSetup complete. Once you have configured an MCP client (see above), open a new session and ask your agent to "register with Nexarch".');
73
+ console.log("\nSetup complete. Once you have configured an MCP client (see above), open a new session your Nexarch registration is already in place. If you want the agent to look for pending work, ask it to check in.");
74
74
  }
75
75
  }
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ import { policyAuditResults } from "./commands/policy-audit-results.js";
25
25
  import { appliedPolicies } from "./commands/applied-policies.js";
26
26
  import { governanceSummary } from "./commands/governance-summary.js";
27
27
  import { proposalsStart } from "./commands/proposals-start.js";
28
+ import { registerRuntime } from "./commands/register-runtime.js";
28
29
  const [, , command, ...args] = process.argv;
29
30
  const commands = {
30
31
  login,
@@ -43,6 +44,7 @@ const commands = {
43
44
  "resolve-names": resolveNames,
44
45
  "list-entities": listEntities,
45
46
  "list-relationships": listRelationships,
47
+ "register-runtime": registerRuntime,
46
48
  "check-in": checkIn,
47
49
  "command-done": commandDone,
48
50
  "command-fail": commandFail,
@@ -180,12 +182,20 @@ Usage:
180
182
  --to <toExternalKey>
181
183
  --limit <1-500>
182
184
  --json
185
+ nexarch register-runtime
186
+ Register or refresh runtime + optional application context
187
+ without performing check-in.
188
+ Options: --application-ref <entityRef>
189
+ --client <name>
190
+ --version <semver>
191
+ --json
183
192
  nexarch check-in Preview pending application-target commands (no auto-claim)
184
193
  and report draft/proposed applications needing review so the
185
194
  agent can prompt the user to explore and instantiate them.
186
195
  Use command-claim to explicitly claim a specific command.
187
196
  Scope is resolved server-side from active company context.
188
197
  Options: --agent-key <key> override stored agent key
198
+ --application-ref <entityRef> narrow preview scope
189
199
  --json JSON output includes draftApplications[] and proposedApplications[]
190
200
  nexarch proposals start
191
201
  Start a new application workspace from a proposed NexArch app.
@@ -205,6 +215,7 @@ Usage:
205
215
  Explicitly claim a pending command by ID.
206
216
  Options: --id <commandId> (required)
207
217
  --agent-key <key> override stored agent key
218
+ --application-ref <entityRef> required for application-target commands
208
219
  --json
209
220
  nexarch command-done
210
221
  Mark a claimed command as completed.
@@ -0,0 +1,51 @@
1
+ import { resolve as resolvePath } from "node:path";
2
+ import { callMcpTool } from "./mcp.js";
3
+ import { detectSourceRepository, scanProject, scoreApplicationCandidate } from "../commands/init-project.js";
4
+ function parseToolText(result) {
5
+ const text = result.content?.[0]?.text ?? "{}";
6
+ return JSON.parse(text);
7
+ }
8
+ export async function inferAccessibleApplicationRefs(params) {
9
+ const explicit = Array.from(new Set((params.explicitApplicationRefs ?? []).map((value) => value.trim()).filter(Boolean)));
10
+ if (explicit.length > 0) {
11
+ return {
12
+ accessibleApplicationRefs: explicit,
13
+ source: "explicit",
14
+ detail: `Using explicit application context (${explicit.join(", ")}).`,
15
+ repoUrl: detectSourceRepository(params.dir ?? process.cwd())?.url ?? null,
16
+ projectNames: [],
17
+ candidates: [],
18
+ };
19
+ }
20
+ const dir = resolvePath(params.dir ?? process.cwd());
21
+ const repoUrl = detectSourceRepository(dir)?.url ?? null;
22
+ const scan = scanProject(dir);
23
+ const projectNames = Array.from(new Set([scan.projectName, ...scan.subPackages.map((sp) => sp.name)])).filter(Boolean);
24
+ const appsRaw = await callMcpTool("nexarch_list_entities", { entityTypeCode: "application", status: "active", limit: 500, companyId: params.companyId }, { companyId: params.companyId });
25
+ const appsData = parseToolText(appsRaw);
26
+ const apps = (appsData.entities ?? []).filter((entity) => (entity.entityRef ?? entity.externalKey));
27
+ const candidateMap = new Map();
28
+ for (const projectName of projectNames) {
29
+ for (const app of apps) {
30
+ const match = scoreApplicationCandidate(app, projectName, repoUrl);
31
+ if (!match)
32
+ continue;
33
+ const existing = candidateMap.get(match.entityRef);
34
+ if (!existing || match.score > existing.score) {
35
+ candidateMap.set(match.entityRef, match);
36
+ }
37
+ }
38
+ }
39
+ const candidates = Array.from(candidateMap.values()).sort((a, b) => b.score - a.score);
40
+ const accessibleApplicationRefs = candidates.filter((candidate) => candidate.score >= 0.85).map((candidate) => candidate.entityRef);
41
+ return {
42
+ accessibleApplicationRefs,
43
+ source: accessibleApplicationRefs.length > 0 ? "inferred" : "none",
44
+ detail: accessibleApplicationRefs.length > 0
45
+ ? `Inferred application context from current folder (${accessibleApplicationRefs.join(", ")}).`
46
+ : "No high-confidence application context inferred from current folder.",
47
+ repoUrl,
48
+ projectNames,
49
+ candidates,
50
+ };
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.12.0",
3
+ "version": "0.12.3",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",