teamai-cli 0.23.0-beta.7 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -195,7 +195,7 @@ Task: Fix duplicate project-level Hook injection
195
195
  Consider running /teamai-share-learnings to summarize what you learned and share it with your team.
196
196
  ```
197
197
 
198
- The hint names the non-zero friction signals that triggered it and, when available, includes a redacted, single-line summary of the first task. The `/teamai-share-learnings` skill summarizes the session and pushes a learning document directly to the team repo. Each session is prompted at most once.
198
+ The hint names the non-zero friction signals that triggered it and, when available, includes a redacted, single-line summary of the first task. The `/teamai-share-learnings` skill summarizes the session and pushes a learning document directly to the team repo. Each session is prompted at most once. Teams can switch the hint off with `sharing.contributeHint.enabled: false` in `teamai.yaml` (members: `contributeHintEnabled` in local config) while keeping the rest of the Stop hook.
199
199
 
200
200
  ### Team Knowledge Recall
201
201
 
@@ -287,3 +287,13 @@ Insight into how the team actually uses its AI tools, and a starting point for t
287
287
  ## Contributing
288
288
 
289
289
  PRs are welcome! Please read [CONTRIBUTING.md](.github/CONTRIBUTING.md) first.
290
+
291
+ ## Contributors
292
+
293
+ Thanks to everyone who has contributed to TeamAI!
294
+
295
+ <a href="https://github.com/Tencent/teamai-cli/graphs/contributors">
296
+ <img src="https://contrib.rocks/image?repo=Tencent/teamai-cli" alt="Contributors" />
297
+ </a>
298
+
299
+ Made with [contrib.rocks](https://contrib.rocks).
package/README.zh-CN.md CHANGED
@@ -195,7 +195,7 @@ Task: Fix duplicate project-level Hook injection
195
195
  Consider running /teamai-share-learnings to summarize what you learned and share it with your team.
196
196
  ```
197
197
 
198
- 提示会列出实际触发它的非零摩擦信号;如果能取得首个任务摘要,还会在脱敏、单行化后附上任务上下文。`/teamai-share-learnings` skill 自动总结 session 经验并推送到团队仓库。每个 session 最多提示一次。
198
+ 提示会列出实际触发它的非零摩擦信号;如果能取得首个任务摘要,还会在脱敏、单行化后附上任务上下文。`/teamai-share-learnings` skill 自动总结 session 经验并推送到团队仓库。每个 session 最多提示一次。团队可在 `teamai.yaml` 设置 `sharing.contributeHint.enabled: false` 关闭该提示(成员可用本地配置 `contributeHintEnabled` 覆盖),Stop hook 的其余功能不受影响。
199
199
 
200
200
  ### 团队知识检索
201
201
 
@@ -287,3 +287,13 @@ WASM 解析器是纯 JavaScript 依赖,无需任何原生编译工具链。若
287
287
  ## 贡献
288
288
 
289
289
  欢迎提交 PR!请先阅读 [CONTRIBUTING.md](.github/CONTRIBUTING.md)。
