teamai-cli 0.20.0-beta.7 → 0.20.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.
@@ -124,7 +124,29 @@ none exists, silently skip.
124
124
  ### Step 2 — Extract keywords from the task description
125
125
 
126
126
  Pick 3–6 high-signal keywords from the user query. Strip filler words
127
- ("the", "how", "please"). Mix English and Chinese terms when both appear.
127
+ ("the", "how", "please").
128
+
129
+ **Always search bilingually (cross-language recall).** The knowledge base
130
+ mixes Chinese and English: a learning may be titled in English while your
131
+ task is described in Chinese, or vice versa. The search is purely lexical
132
+ (BM25 token matching), so a Chinese-only query never matches an
133
+ English-only document. To bridge this, expand every domain term into
134
+ **both languages** and include both in your keyword set:
135
+
136
+ - Chinese task → add the English term for each concept
137
+ (e.g. "重启推理集群" → also add `restart`, `inference`, `cluster`).
138
+ - English task → add the Chinese term for each concept
139
+ (e.g. "restart inference cluster" → also add `重启`, `推理`, `集群`).
140
+ - Only translate terms that genuinely differ across languages. Keep any
141
+ **language-neutral token** as-is — anything that is written the same way
142
+ in both languages, or has no meaningful translation: proper nouns, brand
143
+ and product names, acronyms and initialisms, code identifiers and symbols,
144
+ error codes, version strings, file paths, CLI commands, and API names
145
+ (e.g. `AppID`, `Kubernetes`, `GPU`, `RuntimeError`, `v2.1.3`, `oauth`). Translating
146
+ these only hurts matching.
147
+
148
+ Only translate the meaningful domain terms; do not pad the query with
149
+ low-signal words. Aim for the same 3–6 concepts, each in both languages.
128
150
 
129
151
  **Lead with the terms that pin down *this* task**: proper nouns, customer or
130
152
  product names, service IDs, error codes, versions, symbol names
@@ -208,9 +230,10 @@ Use judgement rather than counting, in both directions:
208
230
  discriminating term as a lead to verify by opening the file, not as a
209
231
  conclusion.
210
232
  - **Fewer than 5 results does not mean the corpus is thin.** Results are
211
- deduplicated when title, date, author and content all match, so a learning
212
- shared twice appears once. Body-
213
- only matches are also dropped. Both are intentional.
233
+ deduplicated when type, domain, title, date, author, tags and content all
234
+ match, so a learning shared twice appears once — including when the two copies
235
+ have collected different vote counts. Body-only matches are also dropped. Both
236
+ are intentional.
214
237
 
215
238
  For each hit you keep, read the source file directly (use `Read`) and
216
239
  condense each into **one or two sentences**.
package/dist/index.js CHANGED
@@ -1188,6 +1188,13 @@ teamai recall "<\u5173\u952E\u8BCD1> <\u5173\u952E\u8BCD2> ..."
1188
1188
 
1189
1189
  \u4ECE\u4EFB\u52A1\u63CF\u8FF0\u4E2D\u63D0\u53D6 3-6 \u4E2A\u9AD8\u4FE1\u53F7\u5173\u952E\u8BCD\u8FDB\u884C\u68C0\u7D22\u3002
1190
1190
 
