teamai-cli 0.23.0-beta.6 → 0.23.0-beta.8

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
 
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
 
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). */
@@ -747,7 +765,18 @@ var init_types = __esm({
747
765
  "instead",
748
766
  "don't",
749
767
  "that's not",
750
- "not what"
768
+ "not what",
769
+ // Japanese: "that's wrong" / "not that" / "redo" / "you got it wrong" / "on your own" / "put it back".
770
+ "\u9055\u3046",
771
+ "\u3061\u304C\u3046",
772
+ "\u305D\u3046\u3058\u3083\u306A",
773
+ "\u305D\u3046\u3067\u306F\u306A",
774
+ "\u3084\u308A\u76F4",
775
+ "\u3084\u308A\u306A\u304A\u3057",
776
+ "\u9593\u9055\u3063\u3066",
777
+ "\u9593\u9055\u3048",
778
+ "\u52DD\u624B\u306B",
779
+ "\u623B\u3057\u3066"
751
780
  ];
752
781
  INTERVENTION_SCAN_MAX_BYTES = 50 * 1024 * 1024;
753
782
  TRANSCRIPT_INTERRUPT_PREFIX = "[Request interrupted by user";
@@ -869,15 +898,17 @@ async function readJson(filePath) {
869
898
  async function writeJson(filePath, data) {
870
899
  await writeFile(filePath, JSON.stringify(data, null, 2) + "\n");
871
900
  }
872
- async function writeJsonAtomic(filePath, data) {
901
+ async function writeJsonAtomic(filePath, data, options) {
873
902
  const expanded = expandHome(filePath);
874
903
  await fse.ensureDir(path3.dirname(expanded));
875
904
  const content = JSON.stringify(data, null, 2) + "\n";
876
- let mode = 384;
877
- try {
878
- mode = (await fse.stat(expanded)).mode & 511;
879
- } catch (error) {
880
- 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
+ }
881
912
  }
882
913
  const tmp = `${expanded}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`;
883
914
  try {
@@ -6562,22 +6593,32 @@ async function readLastAssistantOutput(transcriptPath) {
6562
6593
  }
6563
6594
  }
6564
6595
  async function scanTranscriptStop(transcriptPath, opts) {
6596
+ if (path20.basename(transcriptPath) === "index.json") {
6597
+ const cb = await scanCodebuddyIndex(transcriptPath, opts?.frictionOnly ?? false);
6598
+ if (cb) return cb;
6599
+ }
6600
+ const initial = await scanJsonlTranscriptOnce(transcriptPath);
6601
+ if (opts?.frictionOnly || !isCodexTool(opts?.tool)) return initial.result;
6602
+ const flushedSnapshot = await waitForCodexUsageFlush(transcriptPath, initial.codexSnapshot);
6603
+ return flushedSnapshot ? { ...initial.result, tokens: flushedSnapshot.tokens, tokenScope: flushedSnapshot.scope } : initial.result;
6604
+ }
6605
+ async function scanJsonlTranscriptOnce(transcriptPath) {
6565
6606
  let interrupt = 0;
6566
6607
  let toolReject = 0;
6567
6608
  let toolError = 0;
6568
6609
  let prompts = 0;
6569
6610
  const tokens = emptyTokenUsage();
6611
+ let codexSessionSnapshot = null;
6612
+ let codexTranscriptSnapshot = null;
6570
6613
  const countedUsageKeys = /* @__PURE__ */ new Set();
6571
- if (path20.basename(transcriptPath) === "index.json") {
6572
- const cb = await scanCodebuddyIndex(transcriptPath, opts?.frictionOnly ?? false);
6573
- if (cb) return cb;
6574
- }
6575
6614
  try {
6576
6615
  const stat8 = await fs9.promises.stat(transcriptPath);
6577
- if (stat8.size === 0) return { interrupt, toolReject, toolError, tokens, prompts };
6616
+ if (stat8.size === 0) {
6617
+ return { result: { interrupt, toolReject, toolError, tokens, prompts }, codexSnapshot: null };
6618
+ }
6578
6619
  if (stat8.size > INTERVENTION_SCAN_MAX_BYTES) {
6579
6620
  log.warn(`dashboard: transcript too large to scan (${stat8.size} bytes)`);
6580
- return { interrupt, toolReject, toolError, tokens, prompts };
6621
+ return { result: { interrupt, toolReject, toolError, tokens, prompts }, codexSnapshot: null };
6581
6622
  }
6582
6623
  const rl = readline2.createInterface({
6583
6624
  input: fs9.createReadStream(transcriptPath, { encoding: "utf-8" }),
@@ -6585,13 +6626,19 @@ async function scanTranscriptStop(transcriptPath, opts) {
6585
6626
  });
6586
6627
  for await (const line of rl) {
6587
6628
  const trimmed = line.trim();
6588
- if (!trimmed || !trimmed.includes('"user"') && !trimmed.includes('"assistant"')) continue;
6629
+ if (!trimmed || !trimmed.includes('"user"') && !trimmed.includes('"assistant"') && !trimmed.includes('"token_usage_record"') && !trimmed.includes('"token_count"')) continue;
6589
6630
  let entry;
6590
6631
  try {
6591
6632
  entry = JSON.parse(trimmed);
6592
6633
  } catch {
6593
6634
  continue;
6594
6635
  }
6636
+ const codexUsage = parseCodexCumulativeUsage(entry);
6637
+ if (codexUsage) {
6638
+ if (codexUsage.scope === "session") codexSessionSnapshot = codexUsage;
6639
+ else codexTranscriptSnapshot = codexUsage;
6640
+ continue;
6641
+ }
6595
6642
  if (entry.type === "assistant") {
6596
6643
  const usage = entry.message?.usage;
6597
6644
  const dedupKey2 = typeof entry.message?.id === "string" ? entry.message.id : typeof entry.requestId === "string" ? entry.requestId : void 0;
@@ -6638,7 +6685,97 @@ async function scanTranscriptStop(transcriptPath, opts) {
6638
6685
  } catch (e) {
6639
6686
  log.warn(`dashboard: failed to scan transcript: ${e.message}`);
6640
6687
  }
6641
- return { interrupt, toolReject, toolError, tokens, prompts };
6688
+ const codexSnapshot = codexSessionSnapshot ?? codexTranscriptSnapshot;
6689
+ const result = {
6690
+ interrupt,
6691
+ toolReject,
6692
+ toolError,
6693
+ tokens: codexSnapshot?.tokens ?? tokens,
6694
+ prompts,
6695
+ ...codexSnapshot ? { tokenScope: codexSnapshot.scope } : {}
6696
+ };
6697
+ return { result, codexSnapshot };
6698
+ }
6699
+ function asRecord(value) {
6700
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
6701
+ }
6702
+ function codexUsageToTokenUsage(usage) {
6703
+ const inclusiveInput = toNum(usage.input_tokens);
6704
+ const cacheRead = toNum(usage.cached_input_tokens);
6705
+ const cacheCreation = toNum(usage.cache_write_input_tokens);
6706
+ return {
6707
+ input: Math.max(0, inclusiveInput - cacheRead - cacheCreation),
6708
+ output: toNum(usage.output_tokens),
6709
+ cacheRead,
6710
+ cacheCreation
6711
+ };
6712
+ }
6713
+ function parseCodexCumulativeUsage(entry) {
6714
+ const payload = asRecord(entry.payload);
6715
+ if (entry.type === "token_usage_record") {
6716
+ const usage = asRecord(payload?.thread_token_usage);
6717
+ return usage ? { tokens: codexUsageToTokenUsage(usage), scope: "session" } : null;
6718
+ }
6719
+ if (entry.type === "event_msg" && payload?.type === "token_count") {
6720
+ const usage = asRecord(asRecord(payload.info)?.total_token_usage);
6721
+ return usage ? { tokens: codexUsageToTokenUsage(usage), scope: "transcript" } : null;
6722
+ }
6723
+ return null;
6724
+ }
6725
+ function isCodexTool(tool) {
6726
+ return typeof tool === "string" && tool.toLowerCase().includes("codex");
6727
+ }
6728
+ function codexSnapshotEquals(a, b) {
6729
+ return a !== null && a.scope === b.scope && a.tokens.input === b.tokens.input && a.tokens.output === b.tokens.output && a.tokens.cacheRead === b.tokens.cacheRead && a.tokens.cacheCreation === b.tokens.cacheCreation;
6730
+ }
6731
+ function preferCodexSnapshot(current, observed) {
6732
+ if (current?.scope === "session" && observed.scope === "transcript") return current;
6733
+ return observed;
6734
+ }
6735
+ async function readLatestCodexUsageFromTail(transcriptPath) {
6736
+ try {
6737
+ const stat8 = await fs9.promises.stat(transcriptPath);
6738
+ if (stat8.size === 0) return null;
6739
+ const readSize = Math.min(stat8.size, CODEX_USAGE_TAIL_BYTES);
6740
+ const offset = stat8.size - readSize;
6741
+ const fh = await fs9.promises.open(transcriptPath, "r");
6742
+ try {
6743
+ const buffer = Buffer.alloc(readSize);
6744
+ await fh.read(buffer, 0, readSize, offset);
6745
+ const lines = buffer.toString("utf-8").split("\n");
6746
+ if (offset > 0) lines.shift();
6747
+ let latestSession = null;
6748
+ let latestTranscript = null;
6749
+ for (const line of lines) {
6750
+ if (!line.includes('"token_usage_record"') && !line.includes('"token_count"')) continue;
6751
+ try {
6752
+ const parsed = JSON.parse(line);
6753
+ const snapshot = parseCodexCumulativeUsage(parsed);
6754
+ if (snapshot?.scope === "session") latestSession = snapshot;
6755
+ else if (snapshot) latestTranscript = snapshot;
6756
+ } catch {
6757
+ }
6758
+ }
6759
+ return latestSession ?? latestTranscript;
6760
+ } finally {
6761
+ await fh.close();
6762
+ }
6763
+ } catch {
6764
+ return null;
6765
+ }
6766
+ }
6767
+ async function waitForCodexUsageFlush(transcriptPath, initial) {
6768
+ let latest = initial;
6769
+ for (let attempt = 1; attempt < CODEX_USAGE_MAX_ATTEMPTS; attempt++) {
6770
+ await new Promise((resolve) => setTimeout(resolve, CODEX_USAGE_RETRY_MS));
6771
+ const observed = await readLatestCodexUsageFromTail(transcriptPath);
6772
+ if (!observed) continue;
6773
+ latest = preferCodexSnapshot(latest, observed);
6774
+ if (!codexSnapshotEquals(initial, latest) && (initial !== null || totalTokenCount(latest.tokens) > 0)) {
6775
+ return latest;
6776
+ }
6777
+ }
6778
+ return latest;
6642
6779
  }
6643
6780
  async function readCodebuddyIndexOnce(transcriptPath) {
6644
6781
  try {
@@ -6834,7 +6971,7 @@ async function parseHookEvent(raw, tool) {
6834
6971
  if (output) {
6835
6972
  event.stoppedOutput = output;
6836
6973
  }
6837
- const scan = await scanTranscriptStop(hookData.transcript_path);
6974
+ const scan = await scanTranscriptStop(hookData.transcript_path, { tool });
6838
6975
  if (scan.interrupt > 0 || scan.toolReject > 0 || scan.toolError > 0) {
6839
6976
  event.interventions = {
6840
6977
  interrupt: scan.interrupt,
@@ -6844,6 +6981,7 @@ async function parseHookEvent(raw, tool) {
6844
6981
  }
6845
6982
  if (scan.tokens.input > 0 || scan.tokens.output > 0 || scan.tokens.cacheRead > 0 || scan.tokens.cacheCreation > 0) {
6846
6983
  event.tokens = scan.tokens;
6984
+ if (scan.tokenScope) event.tokenScope = scan.tokenScope;
6847
6985
  }
6848
6986
  if (scan.prompts > 0) {
6849
6987
  event.prompts = scan.prompts;
@@ -6984,11 +7122,23 @@ function rebuildSessions(events) {
6984
7122
  });
6985
7123
  return result;
6986
7124
  }
7125
+ function setLatestTokenSnapshot(snapshots, key, event) {
7126
+ if (!event.tokens) return;
7127
+ const current = snapshots.get(key);
7128
+ const candidateTime = Date.parse(event.timestamp);
7129
+ const currentTime = current ? Date.parse(current.timestamp) : Number.NaN;
7130
+ if (!current || !Number.isFinite(candidateTime) || !Number.isFinite(currentTime) || candidateTime >= currentTime) {
7131
+ snapshots.set(key, { timestamp: event.timestamp, tokens: event.tokens });
7132
+ }
7133
+ }
6987
7134
  function aggregateSessionMetrics(events) {
6988
7135
  const map = /* @__PURE__ */ new Map();
6989
7136
  const lastStopAt = /* @__PURE__ */ new Map();
6990
7137
  const submitCount = /* @__PURE__ */ new Map();
6991
7138
  const stopPrompts = /* @__PURE__ */ new Map();
7139
+ const unscopedTokens = /* @__PURE__ */ new Map();
7140
+ const sessionTokens = /* @__PURE__ */ new Map();
7141
+ const transcriptTokens = /* @__PURE__ */ new Map();
6992
7142
  for (const event of events) {
6993
7143
  let m = map.get(event.sessionId);
6994
7144
  if (!m) {
@@ -7001,7 +7151,18 @@ function aggregateSessionMetrics(events) {
7001
7151
  m.toolReject = event.interventions.toolReject;
7002
7152
  }
7003
7153
  if (event.tokens) {
7004
- m.tokens = { ...event.tokens };
7154
+ if (event.tokenScope === "session") {
7155
+ setLatestTokenSnapshot(sessionTokens, event.sessionId, event);
7156
+ } else if (event.tokenScope === "transcript" && event.transcriptPath) {
7157
+ let segments = transcriptTokens.get(event.sessionId);
7158
+ if (!segments) {
7159
+ segments = /* @__PURE__ */ new Map();
7160
+ transcriptTokens.set(event.sessionId, segments);
7161
+ }
7162
+ setLatestTokenSnapshot(segments, event.transcriptPath, event);
7163
+ } else {
7164
+ setLatestTokenSnapshot(unscopedTokens, event.sessionId, event);
7165
+ }
7005
7166
  }
7006
7167
  if (typeof event.prompts === "number") {
7007
7168
  stopPrompts.set(event.sessionId, event.prompts);
@@ -7020,6 +7181,18 @@ function aggregateSessionMetrics(events) {
7020
7181
  }
7021
7182
  }
7022
7183
  for (const [sid, m] of map) {
7184
+ const sessionSnapshot = sessionTokens.get(sid);
7185
+ const segments = transcriptTokens.get(sid);
7186
+ if (sessionSnapshot) {
7187
+ m.tokens = { ...sessionSnapshot.tokens };
7188
+ } else if (segments && segments.size > 0) {
7189
+ let total = emptyTokenUsage();
7190
+ for (const segment of segments.values()) total = addTokenUsage(total, segment.tokens);
7191
+ m.tokens = total;
7192
+ } else {
7193
+ const unscoped = unscopedTokens.get(sid);
7194
+ if (unscoped) m.tokens = { ...unscoped.tokens };
7195
+ }
7023
7196
  m.prompts = Math.max(submitCount.get(sid) ?? 0, stopPrompts.get(sid) ?? 0);
7024
7197
  }
7025
7198
  return map;
@@ -7062,7 +7235,7 @@ async function dashboardReport(toolArg) {
7062
7235
  compactEvents().catch(() => {
7063
7236
  });
7064
7237
  }
7065
- var TRANSCRIPT_TAIL_BYTES, STOPPED_OUTPUT_MAX_CHARS, CODEBUDDY_USAGE_MAX_ATTEMPTS, CODEBUDDY_USAGE_RETRY_MS, CODEBUDDY_BLOB_MAX_COUNT, CODEBUDDY_REJECT_MARKER;
7238
+ var TRANSCRIPT_TAIL_BYTES, STOPPED_OUTPUT_MAX_CHARS, CODEX_USAGE_TAIL_BYTES, CODEX_USAGE_MAX_ATTEMPTS, CODEX_USAGE_RETRY_MS, CODEBUDDY_USAGE_MAX_ATTEMPTS, CODEBUDDY_USAGE_RETRY_MS, CODEBUDDY_BLOB_MAX_COUNT, CODEBUDDY_REJECT_MARKER;
7066
7239
  var init_dashboard_collector = __esm({
7067
7240
  "src/dashboard-collector.ts"() {
7068
7241
  "use strict";
@@ -7077,6 +7250,9 @@ var init_dashboard_collector = __esm({
7077
7250
  init_home();
7078
7251
  TRANSCRIPT_TAIL_BYTES = 10240;
7079
7252
  STOPPED_OUTPUT_MAX_CHARS = 500;
7253
+ CODEX_USAGE_TAIL_BYTES = 256 * 1024;
7254
+ CODEX_USAGE_MAX_ATTEMPTS = 8;
7255
+ CODEX_USAGE_RETRY_MS = 250;
7080
7256
  CODEBUDDY_USAGE_MAX_ATTEMPTS = 8;
7081
7257
  CODEBUDDY_USAGE_RETRY_MS = 250;
7082
7258
  CODEBUDDY_BLOB_MAX_COUNT = 2e3;
@@ -8270,6 +8446,12 @@ import path26 from "path";
8270
8446
  import { execFile as execFile2 } from "child_process";
8271
8447
  import { promisify } from "util";
8272
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
+ }
8273
8455
  function isUnimplementedCommand(command) {
8274
8456
  const type = command.type ?? "";
8275
8457
  if (IMPLEMENTED_HOOK_COMMAND_TYPES.has(type)) return false;
@@ -8287,6 +8469,9 @@ function getConfigPath2() {
8287
8469
  function getManifestPath() {
8288
8470
  return path26.join(getLocalAgentHome(), MANIFEST_FILE);
8289
8471
  }
8472
+ function getModelManifestPath() {
8473
+ return path26.join(getLocalAgentHome(), MODEL_MANIFEST_FILE);
8474
+ }
8290
8475
  function getErrorLogPath() {
8291
8476
  return path26.join(getTeamaiHomePath(), REPORTER_ERROR_LOG);
8292
8477
  }
@@ -8994,7 +9179,7 @@ function collectManifestSlugs(manifest) {
8994
9179
  }
8995
9180
  return { skills, rules };
8996
9181
  }
8997
- async function scanMcpFromManifest(scope, projectRoot) {
9182
+ async function scanMcpFromManifest(scope, tool, projectRoot) {
8998
9183
  const { resolveDataHomeForScope: resolveDataHomeForScope2 } = await Promise.resolve().then(() => (init_config(), config_exports));
8999
9184
  const dataHome = await resolveDataHomeForScope2(scope, projectRoot);
9000
9185
  let manifest;
@@ -9004,18 +9189,65 @@ async function scanMcpFromManifest(scope, projectRoot) {
9004
9189
  } else {
9005
9190
  manifest = await readJson(managedMcpManifestPath(dataHome)) ?? {};
9006
9191
  }
9192
+ const manifestKey = `${tool}${scope === "project" ? ":project" : ""}`;
9193
+ const records = manifest[manifestKey];
9194
+ if (!Array.isArray(records)) return [];
9007
9195
  const seen = /* @__PURE__ */ new Set();
9008
9196
  const results = [];
9009
- for (const records of Object.values(manifest)) {
9010
- if (!Array.isArray(records)) continue;
9011
- for (const rec of records) {
9012
- if (!rec.name || seen.has(rec.name)) continue;
9013
- seen.add(rec.name);
9014
- results.push({ slug: rec.name, source: "enterprise" });
9015
- }
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" });
9016
9201
  }
9017
9202
  return results.sort((a, b) => a.slug.localeCompare(b.slug));
9018
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
+ }
9019
9251
  async function pruneDeadWorkspaceBindings(config) {
9020
9252
  let changed = false;
9021
9253
  for (const workspacePath of Object.keys(config.workspaceBindings)) {
@@ -9068,8 +9300,10 @@ async function buildReportPayload(config, context) {
9068
9300
  const userLevel = { group_id: config.userGroupId };
9069
9301
  if (userScope.skills.length > 0) userLevel.skills = userScope.skills;
9070
9302
  if (userScope.rules.length > 0) userLevel.rules = userScope.rules;
9071
- const userMcps = await scanMcpFromManifest("user");
9303
+ const userMcps = await scanMcpFromManifest("user", tool);
9072
9304
  if (userMcps.length > 0) userLevel.mcps = userMcps;
9305
+ const userModels = await scanModelsFromDisk(tool);
9306
+ if (userModels.length > 0) userLevel.models = userModels;
9073
9307
  const payload = {
9074
9308
  agent_type: normalizeAgentType(tool),
9075
9309
  agent_version: await getAgentVersion(tool),
@@ -9100,7 +9334,7 @@ async function buildReportPayload(config, context) {
9100
9334
  };
9101
9335
  if (wsScope.skills.length > 0) workspace.skills = wsScope.skills;
9102
9336
  if (wsScope.rules.length > 0) workspace.rules = wsScope.rules;
9103
- const wsMcps = await scanMcpFromManifest("project", wsPath);
9337
+ const wsMcps = await scanMcpFromManifest("project", tool, wsPath);
9104
9338
  if (wsMcps.length > 0) workspace.mcps = wsMcps;
9105
9339
  return workspace;
9106
9340
  })
@@ -9484,6 +9718,226 @@ async function ackCommand(config, tag, command, status2, version2, error) {
9484
9718
  })
9485
9719
  });
9486
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
+ }
9487
9941
  function parseTeamaiCmd(raw) {
9488
9942
  const argv = [];
9489
9943
  let current = "";
@@ -9810,6 +10264,10 @@ async function runMcpCommand(config, command, context) {
9810
10264
  return command.version;
9811
10265
  }
9812
10266
  async function executeCommand(config, command, context) {
10267
+ if (command.type === "apply_model_config") {
10268
+ await applyModelConfig(command, context.tool);
10269
+ return;
10270
+ }
9813
10271
  if (command.type === "uninstall_teamai") {
9814
10272
  return runCmdCommand(command, context);
9815
10273
  }
@@ -9839,18 +10297,20 @@ async function executeCommand(config, command, context) {
9839
10297
  }
9840
10298
  async function processCommands(config, commands, context) {
9841
10299
  const tag = localAgentTag(context);
10300
+ let modelConfigApplied = false;
9842
10301
  for (const command of commands) {
9843
- 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))) {
9844
10303
  log.debug(`${tag} skipping unimplemented command ${command.id} (${command.type})`);
9845
10304
  continue;
9846
10305
  }
9847
10306
  try {
9848
10307
  const version2 = await executeCommand(config, command, context);
9849
10308
  await ackCommand(config, tag, command, "success", version2);
10309
+ if (command.type === "apply_model_config") modelConfigApplied = true;
9850
10310
  log.debug(`${tag} command ${command.id} (${command.type ?? ""}) succeeded`);
9851
10311
  if (command.type === "uninstall_teamai") {
9852
10312
  log.debug(`${tag} uninstall_teamai completed \u2014 remaining commands skipped`);
9853
- return;
10313
+ return modelConfigApplied;
9854
10314
  }
9855
10315
  } catch (e) {
9856
10316
  const error = e.message;
@@ -9862,6 +10322,7 @@ async function processCommands(config, commands, context) {
9862
10322
  }
9863
10323
  }
9864
10324
  }
10325
+ return modelConfigApplied;
9865
10326
  }
9866
10327
  async function reportAndSyncLocalAgent(context) {
9867
10328
  const config = await loadLocalAgentConfig();
@@ -9911,13 +10372,22 @@ async function reportAndSyncLocalAgent(context) {
9911
10372
  config,
9912
10373
  tag,
9913
10374
  "sync",
9914
- { method: "POST", body: JSON.stringify(syncPayload) }
10375
+ { method: "POST", body: JSON.stringify(syncPayload) },
10376
+ { redactResponseLog: true }
9915
10377
  );
9916
10378
  const cmds = syncResponse.cmds;
9917
10379
  const commands = cmds && cmds.length > 0 ? cmds : syncResponse.commands ?? [];
9918
10380
  if (commands.length > 0) {
9919
10381
  log.debug(`${tag} sync returned ${commands.length} command(s): ${commands.map((c) => `${c.type}#${c.id}`).join(", ")}`);
9920
- 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
+ }
9921
10391
  }
9922
10392
  log.debug(`${tag} sync OK (${commands.length} command(s))`);
9923
10393
  } catch (e) {
@@ -10092,7 +10562,7 @@ async function bindCurrentProject(options) {
10092
10562
  log.info("\u672A\u7ED1\u5B9A\u9879\u76EE\u3002");
10093
10563
  }
10094
10564
  }
10095
- 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;
10096
10566
  var init_local_agent = __esm({
10097
10567
  "src/local-agent.ts"() {
10098
10568
  "use strict";
@@ -10123,6 +10593,7 @@ var init_local_agent = __esm({
10123
10593
  LOCAL_AGENT_DIR = "local-agent";
10124
10594
  CONFIG_FILE = "config.json";
10125
10595
  MANIFEST_FILE = "manifest.json";
10596
+ MODEL_MANIFEST_FILE = "model-manifest.json";
10126
10597
  REPORTER_ERROR_LOG = "reporter/errors.jsonl";
10127
10598
  LOCAL_AGENT_FETCH_TIMEOUT_MS = 15e3;
10128
10599
  LOCAL_AGENT_HOOK_FETCH_TIMEOUT_MS = 3e3;
@@ -10139,6 +10610,7 @@ var init_local_agent = __esm({
10139
10610
  PLUGIN_PULL_INTERVAL_MS = 12 * 60 * 60 * 1e3;
10140
10611
  PLUGIN_FAIL_BACKOFF_MS = 60 * 60 * 1e3;
10141
10612
  ZIP_MAGIC = Buffer.from([80, 75, 3, 4]);
10613
+ DEFAULT_MAX_TOKENS = 4096;
10142
10614
  VALID_MCP_TRANSPORTS = /* @__PURE__ */ new Set(["stdio", "http", "sse"]);
10143
10615
  }
10144
10616
  });
@@ -27583,6 +28055,16 @@ var init_mr_hint = __esm({
27583
28055
 
27584
28056
  // src/hook-handlers.ts
27585
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
+ }
27586
28068
  function buildHandlerRegistry() {
27587
28069
  return [
27588
28070
  // ─── SessionStart ─────────────────────────────────
@@ -27729,6 +28211,7 @@ var init_hook_handlers = __esm({
27729
28211
  contributeCheckHandler = {
27730
28212
  name: "contribute-check",
27731
28213
  async execute(stdin, tool) {
28214
+ if (!await contributeHintAllowed()) return null;
27732
28215
  const { contributeCheckForSession: contributeCheckForSession2 } = await Promise.resolve().then(() => (init_contribute_check(), contribute_check_exports));
27733
28216
  const { formatStopHookOutput: formatStopHookOutput2 } = await Promise.resolve().then(() => (init_hook_output(), hook_output_exports));
27734
28217
  const { STOP_STDOUT_UNSUPPORTED_TOOLS: STOP_STDOUT_UNSUPPORTED_TOOLS2 } = await Promise.resolve().then(() => (init_tool_names(), tool_names_exports));
@@ -27748,7 +28231,8 @@ var init_hook_handlers = __esm({
27748
28231
  if (!STOP_STDOUT_UNSUPPORTED_TOOLS2.has(tool)) return null;
27749
28232
  const sessionId = deriveSessionId(stdin, { includeCwd: true });
27750
28233
  const pending = await Promise.resolve().then(() => (init_contribute_check(), contribute_check_exports));
27751
- const hint = await pending.takePendingHint(sessionId);
28234
+ const stashed = await pending.takePendingHint(sessionId);
28235
+ const hint = await contributeHintAllowed() ? stashed : null;
27752
28236
  const votesHint = await pending.takePendingVotesHint(sessionId);
27753
28237
  const combined = [hint, votesHint].filter(Boolean).join("\n");
27754
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.6",
3
+ "version": "0.23.0-beta.8",
4
4
  "description": "TeamAI — Make Every Team AI Native (skill sync + shared knowledge base, powered by Git)",
5
5
  "type": "module",
6
6
  "bin": {