290
+
291
+ ## 贡献者
292
+
293
+ 感谢每一位为 TeamAI 贡献代码的伙伴!
294
+
295
+ <a href="https://github.com/Tencent/teamai-cli/graphs/contributors">
296
+ <img src="https://contrib.rocks/image?repo=Tencent/teamai-cli" alt="Contributors" />
297
+ </a>
298
+
299
+ 由 [contrib.rocks](https://contrib.rocks) 生成。
package/dist/index.js CHANGED
@@ -250,6 +250,7 @@ __export(types_exports, {
250
250
  getStatePath: () => getStatePath,
251
251
  getTeamaiHome: () => getTeamaiHome,
252
252
  isAgentDisabled: () => isAgentDisabled,
253
+ isContributeHintEnabled: () => isContributeHintEnabled,
253
254
  isRecallEnabled: () => isRecallEnabled,
254
255
  isSelfMode: () => isSelfMode,
255
256
  legacyManagedMcpManifestPath: () => legacyManagedMcpManifestPath,
@@ -288,6 +289,11 @@ function isRecallEnabled(localConfig, teamConfig) {
288
289
  if (localConfig.recallEnabled !== void 0) return localConfig.recallEnabled;
289
290
  return getRecallSharing(teamConfig).enabled;
290
291
  }
292
+ function isContributeHintEnabled(localConfig, teamConfig, env = process.env) {
293
+ if (env.TEAMAI_CONTRIBUTE_HINT_DISABLED === "1") return false;
294
+ if (localConfig.contributeHintEnabled !== void 0) return localConfig.contributeHintEnabled;
295
+ return teamConfig.sharing?.contributeHint?.enabled ?? true;
296
+ }
291
297
  function resolveCoAuthor(localConfig, teamConfig) {
292
298
  if (localConfig.coAuthorEnabled !== void 0) return localConfig.coAuthorEnabled;
293
299
  return teamConfig.sharing?.coAuthor?.enabled;
@@ -478,6 +484,16 @@ var init_types = __esm({
478
484
  recall: z.object({
479
485
  enabled: z.boolean().default(false)
480
486
  }).optional(),
487
+ // Optional (not .default) so existing TeamaiConfig literals stay valid; use
488
+ // isContributeHintEnabled() for the resolved view.
489
+ contributeHint: z.object({
490
+ /** Team default: whether the Stop hook nudges members to run
491
+ * /teamai-share-learnings after a high-friction session. Teams that route
492
+ * knowledge sharing through their own review flow can turn the nudge off
493
+ * without disabling the rest of the Stop hook (update check, votes sync,
494
+ * dashboard reporting). */
495
+ enabled: z.boolean().default(true)
496
+ }).optional(),
481
497
  // Optional (not .default) so existing TeamaiConfig literals stay valid, AND so
482
498
  // "team has no opinion" (block absent) stays distinct from "team says off"
483
499
  // (enabled: false). Only the former is a no-op; see resolveCoAuthor().
@@ -638,6 +654,8 @@ var init_types = __esm({
638
654
  excludedSkills: z.array(z.string()).optional(),
639
655
  /** User-level override for recall feature. When set, takes precedence over team config. */
640
656
  recallEnabled: z.boolean().optional(),
657
+ /** User-level override for the share-learnings hint. When set, takes precedence over team config. */
658
+ contributeHintEnabled: z.boolean().optional(),
641
659
  /** Per-machine override for the co-author trailer in AI-tool commits. When set,
642
660
  * takes precedence over the team `sharing.coAuthor` default. Undefined means
643
661
  * "defer to the team" (see resolveCoAuthor). */
@@ -880,15 +898,17 @@ async function readJson(filePath) {
880
898
  async function writeJson(filePath, data) {
881
899
  await writeFile(filePath, JSON.stringify(data, null, 2) + "\n");
882
900
  }
883
- async function writeJsonAtomic(filePath, data) {
901
+ async function writeJsonAtomic(filePath, data, options) {
884
902
  const expanded = expandHome(filePath);
885
903
  await fse.ensureDir(path3.dirname(expanded));
886
904
  const content = JSON.stringify(data, null, 2) + "\n";
887
- let mode = 384;
888
- try {
889
- mode = (await fse.stat(expanded)).mode & 511;
890
- } catch (error) {
891
- if (error.code !== "ENOENT") throw error;
905
+ let mode = options?.mode ?? 384;
906
+ if (options?.mode === void 0) {
907
+ try {
908
+ mode = (await fse.stat(expanded)).mode & 511;
909
+ } catch (error) {
910
+ if (error.code !== "ENOENT") throw error;
911
+ }
892
912
  }
893
913
  const tmp = `${expanded}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`;
894
914
  try {
@@ -8426,6 +8446,12 @@ import path26 from "path";
8426
8446
  import { execFile as execFile2 } from "child_process";
8427
8447
  import { promisify } from "util";
8428
8448
  import fse4 from "fs-extra";
8449
+ function modelAgentKind(tool) {
8450
+ const normalized = normalizeAgentType(tool ?? "");
8451
+ if (normalized === "codebuddy" || normalized === "codebuddy-internal") return "codebuddy";
8452
+ if (normalized === "claude") return "claude";
8453
+ return void 0;
8454
+ }
8429
8455
  function isUnimplementedCommand(command) {
8430
8456
  const type = command.type ?? "";
8431
8457
  if (IMPLEMENTED_HOOK_COMMAND_TYPES.has(type)) return false;
@@ -8443,6 +8469,9 @@ function getConfigPath2() {
8443
8469
  function getManifestPath() {
8444
8470
  return path26.join(getLocalAgentHome(), MANIFEST_FILE);
8445
8471
  }
8472
+ function getModelManifestPath() {
8473
+ return path26.join(getLocalAgentHome(), MODEL_MANIFEST_FILE);
8474
+ }
8446
8475
  function getErrorLogPath() {
8447
8476
  return path26.join(getTeamaiHomePath(), REPORTER_ERROR_LOG);
8448
8477
  }
@@ -9150,7 +9179,7 @@ function collectManifestSlugs(manifest) {
9150
9179
  }
9151
9180
  return { skills, rules };
9152
9181
  }
9153
- async function scanMcpFromManifest(scope, projectRoot) {
9182
+ async function scanMcpFromManifest(scope, tool, projectRoot) {
9154
9183
  const { resolveDataHomeForScope: resolveDataHomeForScope2 } = await Promise.resolve().then(() => (init_config(), config_exports));
9155
9184
  const dataHome = await resolveDataHomeForScope2(scope, projectRoot);
9156
9185
  let manifest;
@@ -9160,18 +9189,65 @@ async function scanMcpFromManifest(scope, projectRoot) {
9160
9189
  } else {
9161
9190
  manifest = await readJson(managedMcpManifestPath(dataHome)) ?? {};
9162
9191
  }
9192
+ const manifestKey = `${tool}${scope === "project" ? ":project" : ""}`;
9193
+ const records = manifest[manifestKey];
9194
+ if (!Array.isArray(records)) return [];
9163
9195
  const seen = /* @__PURE__ */ new Set();
9164
9196
  const results = [];
9165
- for (const records of Object.values(manifest)) {
9166
- if (!Array.isArray(records)) continue;
9167
- for (const rec of records) {
9168
- if (!rec.name || seen.has(rec.name)) continue;
9169
- seen.add(rec.name);
9170
- results.push({ slug: rec.name, source: "enterprise" });
9171
- }
9197
+ for (const rec of records) {
9198
+ if (!rec.name || seen.has(rec.name)) continue;
9199
+ seen.add(rec.name);
9200
+ results.push({ slug: rec.name, source: "enterprise" });
9172
9201
  }
9173
9202
  return results.sort((a, b) => a.slug.localeCompare(b.slug));
9174
9203
  }
9204
+ async function scanModelsFromDisk(tool) {
9205
+ const manifest = await readJson(getModelManifestPath()) ?? {};
9206
+ const agentKind = modelAgentKind(tool);
9207
+ const providers = (agentKind && manifest.providersByAgent?.[agentKind]) ?? manifest.providers ?? {};
9208
+ if (agentKind === "codebuddy") {
9209
+ const doc = await readJson(
9210
+ path26.join(getUserHome(), ".codebuddy", "models.json")
9211
+ );
9212
+ const entries = Array.isArray(doc?.models) ? doc.models : [];
9213
+ const owned = manifest.codebuddy ?? {};
9214
+ const results = [];
9215
+ for (const entry of entries) {
9216
+ if (typeof entry !== "object" || entry === null) continue;
9217
+ const { id, vendor, name } = entry;
9218
+ if (typeof id !== "string" || !id) continue;
9219
+ if (typeof vendor !== "string" || !vendor) continue;
9220
+ if (owned[id] === void 0 || providers[id] !== vendor) continue;
9221
+ results.push({
9222
+ provider: vendor,
9223
+ model_id: id,
9224
+ ...typeof name === "string" && name ? { name } : {},
9225
+ source: "enterprise"
9226
+ });
9227
+ }
9228
+ return results;
9229
+ }
9230
+ if (agentKind === "claude") {
9231
+ const settings = await readJson(
9232
+ path26.join(getUserHome(), ".claude", "settings.json")
9233
+ );
9234
+ const env = settings?.env;
9235
+ if (typeof env !== "object" || env === null || Array.isArray(env)) return [];
9236
+ const { ANTHROPIC_CUSTOM_MODEL_OPTION: modelId, ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: name } = env;
9237
+ if (typeof modelId !== "string" || !modelId) return [];
9238
+ const managed = manifest.claudeEnv?.ANTHROPIC_CUSTOM_MODEL_OPTION;
9239
+ if (managed === void 0 || entryHash(modelId) !== managed) return [];
9240
+ const provider = providers[modelId];
9241
+ if (!provider) return [];
9242
+ return [{
9243
+ provider,
9244
+ model_id: modelId,
9245
+ ...typeof name === "string" && name ? { name } : {},
9246
+ source: "enterprise"
9247
+ }];
9248
+ }
9249
+ return [];
9250
+ }
9175
9251
  async function pruneDeadWorkspaceBindings(config) {
9176
9252
  let changed = false;
9177
9253
  for (const workspacePath of Object.keys(config.workspaceBindings)) {
@@ -9224,8 +9300,10 @@ async function buildReportPayload(config, context) {
9224
9300
  const userLevel = { group_id: config.userGroupId };
9225
9301
  if (userScope.skills.length > 0) userLevel.skills = userScope.skills;
9226
9302
  if (userScope.rules.length > 0) userLevel.rules = userScope.rules;
9227
- const userMcps = await scanMcpFromManifest("user");
9303
+ const userMcps = await scanMcpFromManifest("user", tool);
9228
9304
  if (userMcps.length > 0) userLevel.mcps = userMcps;
9305
+ const userModels = await scanModelsFromDisk(tool);
9306
+ if (userModels.length > 0) userLevel.models = userModels;
9229
9307
  const payload = {
9230
9308
  agent_type: normalizeAgentType(tool),
9231
9309
  agent_version: await getAgentVersion(tool),
@@ -9256,7 +9334,7 @@ async function buildReportPayload(config, context) {
9256
9334
  };
9257
9335
  if (wsScope.skills.length > 0) workspace.skills = wsScope.skills;
9258
9336
  if (wsScope.rules.length > 0) workspace.rules = wsScope.rules;
9259
- const wsMcps = await scanMcpFromManifest("project", wsPath);
9337
+ const wsMcps = await scanMcpFromManifest("project", tool, wsPath);
9260
9338
  if (wsMcps.length > 0) workspace.mcps = wsMcps;
9261
9339
  return workspace;
9262
9340
  })
@@ -9640,6 +9718,226 @@ async function ackCommand(config, tag, command, status2, version2, error) {
9640
9718
  })
9641
9719
  });
9642
9720
  }
9721
+ function requireModelString(value, field) {
9722
+ if (typeof value !== "string" || !value.trim()) {
9723
+ throw new Error(`apply_model_config: ${field} must be a non-empty string`);
9724
+ }
9725
+ return value.trim();
9726
+ }
9727
+ function optionalPositiveInteger(value, field) {
9728
+ if (value === void 0 || value === null || value === "") return void 0;
9729
+ const normalized = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : value;
9730
+ if (!Number.isSafeInteger(normalized) || normalized < 0) {
9731
+ throw new Error(`apply_model_config: ${field} must be a positive integer`);
9732
+ }
9733
+ if (normalized === 0) return void 0;
9734
+ return normalized;
9735
+ }
9736
+ function parseDeliveredModels(raw) {
9737
+ if (!raw) throw new Error("apply_model_config: missing cmd");
9738
+ let parsed;
9739
+ try {
9740
+ parsed = JSON.parse(raw);
9741
+ } catch {
9742
+ throw new Error("apply_model_config: cmd must be valid JSON");
9743
+ }
9744
+ const fullSnapshot = typeof parsed === "object" && parsed !== null && "models" in parsed;
9745
+ const values = fullSnapshot ? parsed.models : [parsed];
9746
+ if (!Array.isArray(values)) {
9747
+ throw new Error("apply_model_config: models must be an array");
9748
+ }
9749
+ const seen = /* @__PURE__ */ new Set();
9750
+ const models = values.map((value) => {
9751
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
9752
+ throw new Error("apply_model_config: each model must be an object");
9753
+ }
9754
+ const input = value;
9755
+ const model = {
9756
+ provider: requireModelString(input.provider, "provider"),
9757
+ model_id: requireModelString(input.model_id, "model_id"),
9758
+ name: requireModelString(input.name, "name"),
9759
+ base_url: requireModelString(input.base_url, "base_url"),
9760
+ api_key: requireModelString(input.api_key, "api_key"),
9761
+ max_tokens: optionalPositiveInteger(input.max_tokens, "max_tokens") ?? DEFAULT_MAX_TOKENS,
9762
+ context_window: optionalPositiveInteger(input.context_window, "context_window")
9763
+ };
9764
+ let parsedUrl;
9765
+ try {
9766
+ parsedUrl = new URL(model.base_url);
9767
+ } catch {
9768
+ throw new Error("apply_model_config: base_url must be a valid URL");
9769
+ }
9770
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
9771
+ throw new Error("apply_model_config: base_url must use http or https");
9772
+ }
9773
+ if (seen.has(model.model_id)) {
9774
+ throw new Error(`apply_model_config: duplicate model_id "${model.model_id}"`);
9775
+ }
9776
+ seen.add(model.model_id);
9777
+ return model;
9778
+ });
9779
+ return { models, fullSnapshot };
9780
+ }
9781
+ function codebuddyModelEntry(model) {
9782
+ const baseUrl = model.base_url.replace(/\/+$/, "");
9783
+ return {
9784
+ id: model.model_id,
9785
+ name: model.name,
9786
+ vendor: model.provider,
9787
+ apiKey: model.api_key,
9788
+ ...model.context_window === void 0 ? {} : { maxInputTokens: model.context_window },
9789
+ ...model.max_tokens === void 0 ? {} : { maxOutputTokens: model.max_tokens },
9790
+ url: baseUrl.endsWith("/chat/completions") ? baseUrl : `${baseUrl}/chat/completions`,
9791
+ supportsToolCall: true
9792
+ };
9793
+ }
9794
+ async function readJsonObject(filePath) {
9795
+ const source = await readFileSafe(filePath);
9796
+ if (source === null) return {};
9797
+ try {
9798
+ const parsed = JSON.parse(source);
9799
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
9800
+ throw new Error("root must be an object");
9801
+ }
9802
+ return parsed;
9803
+ } catch (error) {
9804
+ throw new Error(`apply_model_config: cannot parse ${filePath}: ${error.message}`);
9805
+ }
9806
+ }
9807
+ async function writeModelJson(filePath, data) {
9808
+ let targetPath = filePath;
9809
+ try {
9810
+ if ((await fs12.promises.lstat(filePath)).isSymbolicLink()) {
9811
+ targetPath = await fs12.promises.realpath(filePath);
9812
+ }
9813
+ } catch (error) {
9814
+ if (error.code !== "ENOENT") throw error;
9815
+ }
9816
+ await writeJsonAtomic(targetPath, data, { mode: 384 });
9817
+ }
9818
+ async function reconcileCodebuddyModels(models, fullSnapshot, manifest) {
9819
+ const targetFile = path26.join(getUserHome(), ".codebuddy", "models.json");
9820
+ const doc = await readJsonObject(targetFile);
9821
+ const existing = doc.models === void 0 ? [] : doc.models;
9822
+ if (!Array.isArray(existing)) {
9823
+ throw new Error(`apply_model_config: models must be an array in ${targetFile}`);
9824
+ }
9825
+ const previouslyManaged = manifest.codebuddy ?? {};
9826
+ const nextManaged = fullSnapshot ? {} : { ...previouslyManaged };
9827
+ const incomingIds = new Set(models.map((model) => model.model_id));
9828
+ const removedManaged = /* @__PURE__ */ new Set();
9829
+ const preserved = [];
9830
+ const occupiedIds = /* @__PURE__ */ new Set();
9831
+ for (const entry of existing) {
9832
+ const id = typeof entry === "object" && entry !== null && typeof entry.id === "string" ? entry.id : void 0;
9833
+ if (id && previouslyManaged[id] && entryHash(entry) === previouslyManaged[id]) {
9834
+ if (fullSnapshot || incomingIds.has(id)) {
9835
+ removedManaged.add(id);
9836
+ continue;
9837
+ }
9838
+ preserved.push(entry);
9839
+ occupiedIds.add(id);
9840
+ continue;
9841
+ }
9842
+ preserved.push(entry);
9843
+ if (id) occupiedIds.add(id);
9844
+ if (id && previouslyManaged[id]) delete nextManaged[id];
9845
+ }
9846
+ for (const model of models) {
9847
+ if (occupiedIds.has(model.model_id)) continue;
9848
+ const entry = codebuddyModelEntry(model);
9849
+ preserved.push(entry);
9850
+ nextManaged[model.model_id] = entryHash(entry);
9851
+ }
9852
+ doc.models = preserved;
9853
+ if (Array.isArray(doc.availableModels) && doc.availableModels.length > 0) {
9854
+ const available = doc.availableModels.filter(
9855
+ (id) => typeof id === "string" && !removedManaged.has(id)
9856
+ );
9857
+ for (const id of Object.keys(nextManaged)) {
9858
+ if (!available.includes(id)) available.push(id);
9859
+ }
9860
+ doc.availableModels = available;
9861
+ }
9862
+ await writeModelJson(targetFile, doc);
9863
+ manifest.codebuddy = nextManaged;
9864
+ }
9865
+ function claudeEnvForModel(model) {
9866
+ const baseUrl = model.base_url.replace(/\/+$/, "").replace(/\/v1$/, "");
9867
+ return {
9868
+ ANTHROPIC_BASE_URL: baseUrl,
9869
+ ANTHROPIC_AUTH_TOKEN: model.api_key,
9870
+ ANTHROPIC_CUSTOM_MODEL_OPTION: model.model_id,
9871
+ ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: model.name
9872
+ };
9873
+ }
9874
+ async function reconcileClaudeModels(models, manifest) {
9875
+ const settingsPath = path26.join(getUserHome(), ".claude", "settings.json");
9876
+ const profilePath = path26.join(getUserHome(), ".claude", "teamai-models.json");
9877
+ const previousHashes = manifest.claudeEnv ?? {};
9878
+ const settings = await readJsonObject(settingsPath);
9879
+ const rawEnv = settings.env === void 0 ? {} : settings.env;
9880
+ if (typeof rawEnv !== "object" || rawEnv === null || Array.isArray(rawEnv)) {
9881
+ throw new Error(`apply_model_config: env must be an object in ${settingsPath}`);
9882
+ }
9883
+ const env = { ...rawEnv };
9884
+ if (models.length === 0) {
9885
+ const canRemoveGateway = Object.entries(previousHashes).every(
9886
+ ([key, hash]) => entryHash(env[key]) === hash
9887
+ );
9888
+ if (canRemoveGateway && Object.keys(previousHashes).length > 0) {
9889
+ for (const key of Object.keys(previousHashes)) delete env[key];
9890
+ settings.env = env;
9891
+ await writeModelJson(settingsPath, settings);
9892
+ }
9893
+ await remove(profilePath);
9894
+ manifest.claudeEnv = {};
9895
+ return;
9896
+ }
9897
+ const desired = claudeEnvForModel(models[0]);
9898
+ await writeModelJson(profilePath, { env: desired });
9899
+ const conflictKeys = /* @__PURE__ */ new Set([
9900
+ ...Object.keys(desired),
9901
+ "ANTHROPIC_API_KEY"
9902
+ ]);
9903
+ const canManage = [...conflictKeys].every((key) => env[key] === void 0 || previousHashes[key] !== void 0 && entryHash(env[key]) === previousHashes[key]);
9904
+ if (!canManage) {
9905
+ manifest.claudeEnv = {};
9906
+ return;
9907
+ }
9908
+ for (const [key, hash] of Object.entries(previousHashes)) {
9909
+ if (entryHash(env[key]) === hash) delete env[key];
9910
+ }
9911
+ Object.assign(env, desired);
9912
+ settings.env = env;
9913
+ await writeModelJson(settingsPath, settings);
9914
+ manifest.claudeEnv = Object.fromEntries(
9915
+ Object.entries(desired).map(([key, value]) => [key, entryHash(value)])
9916
+ );
9917
+ }
9918
+ async function applyModelConfig(command, tool) {
9919
+ const { models, fullSnapshot } = parseDeliveredModels(command.cmd);
9920
+ const manifest = await readJson(getModelManifestPath()) ?? {};
9921
+ const agentKind = modelAgentKind(tool);
9922
+ if (!agentKind) {
9923
+ throw new Error(`apply_model_config: unsupported agent "${tool ?? ""}"`);
9924
+ }
9925
+ const previousProviders = manifest.providersByAgent?.[agentKind] ?? manifest.providers ?? {};
9926
+ const providers = {
9927
+ ...fullSnapshot ? {} : previousProviders,
9928
+ ...Object.fromEntries(models.map((model) => [model.model_id, model.provider]))
9929
+ };
9930
+ manifest.providersByAgent = {
9931
+ ...manifest.providersByAgent,
9932
+ [agentKind]: providers
9933
+ };
9934
+ if (agentKind === "codebuddy") {
9935
+ await reconcileCodebuddyModels(models, fullSnapshot, manifest);
9936
+ } else {
9937
+ await reconcileClaudeModels(models, manifest);
9938
+ }
9939
+ await writeJsonAtomic(getModelManifestPath(), manifest);
9940
+ }
9643
9941
  function parseTeamaiCmd(raw) {
9644
9942
  const argv = [];
9645
9943
  let current = "";
@@ -9966,6 +10264,10 @@ async function runMcpCommand(config, command, context) {
9966
10264
  return command.version;
9967
10265
  }
9968
10266
  async function executeCommand(config, command, context) {
10267
+ if (command.type === "apply_model_config") {
10268
+ await applyModelConfig(command, context.tool);
10269
+ return;
10270
+ }
9969
10271
  if (command.type === "uninstall_teamai") {
9970
10272
  return runCmdCommand(command, context);
9971
10273
  }
@@ -9995,18 +10297,20 @@ async function executeCommand(config, command, context) {
9995
10297
  }
9996
10298
  async function processCommands(config, commands, context) {
9997
10299
  const tag = localAgentTag(context);
10300
+ let modelConfigApplied = false;
9998
10301
  for (const command of commands) {
9999
- if (isUnimplementedCommand(command)) {
10302
+ if (isUnimplementedCommand(command) || command.type !== "apply_model_config" && command.type !== "uninstall_teamai" && command.type !== "install_hook_rule" && command.type !== "uninstall_hook_rule" && command.type !== "install_mcp" && command.type !== "uninstall_mcp" && (!commandKind(command) || !commandAction(command))) {
10000
10303
  log.debug(`${tag} skipping unimplemented command ${command.id} (${command.type})`);
10001
10304
  continue;
10002
10305
  }
10003
10306
  try {
10004
10307
  const version2 = await executeCommand(config, command, context);
10005
10308
  await ackCommand(config, tag, command, "success", version2);
10309
+ if (command.type === "apply_model_config") modelConfigApplied = true;
10006
10310
  log.debug(`${tag} command ${command.id} (${command.type ?? ""}) succeeded`);
10007
10311
  if (command.type === "uninstall_teamai") {
10008
10312
  log.debug(`${tag} uninstall_teamai completed \u2014 remaining commands skipped`);
10009
- return;
10313
+ return modelConfigApplied;
10010
10314
  }
10011
10315
  } catch (e) {
10012
10316
  const error = e.message;
@@ -10018,6 +10322,7 @@ async function processCommands(config, commands, context) {
10018
10322
  }
10019
10323
  }
10020
10324
  }
10325
+ return modelConfigApplied;
10021
10326
  }
10022
10327
  async function reportAndSyncLocalAgent(context) {
10023
10328
  const config = await loadLocalAgentConfig();
@@ -10067,13 +10372,22 @@ async function reportAndSyncLocalAgent(context) {
10067
10372
  config,
10068
10373
  tag,
10069
10374
  "sync",
10070
- { method: "POST", body: JSON.stringify(syncPayload) }
10375
+ { method: "POST", body: JSON.stringify(syncPayload) },
10376
+ { redactResponseLog: true }
10071
10377
  );
10072
10378
  const cmds = syncResponse.cmds;
10073
10379
  const commands = cmds && cmds.length > 0 ? cmds : syncResponse.commands ?? [];
10074
10380
  if (commands.length > 0) {
10075
10381
  log.debug(`${tag} sync returned ${commands.length} command(s): ${commands.map((c) => `${c.type}#${c.id}`).join(", ")}`);
10076
- await processCommands(config, commands, context);
10382
+ const modelConfigApplied = await processCommands(config, commands, context);
10383
+ if (modelConfigApplied && !skipReport) {
10384
+ const reportPayload = await buildReportPayload(config, context);
10385
+ await localAgentFetch(config, tag, "report", {
10386
+ method: "POST",
10387
+ body: JSON.stringify(reportPayload)
10388
+ });
10389
+ log.debug(`${tag} model config report OK`);
10390
+ }
10077
10391
  }
10078
10392
  log.debug(`${tag} sync OK (${commands.length} command(s))`);
10079
10393
  } catch (e) {
@@ -10248,7 +10562,7 @@ async function bindCurrentProject(options) {
10248
10562
  log.info("\u672A\u7ED1\u5B9A\u9879\u76EE\u3002");
10249
10563
  }
10250
10564
  }
10251
- var execFileAsync, LOCAL_AGENT_DIR, CONFIG_FILE, MANIFEST_FILE, REPORTER_ERROR_LOG, LOCAL_AGENT_FETCH_TIMEOUT_MS, LOCAL_AGENT_HOOK_FETCH_TIMEOUT_MS, activeFetchTimeoutMs, UNIMPLEMENTED_COMMAND_TYPES, IMPLEMENTED_HOOK_COMMAND_TYPES, DEFAULT_ROUTES, PLUGIN_PULL_INTERVAL_MS, PLUGIN_FAIL_BACKOFF_MS, ZIP_MAGIC, VALID_MCP_TRANSPORTS;
10565
+ var execFileAsync, LOCAL_AGENT_DIR, CONFIG_FILE, MANIFEST_FILE, MODEL_MANIFEST_FILE, REPORTER_ERROR_LOG, LOCAL_AGENT_FETCH_TIMEOUT_MS, LOCAL_AGENT_HOOK_FETCH_TIMEOUT_MS, activeFetchTimeoutMs, UNIMPLEMENTED_COMMAND_TYPES, IMPLEMENTED_HOOK_COMMAND_TYPES, DEFAULT_ROUTES, PLUGIN_PULL_INTERVAL_MS, PLUGIN_FAIL_BACKOFF_MS, ZIP_MAGIC, DEFAULT_MAX_TOKENS, VALID_MCP_TRANSPORTS;
10252
10566
  var init_local_agent = __esm({
10253
10567
  "src/local-agent.ts"() {
10254
10568
  "use strict";
@@ -10279,6 +10593,7 @@ var init_local_agent = __esm({
10279
10593
  LOCAL_AGENT_DIR = "local-agent";
10280
10594
  CONFIG_FILE = "config.json";
10281
10595
  MANIFEST_FILE = "manifest.json";
10596
+ MODEL_MANIFEST_FILE = "model-manifest.json";
10282
10597
  REPORTER_ERROR_LOG = "reporter/errors.jsonl";
10283
10598
  LOCAL_AGENT_FETCH_TIMEOUT_MS = 15e3;
10284
10599
  LOCAL_AGENT_HOOK_FETCH_TIMEOUT_MS = 3e3;
@@ -10295,6 +10610,7 @@ var init_local_agent = __esm({
10295
10610
  PLUGIN_PULL_INTERVAL_MS = 12 * 60 * 60 * 1e3;
10296
10611
  PLUGIN_FAIL_BACKOFF_MS = 60 * 60 * 1e3;
10297
10612
  ZIP_MAGIC = Buffer.from([80, 75, 3, 4]);
10613
+ DEFAULT_MAX_TOKENS = 4096;
10298
10614
  VALID_MCP_TRANSPORTS = /* @__PURE__ */ new Set(["stdio", "http", "sse"]);
10299
10615
  }
10300
10616
  });
@@ -27739,6 +28055,16 @@ var init_mr_hint = __esm({
27739
28055
 
27740
28056
  // src/hook-handlers.ts
27741
28057
  import path86 from "path";
28058
+ async function contributeHintAllowed() {
28059
+ const { isContributeHintEnabled: isContributeHintEnabled2 } = await Promise.resolve().then(() => (init_types(), types_exports));
28060
+ try {
28061
+ const { autoDetectInit: autoDetectInit2 } = await Promise.resolve().then(() => (init_config(), config_exports));
28062
+ const { localConfig, teamConfig } = await autoDetectInit2();
28063
+ return isContributeHintEnabled2(localConfig, teamConfig);
28064
+ } catch {
28065
+ return isContributeHintEnabled2({}, {});
28066
+ }
28067
+ }
27742
28068
  function buildHandlerRegistry() {
27743
28069
  return [
27744
28070
  // ─── SessionStart ─────────────────────────────────
@@ -27885,6 +28211,7 @@ var init_hook_handlers = __esm({
27885
28211
  contributeCheckHandler = {
27886
28212
  name: "contribute-check",
27887
28213
  async execute(stdin, tool) {
28214
+ if (!await contributeHintAllowed()) return null;
27888
28215
  const { contributeCheckForSession: contributeCheckForSession2 } = await Promise.resolve().then(() => (init_contribute_check(), contribute_check_exports));
27889
28216
  const { formatStopHookOutput: formatStopHookOutput2 } = await Promise.resolve().then(() => (init_hook_output(), hook_output_exports));
27890
28217
  const { STOP_STDOUT_UNSUPPORTED_TOOLS: STOP_STDOUT_UNSUPPORTED_TOOLS2 } = await Promise.resolve().then(() => (init_tool_names(), tool_names_exports));
@@ -27904,7 +28231,8 @@ var init_hook_handlers = __esm({
27904
28231
  if (!STOP_STDOUT_UNSUPPORTED_TOOLS2.has(tool)) return null;
27905
28232
  const sessionId = deriveSessionId(stdin, { includeCwd: true });
27906
28233
  const pending = await Promise.resolve().then(() => (init_contribute_check(), contribute_check_exports));
27907
- const hint = await pending.takePendingHint(sessionId);
28234
+ const stashed = await pending.takePendingHint(sessionId);
28235
+ const hint = await contributeHintAllowed() ? stashed : null;
27908
28236
  const votesHint = await pending.takePendingVotesHint(sessionId);
27909
28237
  const combined = [hint, votesHint].filter(Boolean).join("\n");
27910
28238
  if (!combined) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamai-cli",
3
- "version": "0.23.0-beta.7",
3
+ "version": "0.23.0",
4
4
  "description": "TeamAI — Make Every Team AI Native (skill sync + shared knowledge base, powered by Git)",
5
5
  "type": "module",
6
6
  "bin": {