teamai-cli 0.24.0-beta.7 → 0.24.0-beta.9

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/CHANGELOG.md CHANGED
@@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. See [standa
11
11
 
12
12
  ### 🐛 Bug Fixes
13
13
 
14
+ - The GitHub and CNB providers resolve their CLI to a launchable absolute path and start it through cross-spawn, so on Windows they no longer answer "installed" while every call fails silently ([#520](https://github.com/Tencent/teamai-cli/pull/520)).
15
+ - `enabledAgents` now also gates CLI builtin deploy, CLAUDE.md-class injects, and last-pull skip-sync targets, so an already-installed tool outside the whitelist is not written to ([#510](https://github.com/Tencent/teamai-cli/issues/510)).
14
16
  - `teamai status` counts rule files in subdirectories recursively ([#437](https://github.com/Tencent/teamai-cli/pull/437)).
15
17
  - Codex Stop-phase contribution hints are deferred to the next prompt, so the host no longer rejects `additionalContext` ([#441](https://github.com/Tencent/teamai-cli/pull/441)).
16
18
 
package/README.md CHANGED
@@ -199,6 +199,8 @@ teamai codebase --reconcile --output /path/to/repo # map product docs to code pa
199
199
  teamai codebase --lint --output /path/to/repo # check the locally extracted graph
200
200
  ```
201
201
 
202
+ Extract writes `teamwiki/evidence/code/<project>/_manifest.json` even when AI enrichment is skipped or produces nothing, so `--deep-enrich` can start.
203
+
202
204
  The graph stores components, interfaces, configs, and cross-repo import edges. `teamai recall` uses it for graph-boosted re-ranking.
203
205
  When a recall hit comes from a codebase page, the result includes a `Sources:` line listing the relevant source file paths — giving agents a direct starting point for code changes instead of re-exploring the repo.
204
206
 
package/README.zh-CN.md CHANGED
@@ -199,6 +199,8 @@ teamai codebase --reconcile --output /path/to/repo # 将产品文档映射到代
199
199
  teamai codebase --lint --output /path/to/repo # 检查本地提取的图谱
200
200
  ```
201
201
 
202
+ 只要 extract 发现了组件,就会写入 `teamwiki/evidence/code/<project>/_manifest.json`(包括跳过 AI 增强或增强没有产出的情况),因此 `--deep-enrich` 可以接着跑。
203
+
202
204
  图谱存储组件、接口、配置和跨仓库依赖边。`teamai recall` 利用图谱进行增强排名。
203
205
  当召回命中 codebase 页面时,结果会附带一行 `Sources:`,列出相关源文件路径,供 agent 直接作为代码改动的入口,无需重新探索代码库。
204
206
 
package/dist/index.js CHANGED
@@ -171,6 +171,7 @@ __export(fs_exports, {
171
171
  listFiles: () => listFiles,
172
172
  listFilesRecursive: () => listFilesRecursive,
173
173
  pathExists: () => pathExists,
174
+ pruneEmptyDirs: () => pruneEmptyDirs,
174
175
  readFileSafe: () => readFileSafe,
175
176
  readJson: () => readJson,
176
177
  remove: () => remove,
@@ -255,6 +256,32 @@ async function copyDir(src, dest) {
255
256
  filter: (srcPath) => !isIgnored(path2.basename(srcPath))
256
257
  });
257
258
  }
259
+ async function pruneEmptyDirs(target) {
260
+ const expanded = expandHome(target);
261
+ let entries;
262
+ try {
263
+ const stat8 = await fse.lstat(expanded);
264
+ if (!stat8.isDirectory() || stat8.isSymbolicLink()) return false;
265
+ entries = await fse.readdir(expanded, { withFileTypes: true });
266
+ } catch {
267
+ return false;
268
+ }
269
+ let empty = true;
270
+ for (const entry of entries) {
271
+ if (entry.isDirectory()) {
272
+ if (!await pruneEmptyDirs(path2.join(expanded, entry.name))) empty = false;
273
+ } else {
274
+ empty = false;
275
+ }
276
+ }
277
+ if (!empty) return false;
278
+ try {
279
+ await fse.rmdir(expanded);
280
+ return true;
281
+ } catch {
282
+ return false;
283
+ }
284
+ }
258
285
  async function copyFile(src, dest) {
259
286
  const destExpanded = expandHome(dest);
260
287
  await fse.ensureDir(path2.dirname(destExpanded));
@@ -868,6 +895,9 @@ var init_types = __esm({
868
895
  * team repo never receives stat commits (e.g. read-only pull setups).
869
896
  * Default: on. */
870
897
  usageReport: z.boolean().optional(),
898
+ /** Run `git submodule update --init` on pull so skills distributed as git
899
+ * submodules are populated and kept current. Off by default. */
900
+ submodules: z.boolean().optional(),
871
901
  // MCP paths are only set for tools whose config location has been verified.
872
902
  // Tools left without `mcp` are skipped by MCP sync rather than guessed at, so a
873
903
  // wrong guess can never create a junk config file on a user's machine.
@@ -3212,7 +3242,7 @@ async function deployBuiltinRules(teamConfig, localConfig, options) {
3212
3242
  log.debug(`Skipping built-in rules for ${tool}: tool not installed`);
3213
3243
  continue;
3214
3244
  }
3215
- if (localConfig && isAgentDisabled(localConfig, tool)) continue;
3245
+ if (localConfig && isAgentExcluded(localConfig, tool)) continue;
3216
3246
  const rulesDir = path14.join(baseDir, toolPath.rules);
3217
3247
  if (!await pathExists(rulesDir)) continue;
3218
3248
  try {
@@ -3451,7 +3481,7 @@ var init_pre_push_sync = __esm({
3451
3481
  });
3452
3482
 
3453
3483
  // src/providers/types.ts
3454
- var RepoNotFoundError;
3484
+ var RepoNotFoundError, OrganizationNotFoundError, RepoCreatePermissionError;
3455
3485
  var init_types2 = __esm({
3456
3486
  "src/providers/types.ts"() {
3457
3487
  "use strict";
@@ -3461,6 +3491,26 @@ var init_types2 = __esm({
3461
3491
  this.name = "RepoNotFoundError";
3462
3492
  }
3463
3493
  };
3494
+ OrganizationNotFoundError = class extends Error {
3495
+ org;
3496
+ createUrl;
3497
+ constructor(org, createUrl) {
3498
+ super(`Organization "${org}" not found.`);
3499
+ this.name = "OrganizationNotFoundError";
3500
+ this.org = org;
3501
+ this.createUrl = createUrl;
3502
+ }
3503
+ };
3504
+ RepoCreatePermissionError = class extends Error {
3505
+ repo;
3506
+ createUrl;
3507
+ constructor(repo, createUrl) {
3508
+ super(`No permission to create repo "${repo}".`);
3509
+ this.name = "RepoCreatePermissionError";
3510
+ this.repo = repo;
3511
+ this.createUrl = createUrl;
3512
+ }
3513
+ };
3464
3514
  }
3465
3515
  });
3466
3516
 
@@ -4023,20 +4073,82 @@ var init_tgit = __esm({
4023
4073
  }
4024
4074
  });
4025
4075
 
4026
- // src/providers/github/gh-cli.ts
4027
- import { execSync as execSync2, spawnSync as spawnSync2 } from "child_process";
4028
- function getGhPath() {
4076
+ // src/utils/cli-path.ts
4077
+ import { execFileSync } from "child_process";
4078
+ import { existsSync } from "fs";
4079
+ function pickWindowsCommand(whereOutput) {
4080
+ const lines = whereOutput.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
4081
+ for (const ext of WIN_EXEC_EXTENSIONS) {
4082
+ const hit = lines.find((line) => line.toLowerCase().endsWith(ext));
4083
+ if (hit !== void 0) return hit;
4084
+ }
4085
+ return null;
4086
+ }
4087
+ function whereOnWindows(cmd) {
4029
4088
  try {
4030
- const which = execSync2("which gh", {
4031
- encoding: "utf-8",
4032
- stdio: ["pipe", "pipe", "pipe"]
4089
+ const out = execFileSync("where", [cmd], {
4090
+ encoding: "utf8",
4091
+ stdio: ["ignore", "pipe", "ignore"],
4092
+ shell: false,
4093
+ timeout: CLI_DETECT_TIMEOUT_MS
4033
4094
  });
4034
- const trimmed = which.trim();
4035
- return trimmed || null;
4095
+ const p = pickWindowsCommand(out);
4096
+ return p !== null && existsSync(p) ? p : null;
4036
4097
  } catch {
4037
4098
  return null;
4038
4099
  }
4039
4100
  }
4101
+ function whichOnPosix(cmd) {
4102
+ try {
4103
+ const p = execFileSync("bash", ["-lc", `command -v ${cmd}`], {
4104
+ encoding: "utf8",
4105
+ stdio: ["ignore", "pipe", "ignore"],
4106
+ shell: false,
4107
+ timeout: CLI_DETECT_TIMEOUT_MS
4108
+ }).trim();
4109
+ if (p && existsSync(p)) return p;
4110
+ } catch {
4111
+ }
4112
+ try {
4113
+ const p = execFileSync("zsh", ["-lc", `command -v ${cmd}`], {
4114
+ encoding: "utf8",
4115
+ stdio: ["ignore", "pipe", "ignore"],
4116
+ shell: false,
4117
+ timeout: CLI_DETECT_TIMEOUT_MS
4118
+ }).trim();
4119
+ if (p && existsSync(p)) return p;
4120
+ } catch {
4121
+ }
4122
+ try {
4123
+ const p = execFileSync("which", [cmd], {
4124
+ encoding: "utf8",
4125
+ stdio: ["ignore", "pipe", "ignore"],
4126
+ shell: false,
4127
+ timeout: CLI_DETECT_TIMEOUT_MS
4128
+ }).trim();
4129
+ if (p && existsSync(p)) return p;
4130
+ } catch {
4131
+ }
4132
+ return null;
4133
+ }
4134
+ function resolveCliPath(cmd, platform = process.platform) {
4135
+ return platform === "win32" ? whereOnWindows(cmd) : whichOnPosix(cmd);
4136
+ }
4137
+ var CLI_DETECT_TIMEOUT_MS, WIN_EXEC_EXTENSIONS;
4138
+ var init_cli_path = __esm({
4139
+ "src/utils/cli-path.ts"() {
4140
+ "use strict";
4141
+ CLI_DETECT_TIMEOUT_MS = 5e3;
4142
+ WIN_EXEC_EXTENSIONS = [".exe", ".cmd", ".bat"];
4143
+ }
4144
+ });
4145
+
4146
+ // src/providers/github/gh-cli.ts
4147
+ import { spawnSync as spawnSync2 } from "child_process";
4148
+ import crossSpawn from "cross-spawn";
4149
+ function getGhPath() {
4150
+ return resolveCliPath("gh");
4151
+ }
4040
4152
  function isGhInstalled() {
4041
4153
  return getGhPath() !== null;
4042
4154
  }
@@ -4049,14 +4161,14 @@ function ghExec(args, options) {
4049
4161
  }
4050
4162
  log.debug(`gh exec: ${ghPath} ${args.join(" ")}`);
4051
4163
  if (options?.inheritStdio) {
4052
- const result2 = spawnSync2(ghPath, args, {
4164
+ const result2 = crossSpawn.sync(ghPath, args, {
4053
4165
  stdio: "inherit",
4054
4166
  env: { ...process.env, ...options.env ?? {} },
4055
4167
  cwd: options.cwd
4056
4168
  });
4057
4169
  return { stdout: "", stderr: "", status: result2.status ?? 1 };
4058
4170
  }
4059
- const result = spawnSync2(ghPath, args, {
4171
+ const result = crossSpawn.sync(ghPath, args, {
4060
4172
  env: { ...process.env, ...options?.env ?? {} },
4061
4173
  encoding: "utf-8",
4062
4174
  maxBuffer: 10 * 1024 * 1024,
@@ -4304,6 +4416,7 @@ var init_gh_cli = __esm({
4304
4416
  "src/providers/github/gh-cli.ts"() {
4305
4417
  "use strict";
4306
4418
  init_logger();
4419
+ init_cli_path();
4307
4420
  GITHUB_API = "https://api.github.com";
4308
4421
  RepoNotFoundError3 = class extends Error {
4309
4422
  constructor(repo) {
@@ -4566,14 +4679,20 @@ var init_github = __esm({
4566
4679
  });
4567
4680
 
4568
4681
  // src/providers/cnb/cnb-cli.ts
4569
- import { execSync as execSync3, spawnSync as spawnSync3 } from "child_process";
4682
+ import { execSync as execSync2, spawnSync as spawnSync3 } from "child_process";
4683
+ import crossSpawn2 from "cross-spawn";
4570
4684
  function cnbExec(args, options) {
4571
- log.debug(`cnb exec: cnb ${args.join(" ")}`);
4685
+ const cnbPath = resolveCliPath("cnb");
4686
+ if (!cnbPath) {
4687
+ log.debug("cnb CLI not found on PATH");
4688
+ return { stdout: "", stderr: "cnb CLI not found on PATH", status: 127 };
4689
+ }
4690
+ log.debug(`cnb exec: ${cnbPath} ${args.join(" ")}`);
4572
4691
  if (options?.inheritStdio) {
4573
- const r2 = spawnSync3("cnb", args, { stdio: "inherit", env: { ...process.env }, cwd: options.cwd });
4692
+ const r2 = crossSpawn2.sync(cnbPath, args, { stdio: "inherit", env: { ...process.env }, cwd: options.cwd });
4574
4693
  return { stdout: "", stderr: "", status: r2.status ?? 1 };
4575
4694
  }
4576
- const r = spawnSync3("cnb", args, {
4695
+ const r = crossSpawn2.sync(cnbPath, args, {
4577
4696
  env: { ...process.env },
4578
4697
  encoding: "utf-8",
4579
4698
  maxBuffer: 10 * 1024 * 1024,
@@ -4595,12 +4714,7 @@ function assertCnbApiOk(out, action) {
4595
4714
  }
4596
4715
  }
4597
4716
  function isCnbInstalled() {
4598
- try {
4599
- execSync3("which cnb", { stdio: ["pipe", "pipe", "pipe"] });
4600
- return true;
4601
- } catch {
4602
- return false;
4603
- }
4717
+ return resolveCliPath("cnb") !== null;
4604
4718
  }
4605
4719
  async function ensureCnbInstalled() {
4606
4720
  if (isCnbInstalled()) {
@@ -4609,7 +4723,7 @@ async function ensureCnbInstalled() {
4609
4723
  }
4610
4724
  const spin = spinner("Installing cnb CLI (@cnbcool/cnb-cli)...").start();
4611
4725
  try {
4612
- execSync3("npm install -g @cnbcool/cnb-cli", { stdio: ["pipe", "pipe", "pipe"], timeout: 12e4 });
4726
+ execSync2("npm install -g @cnbcool/cnb-cli", { stdio: ["pipe", "pipe", "pipe"], timeout: 12e4 });
4613
4727
  if (!isCnbInstalled()) throw new Error("cnb not found on PATH after install");
4614
4728
  spin.succeed("cnb CLI installed");
4615
4729
  } catch (e) {
@@ -4643,7 +4757,7 @@ function cnbWhoami() {
4643
4757
  }
4644
4758
  function cnbLogin() {
4645
4759
  log.info("Starting cnb authentication (OAuth2 device flow)...");
4646
- const r = cnbExec(["login"], { inheritStdio: true });
4760
+ const r = cnbExec(["login", "--host", CNB_HOST], { inheritStdio: true });
4647
4761
  if (r.status !== 0) throw new Error("cnb login failed. Please try again.");
4648
4762
  }
4649
4763
  function ensureCnbAuthenticated() {
@@ -4690,13 +4804,47 @@ function cnbRepoClone(repo, localPath) {
4690
4804
  const sanitized = out.replace(/cnb:[^@]+@/g, "cnb:***@").trim();
4691
4805
  throw new Error(`git clone failed: ${sanitized}`);
4692
4806
  }
4807
+ if (!token) {
4808
+ const cfg = spawnSync3("git", ["config", "--local", "credential.helper", "!cnb git-credential"], {
4809
+ encoding: "utf-8",
4810
+ stdio: ["pipe", "pipe", "pipe"],
4811
+ cwd: localPath
4812
+ });
4813
+ if (cfg.status !== 0) {
4814
+ log.warn(`Could not persist CNB credential helper: ${(cfg.stderr ?? "").trim()}. Push/pull may prompt for credentials.`);
4815
+ }
4816
+ }
4817
+ }
4818
+ function cnbOrganizationCreateUrl() {
4819
+ return `https://${CNB_HOST}/new/groups`;
4820
+ }
4821
+ function cnbRepoCreateUrl() {
4822
+ return `https://${CNB_HOST}/new/repos`;
4823
+ }
4824
+ function cnbOrganizationExists(org) {
4825
+ const r = cnbExec(["organizations", "get-group", "--group", org]);
4826
+ const out = r.stdout || r.stderr;
4827
+ if (/(?:^|["\s])status["\s:]+\s*200\b/.test(out)) return true;
4828
+ if (/(?:^|["\s])status["\s:]+\s*404\b/.test(out) || /not found|不存在/i.test(out)) return false;
4829
+ throw new Error(`cnb get-group failed for "${org}": ${out || `exit ${r.status}`}`);
4693
4830
  }
4694
4831
  async function cnbCreateRepo(owner, repo) {
4695
- const r = cnbExec(["repositories", "create-repo", "--slug", owner, "--name", repo]);
4696
- if (r.status !== 0) {
4697
- throw new Error(`cnb create-repo failed: ${r.stderr || r.stdout}`);
4832
+ try {
4833
+ const r = cnbExec(["repositories", "create-repo", "--slug", owner, "--name", repo]);
4834
+ if (r.status !== 0) {
4835
+ throw new Error(`cnb create-repo failed: ${r.stderr || r.stdout}`);
4836
+ }
4837
+ assertCnbApiOk(r.stdout, "create-repo");
4838
+ } catch (e) {
4839
+ const msg = e.message;
4840
+ if (/HTTP 404|not found|不存在/i.test(msg)) {
4841
+ throw new OrganizationNotFoundError(owner, cnbOrganizationCreateUrl());
4842
+ }
4843
+ if (/HTTP 403|scope|permission|forbidden|权限/i.test(msg)) {
4844
+ throw new RepoCreatePermissionError(`${owner}/${repo}`, cnbRepoCreateUrl());
4845
+ }
4846
+ throw e;
4698
4847
  }
4699
- assertCnbApiOk(r.stdout, "create-repo");
4700
4848
  }
4701
4849
  function cnbPullCreate(opts) {
4702
4850
  const args = [
@@ -4729,6 +4877,8 @@ var init_cnb_cli = __esm({
4729
4877
  "src/providers/cnb/cnb-cli.ts"() {
4730
4878
  "use strict";
4731
4879
  init_logger();
4880
+ init_cli_path();
4881
+ init_types2();
4732
4882
  CNB_HOST = process.env.TEAMAI_CNB_HOST?.trim() || "cnb.cool";
4733
4883
  CnbRepoNotFoundError = class extends Error {
4734
4884
  constructor(repo) {
@@ -4778,6 +4928,12 @@ var init_cnb = __esm({
4778
4928
  async createRepo(owner, repo) {
4779
4929
  await cnbCreateRepo(owner, repo);
4780
4930
  }
4931
+ organizationExists(org) {
4932
+ return cnbOrganizationExists(org);
4933
+ }
4934
+ getOrganizationCreateUrl() {
4935
+ return cnbOrganizationCreateUrl();
4936
+ }
4781
4937
  async createPullRequest(opts) {
4782
4938
  return cnbPullCreate({
4783
4939
  repo: opts.repo,
@@ -6317,6 +6473,8 @@ __export(providers_exports, {
6317
6473
  GitCodeProvider: () => GitCodeProvider,
6318
6474
  GitHubProvider: () => GitHubProvider,
6319
6475
  GitLabProvider: () => GitLabProvider,
6476
+ OrganizationNotFoundError: () => OrganizationNotFoundError,
6477
+ RepoCreatePermissionError: () => RepoCreatePermissionError,
6320
6478
  RepoNotFoundError: () => RepoNotFoundError,
6321
6479
  TGitProvider: () => TGitProvider,
6322
6480
  detectProvider: () => detectProvider,
@@ -6391,7 +6549,7 @@ async function deployBuiltinSkills(teamConfig, localConfig, options) {
6391
6549
  log.debug(`Skipping built-in skill deployment for ${tool}: tool not installed`);
6392
6550
  continue;
6393
6551
  }
6394
- if (localConfig && isAgentDisabled(localConfig, tool)) continue;
6552
+ if (localConfig && isAgentExcluded(localConfig, tool)) continue;
6395
6553
  for (const skillName of skillNames) {
6396
6554
  const srcDir = path18.join(builtinDir, skillName);
6397
6555
  const destDir = await resolveSkillDestination(tool, toolPath.skills, baseDir, skillName, srcDir);
@@ -6737,7 +6895,7 @@ var init_session_id = __esm({
6737
6895
 
6738
6896
  // src/pid-monitor.ts
6739
6897
  import fs9 from "fs";
6740
- import { execSync as execSync4 } from "child_process";
6898
+ import { execSync as execSync3 } from "child_process";
6741
6899
  function getParentPid(pid) {
6742
6900
  try {
6743
6901
  const stat8 = fs9.readFileSync(`/proc/${pid}/stat`, "utf-8");
@@ -6749,7 +6907,7 @@ function getParentPid(pid) {
6749
6907
  } catch {
6750
6908
  }
6751
6909
  try {
6752
- const out = execSync4(`ps -o ppid= -p ${pid}`, {
6910
+ const out = execSync3(`ps -o ppid= -p ${pid}`, {
6753
6911
  encoding: "utf-8",
6754
6912
  timeout: 2e3,
6755
6913
  stdio: ["pipe", "pipe", "pipe"]
@@ -6766,7 +6924,7 @@ function getProcessComm(pid) {
6766
6924
  } catch {
6767
6925
  }
6768
6926
  try {
6769
- const out = execSync4(`ps -o comm= -p ${pid}`, {
6927
+ const out = execSync3(`ps -o comm= -p ${pid}`, {
6770
6928
  encoding: "utf-8",
6771
6929
  timeout: 2e3,
6772
6930
  stdio: ["pipe", "pipe", "pipe"]
@@ -7875,7 +8033,7 @@ var init_agent_version = __esm({
7875
8033
  // src/machine-id.ts
7876
8034
  import crypto2 from "crypto";
7877
8035
  import fs11 from "fs";
7878
- import { execFileSync } from "child_process";
8036
+ import { execFileSync as execFileSync2 } from "child_process";
7879
8037
  import os6 from "os";
7880
8038
  function getMachineId() {
7881
8039
  if (cachedMachineId !== null) return cachedMachineId;
@@ -7901,7 +8059,7 @@ function detectMachineId(platform = process.platform) {
7901
8059
  return id || os6.hostname() || "";
7902
8060
  }
7903
8061
  function readDarwinMachineId() {
7904
- const out = execFileSync("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
8062
+ const out = execFileSync2("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
7905
8063
  encoding: "utf-8",
7906
8064
  timeout: 3e3
7907
8065
  });
@@ -7909,7 +8067,7 @@ function readDarwinMachineId() {
7909
8067
  return match ? match[1].trim() : "";
7910
8068
  }
7911
8069
  function readWindowsMachineId() {
7912
- const out = execFileSync(
8070
+ const out = execFileSync2(
7913
8071
  "reg",
7914
8072
  ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"],
7915
8073
  { encoding: "utf-8", timeout: 3e3 }
@@ -11762,9 +11920,16 @@ async function scanTeamRepoNamespaces(repoPath) {
11762
11920
  for (const dir of topDirs) {
11763
11921
  const dirPath = path29.join(teamSkillsDir, dir);
11764
11922
  const hasSkillMd = await pathExists(path29.join(dirPath, "SKILL.md"));
11765
- if (!hasSkillMd) {
11766
- namespaces.push(dir);
11923
+ if (hasSkillMd) continue;
11924
+ const subDirs = await listDirs(dirPath);
11925
+ let holdsSkill = false;
11926
+ for (const subDir of subDirs) {
11927
+ if (await pathExists(path29.join(dirPath, subDir, "SKILL.md"))) {
11928
+ holdsSkill = true;
11929
+ break;
11930
+ }
11767
11931
  }
11932
+ if (holdsSkill) namespaces.push(dir);
11768
11933
  }
11769
11934
  return namespaces;
11770
11935
  }
@@ -13179,7 +13344,7 @@ async function deployBuiltinAgents(teamConfig, localConfig, options) {
13179
13344
  log.debug(`Skipping built-in agent deployment for ${tool}: tool not installed`);
13180
13345
  continue;
13181
13346
  }
13182
- if (localConfig && isAgentDisabled(localConfig, tool)) continue;
13347
+ if (localConfig && isAgentExcluded(localConfig, tool)) continue;
13183
13348
  if (!ALL_SUPPORTED_TOOLS.includes(tool)) {
13184
13349
  log.warn(
13185
13350
  `Skipping built-in agent deployment for ${tool}: unsupported agent format; disable this target or add a native renderer`
@@ -15697,6 +15862,15 @@ async function initSelfRepo(options) {
15697
15862
  log.info(" 4. Push your business repo (e.g. `git push -u origin HEAD`) so teammates get the .teamai/ knowledge and are auto-initialized on clone.");
15698
15863
  closePrompt();
15699
15864
  }
15865
+ function guideWebCreation(kind, name, url) {
15866
+ log.info(`${kind} "${name}" can't be created from the CLI (insufficient token permission).`);
15867
+ if (url) {
15868
+ log.info(`Create it here, then re-run this command:`);
15869
+ log.info(` ${url}`);
15870
+ } else {
15871
+ log.info(`Create the ${kind} on the platform, then re-run this command.`);
15872
+ }
15873
+ }
15700
15874
  async function init(options) {
15701
15875
  if (options.http) {
15702
15876
  return initHttp(options.http, options);
@@ -15848,6 +16022,23 @@ async function init(options) {
15848
16022
  } catch (e) {
15849
16023
  if (e instanceof RepoNotFoundError) {
15850
16024
  cloneSpin.info(`Repo ${repoInfo.owner}/${repoInfo.repo} does not exist`);
16025
+ if (typeof provider.organizationExists === "function") {
16026
+ let orgMissing = false;
16027
+ try {
16028
+ orgMissing = !provider.organizationExists(repoInfo.owner);
16029
+ } catch (checkErr) {
16030
+ log.debug(`Organization check failed: ${checkErr.message}`);
16031
+ }
16032
+ if (orgMissing) {
16033
+ cloneSpin.info(`Organization "${repoInfo.owner}" does not exist`);
16034
+ guideWebCreation(
16035
+ "organization",
16036
+ repoInfo.owner,
16037
+ provider.getOrganizationCreateUrl?.() ?? null
16038
+ );
16039
+ process.exit(1);
16040
+ }
16041
+ }
15851
16042
  const confirmed = await askConfirmation(
15852
16043
  `Create repo ${repoInfo.owner}/${repoInfo.repo}? [Y/n] `,
15853
16044
  true
@@ -15861,6 +16052,16 @@ async function init(options) {
15861
16052
  await provider.createRepo(repoInfo.owner, repoInfo.repo);
15862
16053
  createSpin.succeed(`Repo ${repoInfo.owner}/${repoInfo.repo} created`);
15863
16054
  } catch (ce) {
16055
+ if (ce instanceof OrganizationNotFoundError) {
16056
+ createSpin.fail(`Organization "${ce.org}" does not exist`);
16057
+ guideWebCreation("organization", ce.org, ce.createUrl ?? null);
16058
+ process.exit(1);
16059
+ }
16060
+ if (ce instanceof RepoCreatePermissionError) {
16061
+ createSpin.fail(`No permission to create repo "${ce.repo}" from the CLI`);
16062
+ guideWebCreation("repo", ce.repo, ce.createUrl ?? null);
16063
+ process.exit(1);
16064
+ }
15864
16065
  const msg = ce.message;
15865
16066
  if (/already been taken|already exists/i.test(msg)) {
15866
16067
  createSpin.info(`Repo ${repoInfo.owner}/${repoInfo.repo} already exists, retrying clone`);
@@ -16241,6 +16442,9 @@ ${items.map((i) => `- [${i.type}] ${i.name}`).join("\n")}`
16241
16442
  items: toPendingItems(items)
16242
16443
  });
16243
16444
  await checkoutMaster(localConfig.repo.localPath);
16445
+ for (const rel of pushedFiles) {
16446
+ await pruneEmptyDirs(path45.resolve(localConfig.repo.localPath, rel));
16447
+ }
16244
16448
  return true;
16245
16449
  } catch (e) {
16246
16450
  pushSpin.fail(`Push failed: ${e.message}`);
@@ -22021,7 +22225,7 @@ async function refreshTeamRepo(localConfig) {
22021
22225
  if (!apiKey) {
22022
22226
  throw new Error("No API key configured. Re-run `teamai init --http <url> --token <key>` or set TEAMAI_API_TOKEN.");
22023
22227
  }
22024
- return { label: "HTTP (report/sync delivery)", version: null, reportingOnly: true };
22228
+ return { label: "HTTP (report/sync delivery)", version: null, reportingOnly: true, submodulesFailed: false, submodulesChanged: false };
22025
22229
  }
22026
22230
  if (localConfig.repo.kind === "self") {
22027
22231
  try {
@@ -22035,7 +22239,7 @@ async function refreshTeamRepo(localConfig) {
22035
22239
  } catch {
22036
22240
  version3 = null;
22037
22241
  }
22038
- return { label: "single-repo (knowledge on main)", version: version3, reportingOnly: false };
22242
+ return { label: "single-repo (knowledge on main)", version: version3, reportingOnly: false, submodulesFailed: false, submodulesChanged: false };
22039
22243
  }
22040
22244
  const result = await pullRepo(localConfig.repo.localPath);
22041
22245
  try {
@@ -22050,7 +22254,35 @@ async function refreshTeamRepo(localConfig) {
22050
22254
  log.debug("Rev check failed, proceeding with full sync");
22051
22255
  version2 = null;
22052
22256
  }
22053
- return { label: result, version: version2, reportingOnly: false };
22257
+ let submodulesFailed = false;
22258
+ let submodulesChanged = false;
22259
+ try {
22260
+ const teamConfig = await loadTeamConfig(localConfig.repo.localPath);
22261
+ if (teamConfig?.submodules) {
22262
+ const git = createGit2(localConfig.repo.localPath);
22263
+ let before = null;
22264
+ try {
22265
+ before = await git.subModule(["status"]);
22266
+ } catch {
22267
+ before = null;
22268
+ }
22269
+ await git.submoduleUpdate(["--init"]);
22270
+ let after = null;
22271
+ try {
22272
+ after = await git.subModule(["status"]);
22273
+ } catch {
22274
+ after = null;
22275
+ }
22276
+ submodulesChanged = before === null || before !== after;
22277
+ log.debug(
22278
+ submodulesChanged ? "Submodules updated (tree changed \u2014 full sync this pull)" : "Submodules updated (no change)"
22279
+ );
22280
+ }
22281
+ } catch (e) {
22282
+ submodulesFailed = true;
22283
+ log.warn(`Submodule update failed for ${localConfig.repo.localPath}: ${e.message}`);
22284
+ }
22285
+ return { label: result, version: version2, reportingOnly: false, submodulesFailed, submodulesChanged };
22054
22286
  }
22055
22287
  async function usageReportDisabled(repoPath) {
22056
22288
  return (await loadTeamConfig(repoPath))?.usageReport === false;
@@ -22169,7 +22401,7 @@ async function skillSafeToRemove(deployedDir, source) {
22169
22401
  async function cleanupInactiveNamespaceSkills(teamConfig, localConfig, retainedSkillNames, inactiveSkillNames, inactiveSkillSources) {
22170
22402
  const baseDir = resolveBaseDir(localConfig);
22171
22403
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
22172
- if (isAgentDisabled(localConfig, tool)) continue;
22404
+ if (isAgentExcluded(localConfig, tool)) continue;
22173
22405
  if (!toolPath.skills) continue;
22174
22406
  if (!await ResourceHandler.isToolInstalled(toolPath.skills, baseDir)) continue;
22175
22407
  if (!await pathExists(path65.join(baseDir, toolPath.skills))) continue;
@@ -22230,7 +22462,7 @@ async function getInstalledResourceTargets(teamConfig, localConfig) {
22230
22462
  const baseDir = resolveBaseDir(localConfig);
22231
22463
  const targets = [];
22232
22464
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
22233
- if (isAgentDisabled(localConfig, tool)) continue;
22465
+ if (isAgentExcluded(localConfig, tool)) continue;
22234
22466
  const resourcePaths = [toolPath.skills, toolPath.rules, toolPath.agents].filter((resourcePath) => !!resourcePath);
22235
22467
  for (const resourcePath of resourcePaths) {
22236
22468
  if (await ResourceHandler.isToolInstalled(resourcePath, baseDir)) {
@@ -22253,17 +22485,21 @@ async function pullForScope(localConfig, options, policy = {}) {
22253
22485
  const pullSpin = spinner(`[${scopeLabel}] Pulling team repo...`).start();
22254
22486
  let currentRev = null;
22255
22487
  let reportingOnly = false;
22256
- try {
22257
- const { label, version: version2, reportingOnly: ro } = await refreshTeamRepo(localConfig);
22258
- currentRev = version2;
22259
- reportingOnly = ro;
22260
- pullSpin.succeed(`[${scopeLabel}] Team repo: ${label}`);
22488
+ let submodulesFailed = false;
22489
+ let submodulesChanged = false;
22490
+ try {
22491
+ const refresh = await refreshTeamRepo(localConfig);
22492
+ currentRev = refresh.version;
22493
+ reportingOnly = refresh.reportingOnly;
22494
+ submodulesFailed = refresh.submodulesFailed;
22495
+ submodulesChanged = refresh.submodulesChanged;
22496
+ pullSpin.succeed(`[${scopeLabel}] Team repo: ${refresh.label}`);
22261
22497
  } catch (e) {
22262
22498
  pullSpin.fail(`[${scopeLabel}] Pull failed: ${e.message}`);
22263
22499
  return;
22264
22500
  }
22265
22501
  let currentTargets = null;
22266
- if (!options.force && !options.dryRun) {
22502
+ if (!options.force && !options.dryRun && !submodulesChanged) {
22267
22503
  try {
22268
22504
  const state = await loadStateForScope(localConfig);
22269
22505
  if (currentRev && state[revisionField] && state[revisionField] === currentRev) {
@@ -22444,7 +22680,7 @@ async function pullForScope(localConfig, options, policy = {}) {
22444
22680
  const dir = toolPath[toolPathField];
22445
22681
  if (!dir) continue;
22446
22682
  if (!await ResourceHandler.isToolInstalled(dir, baseDir)) continue;
22447
- if (isAgentDisabled(localConfig, tool)) continue;
22683
+ if (isAgentExcluded(localConfig, tool)) continue;
22448
22684
  const extensions = type === "rules" ? [.../* @__PURE__ */ new Set([ruleFileExtensionForTool(tool), ".md"])] : [ext];
22449
22685
  for (const name of tombstones) {
22450
22686
  for (const extension of extensions) {
@@ -22474,7 +22710,7 @@ async function pullForScope(localConfig, options, policy = {}) {
22474
22710
  if (!options.dryRun && desiredSkillNames && knownRepoSkillNames) {
22475
22711
  const baseDir = resolveBaseDir(localConfig);
22476
22712
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(freshConfig, localConfig))) {
22477
- if (isAgentDisabled(localConfig, tool)) continue;
22713
+ if (isAgentExcluded(localConfig, tool)) continue;
22478
22714
  if (!toolPath.skills) continue;
22479
22715
  if (!await ResourceHandler.isToolInstalled(toolPath.skills, baseDir)) continue;
22480
22716
  const skillsDir = path65.join(baseDir, toolPath.skills);
@@ -22593,7 +22829,7 @@ async function pullForScope(localConfig, options, policy = {}) {
22593
22829
  if (compiled) {
22594
22830
  const baseDir = resolveBaseDir(localConfig);
22595
22831
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(freshConfig, localConfig))) {
22596
- if (isAgentDisabled(localConfig, tool)) continue;
22832
+ if (isAgentExcluded(localConfig, tool)) continue;
22597
22833
  if (!toolPath.claudemd) continue;
22598
22834
  if (toolPath.rules && !await ResourceHandler.isToolInstalled(toolPath.rules, baseDir)) continue;
22599
22835
  const claudeMdPath = path65.join(baseDir, toolPath.claudemd);
@@ -22623,7 +22859,7 @@ async function pullForScope(localConfig, options, policy = {}) {
22623
22859
  if (compiled) {
22624
22860
  const baseDir = resolveBaseDir(localConfig);
22625
22861
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(freshConfig, localConfig))) {
22626
- if (isAgentDisabled(localConfig, tool)) continue;
22862
+ if (isAgentExcluded(localConfig, tool)) continue;
22627
22863
  if (!toolPath.claudemd) continue;
22628
22864
  if (toolPath.rules && !await ResourceHandler.isToolInstalled(toolPath.rules, baseDir)) continue;
22629
22865
  const claudeMdPath = path65.join(baseDir, toolPath.claudemd);
@@ -22685,13 +22921,15 @@ async function pullForScope(localConfig, options, policy = {}) {
22685
22921
  if (revisionField === "lastPullRev") {
22686
22922
  state.lastPull = (/* @__PURE__ */ new Date()).toISOString();
22687
22923
  }
22688
- if (currentRev !== null) {
22689
- state[revisionField] = currentRev;
22690
- } else {
22691
- try {
22692
- state[revisionField] = await getHeadRev(localConfig.repo.localPath);
22693
- } catch {
22694
- state[revisionField] = null;
22924
+ if (!submodulesFailed) {
22925
+ if (currentRev !== null) {
22926
+ state[revisionField] = currentRev;
22927
+ } else {
22928
+ try {
22929
+ state[revisionField] = await getHeadRev(localConfig.repo.localPath);
22930
+ } catch {
22931
+ state[revisionField] = null;
22932
+ }
22695
22933
  }
22696
22934
  }
22697
22935
  state[targetsField] = currentTargets ?? await getInstalledResourceTargets(freshConfig, localConfig);
@@ -22800,7 +23038,7 @@ async function injectRecallBlockIntoTools(config, localConfig, scopeLabel) {
22800
23038
  const recallBlock = compileRecallRulesBlock();
22801
23039
  let injected = 0;
22802
23040
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(config, localConfig))) {
22803
- if (isAgentDisabled(localConfig, tool)) continue;
23041
+ if (isAgentExcluded(localConfig, tool)) continue;
22804
23042
  if (!toolPath.claudemd || !toolPath.agents) continue;
22805
23043
  if (!await ResourceHandler.isToolInstalled(toolPath.agents, baseDir)) continue;
22806
23044
  const claudeMdPath = path65.join(baseDir, toolPath.claudemd);
@@ -27039,67 +27277,7 @@ __export(ai_client_exports, {
27039
27277
  pickWindowsCommand: () => pickWindowsCommand,
27040
27278
  resolveCliPath: () => resolveCliPath
27041
27279
  });
27042
- import { execFileSync as execFileSync2 } from "child_process";
27043
27280
  import spawn2 from "cross-spawn";
27044
- import { existsSync } from "fs";
27045
- function pickWindowsCommand(whereOutput) {
27046
- const lines = whereOutput.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
27047
- for (const ext of WIN_EXEC_EXTENSIONS) {
27048
- const hit = lines.find((line) => line.toLowerCase().endsWith(ext));
27049
- if (hit !== void 0) return hit;
27050
- }
27051
- return null;
27052
- }
27053
- function whereOnWindows(cmd) {
27054
- try {
27055
- const out = execFileSync2("where", [cmd], {
27056
- encoding: "utf8",
27057
- stdio: ["ignore", "pipe", "ignore"],
27058
- shell: false,
27059
- timeout: CLI_DETECT_TIMEOUT_MS
27060
- });
27061
- const p = pickWindowsCommand(out);
27062
- return p !== null && existsSync(p) ? p : null;
27063
- } catch {
27064
- return null;
27065
- }
27066
- }
27067
- function whichOnPosix(cmd) {
27068
- try {
27069
- const p = execFileSync2("bash", ["-lc", `command -v ${cmd}`], {
27070
- encoding: "utf8",
27071
- stdio: ["ignore", "pipe", "ignore"],
27072
- shell: false,
27073
- timeout: CLI_DETECT_TIMEOUT_MS
27074
- }).trim();
27075
- if (p && existsSync(p)) return p;
27076
- } catch {
27077
- }
27078
- try {
27079
- const p = execFileSync2("zsh", ["-lc", `command -v ${cmd}`], {
27080
- encoding: "utf8",
27081
- stdio: ["ignore", "pipe", "ignore"],
27082
- shell: false,
27083
- timeout: CLI_DETECT_TIMEOUT_MS
27084
- }).trim();
27085
- if (p && existsSync(p)) return p;
27086
- } catch {
27087
- }
27088
- try {
27089
- const p = execFileSync2("which", [cmd], {
27090
- encoding: "utf8",
27091
- stdio: ["ignore", "pipe", "ignore"],
27092
- shell: false,
27093
- timeout: CLI_DETECT_TIMEOUT_MS
27094
- }).trim();
27095
- if (p && existsSync(p)) return p;
27096
- } catch {
27097
- }
27098
- return null;
27099
- }
27100
- function resolveCliPath(cmd, platform = process.platform) {
27101
- return platform === "win32" ? whereOnWindows(cmd) : whichOnPosix(cmd);
27102
- }
27103
27281
  function detectClaudeCli() {
27104
27282
  for (const cmd of ALLOWED_CLI_CANDIDATES) {
27105
27283
  const absPath = resolveCliPath(cmd);
@@ -27214,11 +27392,13 @@ async function runWithConcurrency(tasks, concurrency) {
27214
27392
  await Promise.all(promises);
27215
27393
  return results;
27216
27394
  }
27217
- var ALLOWED_CLI_CANDIDATES, CLI_DETECT_TIMEOUT_MS, WIN_EXEC_EXTENSIONS, DEFAULT_TIMEOUT_MS, DEFAULT_CONCURRENCY, _cliInfo;
27395
+ var ALLOWED_CLI_CANDIDATES, DEFAULT_TIMEOUT_MS, DEFAULT_CONCURRENCY, _cliInfo;
27218
27396
  var init_ai_client = __esm({
27219
27397
  "src/utils/ai-client.ts"() {
27220
27398
  "use strict";
27221
27399
  init_logger();
27400
+ init_cli_path();
27401
+ init_cli_path();
27222
27402
  ALLOWED_CLI_CANDIDATES = [
27223
27403
  "claude",
27224
27404
  "claude-internal",
@@ -27230,8 +27410,6 @@ var init_ai_client = __esm({
27230
27410
  "workbuddy",
27231
27411
  "openclaw"
27232
27412
  ];
27233
- CLI_DETECT_TIMEOUT_MS = 5e3;
27234
- WIN_EXEC_EXTENSIONS = [".exe", ".cmd", ".bat"];
27235
27413
  DEFAULT_TIMEOUT_MS = 6e5;
27236
27414
  DEFAULT_CONCURRENCY = 3;
27237
27415
  }
@@ -33164,7 +33342,7 @@ var init_import_iwiki = __esm({
33164
33342
  });
33165
33343
 
33166
33344
  // src/providers/github/mr-fetch.ts
33167
- import { execSync as execSync5 } from "child_process";
33345
+ import { execSync as execSync4 } from "child_process";
33168
33346
  import https2 from "https";
33169
33347
  function parseGitHubPRUrl(url) {
33170
33348
  const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
@@ -33251,12 +33429,12 @@ async function fetchGitHubPR(url) {
33251
33429
  const repoArg = `${owner}/${repo}`;
33252
33430
  log.debug(`fetchGitHubPR: ${repoArg}#${number}`);
33253
33431
  try {
33254
- const viewOutput = execSync5(
33432
+ const viewOutput = execSync4(
33255
33433
  `gh pr view ${number} --repo ${repoArg} --json title,body,author,mergedAt,commits`,
33256
33434
  { maxBuffer: 10 * 1024 * 1024, encoding: "utf8" }
33257
33435
  );
33258
33436
  const prView = JSON.parse(viewOutput);
33259
- const rawDiff = execSync5(
33437
+ const rawDiff = execSync4(
33260
33438
  `gh pr diff ${number} --repo ${repoArg}`,
33261
33439
  { maxBuffer: 50 * 1024 * 1024, encoding: "utf8" }
33262
33440
  );
@@ -33670,7 +33848,7 @@ var init_import_mr = __esm({
33670
33848
  });
33671
33849
 
33672
33850
  // src/codebase.ts
33673
- import { execSync as execSync6 } from "child_process";
33851
+ import { execSync as execSync5 } from "child_process";
33674
33852
  import fs36 from "fs";
33675
33853
  import path103 from "path";
33676
33854
  import matter12 from "gray-matter";
@@ -33686,7 +33864,7 @@ ${commitMessages}`);
33686
33864
  log.debug(`gatherRepoContext: git log \u5931\u8D25 \u2014 ${String(err)}`);
33687
33865
  }
33688
33866
  try {
33689
- const rawTree = execSync6(
33867
+ const rawTree = execSync5(
33690
33868
  'find . -maxdepth 4 -not -path "*/.git/*" -not -path "*/node_modules/*" -not -path "*/__pycache__/*" -not -path "*/dist/*" -not -path "*/.claude/worktrees/*" -not -name "*.js.map"',
33691
33869
  { cwd: repoPath, encoding: "utf-8" }
33692
33870
  );
@@ -36835,9 +37013,12 @@ var init_templates = __esm({
36835
37013
  // src/enrich-with-ai.ts
36836
37014
  var enrich_with_ai_exports = {};
36837
37015
  __export(enrich_with_ai_exports, {
37016
+ buildFallbackManifest: () => buildFallbackManifest,
37017
+ describeEvidenceManifest: () => describeEvidenceManifest,
36838
37018
  edgeProvenanceRank: () => edgeProvenanceRank,
36839
37019
  edgeReason: () => edgeReason,
36840
37020
  enrichWithAI: () => enrichWithAI,
37021
+ groupFactsByModule: () => groupFactsByModule,
36841
37022
  parseEdgeProvenance: () => parseEdgeProvenance,
36842
37023
  writeManifest: () => writeManifest
36843
37024
  });
@@ -36907,6 +37088,62 @@ function resolveImportToModule(importerFile, importPath) {
36907
37088
  if (first.startsWith("@")) return void 0;
36908
37089
  return first;
36909
37090
  }
37091
+ function groupFactsByModule(facts) {
37092
+ const modules = /* @__PURE__ */ new Map();
37093
+ for (const fact of facts) {
37094
+ if (fact.kind === "relation") continue;
37095
+ const mod = fact.file.split("/")[0] || "_root";
37096
+ const existing = modules.get(mod) ?? [];
37097
+ existing.push(fact);
37098
+ modules.set(mod, existing);
37099
+ }
37100
+ return modules;
37101
+ }
37102
+ function isSafeSlug(name) {
37103
+ try {
37104
+ assertSafeResourceName(name);
37105
+ return true;
37106
+ } catch {
37107
+ return false;
37108
+ }
37109
+ }
37110
+ function buildFallbackManifest(ctx) {
37111
+ const moduleNames = [...ctx.modules.keys()].filter(isSafeSlug);
37112
+ const componentNames = [...new Set(
37113
+ ctx.facts.filter((f) => f.kind === "component").map((f) => f.name)
37114
+ )].filter(isSafeSlug);
37115
+ const slugs = moduleNames.length > 0 ? moduleNames : componentNames;
37116
+ if (slugs.length === 0) return null;
37117
+ const components = slugs.map((name) => {
37118
+ const moduleFacts = ctx.modules.get(name) ?? ctx.facts.filter((f) => f.name === name);
37119
+ const entrypoints = moduleFacts.filter((f) => f.kind === "component").filter((f) => /handler|route|controller|endpoint|main|server|app/i.test(f.name)).slice(0, 5).map((f) => `${f.name} (${f.file}:${f.lineStart})`);
37120
+ return {
37121
+ slug: name,
37122
+ docPath: `evidence/code/${ctx.project}/${name}.md`,
37123
+ title: name,
37124
+ category: "component",
37125
+ confidence: "EXTRACTED",
37126
+ ...entrypoints.length > 0 ? { entrypoints } : {}
37127
+ };
37128
+ });
37129
+ return {
37130
+ schemaVersion: "team-wiki.codebase-output-manifest.v2",
37131
+ project: ctx.project,
37132
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
37133
+ components,
37134
+ edges: []
37135
+ };
37136
+ }
37137
+ function describeEvidenceManifest(source, componentCount) {
37138
+ if (source === "fallback") {
37139
+ const noun = componentCount === 1 ? "component" : "components";
37140
+ return `Wrote fallback _manifest.json (${componentCount} ${noun}, no AI enrich)`;
37141
+ }
37142
+ if (source === "none") {
37143
+ return "AI enrich produced no manifest; deep-enrich will have no components";
37144
+ }
37145
+ return void 0;
37146
+ }
36910
37147
  async function enrichWithAI(ctx) {
36911
37148
  const moduleEntries = [...ctx.modules.entries()].filter(([, facts]) => facts.length >= 5);
36912
37149
  if (moduleEntries.length === 0) {
@@ -37045,6 +37282,7 @@ var init_enrich_with_ai = __esm({
37045
37282
  "use strict";
37046
37283
  init_ai_client();
37047
37284
  init_logger();
37285
+ init_path_safety();
37048
37286
  }
37049
37287
  });
37050
37288
 
@@ -37566,39 +37804,53 @@ async function extractCodebase(opts) {
37566
37804
  const repoGraph = mergeGraphs(graph, overlay);
37567
37805
  await saveGraphIndex(wikiRoot, repoGraph);
37568
37806
  let aiDomains = [];
37807
+ let manifestSource = "none";
37808
+ let manifestComponentCount = 0;
37809
+ const {
37810
+ enrichWithAI: enrichWithAI2,
37811
+ writeManifest: writeManifest2,
37812
+ buildFallbackManifest: buildFallbackManifest2,
37813
+ groupFactsByModule: groupFactsByModule2,
37814
+ describeEvidenceManifest: describeEvidenceManifest2
37815
+ } = await Promise.resolve().then(() => (init_enrich_with_ai(), enrich_with_ai_exports));
37816
+ const modules = groupFactsByModule2(facts);
37569
37817
  if (opts.skipEnrich) {
37570
37818
  if (!opts.json) console.log(chalk3.dim(" [AI enrich: skipped (--skip-enrich)]"));
37571
- } else try {
37572
- const { enrichWithAI: enrichWithAI2, writeManifest: writeManifest2 } = await Promise.resolve().then(() => (init_enrich_with_ai(), enrich_with_ai_exports));
37573
- const modules = /* @__PURE__ */ new Map();
37574
- for (const fact of facts) {
37575
- if (fact.kind === "relation") continue;
37576
- const mod = fact.file.split("/")[0] || "_root";
37577
- const existing = modules.get(mod) ?? [];
37578
- existing.push(fact);
37579
- modules.set(mod, existing);
37580
- }
37581
- const enrichResult = await enrichWithAI2({ project, facts, interfaceInventory, modules });
37582
- if (enrichResult) {
37583
- await writeManifest2(enrichResult.manifest, evidenceDir);
37584
- aiDomains = enrichResult.domains;
37585
- const domainMeta = {
37586
- domain: enrichResult.repoDomain || (enrichResult.domains[0]?.name ?? ""),
37587
- description: enrichResult.repoDescription || "",
37588
- keywords: enrichResult.repoKeywords || [],
37589
- components: enrichResult.domains[0]?.components ?? []
37590
- };
37591
- await writeFile10(path112.join(evidenceDir, "_domains.json"), JSON.stringify(domainMeta, null, 2), "utf-8");
37819
+ } else {
37820
+ try {
37821
+ const enrichResult = await enrichWithAI2({ project, facts, interfaceInventory, modules });
37822
+ if (enrichResult) {
37823
+ await writeManifest2(enrichResult.manifest, evidenceDir);
37824
+ manifestSource = "ai";
37825
+ manifestComponentCount = enrichResult.manifest.components.length;
37826
+ aiDomains = enrichResult.domains;
37827
+ const domainMeta = {
37828
+ domain: enrichResult.repoDomain || (enrichResult.domains[0]?.name ?? ""),
37829
+ description: enrichResult.repoDescription || "",
37830
+ keywords: enrichResult.repoKeywords || [],
37831
+ components: enrichResult.domains[0]?.components ?? []
37832
+ };
37833
+ await writeFile10(path112.join(evidenceDir, "_domains.json"), JSON.stringify(domainMeta, null, 2), "utf-8");
37834
+ if (!opts.json) {
37835
+ const domainLabel = domainMeta.domain || "uncategorized";
37836
+ console.log(` AI enrich: ${enrichResult.manifest.components.length} modules, domain=${domainLabel}`);
37837
+ }
37838
+ }
37839
+ } catch (e) {
37592
37840
  if (!opts.json) {
37593
- const domainLabel = domainMeta.domain || "uncategorized";
37594
- console.log(` AI enrich: ${enrichResult.manifest.components.length} modules, domain=${domainLabel}`);
37841
+ console.log(chalk3.dim(` [AI enrich skipped: ${e.message}]`));
37595
37842
  }
37596
37843
  }
37597
- } catch (e) {
37598
- if (!opts.json) {
37599
- console.log(chalk3.dim(` [AI enrich skipped: ${e.message}]`));
37844
+ }
37845
+ if (manifestSource === "none") {
37846
+ const fallback = buildFallbackManifest2({ project, facts, modules });
37847
+ if (fallback && fallback.components.length > 0) {
37848
+ await writeManifest2(fallback, evidenceDir);
37849
+ manifestSource = "fallback";
37850
+ manifestComponentCount = fallback.components.length;
37600
37851
  }
37601
37852
  }
37853
+ const manifestNote = describeEvidenceManifest2(manifestSource, manifestComponentCount);
37602
37854
  const moduleSummaries = buildModuleSummaries(facts, graph, project);
37603
37855
  if (moduleSummaries.size > 0) {
37604
37856
  const modulesDir = path112.join(evidenceDir, "modules");
@@ -37709,7 +37961,13 @@ async function extractCodebase(opts) {
37709
37961
  facts: { total: facts.length, byKind },
37710
37962
  graph: { nodes: repoGraph.nodes.length, edges: repoGraph.edges.length },
37711
37963
  incremental: !!opts.incremental && !!changedFiles,
37712
- outputDir: wikiRoot
37964
+ outputDir: wikiRoot,
37965
+ manifest: {
37966
+ written: manifestSource !== "none",
37967
+ source: manifestSource,
37968
+ components: manifestComponentCount,
37969
+ ...manifestNote ? { note: manifestNote } : {}
37970
+ }
37713
37971
  };
37714
37972
  if (opts.json) {
37715
37973
  console.log(JSON.stringify(result, null, 2));
@@ -37727,6 +37985,9 @@ async function extractCodebase(opts) {
37727
37985
  console.log(` Call chains: ${callChains.length} chains (max depth ${Math.max(...callChains.map((c) => c.depth))})`);
37728
37986
  }
37729
37987
  console.log(` Output: ${wikiRoot}`);
37988
+ if (manifestNote) {
37989
+ console.log(` ${manifestNote}`);
37990
+ }
37730
37991
  }
37731
37992
  }
37732
37993
  var init_codebase_extract = __esm({
@@ -37995,7 +38256,8 @@ var init_repo_cache = __esm({
37995
38256
  // src/deep-enrich.ts
37996
38257
  var deep_enrich_exports = {};
37997
38258
  __export(deep_enrich_exports, {
37998
- deepEnrich: () => deepEnrich
38259
+ deepEnrich: () => deepEnrich,
38260
+ runHiddenDeepEnrich: () => runHiddenDeepEnrich
37999
38261
  });
38000
38262
  import { readFile as readFile11, writeFile as writeFile11, readdir as readdir4, mkdir as mkdir8, unlink as unlink2 } from "fs/promises";
38001
38263
  import path114 from "path";
@@ -38577,6 +38839,18 @@ async function invalidateStaleAiDocs(docsDir, slugs) {
38577
38839
  await removeIfExists(architectureDocPath(docsDir));
38578
38840
  await removeIfExists(path114.join(docsDir, "graph-g5-scenarios.md"));
38579
38841
  }
38842
+ async function runHiddenDeepEnrich(opts) {
38843
+ const wikiRoot = opts.wikiRoot ?? path114.join(process.cwd(), ".teamai", "team-repo", "teamwiki");
38844
+ const evidenceDir = path114.join(wikiRoot, "evidence", "code", opts.project);
38845
+ const result = await deepEnrich({
38846
+ project: opts.project,
38847
+ evidenceDir,
38848
+ wikiRoot,
38849
+ maxModules: opts.maxModules
38850
+ });
38851
+ if (!result.complete) process.exitCode = 1;
38852
+ return result;
38853
+ }
38580
38854
  async function deepEnrich(opts) {
38581
38855
  const { project, evidenceDir } = opts;
38582
38856
  const docsDir = path114.join(evidenceDir, "docs");
@@ -40789,7 +41063,7 @@ async function codebaseCmd(opts) {
40789
41063
  componentCount = 0;
40790
41064
  }
40791
41065
  if (componentCount === 0) {
40792
- console.log(`No components in evidence for project "${project}". Run \`teamai codebase --extract\` first.`);
41066
+ console.log(`No components in _manifest.json for project "${project}".`);
40793
41067
  process.exitCode = 1;
40794
41068
  return;
40795
41069
  }
@@ -42433,11 +42707,12 @@ ciCmd.command("extract-mr").description("Extract knowledge from MR/PR and post a
42433
42707
  await ciExtractMr2({ ...globalOpts, ...cmdOpts });
42434
42708
  });
42435
42709
  program.command("deep-enrich", { hidden: true }).description("Run deep AI knowledge generation for an imported repo").requiredOption("--project <slug>", "Project slug (directory name in evidence/code/)").option("--wiki-root <path>", "Teamwiki root path").option("--max-modules <n>", "Max modules to process (cost control)", parseInt).action(async (cmdOpts) => {
42436
- const p = await import("path");
42437
- const wikiRoot = cmdOpts.wikiRoot ?? p.join(process.cwd(), ".teamai", "team-repo", "teamwiki");
42438
- const evidenceDir = p.join(wikiRoot, "evidence", "code", cmdOpts.project);
42439
- const { deepEnrich: deepEnrich2 } = await Promise.resolve().then(() => (init_deep_enrich(), deep_enrich_exports));
42440
- await deepEnrich2({ project: cmdOpts.project, evidenceDir, wikiRoot, maxModules: cmdOpts.maxModules });
42710
+ const { runHiddenDeepEnrich: runHiddenDeepEnrich2 } = await Promise.resolve().then(() => (init_deep_enrich(), deep_enrich_exports));
42711
+ await runHiddenDeepEnrich2({
42712
+ project: cmdOpts.project,
42713
+ wikiRoot: cmdOpts.wikiRoot,
42714
+ maxModules: cmdOpts.maxModules
42715
+ });
42441
42716
  });
42442
42717
  recallCmd.command("feedback").description("Record manual feedback for a recalled document").option("--positive <docId>", "Upvote a document (marks as actually useful)").option("--negative <docId>", "Record negative signal for a document").action(async (cmdOpts) => {
42443
42718
  const { recallFeedback: recallFeedback2 } = await Promise.resolve().then(() => (init_votes(), votes_exports));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamai-cli",
3
- "version": "0.24.0-beta.7",
3
+ "version": "0.24.0-beta.9",
4
4
  "description": "TeamAI — Make Every Team AI Native (skill sync + shared knowledge base, powered by Git)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -56,26 +56,32 @@
56
56
  "ora": "^8.1.0",
57
57
  "semver": "^7.8.5",
58
58
  "simple-git": "^3.36.0",
59
- "smol-toml": "^1.3.1",
59
+ "smol-toml": "^1.7.1",
60
60
  "tree-sitter-wasms": "0.1.13",
61
61
  "web-tree-sitter": "0.25.10",
62
62
  "yaml": "^2.6.0",
63
63
  "zod": "^3.24.0"
64
64
  },
65
65
  "overrides": {
66
- "js-yaml": "^3.15.1"
66
+ "js-yaml": "^3.15.2",
67
+ "brace-expansion@1": "1.1.18",
68
+ "brace-expansion@2": "2.1.4",
69
+ "brace-expansion@5": "5.0.9",
70
+ "nanoid": "3.3.19",
71
+ "postcss": "8.5.28",
72
+ "picomatch": "4.0.7"
67
73
  },
68
74
  "devDependencies": {
69
75
  "@types/cross-spawn": "^6.0.6",
70
76
  "@types/fs-extra": "^11.0.4",
71
77
  "@types/node": "^20.17.0",
72
78
  "@types/semver": "^7.8.0",
73
- "@vitest/coverage-v8": "^2.1.9",
79
+ "@vitest/coverage-v8": "^3.2.7",
74
80
  "opencode-ai": "1.18.23",
75
81
  "standard-version": "^9.5.0",
76
82
  "tsup": "^8.3.0",
77
83
  "typescript": "^5.7.0",
78
- "vitest": "^2.1.0"
84
+ "vitest": "^3.2.7"
79
85
  },
80
86
  "main": "index.js",
81
87
  "directories": {