teamai-cli 0.17.3 → 0.17.4

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/dist/index.js CHANGED
@@ -139,13 +139,19 @@ var init_logger = __esm({
139
139
  var types_exports = {};
140
140
  __export(types_exports, {
141
141
  CONTRIBUTE_BASE_THRESHOLD: () => CONTRIBUTE_BASE_THRESHOLD,
142
+ CONTRIBUTE_CORRECTION_WEIGHT: () => CONTRIBUTE_CORRECTION_WEIGHT,
143
+ CONTRIBUTE_DIVERSITY_BONUS_MAX: () => CONTRIBUTE_DIVERSITY_BONUS_MAX,
142
144
  CONTRIBUTE_FASTPATH_TTL_MS: () => CONTRIBUTE_FASTPATH_TTL_MS,
143
145
  CONTRIBUTE_GIT_COMMIT_DOWNWEIGHT: () => CONTRIBUTE_GIT_COMMIT_DOWNWEIGHT,
146
+ CONTRIBUTE_INTERRUPT_WEIGHT: () => CONTRIBUTE_INTERRUPT_WEIGHT,
144
147
  CONTRIBUTE_KNOWLEDGE_GAP_BONUS: () => CONTRIBUTE_KNOWLEDGE_GAP_BONUS,
145
148
  CONTRIBUTE_LOW_QUALITY_BONUS: () => CONTRIBUTE_LOW_QUALITY_BONUS,
146
149
  CONTRIBUTE_LOW_QUALITY_THRESHOLD: () => CONTRIBUTE_LOW_QUALITY_THRESHOLD,
150
+ CONTRIBUTE_REJECT_WEIGHT: () => CONTRIBUTE_REJECT_WEIGHT,
147
151
  CONTRIBUTE_SESSIONS_DIR: () => CONTRIBUTE_SESSIONS_DIR,
152
+ CONTRIBUTE_SKILL_BONUS: () => CONTRIBUTE_SKILL_BONUS,
148
153
  CONTRIBUTE_SMART_THRESHOLD: () => CONTRIBUTE_SMART_THRESHOLD,
154
+ CONTRIBUTE_TOOLERROR_TIERS: () => CONTRIBUTE_TOOLERROR_TIERS,
149
155
  CORRECTION_KEYWORDS: () => CORRECTION_KEYWORDS,
150
156
  CORRECTION_WINDOW_MS: () => CORRECTION_WINDOW_MS,
151
157
  CultureCompanySchema: () => CultureCompanySchema,
@@ -283,7 +289,7 @@ function getPushignorePath() {
283
289
  function areTeamHooksDisabled() {
284
290
  return process.env.TEAMAI_HOOKS_DISABLED === "1" || process.env.TEAMAI_HOOKS_DISABLED === "true";
285
291
  }
286
- var ToolPathsSchema, ScopeEnum, SharingConfigSchema, SourceConfigSchema, SOURCE_PULL_TTL_MS, TEAMAI_SOURCES_DIR, TeamaiConfigSchema, MemberConfigSchema, LocalConfigSchema, StateSchema, TEAMAI_HOME, TEAMAI_CONFIG_PATH, TEAMAI_STATE_PATH, TEAMAI_TOKEN_PATH, TEAMAI_UPDATE_LOCK_PATH, RESOURCE_TYPES, TEAMAI_RULES_START, TEAMAI_RULES_END, TEAMAI_HOOK_DESCRIPTION_PREFIX, TEAMAI_CUSTOM_HOOK_PREFIX, TEAMAI_ENV_START, TEAMAI_ENV_END, TEAMAI_CULTURE_START, TEAMAI_CULTURE_END, TEAMAI_CLAUDEMD_START, TEAMAI_CLAUDEMD_END, TEAMAI_RECALL_RULES_START, TEAMAI_RECALL_RULES_END, SKILL_NAME_REGEX, TEAMAI_USAGE_PATH, TEAMAI_KNOWN_SKILLS_PATH, TEAMAI_PUSHIGNORE_PATH, TEAMAI_SESSIONS_DIR, UsageEventSchema, DASHBOARD_EVENTS_DIR, DASHBOARD_EVENTS_PATH, DASHBOARD_DEFAULT_PORT, DASHBOARD_IDLE_TIMEOUT_MS, DASHBOARD_STALE_TIMEOUT_MS, DASHBOARD_COMPACTION_THRESHOLD, DASHBOARD_STOPPED_DISPLAY_MS, DASHBOARD_PID_CHECK_INTERVAL_MS, CORRECTION_WINDOW_MS, CORRECTION_KEYWORDS, INTERVENTION_SCAN_MAX_BYTES, TRANSCRIPT_INTERRUPT_PREFIX, TRANSCRIPT_SYSTEM_PREFIXES, TRANSCRIPT_REJECT_MARKERS, CONTRIBUTE_BASE_THRESHOLD, CONTRIBUTE_SMART_THRESHOLD, CONTRIBUTE_FASTPATH_TTL_MS, CONTRIBUTE_KNOWLEDGE_GAP_BONUS, CONTRIBUTE_LOW_QUALITY_BONUS, CONTRIBUTE_LOW_QUALITY_THRESHOLD, CONTRIBUTE_GIT_COMMIT_DOWNWEIGHT, CONTRIBUTE_SESSIONS_DIR, SEARCH_INDEX_VERSION, LEARNINGS_LOCAL_DIR, SEARCH_INDEX_PATH, VOTES_LOCAL_DIR, CultureCompanySchema, CultureTeamSchema, CultureFrontmatterSchema;
292
+ var ToolPathsSchema, ScopeEnum, SharingConfigSchema, SourceConfigSchema, SOURCE_PULL_TTL_MS, TEAMAI_SOURCES_DIR, TeamaiConfigSchema, MemberConfigSchema, LocalConfigSchema, StateSchema, TEAMAI_HOME, TEAMAI_CONFIG_PATH, TEAMAI_STATE_PATH, TEAMAI_TOKEN_PATH, TEAMAI_UPDATE_LOCK_PATH, RESOURCE_TYPES, TEAMAI_RULES_START, TEAMAI_RULES_END, TEAMAI_HOOK_DESCRIPTION_PREFIX, TEAMAI_CUSTOM_HOOK_PREFIX, TEAMAI_ENV_START, TEAMAI_ENV_END, TEAMAI_CULTURE_START, TEAMAI_CULTURE_END, TEAMAI_CLAUDEMD_START, TEAMAI_CLAUDEMD_END, TEAMAI_RECALL_RULES_START, TEAMAI_RECALL_RULES_END, SKILL_NAME_REGEX, TEAMAI_USAGE_PATH, TEAMAI_KNOWN_SKILLS_PATH, TEAMAI_PUSHIGNORE_PATH, TEAMAI_SESSIONS_DIR, UsageEventSchema, DASHBOARD_EVENTS_DIR, DASHBOARD_EVENTS_PATH, DASHBOARD_DEFAULT_PORT, DASHBOARD_IDLE_TIMEOUT_MS, DASHBOARD_STALE_TIMEOUT_MS, DASHBOARD_COMPACTION_THRESHOLD, DASHBOARD_STOPPED_DISPLAY_MS, DASHBOARD_PID_CHECK_INTERVAL_MS, CORRECTION_WINDOW_MS, CORRECTION_KEYWORDS, INTERVENTION_SCAN_MAX_BYTES, TRANSCRIPT_INTERRUPT_PREFIX, TRANSCRIPT_SYSTEM_PREFIXES, TRANSCRIPT_REJECT_MARKERS, CONTRIBUTE_BASE_THRESHOLD, CONTRIBUTE_SMART_THRESHOLD, CONTRIBUTE_INTERRUPT_WEIGHT, CONTRIBUTE_REJECT_WEIGHT, CONTRIBUTE_CORRECTION_WEIGHT, CONTRIBUTE_TOOLERROR_TIERS, CONTRIBUTE_SKILL_BONUS, CONTRIBUTE_DIVERSITY_BONUS_MAX, CONTRIBUTE_FASTPATH_TTL_MS, CONTRIBUTE_KNOWLEDGE_GAP_BONUS, CONTRIBUTE_LOW_QUALITY_BONUS, CONTRIBUTE_LOW_QUALITY_THRESHOLD, CONTRIBUTE_GIT_COMMIT_DOWNWEIGHT, CONTRIBUTE_SESSIONS_DIR, SEARCH_INDEX_VERSION, LEARNINGS_LOCAL_DIR, SEARCH_INDEX_PATH, VOTES_LOCAL_DIR, CultureCompanySchema, CultureTeamSchema, CultureFrontmatterSchema;
287
293
  var init_types = __esm({
288
294
  "src/types.ts"() {
289
295
  "use strict";
@@ -386,6 +392,8 @@ var init_types = __esm({
386
392
  projectRoot: z.string().optional(),
387
393
  /** Tags the user has subscribed to. If empty/undefined, pull all resources. */
388
394
  subscribedTags: z.array(z.string()).optional(),
395
+ /** Skills to exclude from local sync (per-user, does not affect team repo). */
396
+ excludedSkills: z.array(z.string()).optional(),
389
397
  /** User-level override for recall feature. When set, takes precedence over team config. */
390
398
  recallEnabled: z.boolean().optional(),
391
399
  /** When set, only inject hooks into these agents. Additive across multiple init --agent runs. */
@@ -470,7 +478,17 @@ var init_types = __esm({
470
478
  "doesn't want to proceed with this tool use"
471
479
  ];
472
480
  CONTRIBUTE_BASE_THRESHOLD = 15;
473
- CONTRIBUTE_SMART_THRESHOLD = 35;
481
+ CONTRIBUTE_SMART_THRESHOLD = 20;
482
+ CONTRIBUTE_INTERRUPT_WEIGHT = 20;
483
+ CONTRIBUTE_REJECT_WEIGHT = 20;
484
+ CONTRIBUTE_CORRECTION_WEIGHT = 20;
485
+ CONTRIBUTE_TOOLERROR_TIERS = [
486
+ { min: 8, points: 25 },
487
+ { min: 5, points: 18 },
488
+ { min: 3, points: 10 }
489
+ ];
490
+ CONTRIBUTE_SKILL_BONUS = 5;
491
+ CONTRIBUTE_DIVERSITY_BONUS_MAX = 5;
474
492
  CONTRIBUTE_FASTPATH_TTL_MS = 5 * 60 * 1e3;
475
493
  CONTRIBUTE_KNOWLEDGE_GAP_BONUS = 20;
476
494
  CONTRIBUTE_LOW_QUALITY_BONUS = 10;
@@ -1908,15 +1926,18 @@ teamai recall "<\u5173\u952E\u8BCD1> <\u5173\u952E\u8BCD2> ..."
1908
1926
 
1909
1927
  \u4ECE\u4EFB\u52A1\u63CF\u8FF0\u4E2D\u63D0\u53D6 3-6 \u4E2A\u9AD8\u4FE1\u53F7\u5173\u952E\u8BCD\u8FDB\u884C\u68C0\u7D22\u3002
1910
1928
 
1911
- ### \u68C0\u7D22\u540E\u8981\u6C42
1929
+ ### \u68C0\u7D22\u540E\u8981\u6C42\uFF08\u7528\u4E8E\u8BC4\u4F30\u77E5\u8BC6\u88AB\u91C7\u7EB3\u60C5\u51B5\uFF0C\u8BF7\u52A1\u5FC5\u6267\u884C\uFF09
1912
1930
 
1913
- \u5B8C\u6210\u4EFB\u52A1\u540E\uFF0C\u5728\u6700\u7EC8\u56DE\u590D\u4E2D\u58F0\u660E\u5B9E\u9645\u5F15\u7528\u4E86\u54EA\u4E9B\u77E5\u8BC6\u6761\u76EE\uFF1A
1931
+ teamai-recall subagent \u7684\u8FD4\u56DE\u91CC\u5DF2\u5217\u51FA\u672C\u6B21\u68C0\u7D22\u5230\u7684\u5019\u9009 doc-id\uFF08\u89C1\u5176 recalled-doc-ids \u5217\u8868\uFF09\u3002
1932
+ \u4F60**\u4E0D\u9700\u8981\u51ED\u8BB0\u5FC6\u91CD\u5EFA\u5217\u8868**\u2014\u2014\u53EA\u9700\u5728**\u6700\u7EC8\u56DE\u590D\u672B\u5C3E**\u8FFD\u52A0\u4E0B\u9762\u8FD9\u884C\uFF0C
1933
+ \u4ECE\u5019\u9009\u91CC**\u4FDD\u7559\u4F60\u5B9E\u9645\u7528\u5230\u7684**\uFF08\u7167\u6284\u4E86\u547D\u4EE4\u3001\u6216\u6982\u5FF5\u4E0A\u53C2\u8003\u4E86\uFF0C\u90FD\u7B97\u7528\u5230\uFF09\uFF0C\u5220\u6389\u6CA1\u7528\u5230\u7684\uFF1A
1914
1934
 
1915
1935
  \`\`\`
1916
- <!-- teamai:referenced-doc-ids: [doc-id-1, doc-id-2] -->
1936
+ <!-- teamai:referenced-doc-ids: [\u4F60\u5B9E\u9645\u7528\u5230\u7684-doc-id] -->
1917
1937
  \`\`\`
1918
1938
 
1919
- \u5982\u65E0\u76F8\u5173\u547D\u4E2D\u5219\u58F0\u660E\u7A7A\u5217\u8868\uFF1A\`<!-- teamai:referenced-doc-ids: [] -->\`
1939
+ \u4E00\u4E2A\u90FD\u6CA1\u7528\u5230\u5C31\u7559\u7A7A\uFF1A\`<!-- teamai:referenced-doc-ids: [] -->\`\u3002
1940
+ \u82E5\u76F4\u63A5\u7528 \`teamai recall\` \u547D\u4EE4\uFF08\u672A\u8D70 subagent\uFF09\uFF0C\u4ECE\u53EC\u56DE\u7ED3\u679C\u7684 File \u8DEF\u5F84\u63A8\u51FA doc-id \u81EA\u884C\u586B\u5165\u3002
1920
1941
  `;
1921
1942
  }
1922
1943
  });
@@ -3393,7 +3414,10 @@ var init_pid_monitor = __esm({
3393
3414
  function normalizeToolName(name) {
3394
3415
  return IDE_TO_CLI[name] ?? name;
3395
3416
  }
3396
- var IDE_TO_CLI;
3417
+ function normalizeAgentType(name) {
3418
+ return AGENT_TYPE_ALIASES[name] ?? name;
3419
+ }
3420
+ var IDE_TO_CLI, AGENT_TYPE_ALIASES;
3397
3421
  var init_tool_names = __esm({
3398
3422
  "src/utils/tool-names.ts"() {
3399
3423
  "use strict";
@@ -3408,6 +3432,79 @@ var init_tool_names = __esm({
3408
3432
  read_file: "Read",
3409
3433
  task: "Task"
3410
3434
  };
3435
+ AGENT_TYPE_ALIASES = {
3436
+ tcodex: "codex",
3437
+ "codex-internal": "codex",
3438
+ tclaude: "claude",
3439
+ "claude-internal": "claude"
3440
+ };
3441
+ }
3442
+ });
3443
+
3444
+ // src/utils/redact.ts
3445
+ function collectEnvSecrets(env = process.env) {
3446
+ const secrets = {};
3447
+ for (const [name, value] of Object.entries(env)) {
3448
+ if (typeof value !== "string" || value.length < MIN_ENV_VALUE_LENGTH) continue;
3449
+ const upper = name.toUpperCase();
3450
+ if (SECRET_KEY_HINTS.some((hint) => upper.includes(hint))) {
3451
+ secrets[name] = value;
3452
+ }
3453
+ }
3454
+ return secrets;
3455
+ }
3456
+ function redact(text, options = {}) {
3457
+ if (!text) return text;
3458
+ let out = text;
3459
+ const envSecrets = options.envSecrets ?? {};
3460
+ const entries = Object.entries(envSecrets).sort((a, b) => b[1].length - a[1].length);
3461
+ for (const [label, value] of entries) {
3462
+ if (value && out.includes(value)) {
3463
+ out = out.split(value).join(PLACEHOLDER(label));
3464
+ }
3465
+ }
3466
+ if (options.patterns !== false) {
3467
+ for (const [label, pattern] of SECRET_PATTERNS) {
3468
+ out = out.replace(pattern, () => PLACEHOLDER(label));
3469
+ }
3470
+ out = out.replace(KV_PATTERN, (_m, key, sep) => key + sep + PLACEHOLDER("kv"));
3471
+ out = out.replace(BEARER_PATTERN, (_m, scheme) => scheme + PLACEHOLDER("authz"));
3472
+ out = out.replace(
3473
+ CONNECTION_STRING_PATTERN,
3474
+ (_m, prefix, _pw, at) => prefix + PLACEHOLDER("conn") + at
3475
+ );
3476
+ }
3477
+ return out;
3478
+ }
3479
+ function redactWithEnv(text, env = process.env) {
3480
+ return redact(text, { envSecrets: collectEnvSecrets(env) });
3481
+ }
3482
+ var PLACEHOLDER, SECRET_KEY_HINTS, MIN_ENV_VALUE_LENGTH, SECRET_PATTERNS, KV_PATTERN, BEARER_PATTERN, CONNECTION_STRING_PATTERN;
3483
+ var init_redact = __esm({
3484
+ "src/utils/redact.ts"() {
3485
+ "use strict";
3486
+ PLACEHOLDER = (label) => `<REDACTED:${label}>`;
3487
+ SECRET_KEY_HINTS = ["TOKEN", "SECRET", "KEY", "PASSWORD", "PASSWD", "_PAT", "CREDENTIAL"];
3488
+ MIN_ENV_VALUE_LENGTH = 8;
3489
+ SECRET_PATTERNS = [
3490
+ ["pem", /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g],
3491
+ ["anthropic", /\bsk-ant-[A-Za-z0-9_-]{20,}/g],
3492
+ ["openai", /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}/g],
3493
+ ["gh_pat", /\bgithub_pat_[A-Za-z0-9_]{22,}/g],
3494
+ ["gh_tok", /\bgh[pousr]_[A-Za-z0-9]{20,}/g],
3495
+ ["aws", /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA)[0-9A-Z]{16}\b/g],
3496
+ ["slack", /\bxox[baprs]-[A-Za-z0-9-]{10,}/g],
3497
+ ["slackhook", /https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9/]{20,}/g],
3498
+ ["google", /\bAIza[0-9A-Za-z_-]{35}\b/g],
3499
+ ["gcp_oauth", /\bya29\.[0-9A-Za-z_-]{20,}/g],
3500
+ ["stripe", /\b[rs]k_(?:live|test)_[A-Za-z0-9]{16,}/g],
3501
+ ["jwt", /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g],
3502
+ ["sendgrid", /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g],
3503
+ ["provider", /\b(?:xai-|gsk_|pplx-|sk-or-v1-|hf_|dop_v1_|glpat-|npm_|shpat_|tfp_|nvapi-|r8_)[A-Za-z0-9_-]{16,}/g]
3504
+ ];
3505
+ KV_PATTERN = /(secret|token|api[_-]?key|access[_-]?key|client[_-]?secret|password|passwd|auth[_-]?token|private[_-]?key)(["']?\s*[:=]\s*["']?)([A-Za-z0-9._\-/+=]{16,})/gi;
3506
+ BEARER_PATTERN = /\b(bearer\s+)([A-Za-z0-9._-]{16,})/gi;
3507
+ CONNECTION_STRING_PATTERN = /([a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s:/@]+:)([^\s/@]{6,})(@)/g;
3411
3508
  }
3412
3509
  });
3413
3510
 
@@ -3463,7 +3560,7 @@ async function readLastAssistantOutput(transcriptPath) {
3463
3560
  } catch {
3464
3561
  }
3465
3562
  }
3466
- return lastAssistantText.slice(0, STOPPED_OUTPUT_MAX_CHARS);
3563
+ return redactWithEnv(lastAssistantText).slice(0, STOPPED_OUTPUT_MAX_CHARS);
3467
3564
  } finally {
3468
3565
  await fh.close();
3469
3566
  }
@@ -3475,6 +3572,7 @@ async function readLastAssistantOutput(transcriptPath) {
3475
3572
  async function scanTranscriptStop(transcriptPath) {
3476
3573
  let interrupt = 0;
3477
3574
  let toolReject = 0;
3575
+ let toolError = 0;
3478
3576
  let prompts = 0;
3479
3577
  const tokens = emptyTokenUsage();
3480
3578
  const countedUsageKeys = /* @__PURE__ */ new Set();
@@ -3484,10 +3582,10 @@ async function scanTranscriptStop(transcriptPath) {
3484
3582
  }
3485
3583
  try {
3486
3584
  const stat6 = await fs7.promises.stat(transcriptPath);
3487
- if (stat6.size === 0) return { interrupt, toolReject, tokens, prompts };
3585
+ if (stat6.size === 0) return { interrupt, toolReject, toolError, tokens, prompts };
3488
3586
  if (stat6.size > INTERVENTION_SCAN_MAX_BYTES) {
3489
3587
  log.warn(`dashboard: transcript too large to scan (${stat6.size} bytes)`);
3490
- return { interrupt, toolReject, tokens, prompts };
3588
+ return { interrupt, toolReject, toolError, tokens, prompts };
3491
3589
  }
3492
3590
  const rl = readline.createInterface({
3493
3591
  input: fs7.createReadStream(transcriptPath, { encoding: "utf-8" }),
@@ -3538,6 +3636,8 @@ async function scanTranscriptStop(transcriptPath) {
3538
3636
  const text = typeof item.content === "string" ? item.content : Array.isArray(item.content) ? item.content.map((c) => typeof c?.text === "string" ? c.text : "").join(" ") : "";
3539
3637
  if (TRANSCRIPT_REJECT_MARKERS.some((m) => text.includes(m))) {
3540
3638
  toolReject++;
3639
+ } else {
3640
+ toolError++;
3541
3641
  }
3542
3642
  }
3543
3643
  }
@@ -3546,7 +3646,7 @@ async function scanTranscriptStop(transcriptPath) {
3546
3646
  } catch (e) {
3547
3647
  log.warn(`dashboard: failed to scan transcript: ${e.message}`);
3548
3648
  }
3549
- return { interrupt, toolReject, tokens, prompts };
3649
+ return { interrupt, toolReject, toolError, tokens, prompts };
3550
3650
  }
3551
3651
  async function readCodebuddyIndexOnce(transcriptPath) {
3552
3652
  try {
@@ -3563,7 +3663,7 @@ async function readCodebuddyIndexOnce(transcriptPath) {
3563
3663
  tokens.output += toNum(usage.outputTokens);
3564
3664
  }
3565
3665
  const prompts = Array.isArray(data.messages) ? data.messages.filter((m) => m?.role === "user").length : 0;
3566
- return { interrupt: 0, toolReject: 0, tokens, prompts };
3666
+ return { interrupt: 0, toolReject: 0, toolError: 0, tokens, prompts };
3567
3667
  } catch (e) {
3568
3668
  log.warn(`dashboard: failed to scan CodeBuddy index: ${e.message}`);
3569
3669
  return null;
@@ -3590,8 +3690,8 @@ function toNum(v) {
3590
3690
  return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
3591
3691
  }
3592
3692
  async function countInterventions(transcriptPath) {
3593
- const { interrupt, toolReject } = await scanTranscriptStop(transcriptPath);
3594
- return { interrupt, toolReject };
3693
+ const { interrupt, toolReject, toolError } = await scanTranscriptStop(transcriptPath);
3694
+ return { interrupt, toolReject, toolError };
3595
3695
  }
3596
3696
  function isCorrectionPrompt(text) {
3597
3697
  if (!text) return false;
@@ -3664,8 +3764,12 @@ async function parseHookEvent(raw, tool) {
3664
3764
  event.stoppedOutput = output;
3665
3765
  }
3666
3766
  const scan = await scanTranscriptStop(hookData.transcript_path);
3667
- if (scan.interrupt > 0 || scan.toolReject > 0) {
3668
- event.interventions = { interrupt: scan.interrupt, toolReject: scan.toolReject };
3767
+ if (scan.interrupt > 0 || scan.toolReject > 0 || scan.toolError > 0) {
3768
+ event.interventions = {
3769
+ interrupt: scan.interrupt,
3770
+ toolReject: scan.toolReject,
3771
+ toolError: scan.toolError
3772
+ };
3669
3773
  }
3670
3774
  if (scan.tokens.input > 0 || scan.tokens.output > 0 || scan.tokens.cacheRead > 0 || scan.tokens.cacheCreation > 0) {
3671
3775
  event.tokens = scan.tokens;
@@ -3896,6 +4000,7 @@ var init_dashboard_collector = __esm({
3896
4000
  init_fs();
3897
4001
  init_pid_monitor();
3898
4002
  init_tool_names();
4003
+ init_redact();
3899
4004
  init_types();
3900
4005
  TRANSCRIPT_TAIL_BYTES = 10240;
3901
4006
  STOPPED_OUTPUT_MAX_CHARS = 500;
@@ -4159,6 +4264,145 @@ var init_http_log = __esm({
4159
4264
  }
4160
4265
  });
4161
4266
 
4267
+ // src/plugin-lifecycle.ts
4268
+ function makeReadyState(d, deps) {
4269
+ return {
4270
+ slug: d.slug,
4271
+ version: d.version,
4272
+ ready: true,
4273
+ installedAt: new Date(deps.now()).toISOString(),
4274
+ uninstallCmd: d.uninstallCmd,
4275
+ lastAttemptAt: void 0,
4276
+ lastError: void 0
4277
+ };
4278
+ }
4279
+ async function reconcilePlugins(desired, deps) {
4280
+ const local = await deps.readPlugins();
4281
+ const desiredSlugs = new Set(desired.map((d) => d.slug));
4282
+ for (const d of desired) {
4283
+ const e = local[d.slug];
4284
+ try {
4285
+ if (!e || !e.ready) {
4286
+ if (e?.lastAttemptAt && deps.now() - Date.parse(e.lastAttemptAt) < FAILURE_COOLDOWN_MS) {
4287
+ deps.log.debug(`plugin ${d.slug}: in failure cooldown, skip`);
4288
+ continue;
4289
+ }
4290
+ deps.log.debug(`plugin ${d.slug}: installing${d.version ? " version " + d.version : ""}`);
4291
+ await deps.execCommand(d.installCmd, INSTALL_TIMEOUT_MS);
4292
+ await deps.execCommand(d.runCmd, RUN_TIMEOUT_MS);
4293
+ await deps.mutatePlugins((m) => {
4294
+ m[d.slug] = makeReadyState(d, deps);
4295
+ });
4296
+ deps.log.debug(`plugin ${d.slug}: installed and setup complete`);
4297
+ } else if (d.version !== void 0 && e.version !== d.version) {
4298
+ deps.log.debug(`plugin ${d.slug}: updating ${e.version ?? "(unknown)"} -> ${d.version}`);
4299
+ await deps.execCommand(d.updateCmd ?? d.installCmd, UPDATE_TIMEOUT_MS);
4300
+ await deps.execCommand(d.runCmd, RUN_TIMEOUT_MS);
4301
+ await deps.mutatePlugins((m) => {
4302
+ m[d.slug] = makeReadyState(d, deps);
4303
+ });
4304
+ deps.log.debug(`plugin ${d.slug}: updated to version ${d.version}`);
4305
+ }
4306
+ } catch (err) {
4307
+ const msg = err.message;
4308
+ deps.log.warn(`plugin ${d.slug} reconcile failed: ${msg}`);
4309
+ await deps.mutatePlugins((m) => {
4310
+ const prev = m[d.slug];
4311
+ const base = prev ?? {
4312
+ slug: d.slug,
4313
+ version: d.version,
4314
+ ready: false,
4315
+ uninstallCmd: d.uninstallCmd
4316
+ };
4317
+ if (!prev || !prev.ready) {
4318
+ base.ready = false;
4319
+ }
4320
+ base.lastAttemptAt = new Date(deps.now()).toISOString();
4321
+ base.lastError = msg;
4322
+ m[d.slug] = base;
4323
+ });
4324
+ }
4325
+ }
4326
+ for (const slug of Object.keys(local)) {
4327
+ if (desiredSlugs.has(slug)) continue;
4328
+ const e = local[slug];
4329
+ try {
4330
+ deps.log.debug(`plugin ${slug}: uninstalling (removed from desired list)`);
4331
+ await deps.execCommand(e.uninstallCmd, UNINSTALL_TIMEOUT_MS);
4332
+ await deps.mutatePlugins((m) => {
4333
+ delete m[slug];
4334
+ });
4335
+ deps.log.debug(`plugin ${slug}: uninstalled`);
4336
+ } catch (err) {
4337
+ deps.log.warn(`plugin ${slug} uninstall failed: ${err.message}`);
4338
+ }
4339
+ }
4340
+ }
4341
+ async function teardownAllPlugins(deps) {
4342
+ const plugins = await deps.readPlugins();
4343
+ for (const [slug, state] of Object.entries(plugins)) {
4344
+ try {
4345
+ deps.log.debug(`plugin ${slug}: tearing down`);
4346
+ await deps.execCommand(state.uninstallCmd, UNINSTALL_TIMEOUT_MS);
4347
+ await deps.mutatePlugins((m) => {
4348
+ delete m[slug];
4349
+ });
4350
+ deps.log.debug(`plugin ${slug}: torn down`);
4351
+ } catch (err) {
4352
+ deps.log.warn(`plugin ${slug} teardown failed: ${err.message}`);
4353
+ }
4354
+ }
4355
+ }
4356
+ function parseGetConfig(resp) {
4357
+ const vars = {};
4358
+ const plugins = [];
4359
+ if (!resp || typeof resp !== "object") return { vars, plugins };
4360
+ for (const [key, section] of Object.entries(resp)) {
4361
+ if (!section || typeof section !== "object") continue;
4362
+ const s = section;
4363
+ const isStr = (v) => typeof v === "string" && v.length > 0;
4364
+ if (!isStr(s["install_cmd"]) || !isStr(s["run_cmd"]) || !isStr(s["uninstall_cmd"])) continue;
4365
+ for (const f of ["endpoint", "topic_id", "secret_id", "secret_key", "user_name"]) {
4366
+ if (isStr(s[f])) vars[f] = s[f];
4367
+ }
4368
+ if (typeof s["user_id"] === "number") vars["user_id"] = String(s["user_id"]);
4369
+ else if (isStr(s["user_id"])) vars["user_id"] = s["user_id"];
4370
+ plugins.push({
4371
+ slug: key,
4372
+ version: isStr(s["version"]) ? s["version"] : void 0,
4373
+ installCmd: s["install_cmd"],
4374
+ updateCmd: isStr(s["update_cmd"]) ? s["update_cmd"] : void 0,
4375
+ uninstallCmd: s["uninstall_cmd"],
4376
+ runCmd: s["run_cmd"]
4377
+ });
4378
+ }
4379
+ return { vars, plugins };
4380
+ }
4381
+ function substituteVars(cmd, vars) {
4382
+ return cmd.replace(
4383
+ /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g,
4384
+ (match, key) => key in vars ? vars[key] : match
4385
+ );
4386
+ }
4387
+ function unresolvedPlaceholders(cmd) {
4388
+ const found = /* @__PURE__ */ new Set();
4389
+ for (const match of cmd.matchAll(/\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g)) {
4390
+ found.add(match[1]);
4391
+ }
4392
+ return [...found];
4393
+ }
4394
+ var INSTALL_TIMEOUT_MS, UPDATE_TIMEOUT_MS, UNINSTALL_TIMEOUT_MS, RUN_TIMEOUT_MS, FAILURE_COOLDOWN_MS;
4395
+ var init_plugin_lifecycle = __esm({
4396
+ "src/plugin-lifecycle.ts"() {
4397
+ "use strict";
4398
+ INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
4399
+ UPDATE_TIMEOUT_MS = 5 * 60 * 1e3;
4400
+ UNINSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
4401
+ RUN_TIMEOUT_MS = 60 * 1e3;
4402
+ FAILURE_COOLDOWN_MS = 10 * 60 * 1e3;
4403
+ }
4404
+ });
4405
+
4162
4406
  // src/utils/prompt.ts
4163
4407
  var prompt_exports = {};
4164
4408
  __export(prompt_exports, {
@@ -4284,17 +4528,21 @@ var init_claudemd = __esm({
4284
4528
  // src/local-agent.ts
4285
4529
  var local_agent_exports = {};
4286
4530
  __export(local_agent_exports, {
4531
+ DEFAULT_ROUTES: () => DEFAULT_ROUTES,
4287
4532
  bindCurrentProject: () => bindCurrentProject,
4288
- bindWorkspaceToGroup: () => bindWorkspaceToGroup,
4533
+ bindWorkspaceToProject: () => bindWorkspaceToProject,
4289
4534
  buildReportPayload: () => buildReportPayload,
4290
4535
  describeLocalAgent: () => describeLocalAgent,
4291
- fetchUserGroups: () => fetchUserGroups,
4536
+ fetchUserProjects: () => fetchUserProjects,
4292
4537
  initLocalAgentHttp: () => initLocalAgentHttp,
4293
4538
  loadLocalAgentConfig: () => loadLocalAgentConfig,
4294
4539
  pullLocalAgentForCwd: () => pullLocalAgentForCwd,
4295
4540
  removeLocalAgentHttp: () => removeLocalAgentHttp,
4296
4541
  reportAndSyncFromHook: () => reportAndSyncFromHook,
4297
4542
  reportAndSyncLocalAgent: () => reportAndSyncLocalAgent,
4543
+ resolveRoute: () => resolveRoute,
4544
+ runPluginReconcileWorker: () => runPluginReconcileWorker,
4545
+ teardownLocalAgentPlugins: () => teardownLocalAgentPlugins,
4298
4546
  writeTokenFile: () => writeTokenFile
4299
4547
  });
4300
4548
  import fs10 from "fs";
@@ -4335,6 +4583,15 @@ function compileClaudemdBlock(contents) {
4335
4583
  function normalizeEndpoint(endpoint) {
4336
4584
  return endpoint.trim().replace(/\/+$/, "");
4337
4585
  }
4586
+ function normalizeRoute(route) {
4587
+ const trimmed = route.trim();
4588
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
4589
+ }
4590
+ function resolveRoute(config, name) {
4591
+ const override = config.routes?.[name];
4592
+ if (override && override.trim()) return normalizeRoute(override);
4593
+ return DEFAULT_ROUTES[name];
4594
+ }
4338
4595
  function resolveAgentInstallPath(agentType) {
4339
4596
  const home = process.env.HOME ?? "";
4340
4597
  const skillsRel = createLocalAgentTeamConfig("").toolPaths[agentType]?.skills;
@@ -4364,6 +4621,46 @@ async function loadManifest() {
4364
4621
  async function saveManifest(manifest) {
4365
4622
  await writeJson(getManifestPath(), manifest);
4366
4623
  }
4624
+ function getPluginStatePath() {
4625
+ return path18.join(getLocalAgentHome(), "plugins.json");
4626
+ }
4627
+ async function readPluginState() {
4628
+ return await readJson(getPluginStatePath()) ?? {};
4629
+ }
4630
+ async function withPluginStateLock(mutate) {
4631
+ const statePath = getPluginStatePath();
4632
+ const lockPath = `${statePath}.lock`;
4633
+ await ensureDir(path18.dirname(lockPath));
4634
+ const deadline = Date.now() + 5e3;
4635
+ let acquired = false;
4636
+ while (Date.now() <= deadline) {
4637
+ try {
4638
+ const fd = await fs10.promises.open(lockPath, "wx");
4639
+ await fd.close();
4640
+ acquired = true;
4641
+ break;
4642
+ } catch (e) {
4643
+ if (e.code !== "EEXIST") throw e;
4644
+ try {
4645
+ const st = await fs10.promises.stat(lockPath);
4646
+ if (Date.now() - st.mtimeMs > 3e4) {
4647
+ await fs10.promises.rm(lockPath, { force: true });
4648
+ continue;
4649
+ }
4650
+ } catch {
4651
+ }
4652
+ await new Promise((r) => setTimeout(r, 50));
4653
+ }
4654
+ }
4655
+ if (!acquired) throw new Error("could not acquire plugin-state lock");
4656
+ try {
4657
+ const m = await readPluginState();
4658
+ mutate(m);
4659
+ await writeJson(statePath, m);
4660
+ } finally {
4661
+ await fs10.promises.rm(lockPath, { force: true });
4662
+ }
4663
+ }
4367
4664
  function getManifestScope(manifest, scope, workspacePath) {
4368
4665
  const key = scopeKey(scope, workspacePath);
4369
4666
  manifest.scopes[key] ??= emptyManifestScope();
@@ -4372,11 +4669,17 @@ function getManifestScope(manifest, scope, workspacePath) {
4372
4669
  async function loadLocalAgentConfig() {
4373
4670
  const fileConfig = await readJson(getConfigPath2());
4374
4671
  if (fileConfig?.endpoint) {
4375
- return {
4672
+ const config = {
4376
4673
  ...fileConfig,
4377
4674
  endpoint: normalizeEndpoint(fileConfig.endpoint),
4378
4675
  workspaceBindings: fileConfig.workspaceBindings ?? {}
4379
4676
  };
4677
+ for (const [wsPath, binding] of Object.entries(config.workspaceBindings)) {
4678
+ if ("groupId" in binding && !("projectId" in binding)) {
4679
+ delete config.workspaceBindings[wsPath];
4680
+ }
4681
+ }
4682
+ return config;
4380
4683
  }
4381
4684
  const envEndpoint = process.env.TEAMAI_HTTP_ENDPOINT ?? process.env.TEAMAI_ENDPOINT ?? process.env.TEAMAI_API_BASE_URL;
4382
4685
  if (!envEndpoint) return null;
@@ -4439,9 +4742,9 @@ function authHeaders(config, json = true) {
4439
4742
  }
4440
4743
  return headers;
4441
4744
  }
4442
- async function localAgentFetch(config, tag, route, init2) {
4745
+ async function localAgentFetch(config, tag, route, init2, opts) {
4443
4746
  const method = init2?.method ?? "GET";
4444
- const url = `${config.endpoint}${route}`;
4747
+ const url = `${config.endpoint}${resolveRoute(config, route)}`;
4445
4748
  const headers = {
4446
4749
  ...authHeaders(config, init2?.body !== void 0),
4447
4750
  ...init2?.headers ?? {}
@@ -4457,7 +4760,7 @@ async function localAgentFetch(config, tag, route, init2) {
4457
4760
  body = text;
4458
4761
  }
4459
4762
  }
4460
- logHttpResponse(tag, method, url, response.status, response.statusText, body);
4763
+ logHttpResponse(tag, method, url, response.status, response.statusText, opts?.redactResponseLog ? "<redacted>" : body);
4461
4764
  if (!response.ok) {
4462
4765
  const message = typeof body === "object" && body && "error" in body ? String(body.error) : text || `${response.status} ${response.statusText}`;
4463
4766
  throw new Error(message);
@@ -4475,14 +4778,173 @@ async function appendErrorLog(entry) {
4475
4778
  } catch {
4476
4779
  }
4477
4780
  }
4478
- async function fetchUserGroups(config) {
4781
+ async function fetchUserProjects(config) {
4479
4782
  const response = await localAgentFetch(
4480
4783
  config,
4481
4784
  localAgentTag({}),
4482
- "/api/user-groups/mine",
4785
+ "projects",
4483
4786
  { method: "GET" }
4484
4787
  );
4485
- return response.groups ?? [];
4788
+ return response.projects ?? [];
4789
+ }
4790
+ function redactSecrets(s) {
4791
+ const names = "secret[_-]?(?:key|id)|api[_-]?key|access[_-]?token|token|password|passwd|pwd";
4792
+ return s.replace(new RegExp(`(--(?:${names})[= ]+)\\S+`, "gi"), "$1***").replace(new RegExp(`((?:${names})"?\\s*[:=]\\s*"?)[^"\\s,}]+`, "gi"), "$1***").replace(/(bearer\s+)[\w.\-]+/gi, "$1***");
4793
+ }
4794
+ async function execPluginCommand(cmd, timeoutMs) {
4795
+ const { spawn: spawn3 } = await import("child_process");
4796
+ await new Promise((resolve, reject) => {
4797
+ const child = process.platform === "win32" ? spawn3("cmd", ["/c", cmd], { windowsHide: true, stdio: ["ignore", "ignore", "pipe"] }) : spawn3("/bin/sh", ["-lc", cmd], { stdio: ["ignore", "ignore", "pipe"] });
4798
+ let stderr = "";
4799
+ let settled = false;
4800
+ let timer;
4801
+ child.stderr?.on("data", (d) => {
4802
+ stderr += d.toString();
4803
+ if (stderr.length > 8192) stderr = stderr.slice(-8192);
4804
+ });
4805
+ const detachStderr = () => {
4806
+ child.stderr?.removeAllListeners("data");
4807
+ child.stderr?.resume();
4808
+ child.stderr?.unref?.();
4809
+ };
4810
+ const finish = (fn) => {
4811
+ if (settled) return;
4812
+ settled = true;
4813
+ clearTimeout(timer);
4814
+ detachStderr();
4815
+ child.unref();
4816
+ fn();
4817
+ };
4818
+ timer = setTimeout(() => {
4819
+ child.kill("SIGKILL");
4820
+ finish(() => reject(new Error(`command timed out after ${timeoutMs}ms`)));
4821
+ }, timeoutMs);
4822
+ child.on("error", (e) => finish(() => reject(e)));
4823
+ child.on(
4824
+ "exit",
4825
+ (code, signal) => finish(() => {
4826
+ if (signal) return reject(new Error(`command killed by ${signal}`));
4827
+ if (code === 0) return resolve();
4828
+ const tail = stderr ? " :: " + redactSecrets(stderr.slice(0, 200).trim()) : "";
4829
+ reject(new Error(`command failed (exit ${code})${tail}`));
4830
+ })
4831
+ );
4832
+ });
4833
+ }
4834
+ async function fetchPluginConfig(config, tag) {
4835
+ return localAgentFetch(config, tag, "getConfig", { method: "GET" }, { redactResponseLog: true });
4836
+ }
4837
+ function getPluginPullStatePath() {
4838
+ return path18.join(getLocalAgentHome(), "plugin-pull.json");
4839
+ }
4840
+ function buildReconcileDeps(config, tag) {
4841
+ return {
4842
+ readPlugins: () => readPluginState(),
4843
+ mutatePlugins: (fn) => withPluginStateLock(fn),
4844
+ execCommand: (cmd, t) => execPluginCommand(cmd, t),
4845
+ now: () => Date.now(),
4846
+ log: {
4847
+ debug: (msg) => log.debug(`${tag} ${msg}`),
4848
+ // The reconcile worker runs detached (stdio: 'ignore'), so console-only log.warn
4849
+ // output is discarded. Mirror warnings to debug.log so failures are traceable.
4850
+ warn: (msg) => {
4851
+ log.warn(`${tag} ${msg}`);
4852
+ log.debug(`${tag} WARN: ${msg}`);
4853
+ }
4854
+ }
4855
+ };
4856
+ }
4857
+ async function maybeReconcilePlugins(context) {
4858
+ try {
4859
+ const state = await readJson(getPluginPullStatePath()) ?? {};
4860
+ const now = Date.now();
4861
+ if (state.lastPullAt && now - state.lastPullAt < PLUGIN_PULL_INTERVAL_MS) return;
4862
+ if (state.lastFailAt && now - state.lastFailAt < PLUGIN_FAIL_BACKOFF_MS) return;
4863
+ const tool = context.tool ?? "workbuddy";
4864
+ const localAgentId = `${tool}-${resolveLocalAgentId(context)}`;
4865
+ const { spawn: spawn3 } = await import("child_process");
4866
+ if (!process.argv[1]) {
4867
+ log.debug("[local-agent] plugin reconcile: no CLI entrypoint (argv[1]), skipping");
4868
+ return;
4869
+ }
4870
+ const child = spawn3(
4871
+ process.execPath,
4872
+ [process.argv[1], "source", "reconcile-plugins"],
4873
+ { detached: true, stdio: "ignore", env: { ...process.env, TEAMAI_PLUGIN_LOCAL_AGENT_ID: localAgentId } }
4874
+ );
4875
+ child.unref();
4876
+ } catch (e) {
4877
+ log.debug(`[local-agent] plugin reconcile spawn skipped: ${e.message}`);
4878
+ }
4879
+ }
4880
+ async function runPluginReconcileWorker() {
4881
+ const config = await loadLocalAgentConfig();
4882
+ if (!config) return;
4883
+ const lockPath = path18.join(getLocalAgentHome(), "plugin-reconcile.lock");
4884
+ await ensureDir(path18.dirname(lockPath));
4885
+ let acquired = false;
4886
+ try {
4887
+ try {
4888
+ const fd = await fs10.promises.open(lockPath, "wx");
4889
+ await fd.close();
4890
+ acquired = true;
4891
+ } catch (e) {
4892
+ if (e.code !== "EEXIST") throw e;
4893
+ try {
4894
+ const st = await fs10.promises.stat(lockPath);
4895
+ if (Date.now() - st.mtimeMs > 30 * 60 * 1e3) {
4896
+ await fs10.promises.rm(lockPath, { force: true });
4897
+ const fd = await fs10.promises.open(lockPath, "wx");
4898
+ await fd.close();
4899
+ acquired = true;
4900
+ }
4901
+ } catch {
4902
+ }
4903
+ if (!acquired) return;
4904
+ }
4905
+ const tag = "[local-agent] [plugin-reconcile]";
4906
+ const statePath = getPluginPullStatePath();
4907
+ try {
4908
+ const resp = await fetchPluginConfig(config, tag);
4909
+ const { vars, plugins } = parseGetConfig(resp);
4910
+ const declaredSlugs = plugins.length ? ` [${plugins.map((p) => p.slug).join(", ")}]` : "";
4911
+ log.debug(`${tag} get-config: ${plugins.length} plugin(s) declared${declaredSlugs}`);
4912
+ const laid = process.env.TEAMAI_PLUGIN_LOCAL_AGENT_ID;
4913
+ if (!laid) log.debug(tag + " no local_agent_id in env; plugins needing it will be skipped");
4914
+ const allVars = { ...vars, ...laid ? { local_agent_id: laid } : {} };
4915
+ const resolved = [];
4916
+ for (const p of plugins) {
4917
+ const rp = {
4918
+ ...p,
4919
+ installCmd: substituteVars(p.installCmd, allVars),
4920
+ updateCmd: p.updateCmd ? substituteVars(p.updateCmd, allVars) : void 0,
4921
+ uninstallCmd: substituteVars(p.uninstallCmd, allVars),
4922
+ runCmd: substituteVars(p.runCmd, allVars)
4923
+ };
4924
+ const missing = [.../* @__PURE__ */ new Set([
4925
+ ...unresolvedPlaceholders(rp.installCmd),
4926
+ ...unresolvedPlaceholders(rp.runCmd),
4927
+ ...unresolvedPlaceholders(rp.uninstallCmd),
4928
+ ...rp.updateCmd ? unresolvedPlaceholders(rp.updateCmd) : []
4929
+ ])];
4930
+ if (missing.length) {
4931
+ log.warn(`${tag} plugin ${p.slug}: unresolved placeholders [${missing.join(",")}], skipping`);
4932
+ log.debug(`${tag} WARN: plugin ${p.slug}: unresolved placeholders [${missing.join(",")}], skipping`);
4933
+ continue;
4934
+ }
4935
+ resolved.push(rp);
4936
+ }
4937
+ await reconcilePlugins(resolved, buildReconcileDeps(config, tag));
4938
+ log.debug(`${tag} reconcile complete (${resolved.length} plugin(s) processed)`);
4939
+ await writeJson(statePath, { lastPullAt: Date.now() });
4940
+ } catch (e) {
4941
+ const prev = await readJson(statePath) ?? {};
4942
+ await writeJson(statePath, { ...prev, lastFailAt: Date.now() });
4943
+ log.debug(`${tag} reconcile failed: ${e.message}`);
4944
+ }
4945
+ } finally {
4946
+ if (acquired) await fs10.promises.rm(lockPath, { force: true });
4947
+ }
4486
4948
  }
4487
4949
  async function askViaTty(prompt) {
4488
4950
  if (process.stdin.isTTY) {
@@ -4514,69 +4976,67 @@ async function askViaTty(prompt) {
4514
4976
  }
4515
4977
  }
4516
4978
  }
4517
- async function promptForGroupBinding(workspacePath, groups) {
4518
- if (groups.length === 0) return null;
4979
+ async function promptForProjectBinding(workspacePath, projects) {
4980
+ if (projects.length === 0) return null;
4519
4981
  log.debug(`local-agent: workspace not bound: ${workspacePath}`);
4520
- const answer = await askViaTty("\u662F\u5426\u7ED1\u5B9A\u5230\u4E00\u4E2A\u7EC4\u7EC7\uFF1F[y/N] ");
4982
+ const answer = await askViaTty("\u662F\u5426\u7ED1\u5B9A\u5230\u4E00\u4E2A\u9879\u76EE\uFF1F[y/N] ");
4521
4983
  if (!answer || answer.toLowerCase() !== "y") return null;
4522
- if (groups.length === 1) return groups[0];
4523
- log.info("\u53EF\u7528\u7EC4\u7EC7:");
4524
- groups.forEach((group, index2) => {
4525
- const suffix = group.is_primary ? " (\u9ED8\u8BA4)" : "";
4526
- log.info(` ${index2 + 1}. ${group.name}${suffix} [id=${group.id}]`);
4984
+ if (projects.length === 1) return projects[0];
4985
+ log.info("\u53EF\u7528\u9879\u76EE:");
4986
+ projects.forEach((project, index2) => {
4987
+ const desc = project.description ? ` - ${project.description}` : "";
4988
+ log.info(` ${index2 + 1}. ${project.name}${desc} [id=${project.id}]`);
4527
4989
  });
4528
- const primary = groups.find((group) => group.is_primary) ?? groups[0];
4529
- const defaultIndex = groups.indexOf(primary) + 1;
4530
- const selection = await askViaTty(`\u9009\u62E9\u7EC4\u7EC7\u7F16\u53F7\uFF08\u9ED8\u8BA4 ${defaultIndex}\uFF0C0 \u8DF3\u8FC7\uFF09: `);
4990
+ const selection = await askViaTty(`\u9009\u62E9\u9879\u76EE\u7F16\u53F7\uFF081-${projects.length}\uFF0C0 \u8DF3\u8FC7\uFF09: `);
4531
4991
  if (selection === null || selection === "0") return null;
4532
- const index = selection ? Number.parseInt(selection, 10) : defaultIndex;
4533
- if (Number.isNaN(index) || index < 1 || index > groups.length) return null;
4534
- return groups[index - 1];
4992
+ const index = selection ? Number.parseInt(selection, 10) : 0;
4993
+ if (Number.isNaN(index) || index < 1 || index > projects.length) return null;
4994
+ return projects[index - 1];
4535
4995
  }
4536
- async function bindWorkspaceToGroup(workspacePath, groupId) {
4996
+ async function bindWorkspaceToProject(workspacePath, projectId) {
4537
4997
  const config = await loadLocalAgentConfig();
4538
4998
  if (!config) {
4539
4999
  throw new Error("HTTP local agent is not initialized. Run `teamai init --http <ENDPOINT> --token <API_TOKEN>` first.");
4540
5000
  }
4541
- const groups = await fetchUserGroups(config);
4542
- const group = groupId ? groups.find((item) => item.id === groupId) : await promptForGroupBinding(workspacePath, groups);
4543
- if (!group) return null;
5001
+ const projects = await fetchUserProjects(config);
5002
+ const project = projectId ? projects.find((item) => item.id === projectId) : await promptForProjectBinding(workspacePath, projects);
5003
+ if (!project) return null;
4544
5004
  const binding = {
4545
- groupId: group.id,
4546
- groupName: group.name,
5005
+ projectId: project.id,
5006
+ projectName: project.name,
4547
5007
  boundAt: (/* @__PURE__ */ new Date()).toISOString()
4548
5008
  };
4549
5009
  config.workspaceBindings[workspacePath] = binding;
4550
5010
  await saveLocalAgentConfig(config);
4551
- log.success(`\u5DF2\u5C06\u9879\u76EE\u7ED1\u5B9A\u5230\u7EC4\u7EC7\uFF1A${group.name} [id=${group.id}]`);
5011
+ log.success(`\u5DF2\u5C06\u5DE5\u4F5C\u533A\u7ED1\u5B9A\u5230\u9879\u76EE\uFF1A${project.name} [id=${project.id}]`);
4552
5012
  return binding;
4553
5013
  }
4554
5014
  async function ensureWorkspaceBinding(config, workspacePath) {
4555
5015
  if (config.workspaceBindings[workspacePath]) return;
4556
- let groups;
5016
+ let projects;
4557
5017
  try {
4558
- groups = await fetchUserGroups(config);
5018
+ projects = await fetchUserProjects(config);
4559
5019
  } catch (e) {
4560
- log.debug(`local-agent: failed to load user groups: ${e.message}`);
5020
+ log.debug(`local-agent: failed to load user projects: ${e.message}`);
4561
5021
  return;
4562
5022
  }
4563
- if (groups.length === 0) return;
4564
- const group = await promptForGroupBinding(workspacePath, groups);
4565
- if (group) {
5023
+ if (projects.length === 0) return;
5024
+ const project = await promptForProjectBinding(workspacePath, projects);
5025
+ if (project) {
4566
5026
  config.workspaceBindings[workspacePath] = {
4567
- groupId: group.id,
4568
- groupName: group.name,
5027
+ projectId: project.id,
5028
+ projectName: project.name,
4569
5029
  boundAt: (/* @__PURE__ */ new Date()).toISOString()
4570
5030
  };
4571
5031
  await saveLocalAgentConfig(config);
4572
5032
  return;
4573
5033
  }
4574
- const groupList = groups.map((g) => `${g.name} (id=${g.id})`).join(", ");
5034
+ const projectList = projects.map((p) => `${p.name} (id=${p.id})`).join(", ");
4575
5035
  const context = [
4576
- "[teamai] \u5F53\u524D\u9879\u76EE\u5C1A\u672A\u7ED1\u5B9A\u7EC4\u7EC7\uFF0C\u65E0\u6CD5\u63A5\u6536\u7EC4\u7EC7\u4E0B\u53D1\u7684 skills/rules/claudemd \u8D44\u6E90\u3002",
4577
- `\u53EF\u7528\u7EC4\u7EC7: ${groupList}`,
5036
+ "[teamai] \u5F53\u524D\u5DE5\u4F5C\u533A\u5C1A\u672A\u7ED1\u5B9A\u9879\u76EE\uFF0C\u65E0\u6CD5\u63A5\u6536\u9879\u76EE\u4E0B\u53D1\u7684 skills/rules/claudemd \u8D44\u6E90\u3002",
5037
+ `\u53EF\u7528\u9879\u76EE: ${projectList}`,
4578
5038
  `\u8BF7\u63D0\u793A\u7528\u6237\u8FD0\u884C: teamai bind-project`,
4579
- `\u6216\u6307\u5B9A\u7EC4\u7EC7: teamai bind-project --group-id <id>`
5039
+ `\u6216\u6307\u5B9A\u9879\u76EE: teamai bind-project --project-id <id>`
4580
5040
  ].join("\n");
4581
5041
  const hookOutput = JSON.stringify({
4582
5042
  hookSpecificOutput: {
@@ -4598,21 +5058,21 @@ async function emitBindingHint(config, workspacePath) {
4598
5058
  fs10.writeFileSync(hintMarker, "");
4599
5059
  } catch {
4600
5060
  }
4601
- let groups;
5061
+ let projects;
4602
5062
  try {
4603
- groups = await fetchUserGroups(config);
5063
+ projects = await fetchUserProjects(config);
4604
5064
  } catch {
4605
5065
  return;
4606
5066
  }
4607
- if (groups.length === 0) return;
4608
- const skipNum = groups.length + 1;
4609
- const choices = groups.map((g, i) => `${i + 1}. \u7ED1\u5B9A\u5230\u300C${g.name}\u300D\u7EC4\u7EC7`);
4610
- const actions = groups.map((g, i) => `- \u7528\u6237\u56DE\u590D ${i + 1} \u2192 \u6267\u884C \`teamai bind-project --group-id ${g.id}\`\uFF0C\u5B8C\u6210\u540E\u56DE\u590D"\u7ED1\u5B9A\u5B8C\u6210\uFF0C\u8BF7\u5F00\u65B0\u4F1A\u8BDD\u751F\u6548"`);
5067
+ if (projects.length === 0) return;
5068
+ const skipNum = projects.length + 1;
5069
+ const choices = projects.map((p, i) => `${i + 1}. \u7ED1\u5B9A\u5230\u300C${p.name}\u300D\u9879\u76EE`);
5070
+ const actions = projects.map((p, i) => `- \u7528\u6237\u56DE\u590D ${i + 1} \u2192 \u6267\u884C \`teamai bind-project --project-id ${p.id}\`\uFF0C\u5B8C\u6210\u540E\u56DE\u590D"\u7ED1\u5B9A\u5B8C\u6210\uFF0C\u8BF7\u5F00\u65B0\u4F1A\u8BDD\u751F\u6548"`);
4611
5071
  const context = [
4612
- "[ClawPro\u7EC4\u7EC7 \u7ED1\u5B9A\u63D0\u793A] \u4F60\u5FC5\u987B\u539F\u6837\u5C55\u793A\u4E0B\u9762\u7684\u9009\u9879\u5217\u8868\uFF0C\u4E0D\u5F97\u4FEE\u6539\u7F16\u53F7\u6216\u5185\u5BB9\u3002\u7B49\u7528\u6237\u56DE\u590D\u6570\u5B57\u540E\u6267\u884C\u5BF9\u5E94\u547D\u4EE4\u3002",
5072
+ "[ClawPro\u9879\u76EE \u7ED1\u5B9A\u63D0\u793A] \u4F60\u5FC5\u987B\u539F\u6837\u5C55\u793A\u4E0B\u9762\u7684\u9009\u9879\u5217\u8868\uFF0C\u4E0D\u5F97\u4FEE\u6539\u7F16\u53F7\u6216\u5185\u5BB9\u3002\u7B49\u7528\u6237\u56DE\u590D\u6570\u5B57\u540E\u6267\u884C\u5BF9\u5E94\u547D\u4EE4\u3002",
4613
5073
  "",
4614
5074
  "---",
4615
- "\u5F53\u524D\u9879\u76EE\u672A\u7ED1\u5B9AClawPro\u7EC4\u7EC7\u3002\u7ED1\u5B9A\u540E\u53EF\u540C\u6B65\u56E2\u961F skills/rules\u3002\u8BF7\u56DE\u590D\u6570\u5B57\u9009\u62E9\uFF1A",
5075
+ "\u5F53\u524D\u5DE5\u4F5C\u533A\u672A\u7ED1\u5B9AClawPro\u9879\u76EE\u3002\u7ED1\u5B9A\u540E\u53EF\u540C\u6B65\u56E2\u961F skills/rules\u3002\u8BF7\u56DE\u590D\u6570\u5B57\u9009\u62E9\uFF1A",
4616
5076
  "",
4617
5077
  ...choices,
4618
5078
  `${skipNum}. \u4E0D\u7ED1\u5B9A\uFF0C\u4EE5\u540E\u4E5F\u4E0D\u518D\u63D0\u793A`,
@@ -4706,7 +5166,7 @@ async function buildReportPayload(config, context) {
4706
5166
  };
4707
5167
  const userScope = await scanScope(process.env.HOME ?? "");
4708
5168
  const payload = {
4709
- agent_type: tool,
5169
+ agent_type: normalizeAgentType(tool),
4710
5170
  agent_version: await getAgentVersion(tool),
4711
5171
  local_agent_id: resolveLocalAgentId(context),
4712
5172
  host_name: os3.hostname(),
@@ -4729,8 +5189,8 @@ async function buildReportPayload(config, context) {
4729
5189
  {
4730
5190
  path: workspacePath,
4731
5191
  name: path18.basename(workspacePath),
4732
- ide_type: tool,
4733
- group_id: binding?.groupId,
5192
+ ide_type: normalizeAgentType(tool),
5193
+ project_id: binding?.projectId,
4734
5194
  skills: wsScope.skills,
4735
5195
  rules: wsScope.rules
4736
5196
  }
@@ -4742,7 +5202,7 @@ async function buildSyncPayload(config, context) {
4742
5202
  const workspacePath = await resolveWorkspacePath(context.cwd);
4743
5203
  const binding = workspacePath ? config.workspaceBindings[workspacePath] : void 0;
4744
5204
  const payload = {
4745
- agent_type: context.tool ?? "workbuddy",
5205
+ agent_type: normalizeAgentType(context.tool ?? "workbuddy"),
4746
5206
  local_agent_id: resolveLocalAgentId(context),
4747
5207
  status: context.status ?? "running"
4748
5208
  };
@@ -4751,8 +5211,8 @@ async function buildSyncPayload(config, context) {
4751
5211
  {
4752
5212
  path: workspacePath,
4753
5213
  name: path18.basename(workspacePath),
4754
- ide_type: context.tool ?? "workbuddy",
4755
- group_id: binding?.groupId
5214
+ ide_type: normalizeAgentType(context.tool ?? "workbuddy"),
5215
+ project_id: binding?.projectId
4756
5216
  }
4757
5217
  ];
4758
5218
  }
@@ -5036,7 +5496,7 @@ async function removeClaudeMdSection(filePath, startMarker, endMarker) {
5036
5496
  await writeFile(filePath, (before + after).trimEnd() + "\n");
5037
5497
  }
5038
5498
  async function ackCommand(config, tag, command, status2, version2, error) {
5039
- await localAgentFetch(config, tag, "/api/local-agent/commands/ack", {
5499
+ await localAgentFetch(config, tag, "ack", {
5040
5500
  method: "POST",
5041
5501
  body: JSON.stringify({
5042
5502
  id: command.id,
@@ -5098,9 +5558,12 @@ async function reportAndSyncLocalAgent(context) {
5098
5558
  }
5099
5559
  const tag = localAgentTag(context);
5100
5560
  log.debug(`${tag} run: endpoint=${config.endpoint}`);
5561
+ if (context.event?.type === "session_start") {
5562
+ await maybeReconcilePlugins(context);
5563
+ }
5101
5564
  try {
5102
5565
  const reportPayload = await buildReportPayload(config, context);
5103
- await localAgentFetch(config, tag, "/api/local-agent/report", {
5566
+ await localAgentFetch(config, tag, "report", {
5104
5567
  method: "POST",
5105
5568
  body: JSON.stringify(reportPayload)
5106
5569
  });
@@ -5109,7 +5572,7 @@ async function reportAndSyncLocalAgent(context) {
5109
5572
  const syncResponse = await localAgentFetch(
5110
5573
  config,
5111
5574
  tag,
5112
- "/api/local-agent/sync",
5575
+ "sync",
5113
5576
  { method: "POST", body: JSON.stringify(syncPayload) }
5114
5577
  );
5115
5578
  const commands = syncResponse.commands ?? [];
@@ -5183,7 +5646,7 @@ async function pullLocalAgentForCwd(context) {
5183
5646
  async function describeLocalAgent() {
5184
5647
  const config = await loadLocalAgentConfig();
5185
5648
  if (!config) return null;
5186
- const boundGroups = Object.entries(config.workspaceBindings).filter(([, b]) => b.groupId !== 0).map(([workspacePath, b]) => ({ path: workspacePath, groupName: b.groupName, groupId: b.groupId }));
5649
+ const boundProjects = Object.entries(config.workspaceBindings).filter(([, b]) => b.projectId !== 0).map(([workspacePath, b]) => ({ path: workspacePath, projectName: b.projectName, projectId: b.projectId }));
5187
5650
  const manifest = await loadManifest();
5188
5651
  const counts = { skills: 0, rules: 0, claudemd: 0 };
5189
5652
  for (const scope of Object.values(manifest.scopes)) {
@@ -5191,7 +5654,7 @@ async function describeLocalAgent() {
5191
5654
  counts.rules += Object.keys(scope.rules ?? {}).length;
5192
5655
  counts.claudemd += Object.keys(scope.claudemd ?? {}).length;
5193
5656
  }
5194
- return { endpoint: config.endpoint, boundGroups, resourceCounts: counts };
5657
+ return { endpoint: config.endpoint, boundProjects, resourceCounts: counts };
5195
5658
  }
5196
5659
  function parseScopeKey(key) {
5197
5660
  if (key.startsWith("project:")) {
@@ -5199,12 +5662,26 @@ function parseScopeKey(key) {
5199
5662
  }
5200
5663
  return { scope: key === "instance" ? "instance" : "user" };
5201
5664
  }
5665
+ async function teardownLocalAgentPlugins() {
5666
+ try {
5667
+ const config = await loadLocalAgentConfig();
5668
+ if (!config) return;
5669
+ await teardownAllPlugins(buildReconcileDeps(config, "[local-agent] [uninstall]"));
5670
+ } catch (e) {
5671
+ log.warn(`[local-agent] plugin teardown failed: ${e instanceof Error ? e.message : String(e)}`);
5672
+ }
5673
+ }
5202
5674
  async function removeLocalAgentHttp() {
5203
5675
  const config = await loadLocalAgentConfig();
5204
5676
  if (!config) {
5205
5677
  log.info("No HTTP source configured \u2014 nothing to remove.");
5206
5678
  return;
5207
5679
  }
5680
+ try {
5681
+ await teardownAllPlugins(buildReconcileDeps(config, "[local-agent] [uninstall]"));
5682
+ } catch (e) {
5683
+ log.warn(`[local-agent] plugin teardown failed: ${e.message}`);
5684
+ }
5208
5685
  const kinds = ["skill", "rule", "claudemd"];
5209
5686
  const manifest = await loadManifest();
5210
5687
  for (const [key, scopeManifest] of Object.entries(manifest.scopes)) {
@@ -5232,17 +5709,17 @@ async function bindCurrentProject(options) {
5232
5709
  if (!config) {
5233
5710
  throw new Error("Local agent not initialized. Run `teamai init --http` first.");
5234
5711
  }
5235
- config.workspaceBindings[workspacePath] = { groupId: 0, groupName: "__skipped__", boundAt: (/* @__PURE__ */ new Date()).toISOString() };
5712
+ config.workspaceBindings[workspacePath] = { projectId: 0, projectName: "__skipped__", boundAt: (/* @__PURE__ */ new Date()).toISOString() };
5236
5713
  await saveLocalAgentConfig(config);
5237
- log.info(`\u5DF2\u8DF3\u8FC7\u7ED1\u5B9A\uFF0C\u4EE5\u540E\u4E0D\u518D\u63D0\u793A\u6B64\u9879\u76EE\u3002`);
5714
+ log.info(`\u5DF2\u8DF3\u8FC7\u7ED1\u5B9A\uFF0C\u4EE5\u540E\u4E0D\u518D\u63D0\u793A\u6B64\u5DE5\u4F5C\u533A\u3002`);
5238
5715
  return;
5239
5716
  }
5240
- const binding = await bindWorkspaceToGroup(workspacePath, options?.groupId);
5717
+ const binding = await bindWorkspaceToProject(workspacePath, options?.projectId);
5241
5718
  if (!binding) {
5242
5719
  log.info("\u672A\u7ED1\u5B9A\u9879\u76EE\u3002");
5243
5720
  }
5244
5721
  }
5245
- var execFileAsync, LOCAL_AGENT_DIR, CONFIG_FILE, MANIFEST_FILE, REPORTER_ERROR_LOG, ZIP_MAGIC;
5722
+ var execFileAsync, LOCAL_AGENT_DIR, CONFIG_FILE, MANIFEST_FILE, REPORTER_ERROR_LOG, DEFAULT_ROUTES, PLUGIN_PULL_INTERVAL_MS, PLUGIN_FAIL_BACKOFF_MS, ZIP_MAGIC;
5246
5723
  var init_local_agent = __esm({
5247
5724
  "src/local-agent.ts"() {
5248
5725
  "use strict";
@@ -5256,13 +5733,24 @@ var init_local_agent = __esm({
5256
5733
  init_machine_id();
5257
5734
  init_builtin_rules();
5258
5735
  init_path_safety();
5736
+ init_tool_names();
5259
5737
  init_http_log();
5738
+ init_plugin_lifecycle();
5260
5739
  init_types();
5261
5740
  execFileAsync = promisify(execFile2);
5262
5741
  LOCAL_AGENT_DIR = "local-agent";
5263
5742
  CONFIG_FILE = "config.json";
5264
5743
  MANIFEST_FILE = "manifest.json";
5265
5744
  REPORTER_ERROR_LOG = "reporter/errors.jsonl";
5745
+ DEFAULT_ROUTES = {
5746
+ projects: "/api/projects/mine",
5747
+ report: "/api/local-agent/report",
5748
+ sync: "/api/local-agent/sync",
5749
+ ack: "/api/local-agent/commands/ack",
5750
+ getConfig: "/api/local-agent/get-config"
5751
+ };
5752
+ PLUGIN_PULL_INTERVAL_MS = 12 * 60 * 60 * 1e3;
5753
+ PLUGIN_FAIL_BACKOFF_MS = 60 * 60 * 1e3;
5266
5754
  ZIP_MAGIC = Buffer.from([80, 75, 3, 4]);
5267
5755
  }
5268
5756
  });
@@ -5436,8 +5924,8 @@ async function sourceList() {
5436
5924
  log.info("HTTP source (report/sync/ack):");
5437
5925
  log.info(` ${httpSource.endpoint}`);
5438
5926
  log.dim(` ${skills} skill(s), ${rules} rule(s), ${claudemd} claude.md`);
5439
- for (const g of httpSource.boundGroups) {
5440
- log.dim(` bound: ${g.groupName ?? g.groupId} \u2014 ${g.path}`);
5927
+ for (const p of httpSource.boundProjects) {
5928
+ log.dim(` bound: ${p.projectName ?? p.projectId} \u2014 ${p.path}`);
5441
5929
  }
5442
5930
  }
5443
5931
  }
@@ -7737,6 +8225,7 @@ __export(git_exports, {
7737
8225
  getHeadRev: () => getHeadRev,
7738
8226
  getRepoStatus: () => getRepoStatus,
7739
8227
  initRepo: () => initRepo,
8228
+ isMetadataOnlyDiff: () => isMetadataOnlyDiff,
7740
8229
  pullRepo: () => pullRepo,
7741
8230
  pushRepoBranch: () => pushRepoBranch,
7742
8231
  pushRepoDirectly: () => pushRepoDirectly,
@@ -7862,6 +8351,26 @@ async function autoPushViaMR(repoPath, message, files, teamConfig, localConfig)
7862
8351
  return null;
7863
8352
  }
7864
8353
  }
8354
+ function isMetadataOnlyDiff(diff) {
8355
+ if (!diff.trim()) return true;
8356
+ const METADATA_PATTERNS = [
8357
+ /^\s*"?lastUpdated"?\s*[:=]/i,
8358
+ /^\s*"?lastScan"?\s*[:=]/i,
8359
+ /^\s*"?syncedAt"?\s*[:=]/i,
8360
+ /^\s*"?generatedAt"?\s*[:=]/i,
8361
+ /^\s*"?updatedAt"?\s*[:=]/i
8362
+ ];
8363
+ const lines = diff.split("\n");
8364
+ for (const line of lines) {
8365
+ if (!line.startsWith("+") && !line.startsWith("-")) continue;
8366
+ if (line.startsWith("---") || line.startsWith("+++")) continue;
8367
+ const content = line.slice(1);
8368
+ if (!content.trim()) continue;
8369
+ const isMetadata = METADATA_PATTERNS.some((pat) => pat.test(content));
8370
+ if (!isMetadata) return false;
8371
+ }
8372
+ return true;
8373
+ }
7865
8374
  async function pushRepoBranch(localPath, message, files, branchName) {
7866
8375
  const git = createGit2(localPath);
7867
8376
  await git.checkoutLocalBranch(branchName);
@@ -7874,6 +8383,14 @@ async function pushRepoBranch(localPath, message, files, branchName) {
7874
8383
  await git.deleteLocalBranch(branchName, true);
7875
8384
  return false;
7876
8385
  }
8386
+ const diffOutput = await git.diff(["--cached", "--unified=0"]);
8387
+ if (isMetadataOnlyDiff(diffOutput)) {
8388
+ const defaultBranch = await getDefaultBranch(localPath);
8389
+ log.debug(`Only metadata/timestamp changes detected, switching back to ${defaultBranch}`);
8390
+ await git.checkout(defaultBranch);
8391
+ await git.deleteLocalBranch(branchName, true);
8392
+ return false;
8393
+ }
7877
8394
  await git.commit(message);
7878
8395
  await git.push(["-u", "origin", branchName]);
7879
8396
  return true;
@@ -10754,6 +11271,7 @@ async function pullForScope(localConfig, options) {
10754
11271
  }
10755
11272
  const tagsConfig = await loadTagsConfig(localConfig.repo.localPath);
10756
11273
  const subscribedTags = localConfig.subscribedTags;
11274
+ const excludedSkills = new Set(localConfig.excludedSkills ?? []);
10757
11275
  const resourceTypes = ["skills", "rules", "docs", "env", "agents"];
10758
11276
  let totalSynced = 0;
10759
11277
  let desiredSkillNames = null;
@@ -10795,6 +11313,9 @@ async function pullForScope(localConfig, options) {
10795
11313
  if (!merged.has(item.name)) merged.set(item.name, item);
10796
11314
  }
10797
11315
  items = [...merged.values()];
11316
+ if (excludedSkills.size > 0) {
11317
+ items = items.filter((item) => !excludedSkills.has(item.name));
11318
+ }
10798
11319
  desiredSkillNames = new Set(items.map((i) => i.name));
10799
11320
  knownRepoSkillNames = new Set(allTeamSkills.map((i) => i.name));
10800
11321
  } else {
@@ -10903,6 +11424,19 @@ async function pullForScope(localConfig, options) {
10903
11424
  await remove(skillDir);
10904
11425
  log.debug(`Removed excluded skill ${dir} from ${tool}`);
10905
11426
  }
11427
+ if (excludedSkills.size > 0) {
11428
+ for (const namespace of localDirs) {
11429
+ const namespaceDir = path39.join(skillsDir, namespace);
11430
+ if (await pathExists(path39.join(namespaceDir, "SKILL.md"))) continue;
11431
+ for (const skillName of await listDirs(namespaceDir)) {
11432
+ if (!excludedSkills.has(skillName) || BUILTIN_SKILL_NAMES.has(skillName)) continue;
11433
+ const nestedSkillDir = path39.join(namespaceDir, skillName);
11434
+ if (!await pathExists(path39.join(nestedSkillDir, "SKILL.md"))) continue;
11435
+ await remove(nestedSkillDir);
11436
+ log.debug(`Removed excluded skill ${namespace}/${skillName} from ${tool}`);
11437
+ }
11438
+ }
11439
+ }
10906
11440
  }
10907
11441
  }
10908
11442
  if (totalSynced === 0) {
@@ -11978,6 +12512,79 @@ var init_skill_cmd = __esm({
11978
12512
  }
11979
12513
  });
11980
12514
 
12515
+ // src/exclude.ts
12516
+ var exclude_exports = {};
12517
+ __export(exclude_exports, {
12518
+ excludeAdd: () => excludeAdd,
12519
+ excludeList: () => excludeList,
12520
+ excludeRemove: () => excludeRemove
12521
+ });
12522
+ async function resolveExcludeScope() {
12523
+ const projectConfig = await detectProjectConfig();
12524
+ return projectConfig ?? (await requireInit()).localConfig;
12525
+ }
12526
+ async function saveExcludeScopeConfig(localConfig) {
12527
+ if (localConfig.scope === "project") {
12528
+ await saveLocalConfigForScope(localConfig, "project", localConfig.projectRoot);
12529
+ } else {
12530
+ await saveLocalConfig(localConfig);
12531
+ }
12532
+ try {
12533
+ const state = await loadStateForScope(localConfig.scope, localConfig.projectRoot);
12534
+ state.lastPullRev = null;
12535
+ await saveStateForScope(state, localConfig.scope, localConfig.projectRoot);
12536
+ } catch {
12537
+ }
12538
+ }
12539
+ async function excludeList(_options) {
12540
+ const config = await resolveExcludeScope();
12541
+ const excludedSkills = config.excludedSkills ?? [];
12542
+ if (excludedSkills.length === 0) {
12543
+ log.info("No excluded skills. (pull syncs all role skills)");
12544
+ return;
12545
+ }
12546
+ log.info(`Excluded skills (${excludedSkills.length}):`);
12547
+ for (const skill of excludedSkills) log.dim(` ${skill}`);
12548
+ }
12549
+ async function excludeAdd(skills, _options) {
12550
+ const config = await resolveExcludeScope();
12551
+ const existing = new Set(config.excludedSkills ?? []);
12552
+ const added = skills.filter((skill) => {
12553
+ if (existing.has(skill)) return false;
12554
+ existing.add(skill);
12555
+ return true;
12556
+ });
12557
+ if (added.length === 0) {
12558
+ log.info("Already excluded.");
12559
+ return;
12560
+ }
12561
+ await saveExcludeScopeConfig({ ...config, excludedSkills: [...existing].sort() });
12562
+ log.success(`Excluded: ${added.join(", ")}`);
12563
+ log.dim("Run `teamai pull` to remove them from local AI tools.");
12564
+ }
12565
+ async function excludeRemove(skills, _options) {
12566
+ const config = await resolveExcludeScope();
12567
+ const existing = new Set(config.excludedSkills ?? []);
12568
+ const removed = skills.filter((skill) => existing.delete(skill));
12569
+ if (removed.length === 0) {
12570
+ log.info("None of those skills were excluded.");
12571
+ return;
12572
+ }
12573
+ await saveExcludeScopeConfig({
12574
+ ...config,
12575
+ excludedSkills: existing.size > 0 ? [...existing].sort() : void 0
12576
+ });
12577
+ log.success(`Removed from exclude list: ${removed.join(", ")}`);
12578
+ log.dim("Run `teamai pull` to sync them again.");
12579
+ }
12580
+ var init_exclude = __esm({
12581
+ "src/exclude.ts"() {
12582
+ "use strict";
12583
+ init_config();
12584
+ init_logger();
12585
+ }
12586
+ });
12587
+
11981
12588
  // src/members.ts
11982
12589
  var members_exports = {};
11983
12590
  __export(members_exports, {
@@ -13183,6 +13790,7 @@ async function buildRemovalPlan(localConfig, teamConfig) {
13183
13790
  claudeMdFiles: [],
13184
13791
  skillDirs: [],
13185
13792
  ruleFiles: [],
13793
+ agentFiles: [],
13186
13794
  shellProfile: null,
13187
13795
  docsDir: null,
13188
13796
  teamaiHome,
@@ -13192,7 +13800,9 @@ async function buildRemovalPlan(localConfig, teamConfig) {
13192
13800
  };
13193
13801
  const repoPath = localConfig.repo.localPath;
13194
13802
  const teamSkillNames = await collectTeamSkillNames(repoPath);
13803
+ for (const name of BUILTIN_SKILL_NAMES) teamSkillNames.add(name);
13195
13804
  const teamRuleNames = await collectTeamRuleNames(repoPath);
13805
+ for (const name of BUILTIN_RULE_NAMES) teamRuleNames.add(name);
13196
13806
  const localAgentManifestPath = path47.join(
13197
13807
  process.env.HOME ?? "",
13198
13808
  ".teamai",
@@ -13249,13 +13859,23 @@ async function buildRemovalPlan(localConfig, teamConfig) {
13249
13859
  for (const file of files) {
13250
13860
  if (!file.endsWith(".md")) continue;
13251
13861
  const ruleName = file.replace(/\.md$/, "");
13252
- if (EXCLUDED_RULE_NAMES.has(ruleName)) continue;
13253
13862
  if (teamRuleNames.has(ruleName)) {
13254
13863
  plan.ruleFiles.push(path47.join(rulesDir, file));
13255
13864
  }
13256
13865
  }
13257
13866
  }
13258
13867
  }
13868
+ if (toolPath.agents) {
13869
+ const agentsDir = path47.join(baseDir, toolPath.agents);
13870
+ if (await pathExists(agentsDir)) {
13871
+ for (const name of BUILTIN_AGENT_NAMES) {
13872
+ const agentFile = path47.join(agentsDir, `${name}.md`);
13873
+ if (await pathExists(agentFile)) {
13874
+ plan.agentFiles.push(agentFile);
13875
+ }
13876
+ }
13877
+ }
13878
+ }
13259
13879
  }
13260
13880
  const shellProfilePath = teamConfig.sharing.env.shellProfilePath ? expandHome(teamConfig.sharing.env.shellProfilePath) : detectShellProfile();
13261
13881
  if (shellProfilePath) {
@@ -13277,7 +13897,7 @@ async function buildRemovalPlan(localConfig, teamConfig) {
13277
13897
  return plan;
13278
13898
  }
13279
13899
  function isPlanEmpty(plan) {
13280
- return plan.hookFiles.length === 0 && plan.openclawHookDirs.length === 0 && plan.claudeMdFiles.length === 0 && plan.skillDirs.length === 0 && plan.ruleFiles.length === 0 && plan.shellProfile === null && plan.docsDir === null && !plan.teamaiHomeExists;
13900
+ return plan.hookFiles.length === 0 && plan.openclawHookDirs.length === 0 && plan.claudeMdFiles.length === 0 && plan.skillDirs.length === 0 && plan.ruleFiles.length === 0 && plan.agentFiles.length === 0 && plan.shellProfile === null && plan.docsDir === null && !plan.teamaiHomeExists;
13281
13901
  }
13282
13902
  function printSummary(plan) {
13283
13903
  const cn = plan.scope === "project" ? "\u9879\u76EE\u7EA7" : "\u7528\u6237\u7EA7";
@@ -13314,6 +13934,10 @@ function printSummary(plan) {
13314
13934
  console.log(` Rules (${plan.ruleFiles.length} \u4E2A\u6587\u4EF6)`);
13315
13935
  console.log("");
13316
13936
  }
13937
+ if (plan.agentFiles.length > 0) {
13938
+ console.log(` Agents (${plan.agentFiles.length} \u4E2A\u6587\u4EF6)`);
13939
+ console.log("");
13940
+ }
13317
13941
  if (plan.shellProfile) {
13318
13942
  console.log(" Shell profile \u73AF\u5883\u53D8\u91CF\u5757:");
13319
13943
  console.log(` ${plan.shellProfile}`);
@@ -13330,6 +13954,14 @@ function printSummary(plan) {
13330
13954
  console.log("");
13331
13955
  }
13332
13956
  }
13957
+ async function teardownPlugins() {
13958
+ try {
13959
+ const { teardownLocalAgentPlugins: teardownLocalAgentPlugins2 } = await Promise.resolve().then(() => (init_local_agent(), local_agent_exports));
13960
+ await teardownLocalAgentPlugins2();
13961
+ } catch (e) {
13962
+ log.warn(`plugin teardown failed: ${e instanceof Error ? e.message : String(e)}`);
13963
+ }
13964
+ }
13333
13965
  async function executeRemoval(plan) {
13334
13966
  for (const { path: settingsPath, tool } of plan.hookFiles) {
13335
13967
  try {
@@ -13388,6 +14020,16 @@ async function executeRemoval(plan) {
13388
14020
  if (plan.ruleFiles.length > 0) {
13389
14021
  log.success(`\u79FB\u9664\u4E86 ${plan.ruleFiles.length} \u4E2A rule \u6587\u4EF6`);
13390
14022
  }
14023
+ for (const agentFile of plan.agentFiles) {
14024
+ try {
14025
+ await remove(agentFile);
14026
+ } catch (e) {
14027
+ log.warn(`\u79FB\u9664 agent \u5931\u8D25 ${agentFile}: ${e.message}`);
14028
+ }
14029
+ }
14030
+ if (plan.agentFiles.length > 0) {
14031
+ log.success(`\u79FB\u9664\u4E86 ${plan.agentFiles.length} \u4E2A agent \u6587\u4EF6`);
14032
+ }
13391
14033
  if (plan.shellProfile) {
13392
14034
  try {
13393
14035
  const content = await readFileSafe(plan.shellProfile);
@@ -13414,6 +14056,7 @@ async function executeRemoval(plan) {
13414
14056
  }
13415
14057
  }
13416
14058
  if (plan.teamaiHomeExists) {
14059
+ await teardownPlugins();
13417
14060
  try {
13418
14061
  await remove(plan.teamaiHome);
13419
14062
  log.success(`\u79FB\u9664 ${plan.teamaiHome}/`);
@@ -13480,6 +14123,7 @@ async function uninstall(opts) {
13480
14123
  }
13481
14124
  }
13482
14125
  try {
14126
+ await teardownPlugins();
13483
14127
  await remove(home);
13484
14128
  log.success(`\u79FB\u9664 ${home}/`);
13485
14129
  log.success("teamai \u5378\u8F7D\u5B8C\u6210");
@@ -13497,6 +14141,8 @@ var init_uninstall = __esm({
13497
14141
  init_openclaw_hooks();
13498
14142
  init_types();
13499
14143
  init_builtin_rules();
14144
+ init_builtin_agents();
14145
+ init_builtin_skills();
13500
14146
  init_fs();
13501
14147
  init_logger();
13502
14148
  init_prompt();
@@ -14887,11 +15533,16 @@ async function cleanupStaleSessions(dir, currentSessionId) {
14887
15533
  }
14888
15534
  }
14889
15535
  }
14890
- function computeSmartScore(events) {
15536
+ function toolErrorPoints(toolError) {
15537
+ for (const tier of CONTRIBUTE_TOOLERROR_TIERS) {
15538
+ if (toolError >= tier.min) return tier.points;
15539
+ }
15540
+ return 0;
15541
+ }
15542
+ function computeSmartScore(events, friction) {
14891
15543
  if (events.length === 0) return 0;
14892
15544
  const toolNames = /* @__PURE__ */ new Set();
14893
15545
  let hasSkills = false;
14894
- let hasErrors = false;
14895
15546
  let totalToolCalls = 0;
14896
15547
  for (const event of events) {
14897
15548
  if (event.type === "tool_use" && event.toolName) {
@@ -14901,36 +15552,20 @@ function computeSmartScore(events) {
14901
15552
  hasSkills = true;
14902
15553
  }
14903
15554
  }
14904
- if (event.type === "prompt_submit" && event.promptSummary) {
14905
- const lower = event.promptSummary.toLowerCase();
14906
- if (lower.includes("error") || lower.includes("fix") || lower.includes("bug") || lower.includes("fail") || lower.includes("retry")) {
14907
- hasErrors = true;
14908
- }
14909
- }
14910
15555
  }
14911
15556
  let score = 0;
14912
- if (totalToolCalls >= 20) {
14913
- score += Math.min(25, Math.round((totalToolCalls - 20) / 60 * 20) + 5);
14914
- }
14915
- if (totalToolCalls > 0) {
14916
- const diversity = toolNames.size / Math.min(totalToolCalls, 10);
14917
- score += Math.min(Math.round(diversity * 20), 20);
15557
+ if (friction) {
15558
+ score += friction.interrupt * CONTRIBUTE_INTERRUPT_WEIGHT;
15559
+ score += friction.toolReject * CONTRIBUTE_REJECT_WEIGHT;
15560
+ score += friction.correction * CONTRIBUTE_CORRECTION_WEIGHT;
15561
+ score += toolErrorPoints(friction.toolError);
14918
15562
  }
14919
15563
  if (hasSkills) {
14920
- score += 10;
14921
- }
14922
- if (hasErrors) {
14923
- score += 10;
15564
+ score += CONTRIBUTE_SKILL_BONUS;
14924
15565
  }
14925
- if (events.length >= 2) {
14926
- const first = new Date(events[0].timestamp).getTime();
14927
- const last = new Date(events[events.length - 1].timestamp).getTime();
14928
- const durationMin = (last - first) / (1e3 * 60);
14929
- if (durationMin > 30) {
14930
- score += 20;
14931
- } else if (durationMin > 15) {
14932
- score += 10;
14933
- }
15566
+ if (totalToolCalls > 0) {
15567
+ const diversity = toolNames.size / Math.min(totalToolCalls, 10);
15568
+ score += Math.min(Math.round(diversity * CONTRIBUTE_DIVERSITY_BONUS_MAX), CONTRIBUTE_DIVERSITY_BONUS_MAX);
14934
15569
  }
14935
15570
  return score;
14936
15571
  }
@@ -14995,19 +15630,33 @@ function countToolUseEvents(events) {
14995
15630
  function countUniqueTools(events) {
14996
15631
  return new Set(events.filter((e) => e.toolName).map((e) => e.toolName)).size;
14997
15632
  }
14998
- function buildHint(totalToolCalls, uniqueTools, isKnowledgeGap) {
14999
- if (isKnowledgeGap) {
15000
- return [
15001
- `[teamai] \u672C\u6B21 session \u6D89\u53CA\u77E5\u8BC6\u5E93\u5C1A\u672A\u8986\u76D6\u7684\u9886\u57DF\uFF08${totalToolCalls} \u6B21\u5DE5\u5177\u8C03\u7528\uFF0C${uniqueTools} \u79CD\u4E0D\u540C\u5DE5\u5177\uFF09\u3002`,
15002
- `\u5EFA\u8BAE\u8FD0\u884C /teamai-share-learnings \u5C06\u672C\u6B21\u7ECF\u9A8C\u603B\u7ED3\u5206\u4EAB\u7ED9\u56E2\u961F\uFF0C\u5E2E\u52A9\u586B\u8865\u77E5\u8BC6\u5E93\u7A7A\u767D\u3002`,
15003
- `\u4E0B\u6B21\u9047\u5230\u7C7B\u4F3C\u4EFB\u52A1\u65F6\uFF0C\u56E2\u961F\u6210\u5458\u5C06\u76F4\u63A5\u53D7\u76CA\u4E8E\u60A8\u7684\u7ECF\u9A8C\u3002`
15004
- ].join("");
15633
+ function extractFriction(events, sessionId) {
15634
+ let interrupt = 0;
15635
+ let toolReject = 0;
15636
+ let toolError = 0;
15637
+ for (const e of events) {
15638
+ if (e.type === "stop" && e.interventions) {
15639
+ interrupt = e.interventions.interrupt;
15640
+ toolReject = e.interventions.toolReject;
15641
+ toolError = e.interventions.toolError ?? 0;
15642
+ }
15005
15643
  }
15006
- return [
15007
- `[teamai] \u672C\u6B21 session \u5185\u5BB9\u4E30\u5BCC\uFF08${totalToolCalls} \u6B21\u5DE5\u5177\u8C03\u7528\uFF0C${uniqueTools} \u79CD\u4E0D\u540C\u5DE5\u5177\uFF09\u3002`,
15644
+ const correction = aggregateSessionMetrics(events).get(sessionId)?.correction ?? 0;
15645
+ return { interrupt, toolReject, correction, toolError };
15646
+ }
15647
+ function buildHint(totalToolCalls, uniqueTools, isKnowledgeGap) {
15648
+ const body = isKnowledgeGap ? [
15649
+ `[teamai] \u672C\u6B21 session \u6D89\u53CA\u77E5\u8BC6\u5E93\u5C1A\u672A\u8986\u76D6\u7684\u9886\u57DF\uFF0C\u4E14\u8FC7\u7A0B\u4E2D\u7ECF\u5386\u4E86\u591A\u6B21\u8C03\u6574\uFF08${totalToolCalls} \u6B21\u5DE5\u5177\u8C03\u7528\uFF0C${uniqueTools} \u79CD\u4E0D\u540C\u5DE5\u5177\uFF09\u3002`,
15650
+ `\u5EFA\u8BAE\u8FD0\u884C /teamai-share-learnings \u5C06\u672C\u6B21\u7ECF\u9A8C\u603B\u7ED3\u5206\u4EAB\u7ED9\u56E2\u961F\uFF0C\u5E2E\u52A9\u586B\u8865\u77E5\u8BC6\u5E93\u7A7A\u767D\u3002`,
15651
+ `\u4E0B\u6B21\u9047\u5230\u7C7B\u4F3C\u4EFB\u52A1\u65F6\uFF0C\u56E2\u961F\u6210\u5458\u5C06\u76F4\u63A5\u53D7\u76CA\u4E8E\u60A8\u7684\u7ECF\u9A8C\u3002`
15652
+ ].join("") : [
15653
+ `[teamai] \u672C\u6B21 session \u8FC7\u7A0B\u4E2D\u51FA\u73B0\u4E86\u8F83\u591A\u7EA0\u504F\u6216\u5DE5\u5177\u91CD\u8BD5\uFF0C\u5F88\u53EF\u80FD\u8E29\u5230\u4E86\u503C\u5F97\u8BB0\u5F55\u7684\u5751\uFF08${totalToolCalls} \u6B21\u5DE5\u5177\u8C03\u7528\uFF0C${uniqueTools} \u79CD\u4E0D\u540C\u5DE5\u5177\uFF09\u3002`,
15008
15654
  `\u5EFA\u8BAE\u8FD0\u884C /teamai-share-learnings \u603B\u7ED3\u672C\u6B21 session \u7684\u7ECF\u9A8C\u5E76\u5206\u4EAB\u7ED9\u56E2\u961F\u3002`,
15009
15655
  `\u603B\u7ED3\u6587\u6863\u5C06\u4FDD\u5B58\u5230\u56E2\u961F\u4ED3\u5E93\u7684 learnings/ \u76EE\u5F55\u3002`
15010
15656
  ].join("");
15657
+ return `Print the following message verbatim to the user (do NOT paraphrase, summarize, or explain its origin):
15658
+
15659
+ ${body}`;
15011
15660
  }
15012
15661
  async function contributeCheckForSession(sessionId, cwd) {
15013
15662
  const state = await readContributeState(sessionId);
@@ -15040,14 +15689,17 @@ async function contributeCheckForSession(sessionId, cwd) {
15040
15689
  } else {
15041
15690
  const allEvents = await readEvents();
15042
15691
  const sessionEvents = allEvents.filter((e) => e.sessionId === sessionId);
15043
- score = computeSmartScore(sessionEvents);
15692
+ const friction = extractFriction(sessionEvents, sessionId);
15693
+ score = computeSmartScore(sessionEvents, friction);
15044
15694
  toolCount = countToolUseEvents(sessionEvents);
15045
15695
  uniqueTools = countUniqueTools(sessionEvents);
15046
15696
  needsPersist = true;
15047
15697
  if (sessionEvents.length > 0) {
15048
15698
  sessionStartIso = sessionEvents[0].timestamp;
15049
15699
  }
15050
- log.debug(`contribute-check: session ${sessionId.slice(0, 16)} smart score = ${score} (threshold: ${CONTRIBUTE_SMART_THRESHOLD})`);
15700
+ log.debug(
15701
+ `contribute-check: session ${sessionId.slice(0, 16)} friction score = ${score} (interrupt=${friction.interrupt}, reject=${friction.toolReject}, correction=${friction.correction}, toolError=${friction.toolError}, threshold=${CONTRIBUTE_SMART_THRESHOLD})`
15702
+ );
15051
15703
  }
15052
15704
  const phase2 = applyPhase2Adjustments(score, sessionId, cwd, sessionStartIso);
15053
15705
  score = phase2.score;
@@ -15058,7 +15710,7 @@ async function contributeCheckForSession(sessionId, cwd) {
15058
15710
  `contribute-check: phase2 adjustments applied (gap=${isKnowledgeGap}, commit=${hasGitCommit}, adjusted=${score})`
15059
15711
  );
15060
15712
  }
15061
- const willHint = score >= CONTRIBUTE_SMART_THRESHOLD;
15713
+ const willHint = score >= CONTRIBUTE_SMART_THRESHOLD && toolCount >= CONTRIBUTE_BASE_THRESHOLD;
15062
15714
  if (needsPersist || willHint) {
15063
15715
  const latest = await readContributeState(sessionId);
15064
15716
  const updated = {
@@ -15138,7 +15790,9 @@ async function parseTranscriptForVotes(transcriptPath) {
15138
15790
  });
15139
15791
  for await (const line of rl) {
15140
15792
  const trimmed = line.trim();
15141
- if (!trimmed || !trimmed.includes('"assistant"')) continue;
15793
+ if (!trimmed) continue;
15794
+ extractRecalledDocIdsFromComment(trimmed, recalledSet);
15795
+ if (!trimmed.includes('"assistant"')) continue;
15142
15796
  let entry;
15143
15797
  try {
15144
15798
  entry = JSON.parse(trimmed);
@@ -15161,6 +15815,19 @@ async function parseTranscriptForVotes(transcriptPath) {
15161
15815
  referencedDocIds: [...referencedSet]
15162
15816
  };
15163
15817
  }
15818
+ function isValidDocId(docId) {
15819
+ return docId.length > 0 && !/[<>]/.test(docId) && docId !== "...";
15820
+ }
15821
+ function extractRecalledDocIdsFromComment(text, out) {
15822
+ const pattern = /<!--\s*teamai:recalled-doc-ids:\s*\[([^\]]*)\]\s*-->/g;
15823
+ let match;
15824
+ while ((match = pattern.exec(text)) !== null) {
15825
+ for (const item of match[1].split(",")) {
15826
+ const docId = item.trim().replace(/^['"]|['"]$/g, "");
15827
+ if (isValidDocId(docId)) out.add(docId);
15828
+ }
15829
+ }
15830
+ }
15164
15831
  function extractRecalledDocIds(text, out) {
15165
15832
  const START = "--- [teamai:recall:start] ---";
15166
15833
  const END = "--- [teamai:recall:end] ---";
@@ -15177,7 +15844,7 @@ function extractRecalledDocIds(text, out) {
15177
15844
  while ((match = filePattern.exec(region)) !== null) {
15178
15845
  const filePath = match[1].trim();
15179
15846
  const docId = path53.basename(filePath).replace(/\.md$/i, "");
15180
- out.add(docId);
15847
+ if (isValidDocId(docId)) out.add(docId);
15181
15848
  }
15182
15849
  searchFrom = endIdx + END.length;
15183
15850
  }
@@ -15189,7 +15856,7 @@ function extractReferencedDocIds(text, out) {
15189
15856
  const raw = match[1];
15190
15857
  for (const item of raw.split(",")) {
15191
15858
  const docId = item.trim().replace(/^['"]|['"]$/g, "");
15192
- if (docId) out.add(docId);
15859
+ if (isValidDocId(docId)) out.add(docId);
15193
15860
  }
15194
15861
  }
15195
15862
  }
@@ -15568,9 +16235,9 @@ function buildHandlerRegistry() {
15568
16235
  { event: "session-start", matcher: "*", handler: mrHintHandler, timeoutMs: MR_HINT_TIMEOUT_MS },
15569
16236
  { event: "session-start", matcher: "*", handler: localAgentHandler, timeoutMs: LOCAL_AGENT_TIMEOUT_MS },
15570
16237
  // ─── Stop ─────────────────────────────────────────
15571
- { event: "stop", matcher: "*", handler: updateHandler, timeoutMs: UPDATE_TIMEOUT_MS },
15572
- { event: "stop", matcher: "*", handler: contributeCheckHandler, timeoutMs: CONTRIBUTE_CHECK_TIMEOUT_MS },
16238
+ { event: "stop", matcher: "*", handler: updateHandler, timeoutMs: UPDATE_TIMEOUT_MS2 },
15573
16239
  { event: "stop", matcher: "*", handler: votesSyncHandler, timeoutMs: VOTES_SYNC_TIMEOUT_MS },
16240
+ { event: "stop", matcher: "*", handler: contributeCheckHandler, timeoutMs: CONTRIBUTE_CHECK_TIMEOUT_MS },
15574
16241
  { event: "stop", matcher: "*", handler: dashboardReportHandler, timeoutMs: DASHBOARD_TIMEOUT_MS },
15575
16242
  { event: "stop", matcher: "*", handler: localAgentHandler, timeoutMs: LOCAL_AGENT_TIMEOUT_MS },
15576
16243
  // ─── PostToolUse ──────────────────────────────────
@@ -15584,14 +16251,14 @@ function buildHandlerRegistry() {
15584
16251
  { event: "prompt-submit", matcher: "*", handler: localAgentHandler, timeoutMs: LOCAL_AGENT_TIMEOUT_MS }
15585
16252
  ];
15586
16253
  }
15587
- var PULL_TIMEOUT_MS, UPDATE_TIMEOUT_MS, TRACK_TIMEOUT_MS, DASHBOARD_TIMEOUT_MS, CONTRIBUTE_CHECK_TIMEOUT_MS, VOTES_SYNC_TIMEOUT_MS, TODOWRITE_HINT_TIMEOUT_MS, MR_HINT_TIMEOUT_MS, LOCAL_AGENT_TIMEOUT_MS, pullHandler, updateHandler, dashboardReportHandler, trackHandler, trackSlashHandler, contributeCheckHandler, votesSyncHandler, todowriteHintHandler, mrHintHandler, localAgentHandler;
16254
+ var PULL_TIMEOUT_MS, UPDATE_TIMEOUT_MS2, TRACK_TIMEOUT_MS, DASHBOARD_TIMEOUT_MS, CONTRIBUTE_CHECK_TIMEOUT_MS, VOTES_SYNC_TIMEOUT_MS, TODOWRITE_HINT_TIMEOUT_MS, MR_HINT_TIMEOUT_MS, LOCAL_AGENT_TIMEOUT_MS, pullHandler, updateHandler, dashboardReportHandler, trackHandler, trackSlashHandler, contributeCheckHandler, votesSyncHandler, todowriteHintHandler, mrHintHandler, localAgentHandler;
15588
16255
  var init_hook_handlers = __esm({
15589
16256
  "src/hook-handlers.ts"() {
15590
16257
  "use strict";
15591
16258
  init_session_id();
15592
16259
  init_tool_names();
15593
16260
  PULL_TIMEOUT_MS = 6e4;
15594
- UPDATE_TIMEOUT_MS = 1e4;
16261
+ UPDATE_TIMEOUT_MS2 = 1e4;
15595
16262
  TRACK_TIMEOUT_MS = 5e3;
15596
16263
  DASHBOARD_TIMEOUT_MS = 5e3;
15597
16264
  CONTRIBUTE_CHECK_TIMEOUT_MS = 1e4;
@@ -15689,7 +16356,7 @@ var init_hook_handlers = __esm({
15689
16356
  };
15690
16357
  votesSyncHandler = {
15691
16358
  name: "votes-sync",
15692
- async execute(stdin) {
16359
+ async execute(stdin, tool) {
15693
16360
  if (process.env.TEAMAI_RECALL_DISABLED === "1") return null;
15694
16361
  const transcriptPath = typeof stdin.transcript_path === "string" ? stdin.transcript_path : null;
15695
16362
  if (!transcriptPath) return null;
@@ -15699,7 +16366,7 @@ var init_hook_handlers = __esm({
15699
16366
  const { requireInit: requireInit3 } = await Promise.resolve().then(() => (init_config(), config_exports));
15700
16367
  const voteData = await parseTranscriptForVotes2(transcriptPath);
15701
16368
  const { localConfig } = await requireInit3();
15702
- const { VOTES_LOCAL_DIR: VOTES_LOCAL_DIR2 } = await Promise.resolve().then(() => (init_types(), types_exports));
16369
+ const { VOTES_LOCAL_DIR: VOTES_LOCAL_DIR2, TEAMAI_SESSIONS_DIR: TEAMAI_SESSIONS_DIR2 } = await Promise.resolve().then(() => (init_types(), types_exports));
15703
16370
  const votesDir = VOTES_LOCAL_DIR2;
15704
16371
  const votePath = path56.join(votesDir, `${localConfig.username}.yaml`);
15705
16372
  if (voteData.referencedDocIds.length > 0) {
@@ -15707,6 +16374,52 @@ var init_hook_handlers = __esm({
15707
16374
  }
15708
16375
  await syncVotesToTeam2(localConfig.repo.localPath, localConfig.username, votesDir).catch(() => {
15709
16376
  });
16377
+ const sessionId = deriveSessionId(stdin, { includeCwd: true });
16378
+ const recalled = voteData.recalledDocIds;
16379
+ const declared = voteData.referencedDocIds;
16380
+ let nudged = false;
16381
+ if (recalled.length > 0 && declared.length === 0) {
16382
+ const fsp = await import("fs/promises");
16383
+ const safeId = sessionId.replace(/[^a-zA-Z0-9_.-]/g, "_");
16384
+ const marker = path56.join(TEAMAI_SESSIONS_DIR2, `${safeId}-adoption-nudged`);
16385
+ let already = false;
16386
+ try {
16387
+ await fsp.access(marker);
16388
+ already = true;
16389
+ } catch {
16390
+ already = false;
16391
+ }
16392
+ if (!already) {
16393
+ try {
16394
+ const { ensureDir: ensureDir3 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
16395
+ await ensureDir3(TEAMAI_SESSIONS_DIR2);
16396
+ await fsp.writeFile(marker, "");
16397
+ nudged = true;
16398
+ } catch {
16399
+ }
16400
+ }
16401
+ }
16402
+ if (process.env.TEAMAI_ADOPTION_EVAL_LOG) {
16403
+ try {
16404
+ const { appendFile } = await import("fs/promises");
16405
+ await appendFile(
16406
+ process.env.TEAMAI_ADOPTION_EVAL_LOG,
16407
+ JSON.stringify({
16408
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
16409
+ sessionId,
16410
+ recalled: recalled.length,
16411
+ declared: declared.length,
16412
+ nudged
16413
+ }) + "\n"
16414
+ );
16415
+ } catch {
16416
+ }
16417
+ }
16418
+ if (nudged) {
16419
+ const { formatStopHookOutput: formatStopHookOutput2 } = await Promise.resolve().then(() => (init_hook_output(), hook_output_exports));
16420
+ const msg = `\u4F60\u672C\u6B21\u901A\u8FC7 teamai \u53EC\u56DE\u4E86\u56E2\u961F\u77E5\u8BC6\uFF08\u5019\u9009 doc-id\uFF1A${recalled.join(", ")}\uFF09\u3002\u7ED3\u675F\u524D\u8BF7\u5728\u56DE\u590D\u672B\u5C3E\u58F0\u660E\u4F60\u5B9E\u9645\u7528\u5230\u7684\u6761\u76EE\uFF1A<!-- teamai:referenced-doc-ids: [\u7528\u5230\u7684doc-id] -->\uFF1B\u6CA1\u7528\u5230\u5C31\u7559\u7A7A []\u3002`;
16421
+ return formatStopHookOutput2(msg, tool ?? "claude");
16422
+ }
15710
16423
  } catch {
15711
16424
  }
15712
16425
  return null;
@@ -27579,6 +28292,26 @@ skillCmd.command("show <name>").description("Show skill metadata: source / contr
27579
28292
  const { skillShow: skillShow2 } = await Promise.resolve().then(() => (init_skill_cmd(), skill_cmd_exports));
27580
28293
  await skillShow2(name, { ...globalOpts, ...cmdOpts });
27581
28294
  });
28295
+ var excludeCmd = skillCmd.command("exclude").description("Manage per-user skill exclusion (skip sync without affecting team repo)").action(async () => {
28296
+ const globalOpts = program.opts();
28297
+ const { excludeList: excludeList2 } = await Promise.resolve().then(() => (init_exclude(), exclude_exports));
28298
+ await excludeList2(globalOpts);
28299
+ });
28300
+ excludeCmd.command("list").description("List excluded skills").action(async () => {
28301
+ const globalOpts = program.opts();
28302
+ const { excludeList: excludeList2 } = await Promise.resolve().then(() => (init_exclude(), exclude_exports));
28303
+ await excludeList2(globalOpts);
28304
+ });
28305
+ excludeCmd.command("add <skills...>").description("Add skill(s) to the exclude list").action(async (skills) => {
28306
+ const globalOpts = program.opts();
28307
+ const { excludeAdd: excludeAdd2 } = await Promise.resolve().then(() => (init_exclude(), exclude_exports));
28308
+ await excludeAdd2(skills, globalOpts);
28309
+ });
28310
+ excludeCmd.command("remove <skills...>").description("Remove skill(s) from the exclude list").action(async (skills) => {
28311
+ const globalOpts = program.opts();
28312
+ const { excludeRemove: excludeRemove2 } = await Promise.resolve().then(() => (init_exclude(), exclude_exports));
28313
+ await excludeRemove2(skills, globalOpts);
28314
+ });
27582
28315
  var membersCmd = program.command("members").description("Manage team members").action(async () => {
27583
28316
  const globalOpts = program.opts();
27584
28317
  const { listMembers: listMembers2 } = await Promise.resolve().then(() => (init_members(), members_exports));
@@ -27701,6 +28434,10 @@ sourceCmd.command("remove-http").description("Remove the HTTP source and clean u
27701
28434
  const { sourceRemoveHttp: sourceRemoveHttp2 } = await Promise.resolve().then(() => (init_source(), source_exports));
27702
28435
  await sourceRemoveHttp2(globalOpts);
27703
28436
  });
28437
+ sourceCmd.command("reconcile-plugins", { hidden: true }).description("Run plugin reconcile worker (called internally by session_start hook)").action(async () => {
28438
+ const { runPluginReconcileWorker: runPluginReconcileWorker2 } = await Promise.resolve().then(() => (init_local_agent(), local_agent_exports));
28439
+ await runPluginReconcileWorker2();
28440
+ });
27704
28441
  sourceCmd.command("list").description("List all configured sources").action(async () => {
27705
28442
  const { sourceList: sourceList2 } = await Promise.resolve().then(() => (init_source(), source_exports));
27706
28443
  await sourceList2();
@@ -27795,10 +28532,10 @@ program.command("hook-dispatch <event>").description("Unified hook dispatcher \u
27795
28532
  const { hookDispatchCli: hookDispatchCli2 } = await Promise.resolve().then(() => (init_hook_dispatch_cli(), hook_dispatch_cli_exports));
27796
28533
  await hookDispatchCli2(event, cmdOpts.tool ?? "claude", cmdOpts.matcher ?? "*");
27797
28534
  });
27798
- program.command("bind-project").description("Bind the current project to a ClawPro organization/group for HTTP local-agent sync").option("--group-id <id>", "Group ID from /user-groups/mine").option("--skip", "Mark current project as skipped (never prompt again)").action(async (cmdOpts) => {
28535
+ program.command("bind-project").description("Bind the current workspace to a ClawPro project for HTTP local-agent sync").option("--project-id <id>", "Project ID from /projects/mine").option("--skip", "Mark current workspace as skipped (never prompt again)").action(async (cmdOpts) => {
27799
28536
  const { bindCurrentProject: bindCurrentProject2 } = await Promise.resolve().then(() => (init_local_agent(), local_agent_exports));
27800
28537
  await bindCurrentProject2({
27801
- groupId: cmdOpts.groupId ? Number.parseInt(cmdOpts.groupId, 10) : void 0,
28538
+ projectId: cmdOpts.projectId ? Number.parseInt(cmdOpts.projectId, 10) : void 0,
27802
28539
  skip: !!cmdOpts.skip
27803
28540
  });
27804
28541
  });