nexarch 0.4.8 → 0.5.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.
@@ -20,7 +20,7 @@ function parseToolText(result) {
20
20
  const text = result.content?.[0]?.text ?? "{}";
21
21
  return JSON.parse(text);
22
22
  }
23
- function getDefaultAgentId() {
23
+ function getStoredAgentId() {
24
24
  try {
25
25
  const identityPath = join(homedir(), ".nexarch", "identity.json");
26
26
  const parsed = JSON.parse(readFileSync(identityPath, "utf8"));
@@ -29,8 +29,14 @@ function getDefaultAgentId() {
29
29
  }
30
30
  }
31
31
  catch {
32
- // fall through to env-derived default
32
+ // ignore
33
33
  }
34
+ return null;
35
+ }
36
+ function getDefaultAgentId() {
37
+ const stored = getStoredAgentId();
38
+ if (stored)
39
+ return stored;
34
40
  const osUser = process.env.USERNAME || process.env.USER || "unknown";
35
41
  const host = process.env.HOSTNAME || "unknown-host";
36
42
  return `nexarch-cli:${osUser}@${host}`;
@@ -45,7 +51,9 @@ function parseCsv(value) {
45
51
  }
46
52
  export async function agentIdentify(args) {
47
53
  const asJson = parseFlag(args, "--json");
48
- const agentId = parseOptionValue(args, "--agent-id") ?? getDefaultAgentId();
54
+ const explicitAgentId = parseOptionValue(args, "--agent-id");
55
+ const storedAgentId = getStoredAgentId();
56
+ const agentId = explicitAgentId ?? storedAgentId ?? getDefaultAgentId();
49
57
  const looksLikeUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(agentId);
50
58
  const provider = parseOptionValue(args, "--provider");
51
59
  const model = parseOptionValue(args, "--model");
@@ -61,6 +69,9 @@ export async function agentIdentify(args) {
61
69
  if (looksLikeUuid) {
62
70
  throw new Error(`agent-identify expects the stable agent id (e.g. nexarch-cli:user@host), not a graph entity UUID (${agentId}). Re-run using --agent-id \"${getDefaultAgentId()}\" or omit --agent-id.`);
63
71
  }
72
+ if (explicitAgentId && storedAgentId && explicitAgentId !== storedAgentId) {
73
+ throw new Error(`--agent-id \"${explicitAgentId}\" does not match the registered local agent id \"${storedAgentId}\". Omit --agent-id, or use --agent-id \"${storedAgentId}\" to avoid creating duplicates.`);
74
+ }
64
75
  const creds = requireCredentials();
65
76
  await mcpInitialize({ companyId: creds.companyId });
66
77
  const tools = await mcpListTools({ companyId: creds.companyId });
@@ -5,7 +5,7 @@ import process from "process";
5
5
  import { requireCredentials } from "../lib/credentials.js";
6
6
  import { fetchAgentRegistryOrThrow } from "../lib/agent-registry.js";
7
7
  import { callMcpTool, mcpInitialize, mcpListTools } from "../lib/mcp.js";
8
- const CLI_VERSION = "0.4.8";
8
+ const CLI_VERSION = "0.5.0";
9
9
  const AGENT_ENTITY_TYPE = "agent";
10
10
  const TECH_COMPONENT_ENTITY_TYPE = "technology_component";
11
11
  function parseFlag(args, flag) {
@@ -1,4 +1,5 @@
1
1
  import process from "process";
2
+ import { execFileSync } from "node:child_process";
2
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
4
  import { basename, join, relative, resolve as resolvePath } from "node:path";
4
5
  import { homedir } from "node:os";
@@ -24,6 +25,175 @@ function parseToolText(result) {
24
25
  function slugify(name) {
25
26
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
26
27
  }
28
+ function safeExec(command, args, cwd) {
29
+ try {
30
+ const out = execFileSync(command, args, { cwd, stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
31
+ const trimmed = out.trim();
32
+ return trimmed || null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ function normalizeRepoUrl(raw) {
39
+ const trimmed = raw.trim();
40
+ if (!trimmed)
41
+ return null;
42
+ // user@host:path(.git) → https://host/path
43
+ const scpLike = trimmed.match(/^(?:[a-z0-9._-]+)@([^:]+):(.+)$/i);
44
+ if (scpLike) {
45
+ const host = scpLike[1].toLowerCase();
46
+ const path = scpLike[2].replace(/\.git$/i, "").replace(/^\/+/, "");
47
+ return `https://${host}/${path}`;
48
+ }
49
+ // ssh://user@host/path(.git) → https://host/path
50
+ const sshLike = trimmed.match(/^ssh:\/\/(?:[a-z0-9._-]+@)?([^/]+)\/(.+)$/i);
51
+ if (sshLike) {
52
+ const host = sshLike[1].toLowerCase();
53
+ const path = sshLike[2].replace(/\.git$/i, "").replace(/^\/+/, "");
54
+ return `https://${host}/${path}`;
55
+ }
56
+ try {
57
+ const u = new URL(trimmed);
58
+ if (u.protocol === "http:" || u.protocol === "https:") {
59
+ u.hash = "";
60
+ u.search = "";
61
+ u.pathname = u.pathname.replace(/\.git$/i, "");
62
+ return u.toString().replace(/\/$/, "");
63
+ }
64
+ }
65
+ catch {
66
+ // ignore
67
+ }
68
+ return null;
69
+ }
70
+ function inferProvider(ref) {
71
+ const value = ref.toLowerCase();
72
+ if (value.includes("github.com"))
73
+ return "github";
74
+ if (value.includes("gitlab.com"))
75
+ return "gitlab";
76
+ if (value.includes("bitbucket.org"))
77
+ return "bitbucket";
78
+ if (value.includes("dev.azure.com") || value.includes("visualstudio.com"))
79
+ return "azure-repos";
80
+ if (value.includes("codecommit"))
81
+ return "codecommit";
82
+ if (value.includes("git.sr.ht") || value.includes("sourcehut"))
83
+ return "sourcehut";
84
+ if (value.includes("forgejo"))
85
+ return "forgejo";
86
+ if (value.includes("gitea"))
87
+ return "gitea";
88
+ return "unknown";
89
+ }
90
+ function providerCanonicalRepoRef(provider) {
91
+ switch (provider) {
92
+ case "github": return "global:repo:github";
93
+ case "gitlab": return "global:repo:gitlab";
94
+ case "bitbucket": return "global:repo:bitbucket";
95
+ case "azure-repos": return "global:repo:azure-repos";
96
+ case "codecommit": return "global:repo:codecommit";
97
+ case "gitea": return "global:repo:gitea";
98
+ case "forgejo": return "global:repo:forgejo";
99
+ case "sourcehut": return "global:repo:sourcehut";
100
+ default: return null;
101
+ }
102
+ }
103
+ function readPackageRepositoryField(dir) {
104
+ const pkgPath = join(dir, "package.json");
105
+ if (!existsSync(pkgPath))
106
+ return null;
107
+ try {
108
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
109
+ if (typeof pkg.repository === "string")
110
+ return pkg.repository;
111
+ if (pkg.repository && typeof pkg.repository === "object" && "url" in pkg.repository && typeof pkg.repository.url === "string") {
112
+ return pkg.repository.url;
113
+ }
114
+ }
115
+ catch {
116
+ // ignore
117
+ }
118
+ return null;
119
+ }
120
+ function readHgDefaultPath(dir) {
121
+ const hgrcPath = join(dir, ".hg", "hgrc");
122
+ if (!existsSync(hgrcPath))
123
+ return null;
124
+ try {
125
+ const content = readFileSync(hgrcPath, "utf8");
126
+ const section = content.match(/\[paths\]([\s\S]*?)(\n\[[^\]]+\]|$)/i)?.[1] ?? "";
127
+ const line = section
128
+ .split("\n")
129
+ .map((l) => l.trim())
130
+ .find((l) => /^default\s*=\s*/i.test(l));
131
+ if (!line)
132
+ return null;
133
+ return line.replace(/^default\s*=\s*/i, "").trim() || null;
134
+ }
135
+ catch {
136
+ return null;
137
+ }
138
+ }
139
+ function readSvnRepositoryUrl(dir) {
140
+ // Best effort using svn CLI if available.
141
+ const fromCli = safeExec("svn", ["info", "--show-item", "repos-root-url"], dir);
142
+ if (fromCli)
143
+ return fromCli;
144
+ // Very rough fallback for legacy .svn/entries format.
145
+ const entriesPath = join(dir, ".svn", "entries");
146
+ if (!existsSync(entriesPath))
147
+ return null;
148
+ try {
149
+ const content = readFileSync(entriesPath, "utf8");
150
+ const firstUrl = content.match(/https?:\/\/[^\s]+/i)?.[0] ?? null;
151
+ return firstUrl;
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ function detectSourceRepository(dir) {
158
+ let rawRef = null;
159
+ let vcsType = "unknown";
160
+ // Prefer git when present.
161
+ if (existsSync(join(dir, ".git"))) {
162
+ rawRef = safeExec("git", ["remote", "get-url", "origin"], dir)
163
+ ?? safeExec("git", ["config", "--get", "remote.origin.url"], dir);
164
+ if (rawRef)
165
+ vcsType = "git";
166
+ }
167
+ // Mercurial fallback.
168
+ if (!rawRef && existsSync(join(dir, ".hg"))) {
169
+ rawRef = safeExec("hg", ["paths", "default"], dir) ?? readHgDefaultPath(dir);
170
+ if (rawRef)
171
+ vcsType = "hg";
172
+ }
173
+ // Subversion fallback.
174
+ if (!rawRef && existsSync(join(dir, ".svn"))) {
175
+ rawRef = readSvnRepositoryUrl(dir);
176
+ if (rawRef)
177
+ vcsType = "svn";
178
+ }
179
+ // package.json repository fallback (metadata only).
180
+ if (!rawRef) {
181
+ rawRef = readPackageRepositoryField(dir);
182
+ if (rawRef)
183
+ vcsType = "unknown";
184
+ }
185
+ if (!rawRef)
186
+ return null;
187
+ const url = normalizeRepoUrl(rawRef);
188
+ const provider = inferProvider(url ?? rawRef);
189
+ return {
190
+ rawRef,
191
+ url,
192
+ vcsType,
193
+ provider,
194
+ canonicalRepoRef: providerCanonicalRepoRef(provider),
195
+ };
196
+ }
27
197
  // ─── Project scanning ─────────────────────────────────────────────────────────
28
198
  // Noise patterns for env var keys that are internal config, not external service references
29
199
  const ENV_KEY_NOISE = /^(NODE_ENV|PORT|HOST|DEBUG|LOG_LEVEL|TZ|LANG|PATH|HOME|USER|SHELL|TERM)$|(_LOG_LEVEL|_MAX_|_MIN_|_DEFAULT_|_TIMEOUT|_DELAY|_JOBS|_INTERVAL|_LIMIT|_RETRIES|_CONCURREN|_WORKERS)$|(_URL|_SECRET|_TOKEN|_KEY|_PASSWORD|_CREDENTIAL|_DSN|_URI)$/;
@@ -571,6 +741,7 @@ export async function initProject(args) {
571
741
  if (!asJson)
572
742
  console.log(`Scanning ${dir}…`);
573
743
  const { projectName, packageJsonCount, detectedNames, rootDepNames, subPackages, detectedEcosystems } = scanProject(dir);
744
+ const detectedRepo = detectSourceRepository(dir);
574
745
  const displayName = nameOverride ?? projectName;
575
746
  const projectSlug = slugify(displayName);
576
747
  const projectExternalKey = `${entityTypeOverride}:${projectSlug}`;
@@ -587,6 +758,9 @@ export async function initProject(args) {
587
758
  console.log(` Project : ${displayName} (${entityTypeOverride})`);
588
759
  console.log(` Packages: ${packageJsonCount} package.json file(s)`);
589
760
  console.log(` Detected: ${detectedNames.length} raw names`);
761
+ if (detectedRepo) {
762
+ console.log(` Repo : ${detectedRepo.url ?? detectedRepo.rawRef} (${detectedRepo.provider}/${detectedRepo.vcsType})`);
763
+ }
590
764
  }
591
765
  if (detectedNames.length === 0) {
592
766
  if (!asJson)
@@ -613,7 +787,20 @@ export async function initProject(args) {
613
787
  if (dryRun) {
614
788
  const output = {
615
789
  dryRun: true,
616
- project: { name: displayName, externalKey: projectExternalKey, entityType: entityTypeOverride },
790
+ project: {
791
+ name: displayName,
792
+ externalKey: projectExternalKey,
793
+ entityType: entityTypeOverride,
794
+ sourceRepository: detectedRepo
795
+ ? {
796
+ ref: detectedRepo.rawRef,
797
+ url: detectedRepo.url,
798
+ vcsType: detectedRepo.vcsType,
799
+ provider: detectedRepo.provider,
800
+ componentRef: detectedRepo.canonicalRepoRef,
801
+ }
802
+ : null,
803
+ },
617
804
  resolved: resolvedItems.map((r) => ({
618
805
  input: r.input,
619
806
  canonicalName: r.canonicalName,
@@ -630,9 +817,11 @@ export async function initProject(args) {
630
817
  const policies = parseToolText(policiesRaw);
631
818
  const policyBundleHash = policies.policyBundleHash ?? null;
632
819
  const nowIso = new Date().toISOString();
633
- const repoRef = repoRefOverride ?? dir;
820
+ const repoRef = repoRefOverride ?? detectedRepo?.rawRef ?? dir;
634
821
  const repoPath = repoPathOverride ?? dir;
635
- const repoUrl = repoUrlOverride ?? null;
822
+ const repoUrl = repoUrlOverride ?? detectedRepo?.url ?? null;
823
+ const sourceVcsType = detectedRepo?.vcsType ?? "unknown";
824
+ const sourceProvider = detectedRepo?.provider ?? "unknown";
636
825
  const agentContext = {
637
826
  agentId: "nexarch-cli:init-project",
638
827
  agentRunId: `init-project-${Date.now()}`,
@@ -661,8 +850,12 @@ export async function initProject(args) {
661
850
  source_dir: dir,
662
851
  scanned_at: nowIso,
663
852
  package_json_count: packageJsonCount,
664
- ...(repoUrl ? { repository_url: repoUrl } : {}),
853
+ ...(repoUrl ? { repository_url: repoUrl, source_repository_url: repoUrl } : {}),
665
854
  repository_ref: repoRef,
855
+ source_repository_ref: repoRef,
856
+ source_vcs_type: sourceVcsType,
857
+ source_provider: sourceProvider,
858
+ ...(detectedRepo?.canonicalRepoRef ? { source_repository_component_ref: detectedRepo.canonicalRepoRef } : {}),
666
859
  },
667
860
  });
668
861
  // Resolved reference entities (deduplicated by canonical external ref)
@@ -680,6 +873,28 @@ export async function initProject(args) {
680
873
  confidence: 0.95,
681
874
  });
682
875
  }
876
+ // Ensure source repository component is represented when we can infer one.
877
+ if (detectedRepo?.canonicalRepoRef && !seenRefs.has(detectedRepo.canonicalRepoRef)) {
878
+ seenRefs.add(detectedRepo.canonicalRepoRef);
879
+ entities.push({
880
+ externalKey: detectedRepo.canonicalRepoRef,
881
+ entityTypeCode: "technology_component",
882
+ entitySubtypeCode: "tech_infrastructure",
883
+ name: detectedRepo.provider === "unknown" ? "Source Repository" : `Source Repository (${detectedRepo.provider})`,
884
+ confidence: 0.9,
885
+ ...(repoUrl
886
+ ? {
887
+ attributes: {
888
+ repository_url: repoUrl,
889
+ source_repository_url: repoUrl,
890
+ source_repository_ref: repoRef,
891
+ source_vcs_type: sourceVcsType,
892
+ source_provider: sourceProvider,
893
+ },
894
+ }
895
+ : {}),
896
+ });
897
+ }
683
898
  // Auto-create stub entities for each sub-package
684
899
  const seenSubKeys = new Set();
685
900
  for (const sp of subPackages) {
@@ -750,6 +965,10 @@ export async function initProject(args) {
750
965
  addRel("accountable_for", orgExternalKey, sp.externalKey, 1);
751
966
  }
752
967
  }
968
+ // Project depends_on its source repository component when resolved/inferred.
969
+ if (detectedRepo?.canonicalRepoRef) {
970
+ addRel("depends_on", projectExternalKey, detectedRepo.canonicalRepoRef, 0.95);
971
+ }
753
972
  if (!asJson)
754
973
  console.log(`\nWriting to graph…`);
755
974
  // Upsert entities
package/dist/lib/mcp.js CHANGED
@@ -68,7 +68,7 @@ export async function mcpInitialize(options = {}) {
68
68
  return callMcpRpc("initialize", {
69
69
  protocolVersion: "2024-11-05",
70
70
  capabilities: {},
71
- clientInfo: { name: "nexarch-cli", version: "0.4.8" },
71
+ clientInfo: { name: "nexarch-cli", version: "0.5.0" },
72
72
  }, options);
73
73
  }
74
74
  export async function mcpListTools(options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.4.8",
3
+ "version": "0.5.0",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",