1191
+ **\u52A1\u5FC5\u4E2D\u82F1\u53CC\u8BED\u68C0\u7D22\uFF08\u8DE8\u8BED\u8A00\u53EC\u56DE\uFF09**\uFF1A\u77E5\u8BC6\u5E93\u4E2D\u82F1\u6587\u6DF7\u6742\uFF0C\u68C0\u7D22\u662F\u7EAF\u8BCD\u6CD5\u5339\u914D\uFF0C
1192
+ \u4E2D\u6587 query \u65E0\u6CD5\u547D\u4E2D\u7EAF\u82F1\u6587\u6587\u6863\uFF0C\u53CD\u4E4B\u4EA6\u7136\u3002\u56E0\u6B64\u6BCF\u4E2A\u9886\u57DF\u672F\u8BED\u90FD\u5E94**\u540C\u65F6\u7ED9\u51FA\u4E2D\u82F1\u4E24\u79CD\u5199\u6CD5**
1193
+ \u4E00\u8D77\u68C0\u7D22\uFF0C\u4F8B\u5982\u300C\u91CD\u542F\u63A8\u7406\u96C6\u7FA4\u300D\u5E94\u6269\u5C55\u4E3A \`\u91CD\u542F \u63A8\u7406 \u96C6\u7FA4 restart inference cluster\`\uFF1B
1194
+ \u53EA\u7FFB\u8BD1\u4E2D\u82F1\u786E\u5B9E\u4E0D\u540C\u7684\u672F\u8BED\uFF1B\u4EFB\u4F55**\u8BED\u8A00\u65E0\u5173\u7684 token**\uFF08\u4E2D\u82F1\u5199\u6CD5\u76F8\u540C\u3001\u6216\u65E0\u5B9E\u9645\u8BD1\u6CD5\u8005\uFF09\u4E00\u5F8B\u4FDD\u6301\u539F\u6837\uFF0C
1195
+ \u4F8B\u5982\u4E13\u6709\u540D\u8BCD\u3001\u54C1\u724C\u4E0E\u4EA7\u54C1\u540D\u3001\u7F29\u5199\u3001\u4EE3\u7801\u6807\u8BC6\u7B26\u4E0E\u7B26\u53F7\u3001\u9519\u8BEF\u7801\u3001\u7248\u672C\u53F7\u3001\u6587\u4EF6\u8DEF\u5F84\u3001\u547D\u4EE4\u4E0E API \u540D
1196
+ \uFF08\`AppID\`\u3001\`Kubernetes\`\u3001\`GPU\`\u3001\`RuntimeError\`\u3001\`v2.1.3\`\u3001\`oauth\`\uFF09\u2014\u2014\u7FFB\u8BD1\u5B83\u4EEC\u53EA\u4F1A\u635F\u5BB3\u5339\u914D\u3002
1197
+
1191
1198
  ### \u68C0\u7D22\u540E\u8981\u6C42\uFF08\u7528\u4E8E\u8BC4\u4F30\u77E5\u8BC6\u88AB\u91C7\u7EB3\u60C5\u51B5\uFF0C\u8BF7\u52A1\u5FC5\u6267\u884C\uFF09
1192
1199
 
1193
1200
  teamai-recall subagent \u7684\u8FD4\u56DE\u91CC\u5DF2\u5217\u51FA\u672C\u6B21\u68C0\u7D22\u5230\u7684\u5019\u9009 doc-id\uFF08\u89C1\u5176 recalled-doc-ids \u5217\u8868\uFF09\u3002
@@ -4437,9 +4444,9 @@ async function scanTranscriptStop(transcriptPath) {
4437
4444
  }
4438
4445
  if (entry.type === "assistant") {
4439
4446
  const usage = entry.message?.usage;
4440
- const dedupKey = typeof entry.message?.id === "string" ? entry.message.id : typeof entry.requestId === "string" ? entry.requestId : void 0;
4441
- if (usage && dedupKey && !countedUsageKeys.has(dedupKey)) {
4442
- countedUsageKeys.add(dedupKey);
4447
+ const dedupKey2 = typeof entry.message?.id === "string" ? entry.message.id : typeof entry.requestId === "string" ? entry.requestId : void 0;
4448
+ if (usage && dedupKey2 && !countedUsageKeys.has(dedupKey2)) {
4449
+ countedUsageKeys.add(dedupKey2);
4443
4450
  tokens.input += toNum(usage.input_tokens);
4444
4451
  tokens.output += toNum(usage.output_tokens);
4445
4452
  tokens.cacheRead += toNum(usage.cache_read_input_tokens);
@@ -7803,7 +7810,7 @@ var init_skills = __esm({
7803
7810
  if (isAgentDisabled(localConfig, tool)) continue;
7804
7811
  if (!toolPath.skills) continue;
7805
7812
  let dest;
7806
- if (tool === "openclaw" && localConfig.scope !== "project") {
7813
+ if (tool === "openclaw") {
7807
7814
  const wsDir = await resolveOpenclawWorkspaceDir();
7808
7815
  if (!wsDir) {
7809
7816
  log.debug(`Skipping skill sync for openclaw: workspace dir not found`);
@@ -7852,7 +7859,7 @@ var init_skills = __esm({
7852
7859
  for (const [tool, toolPath] of Object.entries(teamConfig.toolPaths)) {
7853
7860
  if (!toolPath.skills) continue;
7854
7861
  let skillDir;
7855
- if (tool === "openclaw" && localConfig.scope !== "project") {
7862
+ if (tool === "openclaw") {
7856
7863
  const wsDir = await resolveOpenclawWorkspaceDir();
7857
7864
  if (!wsDir) continue;
7858
7865
  skillDir = path22.join(wsDir, "skills", name);
@@ -9895,6 +9902,10 @@ async function pushCore(localConfig, teamConfig, options) {
9895
9902
  try {
9896
9903
  const repoPath = localConfig.repo.localPath;
9897
9904
  const git = createGit2(repoPath);
9905
+ if (!await isDedicatedRepoRoot(repoPath)) {
9906
+ pullSpin.fail("Cannot push: team repo path is not a dedicated git root. Run `teamai init` to re-clone the team repo before pushing.");
9907
+ return;
9908
+ }
9898
9909
  await resetToCleanMaster(git, repoPath);
9899
9910
  await pullRepo(repoPath);
9900
9911
  pullSpin.succeed("Up to date");
@@ -10231,6 +10242,7 @@ __export(git_exports, {
10231
10242
  getRepoStatus: () => getRepoStatus,
10232
10243
  hasCommits: () => hasCommits,
10233
10244
  initRepo: () => initRepo,
10245
+ isDedicatedRepoRoot: () => isDedicatedRepoRoot,
10234
10246
  isGitRepo: () => isGitRepo,
10235
10247
  isMetadataOnlyDiff: () => isMetadataOnlyDiff,
10236
10248
  pullRepo: () => pullRepo,
@@ -10239,6 +10251,7 @@ __export(git_exports, {
10239
10251
  resetToCleanMaster: () => resetToCleanMaster
10240
10252
  });
10241
10253
  import fs12 from "fs";
10254
+ import { realpath } from "fs/promises";
10242
10255
  import path33 from "path";
10243
10256
  import fse8 from "fs-extra";
10244
10257
  import simpleGit from "simple-git";
@@ -10468,6 +10481,21 @@ function generateBranchName(username) {
10468
10481
  const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
10469
10482
  return `teamai/push/${username}/${timestamp}`;
10470
10483
  }
10484
+ async function isDedicatedRepoRoot(repoPath) {
10485
+ const git = createGit2(repoPath);
10486
+ let toplevel;
10487
+ try {
10488
+ toplevel = (await git.revparse(["--show-toplevel"])).trim();
10489
+ } catch {
10490
+ return true;
10491
+ }
10492
+ try {
10493
+ const [realTop, realRepo] = await Promise.all([realpath(toplevel), realpath(repoPath)]);
10494
+ return realTop === realRepo;
10495
+ } catch {
10496
+ return false;
10497
+ }
10498
+ }
10471
10499
  async function resetToCleanMaster(git, localPath) {
10472
10500
  const status2 = await git.status();
10473
10501
  const hasConflicts = status2.conflicted.length > 0;
@@ -12532,13 +12560,13 @@ function parseLearningDoc(content, filename) {
12532
12560
  date: typeof data.date === "string" ? data.date : data.date instanceof Date ? data.date.toISOString().slice(0, 10) : void 0,
12533
12561
  tags: Array.isArray(data.tags) ? data.tags.filter((t) => typeof t === "string") : typeof data.Tags === "string" ? data.Tags.split(/[,,]\s*/).map((t) => t.trim()).filter(Boolean) : void 0
12534
12562
  };
12535
- const bodyExcerpt = body.slice(0, MAX_BODY_CHARS);
12563
+ const bodyExcerpt = body;
12536
12564
  return { meta, bodyExcerpt };
12537
12565
  } catch {
12538
12566
  log.error(`Failed to parse frontmatter for ${filename}, using fallback`);
12539
12567
  return {
12540
12568
  meta: {},
12541
- bodyExcerpt: content.slice(0, MAX_BODY_CHARS)
12569
+ bodyExcerpt: content
12542
12570
  };
12543
12571
  }
12544
12572
  }
@@ -12759,6 +12787,16 @@ async function loadIndex(indexPath) {
12759
12787
  }
12760
12788
  return raw;
12761
12789
  }
12790
+ function dedupKey(r) {
12791
+ return JSON.stringify([
12792
+ r.entry.type,
12793
+ r.entry.domain,
12794
+ r.entry.title,
12795
+ r.entry.date,
12796
+ r.entry.author,
12797
+ [...r.entry.tokens].sort()
12798
+ ]);
12799
+ }
12762
12800
  function search(query, index, limit = 5) {
12763
12801
  if (!query.trim()) return [];
12764
12802
  const queryTokens = tokenize(query);
@@ -12794,8 +12832,8 @@ function search(query, index, limit = 5) {
12794
12832
  score += 1 * idf(qt);
12795
12833
  }
12796
12834
  }
12797
- const isCodebaseDoc = entry.type === "docs" && (entry.path ?? entry.filename ?? "").includes("team-codebase");
12798
- if (score > 0 && (hasTitleOrTagMatch || isCodebaseDoc)) {
12835
+ const isDocsEntry = entry.type === "docs";
12836
+ if (score > 0 && (hasTitleOrTagMatch || isDocsEntry)) {
12799
12837
  score /= lengthNorm;
12800
12838
  score += Math.min(entry.votes * 0.5, 5);
12801
12839
  const domainMultiplier = domainWeightRow[entry.domain ?? "neutral"];
@@ -12827,14 +12865,7 @@ function search(query, index, limit = 5) {
12827
12865
  const seen = /* @__PURE__ */ new Set();
12828
12866
  const deduped = [];
12829
12867
  for (const r of results) {
12830
- const key = [
12831
- r.entry.type,
12832
- r.entry.title,
12833
- r.entry.date,
12834
- r.entry.author,
12835
- r.entry.tokens.length,
12836
- r.score
12837
- ].join(" ");
12868
+ const key = dedupKey(r);
12838
12869
  if (seen.has(key)) continue;
12839
12870
  seen.add(key);
12840
12871
  deduped.push(r);
@@ -12842,7 +12873,7 @@ function search(query, index, limit = 5) {
12842
12873
  }
12843
12874
  return deduped;
12844
12875
  }
12845
- var MAX_BODY_CHARS, MAX_DOC_BYTES, TECHNICAL_TAGS, OPS_TAGS, SUPPORT_TAGS, TECHNICAL_PATH_PATTERNS, OPS_PATH_PATTERNS, SUPPORT_PATH_PATTERNS, DOMAIN_WEIGHT, TYPE_BONUS, CODEBASE_INDEX_FILENAME, CODEBASE_FULL_FILENAME, CODEBASE_INDEX_WEIGHT_BOOST;
12876
+ var MAX_DOC_BYTES, TECHNICAL_TAGS, OPS_TAGS, SUPPORT_TAGS, TECHNICAL_PATH_PATTERNS, OPS_PATH_PATTERNS, SUPPORT_PATH_PATTERNS, DOMAIN_WEIGHT, TYPE_BONUS, CODEBASE_INDEX_FILENAME, CODEBASE_FULL_FILENAME, CODEBASE_INDEX_WEIGHT_BOOST;
12846
12877
  var init_search_index = __esm({
12847
12878
  "src/utils/search-index.ts"() {
12848
12879
  "use strict";
@@ -12850,7 +12881,6 @@ var init_search_index = __esm({
12850
12881
  init_tokenizer();
12851
12882
  init_logger();
12852
12883
  init_types();
12853
- MAX_BODY_CHARS = 2e3;
12854
12884
  MAX_DOC_BYTES = 50 * 1024;
12855
12885
  TECHNICAL_TAGS = /* @__PURE__ */ new Set([
12856
12886
  "api",
@@ -14364,6 +14394,10 @@ async function reportUsageToTeam(repoPath, username, options) {
14364
14394
  writeRoot = await ensureReportsWorktree2(selfConfig);
14365
14395
  } else {
14366
14396
  const git = createGit2(repoPath);
14397
+ if (!await isDedicatedRepoRoot(repoPath)) {
14398
+ log.debug(`Skipping report: ${repoPath} is not a dedicated team-repo root (safety guard)`);
14399
+ return;
14400
+ }
14367
14401
  await resetToCleanMaster(git, repoPath);
14368
14402
  await pullRepo(repoPath);
14369
14403
  }
@@ -15813,7 +15847,7 @@ async function pull(options) {
15813
15847
  const { reportUsageToTeam: reportUsageToTeam2 } = await Promise.resolve().then(() => (init_team_push(), team_push_exports));
15814
15848
  const { truncateUsageAfterReport: truncateUsageAfterReport2, readUsageEvents: readUsageEvents2 } = await Promise.resolve().then(() => (init_usage_tracker(), usage_tracker_exports));
15815
15849
  const targets = [];
15816
- if (projectConfig) {
15850
+ if (projectConfig && projectConfig.repo.kind !== "http") {
15817
15851
  targets.push({
15818
15852
  repoPath: projectConfig.repo.localPath,
15819
15853
  username: projectConfig.username,
@@ -15829,7 +15863,13 @@ async function pull(options) {
15829
15863
  targets.push({
15830
15864
  repoPath: activeUserConfig.repo.localPath,
15831
15865
  username: activeUserConfig.username,
15832
- opts: { skipTruncate: true, excludeProjectRoots: projectConfig?.projectRoot ? [projectConfig.projectRoot] : [] }
15866
+ opts: {
15867
+ skipTruncate: true,
15868
+ excludeProjectRoots: projectConfig?.projectRoot ? [projectConfig.projectRoot] : [],
15869
+ // Self mode routes stats/votes to the teamai-reports orphan branch —
15870
+ // never reset/pull the business repo working tree.
15871
+ ...activeUserConfig.repo.kind === "self" ? { selfConfig: activeUserConfig } : {}
15872
+ }
15833
15873
  });
15834
15874
  }
15835
15875
  const eventCount = (await readUsageEvents2()).length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamai-cli",
3
- "version": "0.20.0-beta.7",
3
+ "version": "0.20.0",
4
4
  "description": "TeamAI — the team harness for AI agents (skill sync + shared knowledge base, powered by Git)",
5
5
  "type": "module",
6
6
  "bin": {