teamai-cli 0.23.0-beta.1 → 0.23.0-beta.3

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/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  [![npm downloads](https://img.shields.io/npm/dm/teamai-cli.svg)](https://www.npmjs.com/package/teamai-cli)
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
13
13
 
14
- TeamAI manages your team's skills, rules, MCP, and knowledge across Claude Code, Codex, CodeBuddy, WorkBuddy, OpenCode, Cursor, and other AI agents.
14
+ TeamAI manages your team's skills, rules, MCP, and knowledge across Claude Code, Codex, CodeBuddy, WorkBuddy, OpenCode, Cursor, Qoder, and other AI agents.
15
15
 
16
16
  ## Quick Start
17
17
 
@@ -74,6 +74,7 @@ Once initialized, every AI session automatically pulls the latest skills / rules
74
74
  <tr><td>Claude Code</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td></tr>
75
75
  <tr><td>Codex</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td></tr>
76
76
  <tr><td>Cursor</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td></tr>
77
+ <tr><td>Qoder</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td></tr>
77
78
  <tr><td>CodeBuddy</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td></tr>
78
79
  <tr><td>OpenCode</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">—</td><td align="center">—</td><td align="center">—</td></tr>
79
80
  <tr><td>WorkBuddy</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">—</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td></tr>
package/README.zh-CN.md CHANGED
@@ -11,7 +11,7 @@
11
11
  [![npm downloads](https://img.shields.io/npm/dm/teamai-cli.svg)](https://www.npmjs.com/package/teamai-cli)
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
13
13
 
14
- TeamAI 统一管理团队的 Skills、Rules、MCP 和知识,驾驭 Claude Code、Codex、CodeBuddy、WorkBuddy、OpenCode、Cursor 等 AI Agents。
14
+ TeamAI 统一管理团队的 Skills、Rules、MCP 和知识,驾驭 Claude Code、Codex、CodeBuddy、WorkBuddy、OpenCode、Cursor、Qoder 等 AI Agents。
15
15
 
16
16
  ## 快速开始
17
17
 
@@ -74,6 +74,7 @@ teamai init https://github.com/yourorg/yourrepo --scope user
74
74
  <tr><td>Claude Code</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td></tr>
75
75
  <tr><td>Codex</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td></tr>
76
76
  <tr><td>Cursor</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td></tr>
77
+ <tr><td>Qoder</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td></tr>
77
78
  <tr><td>CodeBuddy</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td></tr>
78
79
  <tr><td>OpenCode</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">—</td><td align="center">—</td><td align="center">—</td></tr>
79
80
  <tr><td>WorkBuddy</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">—</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td><td align="center">✓</td></tr>
package/dist/index.js CHANGED
@@ -536,6 +536,20 @@ var init_types = __esm({
536
536
  tclaude: { skills: ".tclaude/skills", rules: ".tclaude/rules", settings: ".tclaude/settings.json", claudemd: ".tclaude/CLAUDE.md", agents: ".tclaude/agents", mcp: ".tclaude/.claude.json" },
537
537
  tcodex: { skills: ".tcodex/skills", rules: ".tcodex/rules", settings: ".tcodex/hooks.json", agents: ".tcodex/agents" },
538
538
  cursor: { skills: ".cursor/skills", rules: ".cursor/rules", settings: ".cursor/hooks.json", agents: ".cursor/agents", mcp: ".cursor/mcp.json", mcpProject: ".cursor/mcp.json" },
539
+ // JoyCode: skills, rules (.mdc), and subagents are synced to .joycode/.
540
+ // JoyCode currently does not provide a lifecycle hooks system or startup
541
+ // adapter, so it intentionally has no `settings` path. Hook reconciliation
542
+ // skips JoyCode cleanly without generating ghost files; users must sync
543
+ // manually via `teamai pull`.
544
+ joycode: { skills: ".joycode/skills", rules: ".joycode/rules", agents: ".joycode/agents" },
545
+ qoder: {
546
+ skills: ".qoder/skills",
547
+ rules: ".qoder/rules",
548
+ settings: ".qoder/settings.json",
549
+ agents: ".qoder/agents",
550
+ mcp: ".qoder/settings.json",
551
+ mcpProject: ".qoder/settings.json"
552
+ },
539
553
  codebuddy: { skills: ".codebuddy/skills", rules: ".codebuddy/rules", settings: ".codebuddy/settings.json", claudemd: ".codebuddy/CODEBUDDY.md", agents: ".codebuddy/agents", mcp: ".codebuddy/mcp.json", mcpProject: ".codebuddy/mcp.json" },
540
554
  openclaw: { skills: ".openclaw/skills", rules: ".openclaw/rules", claudemd: ".openclaw/workspace/AGENTS.md" },
541
555
  hermes: { skills: ".hermes/skills", claudemd: "AGENTS.md" },
@@ -1202,10 +1216,10 @@ var init_base = __esm({
1202
1216
 
1203
1217
  // src/resources/rule-format.ts
1204
1218
  function ruleFileExtensionForTool(tool) {
1205
- return tool === "cursor" ? ".mdc" : ".md";
1219
+ return usesCursorMdcRules(tool) ? ".mdc" : ".md";
1206
1220
  }
1207
1221
  function usesCursorMdcRules(tool) {
1208
- return tool === "cursor";
1222
+ return CURSOR_MDC_RULE_TOOLS.has(tool);
1209
1223
  }
1210
1224
  function ruleStemFromFilename(filename) {
1211
1225
  if (filename.endsWith(".mdc")) return filename.slice(0, -".mdc".length);
@@ -1215,9 +1229,11 @@ function ruleStemFromFilename(filename) {
1215
1229
  function isLegacyCursorRuleFile(tool, filename) {
1216
1230
  return usesCursorMdcRules(tool) && filename.endsWith(".md");
1217
1231
  }
1232
+ var CURSOR_MDC_RULE_TOOLS;
1218
1233
  var init_rule_format = __esm({
1219
1234
  "src/resources/rule-format.ts"() {
1220
1235
  "use strict";
1236
+ CURSOR_MDC_RULE_TOOLS = /* @__PURE__ */ new Set(["cursor", "joycode"]);
1221
1237
  }
1222
1238
  });
1223
1239
 
@@ -1446,7 +1462,7 @@ async function syncRulesToLocal(teamConfig, localConfig, repoPath, baseDir, last
1446
1462
  const rulesDir = path6.join(baseDir, toolPath.rules);
1447
1463
  if (!await pathExists(rulesDir)) continue;
1448
1464
  const ext = ruleFileExtensionForTool(tool);
1449
- const isCursor = usesCursorMdcRules(tool);
1465
+ const isMdcTool = usesCursorMdcRules(tool);
1450
1466
  const files = await listFilesRecursive(rulesDir);
1451
1467
  for (const file of files) {
1452
1468
  if (!file.endsWith(ext)) continue;
@@ -1456,7 +1472,7 @@ async function syncRulesToLocal(teamConfig, localConfig, repoPath, baseDir, last
1456
1472
  const teamRelPath = `rules/${name}.md`;
1457
1473
  const teamFilePath = path6.join(teamRulesDir, `${name}.md`);
1458
1474
  if (!await pathExists(teamFilePath)) continue;
1459
- if (isCursor) {
1475
+ if (isMdcTool) {
1460
1476
  const localRaw = await readFileSafe(localFilePath);
1461
1477
  const teamRaw = await readFileSafe(teamFilePath);
1462
1478
  if (localRaw === null || teamRaw === null) continue;
@@ -7405,7 +7421,7 @@ var CLAUDE_TOOLS, CURSOR_TOOLS2, CODEX_TOOLS2, BUDDY_TOOLS, OPENCODE_TOOLS, MCP_
7405
7421
  var init_mcp_format = __esm({
7406
7422
  "src/resources/mcp-format.ts"() {
7407
7423
  "use strict";
7408
- CLAUDE_TOOLS = /* @__PURE__ */ new Set(["claude", "claude-internal", "tclaude"]);
7424
+ CLAUDE_TOOLS = /* @__PURE__ */ new Set(["claude", "claude-internal", "tclaude", "qoder"]);
7409
7425
  CURSOR_TOOLS2 = /* @__PURE__ */ new Set(["cursor"]);
7410
7426
  CODEX_TOOLS2 = /* @__PURE__ */ new Set(["codex", "codex-internal", "tcodex"]);
7411
7427
  BUDDY_TOOLS = /* @__PURE__ */ new Set(["codebuddy", "workbuddy"]);
@@ -10956,7 +10972,7 @@ var init_rules = __esm({
10956
10972
  const rulesDir = path29.join(resolveBaseDir(localConfig), rulesPath);
10957
10973
  if (!await pathExists(rulesDir)) continue;
10958
10974
  const ext = ruleFileExtensionForTool(tool);
10959
- const isCursor = usesCursorMdcRules(tool);
10975
+ const isMdcTool = usesCursorMdcRules(tool);
10960
10976
  const files = await listFilesRecursive(rulesDir);
10961
10977
  for (const file of files) {
10962
10978
  if (!file.endsWith(ext)) continue;
@@ -10967,7 +10983,7 @@ var init_rules = __esm({
10967
10983
  const teamFileName = `${name}.md`;
10968
10984
  if (teamRules.has(teamFileName)) {
10969
10985
  const teamFilePath = path29.join(teamRulesDir, teamFileName);
10970
- const equal = isCursor ? cursorMdcBodyEqualsTeamMd(
10986
+ const equal = isMdcTool ? cursorMdcBodyEqualsTeamMd(
10971
10987
  await readFileSafe(localFilePath) ?? "",
10972
10988
  await readTeamRule(teamFilePath)
10973
10989
  ) : await fileContentEqual(localFilePath, teamFilePath);
@@ -10978,7 +10994,7 @@ var init_rules = __esm({
10978
10994
  candidates.set(name, { sourcePath: localFilePath, mtime, status: "modified" });
10979
10995
  }
10980
10996
  } else {
10981
- if (isCursor) continue;
10997
+ if (isMdcTool) continue;
10982
10998
  const existing = candidates.get(name);
10983
10999
  if (!existing) {
10984
11000
  const mtime = await getFileMtime(localFilePath);
@@ -11113,6 +11129,7 @@ var init_rules = __esm({
11113
11129
  await this.pullItem(rule, teamConfig, localConfig);
11114
11130
  }
11115
11131
  const teamRuleNames = new Set(rules.map((r) => r.name));
11132
+ const tombstones = await this.readTombstones(localConfig);
11116
11133
  const baseDir = resolveBaseDir(localConfig);
11117
11134
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
11118
11135
  if (!toolPath.rules) continue;
@@ -11124,6 +11141,7 @@ var init_rules = __esm({
11124
11141
  for (const localFile of localFiles) {
11125
11142
  const ruleName = ruleStemFromFilename(localFile);
11126
11143
  if (ruleName === null) continue;
11144
+ if (tool === "joycode" && !tombstones.has(ruleName)) continue;
11127
11145
  if (isLegacyCursorRuleFile(tool, localFile)) {
11128
11146
  await remove(path29.join(destDir, localFile));
11129
11147
  log.debug(`Removed legacy .md rule ${localFile} from ${tool}`);
@@ -11551,6 +11569,12 @@ function renderForCodebuddy(spec) {
11551
11569
  content: renderMarkdownAgent(spec, spec.tool_extras?.["codebuddy"])
11552
11570
  };
11553
11571
  }
11572
+ function renderForJoycode(spec) {
11573
+ return {
11574
+ ext: agentFileExtensionForTool("joycode"),
11575
+ content: renderMarkdownAgent(spec, spec.tool_extras?.["joycode"])
11576
+ };
11577
+ }
11554
11578
  function renderForCodex(spec) {
11555
11579
  return {
11556
11580
  ext: agentFileExtensionForTool("codex"),
@@ -11669,6 +11693,15 @@ function reverseFromCodebuddy(filePath, content) {
11669
11693
  }
11670
11694
  return { ok: true, spec };
11671
11695
  }
11696
+ function reverseFromJoycode(filePath, content) {
11697
+ const result = reverseFromClaude(filePath, content);
11698
+ if (!result.ok) return result;
11699
+ const spec = result.spec;
11700
+ if (spec.tool_extras?.["claude"]) {
11701
+ spec.tool_extras = { joycode: spec.tool_extras["claude"] };
11702
+ }
11703
+ return { ok: true, spec };
11704
+ }
11672
11705
  function reverseFromCodex(filePath, content) {
11673
11706
  let parsed;
11674
11707
  try {
@@ -11813,6 +11846,10 @@ function renderForTool(spec, tool) {
11813
11846
  return renderForCodex(spec);
11814
11847
  case "cursor":
11815
11848
  return renderForCursor(spec);
11849
+ case "joycode":
11850
+ return renderForJoycode(spec);
11851
+ case "qoder":
11852
+ return renderForClaude(spec);
11816
11853
  case "opencode":
11817
11854
  return renderForOpencode(spec);
11818
11855
  }
@@ -11830,6 +11867,8 @@ var init_agent_format = __esm({
11830
11867
  "codex-internal",
11831
11868
  "tcodex",
11832
11869
  "cursor",
11870
+ "joycode",
11871
+ "qoder",
11833
11872
  "opencode"
11834
11873
  ];
11835
11874
  COMMON_CLAUDE_FIELDS = /* @__PURE__ */ new Set(["name", "description", "model", "tools"]);
@@ -11953,6 +11992,49 @@ var init_builtin_agents = __esm({
11953
11992
 
11954
11993
  // src/resources/agents.ts
11955
11994
  import path34 from "path";
11995
+ import { isDeepStrictEqual } from "util";
11996
+ import { parse as parseYaml2 } from "yaml";
11997
+ function mergeCanonicalEdits(canonical, perTool) {
11998
+ const merged = { ...canonical };
11999
+ const extras = { ...canonical.tool_extras };
12000
+ const changes = /* @__PURE__ */ new Map();
12001
+ const conflicts = [];
12002
+ const propose = (key, value, apply) => {
12003
+ const previous = changes.get(key);
12004
+ if (previous && !isDeepStrictEqual(previous.value, value)) {
12005
+ conflicts.push({ field: key, values: { previous: previous.value, next: value } });
12006
+ } else {
12007
+ changes.set(key, { value, apply });
12008
+ }
12009
+ };
12010
+ for (const [tool, edited] of Object.entries(perTool)) {
12011
+ const rendered = renderForTool(canonical, tool);
12012
+ const baseline = reverseByTool(tool, `${canonical.name}${rendered.ext}`, rendered.content);
12013
+ if (!baseline.ok) return { ok: false, conflicts: [{ field: tool, values: { error: baseline.reason } }] };
12014
+ for (const field of ["name", "description", "instructions", "model", "tools"]) {
12015
+ if (isDeepStrictEqual(baseline.spec[field], edited[field])) continue;
12016
+ propose(field, edited[field], () => {
12017
+ const output = merged;
12018
+ if (edited[field] === void 0) delete output[field];
12019
+ else output[field] = edited[field];
12020
+ });
12021
+ }
12022
+ const extrasKey = tool === "tclaude" ? "claude" : tool === "tcodex" ? "codex" : tool;
12023
+ const before = Object.values(baseline.spec.tool_extras ?? {})[0] ?? {};
12024
+ const after = Object.values(edited.tool_extras ?? {})[0] ?? {};
12025
+ if (!isDeepStrictEqual(before, after)) {
12026
+ propose(`tool_extras.${extrasKey}`, after, () => {
12027
+ if (Object.keys(after).length) extras[extrasKey] = after;
12028
+ else delete extras[extrasKey];
12029
+ });
12030
+ }
12031
+ }
12032
+ if (conflicts.length) return { ok: false, conflicts };
12033
+ for (const change of changes.values()) change.apply();
12034
+ if (Object.keys(extras).length) merged.tool_extras = extras;
12035
+ else delete merged.tool_extras;
12036
+ return { ok: true, spec: merged };
12037
+ }
11956
12038
  function getAgentStem(filename) {
11957
12039
  if (filename.endsWith(".md")) return filename.slice(0, -3);
11958
12040
  if (filename.endsWith(".toml")) return filename.slice(0, -5);
@@ -11986,6 +12068,10 @@ function reverseByTool(tool, filePath, content) {
11986
12068
  return reverseFromCodex(filePath, content);
11987
12069
  case "cursor":
11988
12070
  return reverseFromCursor(filePath, content);
12071
+ case "joycode":
12072
+ return reverseFromJoycode(filePath, content);
12073
+ case "qoder":
12074
+ return reverseFromClaude(filePath, content);
11989
12075
  case "opencode":
11990
12076
  return reverseFromOpencode(filePath, content);
11991
12077
  }
@@ -12070,8 +12156,36 @@ var init_agents = __esm({
12070
12156
  const teamMdPath = path34.join(teamAgentsDir, `${stem}.md`);
12071
12157
  const hasTeamYaml = await pathExists(teamYamlPath);
12072
12158
  const hasTeamMd = await pathExists(teamMdPath);
12159
+ let canonicalSpec;
12160
+ if (hasTeamYaml) {
12161
+ const raw = await readFileSafe(teamYamlPath);
12162
+ const parsed = raw === null ? null : parseAgentYaml(raw, `${stem}.yaml`);
12163
+ if (!parsed?.ok) {
12164
+ items.push({
12165
+ name: stem,
12166
+ type: "agents",
12167
+ sourcePath: teamYamlPath,
12168
+ relativePath: `agents/${stem}.yaml`,
12169
+ status: "modified",
12170
+ skipReason: "cannot read or parse canonical agent YAML"
12171
+ });
12172
+ continue;
12173
+ }
12174
+ canonicalSpec = { ...parseYaml2(raw), ...parsed.spec };
12175
+ for (const [tool, filePath] of toolFiles) {
12176
+ if (!isKnownTool(tool) || isAgentDisabled(localConfig, tool) || canonicalSpec.targets && !canonicalSpec.targets.includes(tool)) {
12177
+ toolFiles.delete(tool);
12178
+ continue;
12179
+ }
12180
+ if (await readFileSafe(filePath) === renderForTool(canonicalSpec, tool).content) {
12181
+ toolFiles.delete(tool);
12182
+ }
12183
+ }
12184
+ }
12073
12185
  let hasChange = false;
12074
- if (!hasTeamYaml && !hasTeamMd) {
12186
+ if (hasTeamYaml) {
12187
+ hasChange = toolFiles.size > 0;
12188
+ } else if (!hasTeamMd) {
12075
12189
  hasChange = true;
12076
12190
  } else {
12077
12191
  for (const [tool, filePath] of toolFiles) {
@@ -12104,22 +12218,27 @@ var init_agents = __esm({
12104
12218
  for (const [tool, filePath] of toolFiles) {
12105
12219
  if (!isKnownTool(tool)) continue;
12106
12220
  const content = await readFileSafe(filePath);
12107
- if (!content) continue;
12221
+ if (!content) {
12222
+ if (canonicalSpec) skipReason = `cannot read edited agent file for ${tool}`;
12223
+ continue;
12224
+ }
12108
12225
  const result = reverseByTool(tool, filePath, content);
12109
12226
  if (result.ok) {
12110
12227
  perToolSpecs[tool] = result.spec;
12111
12228
  } else {
12229
+ if (canonicalSpec) skipReason = `cannot parse edited agent file for ${tool}: ${result.reason}`;
12112
12230
  log.debug(`Reverse failed for ${stem} from ${tool}: ${result.reason}`);
12113
12231
  }
12114
12232
  }
12115
- if (Object.keys(perToolSpecs).length === 0) {
12233
+ if (!skipReason && Object.keys(perToolSpecs).length === 0) {
12116
12234
  skipReason = `could not reverse-parse any tool's agent file for ${stem}`;
12117
- } else {
12118
- const mergeResult = mergeReverseResults(perToolSpecs);
12235
+ } else if (!skipReason) {
12236
+ const mergeResult = canonicalSpec ? mergeCanonicalEdits(canonicalSpec, perToolSpecs) : mergeReverseResults(perToolSpecs);
12119
12237
  if (!mergeResult.ok) {
12120
12238
  const conflictSummary = mergeResult.conflicts.map((c) => `${c.field}: ${JSON.stringify(c.values)}`).join("; ");
12121
12239
  skipReason = `conflicting values across tools \u2014 ${conflictSummary}`;
12122
12240
  } else {
12241
+ if (canonicalSpec && isDeepStrictEqual(canonicalSpec, mergeResult.spec)) continue;
12123
12242
  items.push({
12124
12243
  name: stem,
12125
12244
  type: "agents",
@@ -12208,7 +12327,7 @@ var init_agents = __esm({
12208
12327
  * Pull an agent to every installed tool's agents/ directory.
12209
12328
  *
12210
12329
  * New format (.yaml): parses spec, respects spec.targets, renders per-tool native format.
12211
- * Legacy format (.md): copies .md as-is to claude/claude-internal/codebuddy only.
12330
+ * Legacy format (.md): copies .md as-is to Claude-compatible tools.
12212
12331
  */
12213
12332
  async pullItem(item, teamConfig, localConfig) {
12214
12333
  const agentItem = item;
@@ -12287,10 +12406,10 @@ var init_agents = __esm({
12287
12406
  }
12288
12407
  // ─── Private helpers ──────────────────────────────────────────────────────
12289
12408
  /**
12290
- * Legacy pull: copies .md as-is to claude/claude-internal/codebuddy.
12409
+ * Legacy pull: copies .md as-is to Claude-compatible tools, including JoyCode.
12291
12410
  */
12292
12411
  async pullLegacyMd(item, teamConfig, baseDir, localConfig) {
12293
- const legacyTools = /* @__PURE__ */ new Set(["claude", "claude-internal", "tclaude", "codebuddy"]);
12412
+ const legacyTools = /* @__PURE__ */ new Set(["claude", "claude-internal", "tclaude", "codebuddy", "joycode"]);
12294
12413
  for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) {
12295
12414
  if (!legacyTools.has(tool)) continue;
12296
12415
  if (!toolPath.agents) {
@@ -13071,7 +13190,7 @@ async function ensureReportsWorktree(localConfig, options = {}) {
13071
13190
  await writeWorktreeGitignore(wt);
13072
13191
  const wtGit = createGit2(wt);
13073
13192
  await wtGit.add([".gitignore"]);
13074
- await wtGit.commit("[teamai] Initialize reports branch");
13193
+ await commitSkippingHooks(wtGit, "[teamai] Initialize reports branch");
13075
13194
  if (options.pushIfCreated !== false) {
13076
13195
  try {
13077
13196
  await wtGit.push(["-u", "origin", REPORTS_BRANCH]);
@@ -13134,7 +13253,7 @@ async function commitAndPushReports(localConfig, message, files) {
13134
13253
  log.debug("[reports] nothing to commit");
13135
13254
  return false;
13136
13255
  }
13137
- await git.commit(message);
13256
+ await commitSkippingHooks(git, message);
13138
13257
  for (let attempt = 1; attempt <= MAX_PUSH_RETRIES; attempt++) {
13139
13258
  try {
13140
13259
  await git.push(["origin", REPORTS_BRANCH]);
@@ -13894,6 +14013,7 @@ __export(git_exports, {
13894
14013
  autoPushViaMR: () => autoPushViaMR,
13895
14014
  checkoutMaster: () => checkoutMaster,
13896
14015
  commitPaths: () => commitPaths,
14016
+ commitSkippingHooks: () => commitSkippingHooks,
13897
14017
  configureGitUser: () => configureGitUser,
13898
14018
  createGit: () => createGit2,
13899
14019
  generateBranchName: () => generateBranchName,
@@ -13929,6 +14049,9 @@ function createGit2(basePath) {
13929
14049
  }
13930
14050
  return simpleGit();
13931
14051
  }
14052
+ function commitSkippingHooks(git, message) {
14053
+ return git.commit(message, { "--no-verify": null });
14054
+ }
13932
14055
  async function isGitRepo(localPath) {
13933
14056
  if (!await fse9.pathExists(localPath)) {
13934
14057
  return false;
@@ -14198,7 +14321,7 @@ async function pushRepoBranch(localPath, message, files, branchName, opts = {})
14198
14321
  await leaveAndDeletePushBranch(git, defaultBranch, branchName);
14199
14322
  return false;
14200
14323
  }
14201
- await git.commit(message);
14324
+ await commitSkippingHooks(git, message);
14202
14325
  if (opts.reuseBranch) {
14203
14326
  if (await treeMatchesRemoteBranch(git, branchName)) {
14204
14327
  log.debug(`Remote branch ${branchName} already holds this tree, skipping force-push`);
@@ -14503,7 +14626,7 @@ var init_known_agents = __esm({
14503
14626
  init_types();
14504
14627
  init_base();
14505
14628
  init_home();
14506
- SELF_MODE_AGENT_CHOICES = ["claude", "codex", "cursor", "codebuddy", "workbuddy"];
14629
+ SELF_MODE_AGENT_CHOICES = ["claude", "codex", "cursor", "joycode", "codebuddy", "workbuddy"];
14507
14630
  KNOWN_AGENTS = [
14508
14631
  // Coding agents already wired through teamConfig.toolPaths defaults
14509
14632
  { id: "claude", displayName: "Claude Code", category: "coding", skillsPath: ".claude/skills" },
@@ -14513,6 +14636,7 @@ var init_known_agents = __esm({
14513
14636
  { id: "codex-internal", displayName: "Codex CLI Internal", category: "coding", skillsPath: ".codex-internal/skills" },
14514
14637
  { id: "tcodex", displayName: "TCodex", category: "coding", skillsPath: ".tcodex/skills" },
14515
14638
  { id: "cursor", displayName: "Cursor", category: "coding", skillsPath: ".cursor/skills" },
14639
+ { id: "joycode", displayName: "JoyCode", category: "coding", skillsPath: ".joycode/skills" },
14516
14640
  { id: "codebuddy", displayName: "CodeBuddy", category: "coding", skillsPath: ".codebuddy/skills" },
14517
14641
  // Additional coding agents from skills-manage
14518
14642
  { id: "gemini", displayName: "Gemini CLI", category: "coding", skillsPath: ".gemini/skills" },
@@ -18456,14 +18580,6 @@ function filterEventsByScope(events, opts) {
18456
18580
  async function reportUsageToTeam(repoPath, username, options) {
18457
18581
  const selfConfig = options?.selfConfig;
18458
18582
  const selfMode = selfConfig?.repo.kind === "self";
18459
- const syncLock = selfMode ? null : path55.join(path55.dirname(repoPath), SYNC_LOCK_FILENAME);
18460
- if (syncLock) {
18461
- const locked = await acquireLock(syncLock);
18462
- if (!locked) {
18463
- log.debug("Skipping report: another pull/push holds the sync lock");
18464
- return;
18465
- }
18466
- }
18467
18583
  try {
18468
18584
  const events = await readUsageEvents();
18469
18585
  const filesToPush = [];
@@ -18583,8 +18699,6 @@ async function reportUsageToTeam(repoPath, username, options) {
18583
18699
  }
18584
18700
  } catch (e) {
18585
18701
  log.error(`Auto-report skipped: ${e.message}`);
18586
- } finally {
18587
- if (syncLock) await releaseLock(syncLock);
18588
18702
  }
18589
18703
  }
18590
18704
  var init_team_push = __esm({
@@ -18599,7 +18713,6 @@ var init_team_push = __esm({
18599
18713
  init_logger();
18600
18714
  init_types();
18601
18715
  init_home();
18602
- init_update();
18603
18716
  }
18604
18717
  });
18605
18718
 
@@ -18793,38 +18906,28 @@ async function refreshTeamRepo(localConfig) {
18793
18906
  await migrateSelfModeGitignore2(localConfig);
18794
18907
  } catch {
18795
18908
  }
18796
- let version2 = null;
18909
+ let version3 = null;
18797
18910
  try {
18798
- version2 = await getHeadRev(localConfig.repo.localPath);
18911
+ version3 = await getHeadRev(localConfig.repo.localPath);
18799
18912
  } catch {
18800
- version2 = null;
18913
+ version3 = null;
18801
18914
  }
18802
- return { label: "single-repo (knowledge on main)", version: version2, reportingOnly: false };
18915
+ return { label: "single-repo (knowledge on main)", version: version3, reportingOnly: false };
18803
18916
  }
18804
- const syncLock = path57.join(getDataHome(localConfig), SYNC_LOCK_FILENAME);
18805
- const locked = await acquireLock(syncLock);
18806
- if (!locked) {
18807
- log.debug("[pull] another pull/push holds the sync lock; skipping this scope");
18808
- return { label: "sync in progress elsewhere \u2014 skipped", version: null, reportingOnly: false, skipped: true };
18917
+ const result = await pullRepo(localConfig.repo.localPath);
18918
+ try {
18919
+ await flushPendingLearnings(localConfig.repo.localPath, localConfig.username);
18920
+ } catch (e) {
18921
+ log.debug(`pending-learnings flush skipped: ${e.message}`);
18809
18922
  }
18923
+ let version2 = null;
18810
18924
  try {
18811
- const result = await pullRepo(localConfig.repo.localPath);
18812
- try {
18813
- await flushPendingLearnings(localConfig.repo.localPath, localConfig.username);
18814
- } catch (e) {
18815
- log.debug(`pending-learnings flush skipped: ${e.message}`);
18816
- }
18817
- let version2 = null;
18818
- try {
18819
- version2 = await getHeadRev(localConfig.repo.localPath);
18820
- } catch {
18821
- log.debug("Rev check failed, proceeding with full sync");
18822
- version2 = null;
18823
- }
18824
- return { label: result, version: version2, reportingOnly: false };
18825
- } finally {
18826
- await releaseLock(syncLock);
18925
+ version2 = await getHeadRev(localConfig.repo.localPath);
18926
+ } catch {
18927
+ log.debug("Rev check failed, proceeding with full sync");
18928
+ version2 = null;
18827
18929
  }
18930
+ return { label: result, version: version2, reportingOnly: false };
18828
18931
  }
18829
18932
  async function buildRolePullContext(localConfig) {
18830
18933
  if (!localConfig.primaryRole) return null;
@@ -18983,11 +19086,7 @@ async function pullForScope(localConfig, options, policy = {}) {
18983
19086
  let currentRev = null;
18984
19087
  let reportingOnly = false;
18985
19088
  try {
18986
- const { label, version: version2, reportingOnly: ro, skipped } = await refreshTeamRepo(localConfig);
18987
- if (skipped) {
18988
- pullSpin.succeed(`[${scopeLabel}] Team repo: ${label}`);
18989
- return;
18990
- }
19089
+ const { label, version: version2, reportingOnly: ro } = await refreshTeamRepo(localConfig);
18991
19090
  currentRev = version2;
18992
19091
  reportingOnly = ro;
18993
19092
  pullSpin.succeed(`[${scopeLabel}] Team repo: ${label}`);
@@ -19618,18 +19717,21 @@ async function collectClaudemdFiles(repoPath, roleContext) {
19618
19717
  }
19619
19718
  return contents;
19620
19719
  }
19621
- async function autoMigrateHooksIfNeeded() {
19720
+ async function legacyHooksNeedReinject() {
19622
19721
  const home = getUserHome();
19623
19722
  const primarySettings = path57.join(home, ".claude", "settings.json");
19624
- if (!await pathExists(primarySettings)) return;
19723
+ if (!await pathExists(primarySettings)) return false;
19625
19724
  const content = await readFileSafe(primarySettings);
19626
- if (!content) return;
19627
- if (content.includes("hook-dispatch")) return;
19628
- if (!content.includes("teamai")) return;
19725
+ if (!content) return false;
19726
+ if (content.includes("hook-dispatch")) return false;
19727
+ if (!content.includes("teamai")) return false;
19728
+ return true;
19729
+ }
19730
+ async function reinjectLegacyHooks(localConfig) {
19629
19731
  log.debug("Auto-migrating hooks to dispatch format...");
19630
- const { autoDetectInit: autoDetectInit2 } = await Promise.resolve().then(() => (init_config(), config_exports));
19732
+ const teamConfig = await loadTeamConfig(localConfig.repo.localPath);
19733
+ if (!teamConfig) return;
19631
19734
  const { injectHooksToAllTools: injectHooksToAllTools2 } = await Promise.resolve().then(() => (init_hooks2(), hooks_exports));
19632
- const { localConfig, teamConfig } = await autoDetectInit2();
19633
19735
  const { baseDir } = resolveHookScope(localConfig);
19634
19736
  const disabled = localConfig.disabledAgents;
19635
19737
  let hookFilter = localConfig.enabledAgents;
@@ -19641,108 +19743,141 @@ async function autoMigrateHooksIfNeeded() {
19641
19743
  log.debug("Hooks migrated to dispatch format");
19642
19744
  }
19643
19745
  async function pull(options) {
19746
+ const needsHookMigration = await legacyHooksNeedReinject().catch(() => false);
19747
+ const contended = /* @__PURE__ */ new Set();
19748
+ const heldLocks = /* @__PURE__ */ new Map();
19749
+ const lockScope = async (config) => {
19750
+ if (config.repo.kind && config.repo.kind !== "git") return true;
19751
+ const lock = path57.join(getDataHome(config), SYNC_LOCK_FILENAME);
19752
+ if (await acquireLock(lock)) {
19753
+ heldLocks.set(config, lock);
19754
+ return true;
19755
+ }
19756
+ log.info(`[${config.scope}] sync in progress elsewhere \u2014 skipped (another pull/push holds the lock)`);
19757
+ contended.add(config);
19758
+ return false;
19759
+ };
19644
19760
  try {
19645
- await autoMigrateHooksIfNeeded();
19646
- } catch {
19647
- }
19648
- let projectConfig = null;
19649
- try {
19650
- projectConfig = await detectProjectConfig();
19651
- } catch (e) {
19652
- log.warn(`Project-scope detection error: ${e.message}`);
19653
- }
19654
- const projectMode = projectConfig !== null;
19655
- const inheritUserScope = projectConfig?.inheritUserScope === true;
19656
- let activeUserConfig = null;
19657
- let inheritedUserConfig = null;
19658
- if (projectMode && !inheritUserScope) {
19659
- log.info("project scope detected, skipped user scope");
19660
- } else {
19761
+ let projectConfig = null;
19661
19762
  try {
19662
- const loadedUserConfig = await loadLocalConfigForScope("user");
19663
- if (loadedUserConfig) {
19664
- if (inheritUserScope) {
19665
- inheritedUserConfig = loadedUserConfig;
19666
- log.info("project scope detected, inheriting user-scope resources and knowledge");
19667
- await pullForScope(inheritedUserConfig, options, {
19668
- resourceTypes: ["skills", "rules", "docs", "agents"],
19669
- revisionField: "lastInheritedPullRev"
19670
- });
19763
+ projectConfig = await detectProjectConfig();
19764
+ } catch (e) {
19765
+ log.warn(`Project-scope detection error: ${e.message}`);
19766
+ }
19767
+ const projectMode = projectConfig !== null;
19768
+ const inheritUserScope = projectConfig?.inheritUserScope === true;
19769
+ let activeUserConfig = null;
19770
+ let inheritedUserConfig = null;
19771
+ if (projectMode && !inheritUserScope) {
19772
+ log.info("project scope detected, skipped user scope");
19773
+ } else {
19774
+ try {
19775
+ const loadedUserConfig = await loadLocalConfigForScope("user");
19776
+ if (loadedUserConfig) {
19777
+ if (inheritUserScope) {
19778
+ inheritedUserConfig = loadedUserConfig;
19779
+ log.info("project scope detected, inheriting user-scope resources and knowledge");
19780
+ if (await lockScope(inheritedUserConfig)) {
19781
+ await pullForScope(inheritedUserConfig, options, {
19782
+ resourceTypes: ["skills", "rules", "docs", "agents"],
19783
+ revisionField: "lastInheritedPullRev"
19784
+ });
19785
+ }
19786
+ } else {
19787
+ activeUserConfig = loadedUserConfig;
19788
+ if (await lockScope(activeUserConfig)) {
19789
+ await pullForScope(activeUserConfig, options);
19790
+ }
19791
+ }
19792
+ } else if (inheritUserScope) {
19793
+ log.warn("user-scope inheritance is enabled, but user scope is not initialized");
19671
19794
  } else {
19672
- activeUserConfig = loadedUserConfig;
19673
- await pullForScope(activeUserConfig, options);
19795
+ log.debug("No user-scope config found, skipping user pull");
19674
19796
  }
19675
- } else if (inheritUserScope) {
19676
- log.warn("user-scope inheritance is enabled, but user scope is not initialized");
19677
- } else {
19678
- log.debug("No user-scope config found, skipping user pull");
19797
+ } catch (e) {
19798
+ log.warn(`User-scope pull error: ${e.message}`);
19679
19799
  }
19680
- } catch (e) {
19681
- log.warn(`User-scope pull error: ${e.message}`);
19682
19800
  }
19683
- }
19684
- if (projectConfig) {
19685
- try {
19686
- await pullForScope(projectConfig, options);
19687
- } catch (e) {
19688
- log.warn(`Project-scope pull error: ${e.message}`);
19801
+ if (projectConfig) {
19802
+ try {
19803
+ if (await lockScope(projectConfig)) {
19804
+ await pullForScope(projectConfig, options);
19805
+ }
19806
+ } catch (e) {
19807
+ log.warn(`Project-scope pull error: ${e.message}`);
19808
+ }
19689
19809
  }
19690
- }
19691
- await reconcileHooksAllScopes(activeUserConfig, projectConfig, options);
19692
- await reconcileMcpAllScopes(activeUserConfig, projectConfig, options);
19693
- await reconcileCoAuthorAllScopes(activeUserConfig, projectConfig, options);
19694
- if (!options.dryRun) {
19695
- try {
19696
- const { reportUsageToTeam: reportUsageToTeam2 } = await Promise.resolve().then(() => (init_team_push(), team_push_exports));
19697
- const { truncateUsageAfterReport: truncateUsageAfterReport2, readUsageEvents: readUsageEvents2 } = await Promise.resolve().then(() => (init_usage_tracker(), usage_tracker_exports));
19698
- const targets = [];
19699
- if (projectConfig && projectConfig.repo.kind !== "http") {
19700
- targets.push({
19701
- repoPath: projectConfig.repo.localPath,
19702
- username: projectConfig.username,
19703
- opts: {
19704
- skipTruncate: true,
19705
- projectRoot: projectConfig.projectRoot,
19706
- // Self mode routes stats/votes to the teamai-reports orphan branch.
19707
- ...projectConfig.repo.kind === "self" ? { selfConfig: projectConfig } : {}
19708
- }
19709
- });
19810
+ const reconcileUser = activeUserConfig && !contended.has(activeUserConfig) ? activeUserConfig : null;
19811
+ const reconcileProject = projectConfig && !contended.has(projectConfig) ? projectConfig : null;
19812
+ if (needsHookMigration) {
19813
+ const migrateScope = reconcileProject ?? reconcileUser;
19814
+ if (migrateScope) {
19815
+ try {
19816
+ await reinjectLegacyHooks(migrateScope);
19817
+ } catch {
19818
+ }
19710
19819
  }
19711
- if (activeUserConfig && activeUserConfig.repo.kind !== "http") {
19712
- targets.push({
19713
- repoPath: activeUserConfig.repo.localPath,
19714
- username: activeUserConfig.username,
19715
- opts: {
19716
- skipTruncate: true,
19717
- excludeProjectRoots: projectConfig?.projectRoot ? [projectConfig.projectRoot] : [],
19718
- // Self mode routes stats/votes to the teamai-reports orphan branch
19719
- // never reset/pull the business repo working tree.
19720
- ...activeUserConfig.repo.kind === "self" ? { selfConfig: activeUserConfig } : {}
19820
+ }
19821
+ await reconcileHooksAllScopes(reconcileUser, reconcileProject, options);
19822
+ await reconcileMcpAllScopes(reconcileUser, reconcileProject, options);
19823
+ await reconcileCoAuthorAllScopes(reconcileUser, reconcileProject, options);
19824
+ if (!options.dryRun) {
19825
+ try {
19826
+ const { reportUsageToTeam: reportUsageToTeam2 } = await Promise.resolve().then(() => (init_team_push(), team_push_exports));
19827
+ const { truncateUsageAfterReport: truncateUsageAfterReport2, readUsageEvents: readUsageEvents2 } = await Promise.resolve().then(() => (init_usage_tracker(), usage_tracker_exports));
19828
+ const targets = [];
19829
+ if (reconcileProject && reconcileProject.repo.kind !== "http") {
19830
+ targets.push({
19831
+ repoPath: reconcileProject.repo.localPath,
19832
+ username: reconcileProject.username,
19833
+ opts: {
19834
+ skipTruncate: true,
19835
+ projectRoot: reconcileProject.projectRoot,
19836
+ // Self mode routes stats/votes to the teamai-reports orphan branch.
19837
+ ...reconcileProject.repo.kind === "self" ? { selfConfig: reconcileProject } : {}
19838
+ }
19839
+ });
19840
+ }
19841
+ if (reconcileUser && reconcileUser.repo.kind !== "http") {
19842
+ targets.push({
19843
+ repoPath: reconcileUser.repo.localPath,
19844
+ username: reconcileUser.username,
19845
+ opts: {
19846
+ skipTruncate: true,
19847
+ excludeProjectRoots: projectConfig?.projectRoot ? [projectConfig.projectRoot] : [],
19848
+ // Self mode routes stats/votes to the teamai-reports orphan branch —
19849
+ // never reset/pull the business repo working tree.
19850
+ ...reconcileUser.repo.kind === "self" ? { selfConfig: reconcileUser } : {}
19851
+ }
19852
+ });
19853
+ }
19854
+ const eventCount = (await readUsageEvents2()).length;
19855
+ for (const t of targets) {
19856
+ try {
19857
+ await reportUsageToTeam2(t.repoPath, t.username, t.opts);
19858
+ } catch (e) {
19859
+ log.error(`Auto-report to ${t.repoPath} skipped: ${e.message}`);
19721
19860
  }
19722
- });
19723
- }
19724
- const eventCount = (await readUsageEvents2()).length;
19725
- for (const t of targets) {
19726
- try {
19727
- await reportUsageToTeam2(t.repoPath, t.username, t.opts);
19728
- } catch (e) {
19729
- log.error(`Auto-report to ${t.repoPath} skipped: ${e.message}`);
19730
19861
  }
19862
+ if (eventCount > 0 && targets.length > 0) {
19863
+ await truncateUsageAfterReport2(eventCount);
19864
+ }
19865
+ } catch (e) {
19866
+ log.debug(`Auto-report skipped: ${e.message}`);
19731
19867
  }
19732
- if (eventCount > 0 && targets.length > 0) {
19733
- await truncateUsageAfterReport2(eventCount);
19868
+ }
19869
+ const sourceConfig = reconcileProject ?? reconcileUser;
19870
+ if (sourceConfig) {
19871
+ try {
19872
+ const { pullSources: pullSources2 } = await Promise.resolve().then(() => (init_source(), source_exports));
19873
+ await pullSources2(sourceConfig, options);
19874
+ } catch (e) {
19875
+ log.debug(`Source pull skipped: ${e.message}`);
19734
19876
  }
19735
- } catch (e) {
19736
- log.debug(`Auto-report skipped: ${e.message}`);
19737
19877
  }
19738
- }
19739
- const sourceConfig = projectConfig ?? activeUserConfig;
19740
- if (sourceConfig) {
19741
- try {
19742
- const { pullSources: pullSources2 } = await Promise.resolve().then(() => (init_source(), source_exports));
19743
- await pullSources2(sourceConfig, options);
19744
- } catch (e) {
19745
- log.debug(`Source pull skipped: ${e.message}`);
19878
+ } finally {
19879
+ for (const lock of heldLocks.values()) {
19880
+ await releaseLock(lock);
19746
19881
  }
19747
19882
  }
19748
19883
  }
@@ -36315,7 +36450,7 @@ var init_schema = __esm({
36315
36450
 
36316
36451
  // src/repo-list/store.ts
36317
36452
  import fs40 from "fs-extra";
36318
- import { parse as parseYaml2 } from "yaml";
36453
+ import { parse as parseYaml3 } from "yaml";
36319
36454
  async function loadRepoList(filePath) {
36320
36455
  const exists3 = await fs40.pathExists(filePath);
36321
36456
  if (!exists3) {
@@ -36326,7 +36461,7 @@ async function loadRepoList(filePath) {
36326
36461
  throw new Error(`${filePath} exceeds max allowed size 10MB`);
36327
36462
  }
36328
36463
  const raw = await fs40.readFile(filePath, "utf8");
36329
- const parsed = parseYaml2(raw);
36464
+ const parsed = parseYaml3(raw);
36330
36465
  const result = RepoListFileSchema.parse(parsed);
36331
36466
  return result;
36332
36467
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamai-cli",
3
- "version": "0.23.0-beta.1",
3
+ "version": "0.23.0-beta.3",
4
4
  "description": "TeamAI — Make Every Team AI Native (skill sync + shared knowledge base, powered by Git)",
5
5
  "type": "module",
6
6
  "bin": {