kairn-cli 2.15.0 → 2.16.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/dist/cli.js CHANGED
@@ -3892,14 +3892,56 @@ function detectFramework(deps) {
3892
3892
  }
3893
3893
  return detected.length > 0 ? detected.join(" + ") : null;
3894
3894
  }
3895
- function detectLanguage(dir, keyFiles) {
3896
- if (keyFiles.some((f) => f === "tsconfig.json")) return "TypeScript";
3897
- if (keyFiles.some((f) => f === "package.json")) return "JavaScript";
3898
- if (keyFiles.some((f) => f === "pyproject.toml" || f === "setup.py" || f === "requirements.txt")) return "Python";
3899
- if (keyFiles.some((f) => f === "Cargo.toml")) return "Rust";
3900
- if (keyFiles.some((f) => f === "go.mod")) return "Go";
3901
- if (keyFiles.some((f) => f === "Gemfile")) return "Ruby";
3902
- return null;
3895
+ function detectLanguagesFromFiles(files) {
3896
+ const matched = [];
3897
+ for (const signal of LANGUAGE_SIGNALS) {
3898
+ if (files.some((f) => signal.files.includes(f))) {
3899
+ matched.push(signal.language);
3900
+ }
3901
+ }
3902
+ return matched;
3903
+ }
3904
+ async function detectLanguageLocations(dir, keyFiles) {
3905
+ const rootHits = detectLanguagesFromFiles(keyFiles);
3906
+ if (rootHits.length > 0) {
3907
+ return rootHits.map((language) => ({ language, subdirs: [] }));
3908
+ }
3909
+ const entries = await listDirSafe(dir);
3910
+ const langSubdirs = /* @__PURE__ */ new Map();
3911
+ const counts = /* @__PURE__ */ new Map();
3912
+ for (const entry of entries) {
3913
+ const subPath = path10.join(dir, entry);
3914
+ try {
3915
+ const stat = await fs10.stat(subPath);
3916
+ if (!stat.isDirectory()) continue;
3917
+ } catch {
3918
+ continue;
3919
+ }
3920
+ const subFiles = await listDirSafe(subPath);
3921
+ const subLangs = detectLanguagesFromFiles(subFiles);
3922
+ for (const lang of subLangs) {
3923
+ counts.set(lang, (counts.get(lang) ?? 0) + 1);
3924
+ const existing = langSubdirs.get(lang);
3925
+ if (existing) {
3926
+ existing.push(entry);
3927
+ } else {
3928
+ langSubdirs.set(lang, [entry]);
3929
+ }
3930
+ }
3931
+ }
3932
+ if (counts.size === 0) return [];
3933
+ const precedence = /* @__PURE__ */ new Map();
3934
+ for (let i = 0; i < LANGUAGE_SIGNALS.length; i++) {
3935
+ precedence.set(LANGUAGE_SIGNALS[i].language, i);
3936
+ }
3937
+ return [...counts.entries()].sort((a, b) => {
3938
+ const freqDiff = b[1] - a[1];
3939
+ if (freqDiff !== 0) return freqDiff;
3940
+ return (precedence.get(a[0]) ?? 999) - (precedence.get(b[0]) ?? 999);
3941
+ }).map(([lang]) => ({
3942
+ language: lang,
3943
+ subdirs: langSubdirs.get(lang) ?? []
3944
+ }));
3903
3945
  }
3904
3946
  function extractEnvKeys(content) {
3905
3947
  const keys = [];
@@ -3934,7 +3976,9 @@ async function scanProject(dir) {
3934
3976
  "CLAUDE.md"
3935
3977
  ].includes(f)
3936
3978
  );
3937
- const language = detectLanguage(dir, keyFiles);
3979
+ const languageLocations = await detectLanguageLocations(dir, keyFiles);
3980
+ const detectedLanguages = languageLocations.map((d) => d.language);
3981
+ const language = detectedLanguages[0] ?? null;
3938
3982
  const framework = detectFramework(allDeps);
3939
3983
  const typescript = keyFiles.includes("tsconfig.json") || allDeps.includes("typescript");
3940
3984
  const testCommand = scripts.test && scripts.test !== 'echo "Error: no test specified" && exit 1' ? scripts.test : null;
@@ -3983,6 +4027,8 @@ async function scanProject(dir) {
3983
4027
  description,
3984
4028
  directory: dir,
3985
4029
  language,
4030
+ languages: detectedLanguages,
4031
+ languageLocations,
3986
4032
  framework,
3987
4033
  typescript,
3988
4034
  dependencies: deps,
@@ -4010,9 +4056,18 @@ async function scanProject(dir) {
4010
4056
  keyFiles
4011
4057
  };
4012
4058
  }
4059
+ var LANGUAGE_SIGNALS;
4013
4060
  var init_scan = __esm({
4014
4061
  "src/scanner/scan.ts"() {
4015
4062
  "use strict";
4063
+ LANGUAGE_SIGNALS = [
4064
+ { files: ["tsconfig.json"], language: "TypeScript" },
4065
+ { files: ["package.json"], language: "JavaScript" },
4066
+ { files: ["pyproject.toml", "setup.py", "requirements.txt"], language: "Python" },
4067
+ { files: ["Cargo.toml"], language: "Rust" },
4068
+ { files: ["go.mod"], language: "Go" },
4069
+ { files: ["Gemfile"], language: "Ruby" }
4070
+ ];
4016
4071
  }
4017
4072
  });
4018
4073
 
@@ -4040,6 +4095,7 @@ function getStrategy(language) {
4040
4095
  return null;
4041
4096
  }
4042
4097
  const key = language.toLowerCase();
4098
+ if (key === "javascript") return STRATEGIES["typescript"] ?? null;
4043
4099
  return STRATEGIES[key] ?? null;
4044
4100
  }
