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.
- package/dist/commands/agent-identify.js +88 -1
- package/dist/commands/init-agent.js +1 -1
- package/dist/commands/init-project.js +339 -0
- package/dist/index.js +12 -0
- package/package.json +1 -1
|
@@ -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.
|
|
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
|
}
|