nexarch 0.13.8 → 0.15.0

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.
package/README.md CHANGED
@@ -1,63 +1,69 @@
1
- # nexarch
2
-
3
- Your architecture workspace for AI delivery.
4
-
5
- ## Quick start
6
-
7
- ```sh
8
- npx nexarch login
9
- npx nexarch setup
10
- ```
11
-
12
- > `setup`, `mcp-config`, and `init-agent` are registry-driven and fail closed.
13
- > If the published integration registry is unavailable/invalid, these commands stop until service is restored.
14
-
15
- ## Commands
16
-
17
- | Command | Description |
18
- |---|---|
19
- | `nexarch login [--company <id>]` | Authenticate via browser and store company-scoped credentials; prompts for company when multiple memberships exist |
20
- | `nexarch logout` | Remove local credentials |
21
- | `nexarch status` | Check connection, registry health/version, and architecture summary for the company selected at login |
22
- | `nexarch setup` | Auto-configure detected MCP clients |
23
- | `nexarch mcp-config [client]` | Print config block for manual setup (supported client codes are registry-managed) |
24
- | `nexarch mcp-proxy` | stdio→HTTP bridge used internally by MCP clients |
25
- | `nexarch init-agent [--json] [--strict] [--agent-id <id>] [--redact-hostname]` | Run onboarding handshake and mandatory `agent` entity registration in graph |
26
- | `nexarch init-agent --bind-to-external-key <key> [--bind-relationship-type <code>]` | Optionally bind the agent node to an existing graph external key |
27
- | `nexarch init-agent` (default behavior) | Also upserts technology component entities (host, OS, Node.js runtime) and links them to the agent |
28
- | `nexarch agent identify ...` | Submit provider/model/client identity metadata to complete agent profile enrichment |
29
-
30
- ## Bootstrap flow after `init-agent`
31
-
32
- `nexarch init-agent` writes a bootstrap file at:
33
-
34
- ```sh
35
- ~/.nexarch/agent-bootstrap.md
36
- ```
37
-
38
- That file contains a pre-filled `npx nexarch agent identify ...` command template.
39
- The current workflow is:
40
-
41
- 1. run `npx nexarch init-agent`
42
- 2. open `~/.nexarch/agent-bootstrap.md`
43
- 3. copy the generated `npx nexarch agent identify ...` template
44
- 4. fill in the real provider/model/client details
45
- 5. run the completed command
46
-
47
- Do not assume an `npx nexarch agent prompt` command exists unless/until it is explicitly implemented and documented.
48
-
49
- ## Company context behavior
50
-
51
- - Company is selected during `nexarch login`.
52
- - If your user has one active company, login auto-selects it.
53
- - If your user has multiple active companies, login requires explicit selection.
54
- - To switch company later, run `nexarch login` again and pick another company.
55
-
56
- ## Requirements
57
-
58
- - Node.js 18 or later
59
- - A Nexarch workspace account
60
-
61
- ## Documentation
62
-
63
- [nexarch.ai/docs/cli-reference](https://nexarch.ai/docs/cli-reference)
1
+ # nexarch
2
+
3
+ Your architecture workspace for AI delivery.
4
+
5
+ ## Quick start
6
+
7
+ ```sh
8
+ npx nexarch login
9
+ npx nexarch setup
10
+ ```
11
+
12
+ For browserless agent enrollment, use `npx nexarch enroll --code <code> --client <client>`.
13
+ Known clients receive their MCP connection configuration and Nexarch skills as part
14
+ of the same enrollment.
15
+
16
+ > `setup`, `mcp-config`, and `init-agent` are registry-driven and fail closed.
17
+ > If the published integration registry is unavailable/invalid, these commands stop until service is restored.
18
+
19
+ ## Commands
20
+
21
+ | Command | Description |
22
+ |---|---|
23
+ | `nexarch login [--company <id>]` | Authenticate via browser and store company-scoped credentials; prompts for company when multiple memberships exist |
24
+ | `nexarch logout` | Remove local credentials |
25
+ | `nexarch status` | Check connection, registry health/version, and architecture summary for the company selected at login |
26
+ | `nexarch setup` | Auto-configure detected MCP clients, write instructions, and refresh all managed skills |
27
+ | `nexarch skills update` | Refresh all Nexarch-managed skills for installed agent runtimes |
28
+ | `nexarch enroll --code <code> --client <client>` | Redeem a one-time enrollment, configure the known client, and install its Nexarch skills |
29
+ | `nexarch mcp-config [client]` | Print config block for manual setup (supported client codes are registry-managed) |
30
+ | `nexarch mcp-proxy` | stdio→HTTP bridge used internally by MCP clients |
31
+ | `nexarch init-agent [--json] [--strict] [--agent-id <id>] [--redact-hostname]` | Run onboarding handshake and mandatory `agent` entity registration in graph |
32
+ | `nexarch init-agent --bind-to-external-key <key> [--bind-relationship-type <code>]` | Optionally bind the agent node to an existing graph external key |
33
+ | `nexarch init-agent` (default behavior) | Also upserts technology component entities (host, OS, Node.js runtime) and links them to the agent |
34
+ | `nexarch agent identify ...` | Submit provider/model/client identity metadata to complete agent profile enrichment |
35
+
36
+ ## Bootstrap flow after `init-agent`
37
+
38
+ `nexarch init-agent` writes a bootstrap file at:
39
+
40
+ ```sh
41
+ ~/.nexarch/agent-bootstrap.md
42
+ ```
43
+
44
+ That file contains a pre-filled `npx nexarch agent identify ...` command template.
45
+ The current workflow is:
46
+
47
+ 1. run `npx nexarch init-agent`
48
+ 2. open `~/.nexarch/agent-bootstrap.md`
49
+ 3. copy the generated `npx nexarch agent identify ...` template
50
+ 4. fill in the real provider/model/client details
51
+ 5. run the completed command
52
+
53
+ Do not assume an `npx nexarch agent prompt` command exists unless/until it is explicitly implemented and documented.
54
+
55
+ ## Company context behavior
56
+
57
+ - Company is selected during `nexarch login`.
58
+ - If your user has one active company, login auto-selects it.
59
+ - If your user has multiple active companies, login requires explicit selection.
60
+ - To switch company later, run `nexarch login` again and pick another company.
61
+
62
+ ## Requirements
63
+
64
+ - Node.js 18 or later
65
+ - A Nexarch workspace account
66
+
67
+ ## Documentation
68
+
69
+ [nexarch.ai/docs/cli-reference](https://nexarch.ai/docs/cli-reference)
@@ -3,6 +3,8 @@ import { mkdirSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  import { expandHomePath, supportsAutoWrite, writeClientConfig } from "../lib/client-config-writers.js";
6
+ import { fetchAgentRegistryOrThrow } from "../lib/agent-registry.js";
7
+ import { installSkillsForRuntime } from "../lib/skills.js";
6
8
  // Matches www.nexarch.ai — the default production host for the web app that
7
9
  // owns /api/agent-enrollments/exchange. Not the same service as
8
10
  // mcp.nexarch.ai (the MCP gateway); --host exists because staging and
@@ -36,6 +38,18 @@ function parseOptionValue(args, option) {
36
38
  function redactedManifest(manifest) {
37
39
  return { ...manifest, credential: { ...manifest.credential, token: "(redacted — see saved file, or pass --print-token)" } };
38
40
  }
41
+ function skillRuntimeForClient(clientCode) {
42
+ switch (clientCode) {
43
+ case "claude-code":
44
+ return "claude-code";
45
+ case "codex-cli":
46
+ return "codex-cli";
47
+ case "hermes-agent":
48
+ return "hermes-agent";
49
+ default:
50
+ return null;
51
+ }
52
+ }
39
53
  /**
40
54
  * Bootstraps a headless (browserless) MCP agent by redeeming a one-time
41
55
  * enrollment code for a long-lived credential — the CLI equivalent of the
@@ -95,9 +109,15 @@ export async function enroll(args) {
95
109
  });
96
110
  wroteClientConfig = true;
97
111
  }
112
+ let skills = [];
113
+ const skillRuntime = skillRuntimeForClient(clientCode);
114
+ if (skillRuntime) {
115
+ const registry = await fetchAgentRegistryOrThrow();
116
+ skills = installSkillsForRuntime(registry, skillRuntime);
117
+ }
98
118
  if (asJson) {
99
119
  const output = printToken ? manifest : redactedManifest(manifest);
100
- process.stdout.write(`${JSON.stringify({ ...output, wroteClientConfig }, null, 2)}\n`);
120
+ process.stdout.write(`${JSON.stringify({ ...output, wroteClientConfig, skills }, null, 2)}\n`);
101
121
  return;
102
122
  }
103
123
  console.log(`✓ Enrolled agent ${manifest.agent.ref} in workspace "${manifest.workspace.name}"`);
@@ -114,6 +134,13 @@ export async function enroll(args) {
114
134
  else {
115
135
  console.log(`\nNo known auto-write target for client "${clientCode}" — connect it manually:`);
116
136
  }
137
+ if (skills.length > 0) {
138
+ console.log(`\n${clientCode} skills:`);
139
+ for (const skill of skills) {
140
+ const verb = skill.status === "installed" ? "installed" : skill.status === "updated" ? "updated" : "already current";
141
+ console.log(` ${verb.padEnd(15)} ${skill.dirName}`);
142
+ }
143
+ }
117
144
  const shownToken = printToken ? manifest.credential.token : `<see ${destination}>`;
118
145
  if (manifest.connectInstructionsTemplate) {
119
146
  console.log(renderInstructions(manifest.connectInstructionsTemplate, manifest, shownToken, mcpEndpoint));
@@ -24,15 +24,15 @@ async function promptCapabilityProfile() {
24
24
  return selectFromList("Select a capability profile for this credential:", PROFILE_CHOICES, "worker");
25
25
  }
26
26
  function printLoginBanner() {
27
- const logo = String.raw `
28
- ###### ######
29
- ####### ######
30
- ######### ###### ##
31
- ################## #####
32
- ##### ########## ######
33
- ###### ####### ######
34
- ##### #### #############
35
- ###### ## ###############
27
+ const logo = String.raw `
28
+ ###### ######
29
+ ####### ######
30
+ ######### ###### ##
31
+ ################## #####
32
+ ##### ########## ######
33
+ ###### ####### ######
34
+ ##### #### #############
35
+ ###### ## ###############
36
36
  `;
37
37
  console.log(logo);
38
38
  console.log("NEXARCH CLI login\n");
@@ -95,10 +95,10 @@ function waitForCallback(port, expectedState) {
95
95
  const companyName = url.searchParams.get("companyName") ?? undefined;
96
96
  const companyCode = url.searchParams.get("companyCode") ?? undefined;
97
97
  const error = url.searchParams.get("error");
98
- const html = (message) => `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Nexarch CLI</title>
99
- <style>body{font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f8fafc}
100
- .card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:2rem;max-width:400px;text-align:center}
101
- h1{font-size:1.25rem;margin:0 0 .5rem;color:#0f172a}p{color:#64748b;font-size:.9rem;margin:.5rem 0}</style></head>
98
+ const html = (message) => `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Nexarch CLI</title>
99
+ <style>body{font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f8fafc}
100
+ .card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:2rem;max-width:400px;text-align:center}
101
+ h1{font-size:1.25rem;margin:0 0 .5rem;color:#0f172a}p{color:#64748b;font-size:.9rem;margin:.5rem 0}</style></head>
102
102
  <body><div class="card"><h1>Nexarch CLI</h1><p>${message}</p></div></body></html>`;
103
103
  if (state !== expectedState) {
104
104
  res.writeHead(400, { "Content-Type": "text/html" });
@@ -247,6 +247,7 @@ export async function login(args) {
247
247
  companyCode: companyCode ?? undefined,
248
248
  companyId,
249
249
  expiresAt: expiresAt.toISOString(),
250
+ source: "interactive",
250
251
  });
251
252
  try {
252
253
  const identityDir = join(homedir(), ".nexarch");
@@ -3,7 +3,7 @@ import { requireCredentials } from "../lib/credentials.js";
3
3
  import { detectClientsFromRegistry, writeClientConfig, nexarchServerBlockFromRegistry } from "../lib/clients.js";
4
4
  import { fetchAgentRegistryOrThrow } from "../lib/agent-registry.js";
5
5
  import { initAgent } from "./init-agent.js";
6
- import { installSkillsForRuntime } from "../lib/skills.js";
6
+ import { installSkillsForRuntime, installedSkillRuntimes } from "../lib/skills.js";
7
7
  import { login } from "./login.js";
8
8
  function argValue(args, flag) {
9
9
  const idx = args.indexOf(flag);
@@ -168,22 +168,19 @@ export async function setup(args) {
168
168
  initAgentArgs.push("--instruction-runtime-targets", instructionRuntimeTargets.join(","));
169
169
  }
170
170
  await initAgent(initAgentArgs);
171
- // Claude Code and Codex CLI both support Agent Skills: a trigger
172
- // description that sits permanently in the agent's context and loads a
173
- // playbook when a matching moment comes up. MCP alone makes the graph
174
- // available; skills are what make an agent actually consult it — for
175
- // building, diagramming, working its command queue, and governance
176
- // review. Both runtimes use the same SKILL.md format, just a different
177
- // root directory (see RUNTIME_SKILLS_ROOT in lib/skills.ts), so the same
178
- // content installs to both unmodified. Installed under the same consent
179
- // as instruction writes — setup already opts into those above.
180
- const skillRuntimes = [
181
- { clientCode: "claude-code", runtime: "claude-code", label: "Claude Code" },
182
- { clientCode: "codex-cli", runtime: "codex-cli", label: "Codex CLI" },
183
- ];
184
- for (const { clientCode, runtime, label } of skillRuntimes) {
185
- if (!clients.some((client) => client.code === clientCode))
186
- continue;
171
+ // Refresh every installed skill-capable runtime, not only MCP clients
172
+ // detected in this setup pass. This keeps `setup` useful for existing
173
+ // installations and gives it the same managed-skill behavior as
174
+ // `nexarch skills update`.
175
+ const skillRuntimes = new Set(installedSkillRuntimes());
176
+ for (const client of clients) {
177
+ if (client.code === "claude-code")
178
+ skillRuntimes.add("claude-code");
179
+ if (client.code === "codex-cli")
180
+ skillRuntimes.add("codex-cli");
181
+ }
182
+ for (const runtime of skillRuntimes) {
183
+ const label = runtime === "claude-code" ? "Claude Code" : runtime === "codex-cli" ? "Codex CLI" : "Hermes Agent";
187
184
  try {
188
185
  const skills = installSkillsForRuntime(registry, runtime);
189
186
  const anyChanged = skills.some((s) => s.status !== "already_current");
@@ -0,0 +1,51 @@
1
+ import { requireCredentials } from "../lib/credentials.js";
2
+ import { fetchAgentRegistryOrThrow } from "../lib/agent-registry.js";
3
+ import { installSkillsForRuntime, installedSkillRuntimes } from "../lib/skills.js";
4
+ const RUNTIME_LABELS = {
5
+ "claude-code": "Claude Code",
6
+ "codex-cli": "Codex CLI",
7
+ "hermes-agent": "Hermes Agent",
8
+ };
9
+ function hasFlag(args, flag) {
10
+ return args.includes(flag);
11
+ }
12
+ export async function skillsUpdate(args) {
13
+ if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
14
+ console.log(`
15
+ Usage:
16
+ nexarch skills update [--json]
17
+
18
+ Refresh all Nexarch-managed skills for installed agent runtimes.
19
+ `);
20
+ return;
21
+ }
22
+ const asJson = hasFlag(args, "--json");
23
+ requireCredentials();
24
+ const registry = await fetchAgentRegistryOrThrow();
25
+ const runtimes = installedSkillRuntimes();
26
+ const results = [];
27
+ for (const runtime of runtimes) {
28
+ results.push({
29
+ runtime,
30
+ label: RUNTIME_LABELS[runtime],
31
+ skills: installSkillsForRuntime(registry, runtime),
32
+ });
33
+ }
34
+ if (asJson) {
35
+ process.stdout.write(`${JSON.stringify({ registryVersion: registry.release.version, runtimes: results }, null, 2)}\n`);
36
+ return;
37
+ }
38
+ console.log(`Using integration registry release v${registry.release.version}.`);
39
+ if (results.length === 0) {
40
+ console.log("No installed Nexarch skill runtimes found.");
41
+ return;
42
+ }
43
+ for (const result of results) {
44
+ console.log(`\n${result.label} skills:`);
45
+ for (const skill of result.skills) {
46
+ const verb = skill.status === "installed" ? "installed" : skill.status === "updated" ? "updated" : "already current";
47
+ console.log(` ${verb.padEnd(15)} ${skill.dirName}`);
48
+ }
49
+ }
50
+ console.log("\nNexarch-managed skills refreshed.");
51
+ }
@@ -5,8 +5,8 @@ import { callMcpTool } from "../lib/mcp.js";
5
5
  export async function status(_args) {
6
6
  const creds = requireCredentials();
7
7
  const selectedCompanyId = creds.companyId;
8
- const expiresAt = new Date(creds.expiresAt);
9
- const daysLeft = Math.ceil((expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
8
+ const expiresAt = creds.expiresAt ? new Date(creds.expiresAt) : null;
9
+ const daysLeft = expiresAt ? Math.ceil((expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24)) : null;
10
10
  const lease = loadCheckInLease();
11
11
  const leaseState = lease && lease.companyId === selectedCompanyId ? getCheckInLeaseState(lease) : { status: "missing", expiresInSeconds: null };
12
12
  process.stdout.write("Connecting to mcp.nexarch.ai… ");
@@ -28,6 +28,15 @@ export async function status(_args) {
28
28
  if (creds.email) {
29
29
  console.log(` Signed in as: ${creds.email}`);
30
30
  }
31
+ else if (creds.source === "agent") {
32
+ console.log(" Authentication: Hermes agent enrollment");
33
+ if (creds.agentRef) {
34
+ console.log(` Agent: ${creds.agentRef}`);
35
+ }
36
+ }
37
+ else if (creds.source === "service") {
38
+ console.log(" Authentication: service credential");
39
+ }
31
40
  if (creds.company) {
32
41
  console.log(` Workspace: ${creds.company}`);
33
42
  }
@@ -53,7 +62,7 @@ export async function status(_args) {
53
62
  else {
54
63
  console.log(` Latest snapshot: none published yet`);
55
64
  }
56
- console.log(`\n Token expires: ${expiresAt.toLocaleDateString()} (${daysLeft} days)`);
65
+ console.log(`\n Token expires: ${expiresAt ? `${expiresAt.toLocaleDateString()} (${daysLeft} days)` : "not reported"}`);
57
66
  if (leaseState.status === "valid" && lease) {
58
67
  console.log(` Check-in lease: ${new Date(lease.leaseExpiresAt).toLocaleString()} (${Math.ceil((leaseState.expiresInSeconds ?? 0) / 3600)}h left)`);
59
68
  }
@@ -63,7 +72,7 @@ export async function status(_args) {
63
72
  else {
64
73
  console.log(" Check-in lease: none stored");
65
74
  }
66
- if (daysLeft <= 14) {
75
+ if (daysLeft !== null && daysLeft <= 14) {
67
76
  console.log(`\n ⚠ Your token expires soon. Run \`nexarch login\` to renew.`);
68
77
  }
69
78
  if (leaseState.status !== "valid") {
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { enroll } from "./commands/enroll.js";
4
4
  import { logout } from "./commands/logout.js";
5
5
  import { status } from "./commands/status.js";
6
6
  import { setup } from "./commands/setup.js";
7
+ import { skillsUpdate } from "./commands/skills-update.js";
7
8
  import { mcpConfig } from "./commands/mcp-config.js";
8
9
  import { mcpProxy } from "./commands/mcp-proxy.js";
9
10
  import { initAgent } from "./commands/init-agent.js";
@@ -82,6 +83,13 @@ async function main() {
82
83
  return;
83
84
  }
84
85
  }
86
+ if (command === "skills") {
87
+ const [subcommand, ...subArgs] = args;
88
+ if (subcommand === "update") {
89
+ await skillsUpdate(subArgs);
90
+ return;
91
+ }
92
+ }
85
93
  if (command === "proposals") {
86
94
  const [subcommand, ...subArgs] = args;
87
95
  if (subcommand === "start") {
@@ -119,8 +127,8 @@ Usage:
119
127
  credential to ~/.nexarch/agent-credential.json (mode 600)
120
128
  rather than printing it. For a client with a known config
121
129
  format (e.g. hermes-agent), also writes the connection
122
- directly into that client's own env/config files; for
123
- anything else, prints instructions instead.
130
+ and installs its Nexarch skills; for anything else,
131
+ prints instructions instead.
124
132
  Options: --code <code> (required, starts with nxe_)
125
133
  --client <code> (required, e.g. hermes-agent)
126
134
  --client-version <v>
@@ -141,6 +149,8 @@ Usage:
141
149
  Options: --company <id|code> target a workspace directly
142
150
  --yes accept the stored workspace without asking
143
151
  --no-browser log in by pasting a one-time code (see login)
152
+ nexarch skills update
153
+ Refresh all Nexarch-managed skills for installed agent runtimes
144
154
  nexarch mcp-config Print MCP server config block for manual setup
145
155
  Client list is registry-managed (see 'nexarch mcp-config --client <code>')
146
156
  nexarch mcp-proxy Run as stdio MCP proxy (used by MCP clients)
@@ -4,6 +4,9 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from "fs";
4
4
  function credentialsPath() {
5
5
  return join(homedir(), ".nexarch", "credentials.json");
6
6
  }
7
+ function agentCredentialPath() {
8
+ return join(homedir(), ".nexarch", "agent-credential.json");
9
+ }
7
10
  /**
8
11
  * Credentials supplied by the environment, for CI.
9
12
  *
@@ -27,24 +30,49 @@ function credentialsFromEnvironment() {
27
30
  company: "",
28
31
  companyId: process.env.NEXARCH_COMPANY_ID?.trim() ?? "",
29
32
  expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
33
+ source: "service",
30
34
  };
31
35
  }
36
+ function credentialsFromAgentManifest() {
37
+ const path = agentCredentialPath();
38
+ if (!existsSync(path))
39
+ return null;
40
+ try {
41
+ const manifest = JSON.parse(readFileSync(path, "utf-8"));
42
+ const token = typeof manifest.credential?.token === "string" ? manifest.credential.token.trim() : "";
43
+ const companyId = typeof manifest.workspace?.id === "string" ? manifest.workspace.id : "";
44
+ if (!token || !companyId)
45
+ return null;
46
+ return {
47
+ token,
48
+ email: "",
49
+ company: typeof manifest.workspace?.name === "string" ? manifest.workspace.name : "",
50
+ companyId,
51
+ expiresAt: typeof manifest.credential?.expiresAt === "string" ? manifest.credential.expiresAt : null,
52
+ source: "agent",
53
+ agentRef: typeof manifest.agent?.ref === "string" ? manifest.agent.ref : undefined,
54
+ };
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
32
60
  export function loadCredentials() {
33
61
  const fromEnv = credentialsFromEnvironment();
34
62
  if (fromEnv)
35
63
  return fromEnv;
36
64
  const path = credentialsPath();
37
65
  if (!existsSync(path))
38
- return null;
66
+ return credentialsFromAgentManifest();
39
67
  try {
40
68
  const raw = readFileSync(path, "utf-8");
41
69
  const creds = JSON.parse(raw);
42
70
  if (!creds.token)
43
- return null;
44
- return creds;
71
+ return credentialsFromAgentManifest();
72
+ return { ...creds, source: "interactive" };
45
73
  }
46
74
  catch {
47
- return null;
75
+ return credentialsFromAgentManifest();
48
76
  }
49
77
  }
50
78
  export function saveCredentials(creds) {
@@ -63,13 +91,13 @@ export function removeCredentials() {
63
91
  export function requireCredentials() {
64
92
  const creds = loadCredentials();
65
93
  if (!creds) {
66
- throw new Error("Not logged in. Run `nexarch login` to authenticate, or set NEXARCH_TOKEN for unattended use.");
94
+ throw new Error("No Nexarch credential found. Run `nexarch login` for interactive CLI access, or `nexarch enroll` for headless agent access.");
67
95
  }
68
96
  // Environment-supplied service credentials are validated by the gateway, not
69
97
  // by this process; only the stored interactive session carries a local expiry.
70
98
  if (process.env.NEXARCH_TOKEN?.trim())
71
99
  return creds;
72
- if (new Date(creds.expiresAt) < new Date()) {
100
+ if (creds.expiresAt && new Date(creds.expiresAt) < new Date()) {
73
101
  throw new Error("Your session has expired. Run `nexarch login` to re-authenticate.");
74
102
  }
75
103
  return creds;
@@ -408,7 +408,16 @@ export const SKILLS = [
408
408
  const RUNTIME_SKILLS_ROOT = {
409
409
  "claude-code": [".claude", "skills"],
410
410
  "codex-cli": [".agents", "skills"],
411
+ "hermes-agent": [".hermes", "skills"],
411
412
  };
413
+ export function skillRootForRuntime(runtime, homeDir = homedir()) {
414
+ return runtime === "hermes-agent" && process.env.HERMES_HOME
415
+ ? join(process.env.HERMES_HOME, "skills")
416
+ : join(homeDir, ...RUNTIME_SKILLS_ROOT[runtime]);
417
+ }
418
+ export function installedSkillRuntimes(homeDir = homedir()) {
419
+ return Object.keys(RUNTIME_SKILLS_ROOT).filter((runtime) => existsSync(skillRootForRuntime(runtime, homeDir)));
420
+ }
412
421
  function installSkill(spec, registry, skillsRoot) {
413
422
  const template = registry.instructionTemplates.find((t) => t.code === spec.templateCode);
414
423
  const body = template ? template.body.trim() + "\n" : spec.fallbackBody;
@@ -428,7 +437,6 @@ function installSkill(spec, registry, skillsRoot) {
428
437
  return { dirName: spec.dirName, path: skillPath, status: "installed", source };
429
438
  }
430
439
  export function installSkillsForRuntime(registry, runtime, options = {}) {
431
- const homeDir = options.homeDir ?? homedir();
432
- const skillsRoot = join(homeDir, ...RUNTIME_SKILLS_ROOT[runtime]);
440
+ const skillsRoot = skillRootForRuntime(runtime, options.homeDir ?? homedir());
433
441
  return SKILLS.map((spec) => installSkill(spec, registry, skillsRoot));
434
442
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.13.8",
3
+ "version": "0.15.0",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",
@@ -26,7 +26,7 @@
26
26
  "prepublishOnly": "tsc",
27
27
  "dev": "tsx src/index.ts",
28
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"
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 && tsx scripts/test-credentials-resolution.ts && tsx scripts/test-hermes-skills.ts"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^22",