4045
4101
  function getAlwaysInclude() {
@@ -4070,6 +4126,26 @@ function classifyFilePriority(filePath, strategy) {
4070
4126
  }
4071
4127
  return 3 /* OTHER */;
4072
4128
  }
4129
+ function dedupe(arr) {
4130
+ return Array.from(new Set(arr));
4131
+ }
4132
+ function mergeStrategies(strategies) {
4133
+ if (strategies.length === 0) {
4134
+ throw new Error("mergeStrategies requires at least one strategy");
4135
+ }
4136
+ if (strategies.length === 1) {
4137
+ return strategies[0];
4138
+ }
4139
+ return {
4140
+ language: strategies.map((s) => s.language).join("/"),
4141
+ extensions: dedupe(strategies.flatMap((s) => s.extensions)),
4142
+ entryPoints: dedupe(strategies.flatMap((s) => s.entryPoints)),
4143
+ domainPatterns: dedupe(strategies.flatMap((s) => s.domainPatterns)),
4144
+ configPatterns: dedupe(strategies.flatMap((s) => s.configPatterns)),
4145
+ excludePatterns: dedupe(strategies.flatMap((s) => s.excludePatterns)),
4146
+ maxFilesPerCategory: Math.max(...strategies.map((s) => s.maxFilesPerCategory))
4147
+ };
4148
+ }
4073
4149
  function extractPathsFromScripts(scripts) {
4074
4150
  const paths = [];
4075
4151
  const interestingScripts = ["start", "dev", "serve", "main", "server"];
@@ -4541,6 +4617,39 @@ function validateArray(raw, guard) {
4541
4617
  if (!Array.isArray(raw)) return [];
4542
4618
  return raw.filter(guard);
4543
4619
  }
4620
+ function scopeStrategyToSubdirs(strategy, subdirs) {
4621
+ if (subdirs.length === 0) return strategy;
4622
+ return {
4623
+ ...strategy,
4624
+ entryPoints: subdirs.flatMap(
4625
+ (dir) => strategy.entryPoints.map((ep) => `${dir}/${ep}`)
4626
+ ),
4627
+ domainPatterns: subdirs.flatMap(
4628
+ (dir) => strategy.domainPatterns.map((dp) => `${dir}/${dp}`)
4629
+ ),
4630
+ configPatterns: subdirs.flatMap(
4631
+ (dir) => strategy.configPatterns.map((cp) => `${dir}/${cp}`)
4632
+ )
4633
+ // excludePatterns: keep global (they use ** globs)
4634
+ };
4635
+ }
4636
+ function getLanguageWeight(filePath, languageLocations) {
4637
+ if (languageLocations.length <= 1) return 0;
4638
+ const subdirToRank = /* @__PURE__ */ new Map();
4639
+ for (let rank2 = 0; rank2 < languageLocations.length; rank2++) {
4640
+ for (const subdir of languageLocations[rank2].subdirs) {
4641
+ subdirToRank.set(subdir, rank2);
4642
+ }
4643
+ }
4644
+ if (subdirToRank.size === 0) return 0;
4645
+ const firstSlash = filePath.indexOf("/");
4646
+ if (firstSlash === -1) {
4647
+ return 0;
4648
+ }
4649
+ const firstSegment = filePath.slice(0, firstSlash);
4650
+ const rank = subdirToRank.get(firstSegment);
4651
+ return rank ?? 0;
4652
+ }
4544
4653
  async function analyzeProject(dir, profile, config, options) {
4545
4654
  if (!options?.refresh) {
4546
4655
  const cache = await readCache(dir);
@@ -4557,20 +4666,24 @@ async function analyzeProject(dir, profile, config, options) {
4557
4666
  }
4558
4667
  }
4559
4668
  }
4560
- const baseStrategy = getStrategy(profile.language);
4561
- if (!baseStrategy) {
4669
+ const languageLocations = profile.languageLocations ?? profile.languages.map((lang) => ({ language: lang, subdirs: [] }));
4670
+ const resolvedStrategies = await Promise.all(
4671
+ languageLocations.map(async ({ language, subdirs }) => {
4672
+ const base = getStrategy(language);
4673
+ if (!base) return null;
4674
+ const enriched = await resolveStrategy(dir, base, profile.framework, profile.scripts);
4675
+ return scopeStrategyToSubdirs(enriched, subdirs);
4676
+ })
4677
+ );
4678
+ const validStrategies = resolvedStrategies.filter((s) => s !== null);
4679
+ if (validStrategies.length === 0) {
4562
4680
  throw new AnalysisError(
4563
- `No sampling strategy for language: ${profile.language ?? "unknown"}`,
4681
+ `No sampling strategy for languages: ${profile.languages.join(", ") || "none detected"}`,
4564
4682
  "no_entry_point",
4565
4683
  "Supported: Python, TypeScript, Go, Rust"
4566
4684
  );
4567
4685
  }
4568
- const strategy = await resolveStrategy(
4569
- dir,
4570
- baseStrategy,
4571
- profile.framework,
4572
- profile.scripts
4573
- );
4686
+ const strategy = mergeStrategies(validStrategies);
4574
4687
  const include = [
4575
4688
  ...strategy.entryPoints,
4576
4689
  ...strategy.domainPatterns.map((p) => p + "**/*"),
@@ -4581,7 +4694,11 @@ async function analyzeProject(dir, profile, config, options) {
4581
4694
  include,
4582
4695
  exclude: strategy.excludePatterns,
4583
4696
  maxTokens: options?.tokenBudget ?? DEFAULT_TOKEN_BUDGET,
4584
- prioritize: (filePath) => classifyFilePriority(filePath, strategy)
4697
+ prioritize: (filePath) => {
4698
+ const baseTier = classifyFilePriority(filePath, strategy);
4699
+ const langWeight = getLanguageWeight(filePath, languageLocations);
4700
+ return baseTier + langWeight * 0.1;
4701
+ }
4585
4702
  });
4586
4703
  if (packed.fileCount === 0) {
4587
4704
  throw new AnalysisError(
@@ -4751,7 +4868,7 @@ function buildProfileSummary(profile) {
4751
4868
  const lines = [];
4752
4869
  lines.push(`Project: ${profile.name}`);
4753
4870
  if (profile.description) lines.push(`Description: ${profile.description}`);
4754
- if (profile.language) lines.push(`Language: ${profile.language}`);
4871
+ if (profile.languages.length > 0) lines.push(`Languages: ${profile.languages.join(", ")}`);
4755
4872
  if (profile.framework) lines.push(`Framework: ${profile.framework}`);
4756
4873
  if (profile.dependencies.length > 0) {
4757
4874
  lines.push(`Dependencies: ${profile.dependencies.join(", ")}`);
@@ -4900,7 +5017,7 @@ var init_optimize = __esm({
4900
5017
  const scanSpinner = ora({ text: "Scanning project...", indent: 2 }).start();
4901
5018
  const profile = await scanProject(targetDir);
4902
5019
  scanSpinner.stop();
4903
- if (profile.language) console.log(ui.kv("Language:", profile.language));
5020
+ if (profile.languages.length > 0) console.log(ui.kv("Languages:", profile.languages.join(", ")));
4904
5021
  if (profile.framework) console.log(ui.kv("Framework:", profile.framework));
4905
5022
  console.log(ui.kv("Dependencies:", String(profile.dependencies.length)));
4906
5023
  if (profile.testCommand) console.log(ui.kv("Tests:", profile.testCommand));
@@ -11392,7 +11509,7 @@ function buildAnalysisContext(analysis) {
11392
11509
  }
11393
11510
  function buildTaskGenerationMessage(claudeMd, projectProfile, templates, analysis) {
11394
11511
  const profileLines = [
11395
- `Language: ${projectProfile.language ?? "unknown"}`,
11512
+ `Languages: ${projectProfile.languages.length > 0 ? projectProfile.languages.join(", ") : "unknown"}`,
11396
11513
  `Framework: ${projectProfile.framework ?? "none"}`,
11397
11514
  `Scripts: ${Object.entries(projectProfile.scripts).map(([k, v]) => `${k}=${v}`).join(", ") || "none"}`,
11398
11515
  `Key files: ${projectProfile.keyFiles.join(", ") || "none"}`
@@ -11489,6 +11606,7 @@ async function writeTasksFile(workspacePath, tasks) {
11489
11606
  async function buildProjectProfile(projectRoot) {
11490
11607
  const profile = {
11491
11608
  language: null,
11609
+ languages: [],
11492
11610
  framework: null,
11493
11611
  scripts: {},
11494
11612
  keyFiles: []
@@ -11500,6 +11618,7 @@ async function buildProjectProfile(projectRoot) {
11500
11618
  );
11501
11619
  const pkg2 = JSON.parse(pkgStr);
11502
11620
  profile.language = "typescript";
11621
+ profile.languages = ["typescript"];
11503
11622
  if (pkg2.scripts && typeof pkg2.scripts === "object") {
11504
11623
  profile.scripts = pkg2.scripts;
11505
11624
  }
@@ -11524,10 +11643,12 @@ async function buildProjectProfile(projectRoot) {
11524
11643
  try {
11525
11644
  await fs20.access(path20.join(projectRoot, "pyproject.toml"));
11526
11645
  profile.language = "python";
11646
+ profile.languages = ["python"];
11527
11647
  } catch {
11528
11648
  try {
11529
11649
  await fs20.access(path20.join(projectRoot, "requirements.txt"));
11530
11650
  profile.language = "python";
11651
+ profile.languages = ["python"];
11531
11652
  } catch {
11532
11653
  }
11533
11654
  }
@@ -12910,8 +13031,8 @@ async function analyzeAction(options) {
12910
13031
  const profile = await scanProject(targetDir);
12911
13032
  scanSpinner?.succeed("Project scanned");
12912
13033
  if (!options.json) {
12913
- if (profile.language)
12914
- console.log(ui.kv("Language:", profile.language));
13034
+ if (profile.languages.length > 0)
13035
+ console.log(ui.kv("Languages:", profile.languages.join(", ")));
12915
13036
  if (profile.framework)
12916
13037
  console.log(ui.kv("Framework:", profile.framework));
12917
13038
  }