nexarch 0.1.13 → 0.1.16

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.
@@ -48,7 +48,7 @@ export async function agentIdentify(args) {
48
48
  await mcpInitialize({ companyId: creds.companyId });
49
49
  const tools = await mcpListTools({ companyId: creds.companyId });
50
50
  const toolNames = new Set(tools.map((t) => t.name));
51
- const required = ["nexarch_upsert_entities"];
51
+ const required = ["nexarch_upsert_entities", "nexarch_upsert_relationships"];
52
52
  const missing = required.filter((name) => !toolNames.has(name));
53
53
  if (missing.length) {
54
54
  throw new Error(`missing required MCP tools: ${missing.join(", ")}`);
@@ -112,6 +112,93 @@ export async function agentIdentify(args) {
112
112
  const upsert = parseToolText(upsertRaw);
113
113
  const failed = Number(upsert.summary?.failed ?? 0) > 0;
114
114
  const firstResult = upsert.results?.[0];
115
+ // Upsert provider, model, and client as first-class graph entities
116
+ const providerExternalKey = `org:provider:${provider.toLowerCase().replace(/[^a-z0-9]+/g, "_")}`;
117
+ const modelExternalKey = `model:${model.toLowerCase().replace(/[^a-z0-9]+/g, "_")}`;
118
+ const clientExternalKey = `tool:client:${client.toLowerCase().replace(/[^a-z0-9]+/g, "_")}`;
119
+ const agentExternalKey = `agent:${agentId}`;
120
+ await callMcpTool("nexarch_upsert_entities", {
121
+ entities: [
122
+ {
123
+ externalKey: providerExternalKey,
124
+ entityTypeCode: "organisation",
125
+ entitySubtypeCode: "org_partner",
126
+ name: provider,
127
+ description: `AI provider: ${provider}`,
128
+ confidence: 1,
129
+ },
130
+ {
131
+ externalKey: modelExternalKey,
132
+ entityTypeCode: "model",
133
+ entitySubtypeCode: "model_llm",
134
+ name: model,
135
+ description: `Large language model: ${model}`,
136
+ confidence: 1,
137
+ },
138
+ {
139
+ externalKey: clientExternalKey,
140
+ entityTypeCode: "tool",
141
+ name: client,
142
+ description: `AI agent client: ${client}`,
143
+ confidence: 1,
144
+ },
145
+ ],
146
+ agentContext: {
147
+ agentId,
148
+ agentRunId: `agent-identify-entities-${Date.now()}`,
149
+ repoRef: "nexarch-cli/agent-identify",
150
+ observedAt: nowIso,
151
+ source: "nexarch-cli",
152
+ model,
153
+ provider,
154
+ },
155
+ policyContext: policyBundleHash
156
+ ? {
157
+ policyBundleHash,
158
+ alignmentSummary: { score: 1, violations: [], waivers: [] },
159
+ }
160
+ : undefined,
161
+ dryRun: false,
162
+ }, { companyId: creds.companyId });
163
+ // Wire relationships: uses_model, uses, accountable_for
164
+ await callMcpTool("nexarch_upsert_relationships", {
165
+ relationships: [
166
+ {
167
+ relationshipTypeCode: "uses_model",
168
+ fromEntityExternalKey: agentExternalKey,
169
+ toEntityExternalKey: modelExternalKey,
170
+ confidence: 1,
171
+ },
172
+ {
173
+ relationshipTypeCode: "uses",
174
+ fromEntityExternalKey: agentExternalKey,
175
+ toEntityExternalKey: clientExternalKey,
176
+ confidence: 1,
177
+ },
178
+ {
179
+ relationshipTypeCode: "accountable_for",
180
+ fromEntityExternalKey: providerExternalKey,
181
+ toEntityExternalKey: agentExternalKey,
182
+ confidence: 1,
183
+ },
184
+ ],
185
+ agentContext: {
186
+ agentId,
187
+ agentRunId: `agent-identify-rels-${Date.now()}`,
188
+ repoRef: "nexarch-cli/agent-identify",
189
+ observedAt: nowIso,
190
+ source: "nexarch-cli",
191
+ model,
192
+ provider,
193
+ },
194
+ policyContext: policyBundleHash
195
+ ? {
196
+ policyBundleHash,
197
+ alignmentSummary: { score: 1, violations: [], waivers: [] },
198
+ }
199
+ : undefined,
200
+ dryRun: false,
201
+ }, { companyId: creds.companyId });
115
202
  const output = {
116
203
  ok: !failed,
117
204
  detail: failed ? `failed (${upsert.errors?.[0]?.error ?? "unknown"})` : `agent ${firstResult?.action ?? "updated"}`,
@@ -4,7 +4,7 @@ import { join } from "path";
4
4
  import process from "process";
5
5
  import { requireCredentials } from "../lib/credentials.js";
6
6
  import { callMcpTool, mcpInitialize, mcpListTools } from "../lib/mcp.js";
7
- const CLI_VERSION = "0.1.13";
7
+ const CLI_VERSION = "0.1.16";
8
8
  const AGENT_ENTITY_TYPE = "agent";
9
9
  const TECH_COMPONENT_ENTITY_TYPE = "technology_component";
10
10
  function parseFlag(args, flag) {
@@ -0,0 +1,339 @@
1
+ import process from "process";
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { basename, join, resolve as resolvePath } from "node:path";
4
+ import { requireCredentials } from "../lib/credentials.js";
5
+ import { callMcpTool } from "../lib/mcp.js";
6
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
7
+ function parseFlag(args, flag) {
8
+ return args.includes(flag);
9
+ }
10
+ function parseOptionValue(args, option) {
11
+ const idx = args.indexOf(option);
12
+ if (idx === -1)
13
+ return null;
14
+ const value = args[idx + 1];
15
+ if (!value || value.startsWith("--"))
16
+ return null;
17
+ return value;
18
+ }
19
+ function parseToolText(result) {
20
+ const text = result.content?.[0]?.text ?? "{}";
21
+ return JSON.parse(text);
22
+ }
23
+ function slugify(name) {
24
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
25
+ }
26
+ // ─── Project scanning ─────────────────────────────────────────────────────────
27
+ function parseEnvKeys(content) {
28
+ return content
29
+ .split("\n")
30
+ .map((line) => line.trim())
31
+ .filter((line) => line && !line.startsWith("#"))
32
+ .map((line) => line.replace(/^export\s+/, ""))
33
+ .map((line) => line.split("=")[0].trim())
34
+ .filter((key) => /^[A-Z][A-Z0-9_]{2,}$/.test(key)); // min 3 chars, all caps
35
+ }
36
+ function detectFromGitConfig(dir) {
37
+ const configPath = join(dir, ".git", "config");
38
+ if (!existsSync(configPath))
39
+ return [];
40
+ try {
41
+ const content = readFileSync(configPath, "utf8");
42
+ // Match url under [remote "origin"]
43
+ const match = content.match(/\[remote\s+"origin"\][^\[]*url\s*=\s*([^\n]+)/s);
44
+ if (!match)
45
+ return [];
46
+ const url = match[1].trim();
47
+ if (url.includes("github.com"))
48
+ return ["github"];
49
+ if (url.includes("gitlab.com"))
50
+ return ["gitlab"];
51
+ if (url.includes("bitbucket.org"))
52
+ return ["bitbucket"];
53
+ if (url.includes("dev.azure.com") || url.includes("visualstudio.com"))
54
+ return ["azure-devops"];
55
+ }
56
+ catch { }
57
+ return [];
58
+ }
59
+ function detectFromFilesystem(dir) {
60
+ const detected = [];
61
+ // CI/CD
62
+ if (existsSync(join(dir, ".github", "workflows")))
63
+ detected.push("github-actions");
64
+ if (existsSync(join(dir, ".gitlab-ci.yml")))
65
+ detected.push("gitlab");
66
+ if (existsSync(join(dir, "Jenkinsfile")))
67
+ detected.push("jenkins");
68
+ if (existsSync(join(dir, ".circleci", "config.yml")))
69
+ detected.push("circleci");
70
+ // Deployment / hosting
71
+ if (existsSync(join(dir, "vercel.json")) || existsSync(join(dir, ".vercel")))
72
+ detected.push("vercel");
73
+ if (existsSync(join(dir, "netlify.toml")))
74
+ detected.push("netlify");
75
+ if (existsSync(join(dir, "fly.toml")))
76
+ detected.push("fly");
77
+ if (existsSync(join(dir, "railway.json")) || existsSync(join(dir, "railway.toml")))
78
+ detected.push("railway");
79
+ if (existsSync(join(dir, "render.yaml")))
80
+ detected.push("render");
81
+ // Containers
82
+ if (existsSync(join(dir, "Dockerfile")))
83
+ detected.push("docker");
84
+ if (existsSync(join(dir, "docker-compose.yml")) || existsSync(join(dir, "docker-compose.yaml")))
85
+ detected.push("docker");
86
+ // Infrastructure
87
+ if (existsSync(join(dir, "terraform")) || existsSync(join(dir, "main.tf")))
88
+ detected.push("terraform");
89
+ if (existsSync(join(dir, "pulumi.yaml")))
90
+ detected.push("pulumi");
91
+ return detected;
92
+ }
93
+ function collectPackageDeps(pkgPath) {
94
+ try {
95
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
96
+ return Object.keys({
97
+ ...pkg.dependencies,
98
+ ...pkg.devDependencies,
99
+ ...pkg.peerDependencies,
100
+ });
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ }
106
+ function readProjectName(pkgPath) {
107
+ try {
108
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
109
+ return typeof pkg.name === "string" ? pkg.name : null;
110
+ }
111
+ catch {
112
+ return null;
113
+ }
114
+ }
115
+ function scanProject(dir) {
116
+ const names = new Set();
117
+ let projectName = basename(dir);
118
+ let packageJsonCount = 0;
119
+ // Root package.json
120
+ const rootPkgPath = join(dir, "package.json");
121
+ if (existsSync(rootPkgPath)) {
122
+ packageJsonCount++;
123
+ const rootName = readProjectName(rootPkgPath);
124
+ if (rootName)
125
+ projectName = rootName;
126
+ for (const dep of collectPackageDeps(rootPkgPath))
127
+ names.add(dep);
128
+ }
129
+ // One level of subdirectories (monorepo packages)
130
+ try {
131
+ for (const entry of readdirSync(dir)) {
132
+ if (entry.startsWith(".") || entry === "node_modules" || entry === "dist")
133
+ continue;
134
+ const entryPath = join(dir, entry);
135
+ try {
136
+ if (!statSync(entryPath).isDirectory())
137
+ continue;
138
+ const subPkgPath = join(entryPath, "package.json");
139
+ if (existsSync(subPkgPath)) {
140
+ packageJsonCount++;
141
+ for (const dep of collectPackageDeps(subPkgPath))
142
+ names.add(dep);
143
+ }
144
+ }
145
+ catch { }
146
+ }
147
+ }
148
+ catch { }
149
+ // .env files
150
+ for (const envFile of [".env", ".env.example", ".env.local", ".env.runtime"]) {
151
+ const envPath = join(dir, envFile);
152
+ if (existsSync(envPath)) {
153
+ try {
154
+ for (const key of parseEnvKeys(readFileSync(envPath, "utf8")))
155
+ names.add(key);
156
+ }
157
+ catch { }
158
+ }
159
+ }
160
+ // Git remote, CI/CD, deployment config
161
+ for (const name of detectFromGitConfig(dir))
162
+ names.add(name);
163
+ for (const name of detectFromFilesystem(dir))
164
+ names.add(name);
165
+ return { projectName, packageJsonCount, detectedNames: Array.from(names) };
166
+ }
167
+ // ─── Relationship type selection ──────────────────────────────────────────────
168
+ function pickRelationshipType(entityTypeCode) {
169
+ switch (entityTypeCode) {
170
+ case "model":
171
+ return "uses_model";
172
+ case "platform":
173
+ case "platform_component":
174
+ case "skill":
175
+ return "uses";
176
+ default:
177
+ return "depends_on";
178
+ }
179
+ }
180
+ // ─── Main command ─────────────────────────────────────────────────────────────
181
+ export async function initProject(args) {
182
+ const asJson = parseFlag(args, "--json");
183
+ const dryRun = parseFlag(args, "--dry-run");
184
+ const dirArg = parseOptionValue(args, "--dir") ?? process.cwd();
185
+ const dir = resolvePath(dirArg);
186
+ const nameOverride = parseOptionValue(args, "--name");
187
+ const entityTypeOverride = parseOptionValue(args, "--entity-type") ?? "service";
188
+ const creds = requireCredentials();
189
+ const mcpOpts = { companyId: creds.companyId };
190
+ if (!asJson)
191
+ console.log(`Scanning ${dir}…`);
192
+ const { projectName, packageJsonCount, detectedNames } = scanProject(dir);
193
+ const displayName = nameOverride ?? projectName;
194
+ const projectSlug = slugify(displayName);
195
+ const projectExternalKey = `${entityTypeOverride}:${projectSlug}`;
196
+ if (!asJson) {
197
+ console.log(` Project : ${displayName} (${entityTypeOverride})`);
198
+ console.log(` Packages: ${packageJsonCount} package.json file(s)`);
199
+ console.log(` Detected: ${detectedNames.length} raw names`);
200
+ }
201
+ if (detectedNames.length === 0) {
202
+ if (!asJson)
203
+ console.log("Nothing detected — is this a supported project type?");
204
+ return;
205
+ }
206
+ // Resolve all detected names against the reference library
207
+ if (!asJson)
208
+ console.log("\nResolving against reference library…");
209
+ const allResolveResults = [];
210
+ const BATCH_SIZE = 200;
211
+ for (let i = 0; i < detectedNames.length; i += BATCH_SIZE) {
212
+ const batch = detectedNames.slice(i, i + BATCH_SIZE);
213
+ const raw = await callMcpTool("nexarch_resolve_reference", { names: batch, companyId: creds.companyId }, mcpOpts);
214
+ const data = parseToolText(raw);
215
+ allResolveResults.push(...data.results);
216
+ }
217
+ const resolvedItems = allResolveResults.filter((r) => r.resolved);
218
+ const unresolvedItems = allResolveResults.filter((r) => !r.resolved);
219
+ if (!asJson) {
220
+ console.log(` Resolved : ${resolvedItems.length}/${detectedNames.length}`);
221
+ console.log(` Candidates: ${unresolvedItems.length} unresolved (logged to reference candidates)`);
222
+ }
223
+ if (dryRun) {
224
+ const output = {
225
+ dryRun: true,
226
+ project: { name: displayName, externalKey: projectExternalKey, entityType: entityTypeOverride },
227
+ resolved: resolvedItems.map((r) => ({
228
+ input: r.input,
229
+ canonicalName: r.canonicalName,
230
+ entityTypeCode: r.entityTypeCode,
231
+ externalRef: r.canonicalExternalRef,
232
+ })),
233
+ unresolved: unresolvedItems.map((r) => r.input),
234
+ };
235
+ process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
236
+ return;
237
+ }
238
+ // Policy bootstrap
239
+ const policiesRaw = await callMcpTool("nexarch_get_applied_policies", {}, mcpOpts);
240
+ const policies = parseToolText(policiesRaw);
241
+ const policyBundleHash = policies.policyBundleHash ?? null;
242
+ const nowIso = new Date().toISOString();
243
+ const agentContext = {
244
+ agentId: "nexarch-cli:init-project",
245
+ agentRunId: `init-project-${Date.now()}`,
246
+ repoRef: dir,
247
+ observedAt: nowIso,
248
+ source: "nexarch-cli",
249
+ model: "n/a",
250
+ provider: "n/a",
251
+ };
252
+ const policyContext = policyBundleHash
253
+ ? { policyBundleHash, alignmentSummary: { score: 1, violations: [], waivers: [] } }
254
+ : undefined;
255
+ // Build entity list — project entity + all resolved reference entities
256
+ const entities = [];
257
+ // The project itself
258
+ entities.push({
259
+ externalKey: projectExternalKey,
260
+ entityTypeCode: entityTypeOverride,
261
+ name: displayName,
262
+ description: `Project initialised from ${dir}`,
263
+ confidence: 1,
264
+ attributes: { source_dir: dir, scanned_at: nowIso, package_json_count: packageJsonCount },
265
+ });
266
+ // Resolved reference entities (deduplicated by canonical external ref)
267
+ const seenRefs = new Set();
268
+ for (const r of resolvedItems) {
269
+ if (!r.canonicalExternalRef || seenRefs.has(r.canonicalExternalRef))
270
+ continue;
271
+ seenRefs.add(r.canonicalExternalRef);
272
+ entities.push({
273
+ externalKey: r.canonicalExternalRef,
274
+ entityTypeCode: r.entityTypeCode,
275
+ ...(r.entitySubtypeCode ? { entitySubtypeCode: r.entitySubtypeCode } : {}),
276
+ name: r.canonicalName,
277
+ ...(r.description ? { description: r.description } : {}),
278
+ confidence: 0.95,
279
+ });
280
+ }
281
+ // Build relationships from project entity to each resolved dependency
282
+ const relationships = [];
283
+ const seenRelPairs = new Set();
284
+ for (const r of resolvedItems) {
285
+ if (!r.canonicalExternalRef || !r.entityTypeCode)
286
+ continue;
287
+ const relType = pickRelationshipType(r.entityTypeCode);
288
+ const pairKey = `${relType}::${projectExternalKey}::${r.canonicalExternalRef}`;
289
+ if (seenRelPairs.has(pairKey))
290
+ continue;
291
+ seenRelPairs.add(pairKey);
292
+ relationships.push({
293
+ relationshipTypeCode: relType,
294
+ fromEntityExternalKey: projectExternalKey,
295
+ toEntityExternalKey: r.canonicalExternalRef,
296
+ confidence: 0.9,
297
+ });
298
+ }
299
+ if (!asJson)
300
+ console.log(`\nWriting to graph…`);
301
+ // Upsert entities
302
+ const entitiesRaw = await callMcpTool("nexarch_upsert_entities", { entities, agentContext, policyContext }, mcpOpts);
303
+ const entitiesResult = parseToolText(entitiesRaw);
304
+ // Upsert relationships
305
+ let relsResult = null;
306
+ if (relationships.length > 0) {
307
+ const relsRaw = await callMcpTool("nexarch_upsert_relationships", { relationships, agentContext, policyContext }, mcpOpts);
308
+ relsResult = parseToolText(relsRaw);
309
+ }
310
+ const output = {
311
+ ok: Number(entitiesResult.summary?.failed ?? 0) === 0,
312
+ project: { name: displayName, externalKey: projectExternalKey, entityType: entityTypeOverride },
313
+ entities: entitiesResult.summary ?? {},
314
+ relationships: relsResult?.summary ?? { requested: 0, succeeded: 0, failed: 0 },
315
+ resolved: resolvedItems.length,
316
+ unresolved: unresolvedItems.length,
317
+ unresolvedSample: unresolvedItems.slice(0, 10).map((r) => r.input),
318
+ entityErrors: entitiesResult.errors ?? [],
319
+ relationshipErrors: relsResult?.errors ?? [],
320
+ };
321
+ if (asJson) {
322
+ process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
323
+ if (!output.ok)
324
+ process.exitCode = 1;
325
+ return;
326
+ }
327
+ console.log(`\nDone.`);
328
+ console.log(` Entities : ${output.entities.succeeded ?? 0} written, ${output.entities.failed ?? 0} failed`);
329
+ console.log(` Relationships: ${output.relationships.succeeded ?? 0} written`);
330
+ if (unresolvedItems.length > 0) {
331
+ console.log(` Candidates : ${unresolvedItems.length} added to reference candidates`);
332
+ }
333
+ if (output.entityErrors.length > 0) {
334
+ console.log("\nEntity errors:");
335
+ for (const err of output.entityErrors) {
336
+ console.log(` ${err.externalKey}: ${err.error} — ${err.message}`);
337
+ }
338
+ }
339
+ }
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import { mcpConfig } from "./commands/mcp-config.js";
10
10
  import { mcpProxy } from "./commands/mcp-proxy.js";
11
11
  import { initAgent } from "./commands/init-agent.js";
12
12
  import { agentIdentify } from "./commands/agent-identify.js";
13
+ import { initProject } from "./commands/init-project.js";
13
14
  const [, , command, ...args] = process.argv;
14
15
  const commands = {
15
16
  login,
@@ -20,6 +21,7 @@ const commands = {
20
21
  "mcp-proxy": mcpProxy,
21
22
  "init-agent": initAgent,
22
23
  "agent-identify": agentIdentify,
24
+ "init-project": initProject,
23
25
  };
24
26
  async function main() {
25
27
  if (command === "agent") {
@@ -69,6 +71,16 @@ Usage:
69
71
  [--notes <text>] [--json]
70
72
  nexarch agent-identify
71
73
  Alias of 'nexarch agent identify'
74
+ nexarch init-project
75
+ Scan a project directory, resolve detected packages/env vars/
76
+ config files against the reference library, write entities and
77
+ relationships to the architecture graph, and log unresolved
78
+ names as reference candidates.
79
+ Options: --dir <path> (default: cwd)
80
+ --name <name> override project name
81
+ --entity-type <code> (default: service)
82
+ --dry-run preview without writing
83
+ --json
72
84
  `);
73
85
  process.exit(command ? 1 : 0);
74
86
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.1.13",
3
+ "version": "0.1.16",
4
4
  "description": "Connect AI coding tools to your Nexarch architecture workspace",
5
5
  "keywords": [
6
6
  "nexarch",