teamai-cli 0.25.0-beta.6 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -838,12 +838,17 @@ function getWebhookSharing(config) {
838
838
  const w = config.sharing?.webhooks;
839
839
  return {
840
840
  enabled: w?.enabled ?? false,
841
+ // Preserve every schema-accepted field. Previously `secret` was dropped and
842
+ // `timeout`/`retries` were force-overridden, so a configured signing secret
843
+ // never reached the request and receivers with signature verification
844
+ // rejected the (unsigned) webhook (#703).
841
845
  endpoints: (w?.endpoints ?? []).map((ep) => ({
842
846
  url: ep.url,
843
847
  type: ep.type,
848
+ secret: ep.secret,
844
849
  events: ep.events ?? ["push", "pull", "skill-use", "session-start", "session-stop"],
845
- timeout: 5e3,
846
- retries: 3
850
+ timeout: ep.timeout ?? 5e3,
851
+ retries: ep.retries ?? 3
847
852
  }))
848
853
  };
849
854
  }
@@ -11378,16 +11383,19 @@ function selectToolWorkspaces(config, currentPath, currentTool) {
11378
11383
  async function buildReportPayload(config, context) {
11379
11384
  const manifest = await loadManifest();
11380
11385
  const tool = context.tool ?? "workbuddy";
11381
- const toolPaths = createLocalAgentTeamConfig(config.endpoint).toolPaths;
11382
- const toolPath = toolPaths[tool];
11386
+ const teamConfig = createLocalAgentTeamConfig(config.endpoint);
11383
11387
  const manifestSlugs = collectManifestSlugs(manifest);
11384
- const scanScope = async (baseDir) => {
11388
+ const scanScope = async (workspacePath) => {
11389
+ const scope = workspacePath ? "project" : "user";
11390
+ const localConfig = createResourceLocalConfig(config, scope, workspacePath ?? getUserHome(), workspacePath);
11391
+ const toolPath = scopedToolPaths(teamConfig, localConfig)[tool];
11385
11392
  if (!toolPath) return { skills: [], rules: [] };
11393
+ const baseDir = resolveToolBaseDir(tool, localConfig);
11386
11394
  const skills = toolPath.skills ? await scanSkillsFromDisk(path30.join(baseDir, toolPath.skills), manifestSlugs.skills) : [];
11387
11395
  const rules = toolPath.rules ? await scanRulesFromDisk(path30.join(baseDir, toolPath.rules), manifestSlugs.rules) : [];
11388
11396
  return { skills, rules };
11389
11397
  };
11390
- const userScope = await scanScope(getUserHome());
11398
+ const userScope = await scanScope();
11391
11399
  const userLevel = { group_id: config.userGroupId };
11392
11400
  if (userScope.skills.length > 0) userLevel.skills = userScope.skills;
11393
11401
  if (userScope.rules.length > 0) userLevel.rules = userScope.rules;
@@ -16941,6 +16949,9 @@ async function remoteBranchExists2(spec, repoRoot) {
16941
16949
  return false;
16942
16950
  }
16943
16951
  }
16952
+ async function fetchTrackingRef(git, branch) {
16953
+ await git.fetch(["origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`]);
16954
+ }
16944
16955
  async function ensureWorktree(spec, localConfig, options = {}) {
16945
16956
  if (!usesBranchWorktree(localConfig)) {
16946
16957
  return getWorktreeDir(localConfig, spec.worktreeDirname);
@@ -16970,14 +16981,14 @@ async function ensureWorktree(spec, localConfig, options = {}) {
16970
16981
  }
16971
16982
  if (await remoteBranchExists2(spec, repoRoot)) {
16972
16983
  try {
16973
- await git.fetch(["origin", spec.branch]);
16984
+ await fetchTrackingRef(git, spec.branch);
16974
16985
  } catch {
16975
16986
  }
16976
16987
  const branches = await git.branchLocal();
16977
16988
  if (branches.all.includes(spec.branch)) {
16978
16989
  await git.raw(["worktree", "add", wt, spec.branch]);
16979
16990
  } else {
16980
- await git.raw(["worktree", "add", wt, "--track", "-b", spec.branch, `origin/${spec.branch}`]);
16991
+ await git.raw(["worktree", "add", wt, "--no-track", "-b", spec.branch, `origin/${spec.branch}`]);
16981
16992
  }
16982
16993
  } else {
16983
16994
  const branches = await git.branchLocal();
@@ -17054,7 +17065,7 @@ async function commitAndPushAt(spec, wt, message, files, options = {}) {
17054
17065
  return { status: "failed", reason: pushErr.message };
17055
17066
  }
17056
17067
  try {
17057
- await git.fetch(["origin", spec.branch]);
17068
+ await fetchTrackingRef(git, spec.branch);
17058
17069
  await git.rebase([`origin/${spec.branch}`]);
17059
17070
  } catch (rebaseErr) {
17060
17071
  log.debug(`[${spec.logTag}] rebase failed, retrying: ${rebaseErr.message}`);
@@ -17168,7 +17179,7 @@ async function syncWorktree(spec, wt) {
17168
17179
  const git = createGit2(wt);
17169
17180
  const upstream = `origin/${spec.branch}`;
17170
17181
  try {
17171
- await git.fetch(["origin", spec.branch]);
17182
+ await fetchTrackingRef(git, spec.branch);
17172
17183
  } catch (e) {
17173
17184
  log.debug(`[${spec.logTag}] fetch failed, using the local copy: ${e.message}`);
17174
17185
  return;
@@ -18911,17 +18922,21 @@ async function pushGroup(args) {
18911
18922
  { reuseBranch: Boolean(reuse) }
18912
18923
  );
18913
18924
  workingTreeDirtied = false;
18914
- if (!hasChanges) {
18925
+ const needsPrRetry = Boolean(reuse) && !reuse?.prUrl;
18926
+ if (!hasChanges && !needsPrRetry) {
18915
18927
  pushSpin.succeed(
18916
18928
  reuse ? `No changes to push (PR already up to date: ${reuse.prUrl ?? branchName})` : "No changes to push (files already up to date)"
18917
18929
  );
18918
- return true;
18930
+ return "nochange";
18919
18931
  }
18920
- pushSpin.succeed(`Pushed branch ${branchName}`);
18932
+ pushSpin.succeed(
18933
+ hasChanges ? `Pushed branch ${branchName}` : `No new changes; retrying PR creation for ${branchName}`
18934
+ );
18921
18935
  let prUrl;
18922
- if (reuse) {
18936
+ let prFailed = false;
18937
+ if (reuse?.prUrl) {
18923
18938
  prUrl = reuse.prUrl;
18924
- log.success(`Existing PR updated: ${prUrl ?? branchName}`);
18939
+ log.success(`Existing PR updated: ${prUrl}`);
18925
18940
  } else {
18926
18941
  prUrl = await createPrWithFallback(
18927
18942
  teamConfig,
@@ -18933,6 +18948,7 @@ ${items.map((i) => `- [${i.type}] ${i.name}`).join("\n")}`
18933
18948
  );
18934
18949
  if (!prUrl) {
18935
18950
  process.exitCode = 1;
18951
+ prFailed = true;
18936
18952
  }
18937
18953
  }
18938
18954
  recordPendingPush(pushState, {
@@ -18945,7 +18961,7 @@ ${items.map((i) => `- [${i.type}] ${i.name}`).join("\n")}`
18945
18961
  for (const rel of pushedFiles) {
18946
18962
  await pruneEmptyDirs(path50.resolve(localConfig.repo.localPath, rel));
18947
18963
  }
18948
- return true;
18964
+ return prFailed ? "pr-failed" : "pushed";
18949
18965
  } catch (e) {
18950
18966
  pushSpin.fail(`Push failed: ${e.message}`);
18951
18967
  if (workingTreeDirtied) {
@@ -18960,10 +18976,10 @@ ${items.map((i) => `- [${i.type}] ${i.name}`).join("\n")}`
18960
18976
  );
18961
18977
  }
18962
18978
  }
18963
- return false;
18979
+ return "failed";
18964
18980
  }
18965
18981
  }
18966
- async function push(options) {
18982
+ async function push(options, result) {
18967
18983
  const { localConfig, teamConfig } = await autoDetectInit();
18968
18984
  assertNotReadOnly(localConfig, "teamai push");
18969
18985
  if (options.project) {
@@ -19036,7 +19052,7 @@ async function push(options) {
19036
19052
  if (pendingTeamConfig !== null) {
19037
19053
  await writeFile(path50.join(wtConfig.repo.localPath, "teamai.yaml"), pendingTeamConfig);
19038
19054
  }
19039
- await pushCore(wtConfig, teamConfig, options, pendingTeamConfig);
19055
+ await pushCore(wtConfig, teamConfig, options, pendingTeamConfig, result);
19040
19056
  });
19041
19057
  } catch (e) {
19042
19058
  if (e instanceof EmptyRepoError2) {
@@ -19059,12 +19075,12 @@ async function push(options) {
19059
19075
  return;
19060
19076
  }
19061
19077
  try {
19062
- await pushCore(localConfig, teamConfig, options);
19078
+ await pushCore(localConfig, teamConfig, options, null, result);
19063
19079
  } finally {
19064
19080
  await releaseLock(syncLock);
19065
19081
  }
19066
19082
  }
19067
- async function pushCore(localConfig, teamConfig, options, initialPendingTeamConfig = null) {
19083
+ async function pushCore(localConfig, teamConfig, options, initialPendingTeamConfig = null, result) {
19068
19084
  const selfMode = localConfig.repo.kind === "self";
19069
19085
  const scopeLabel = localConfig.scope;
19070
19086
  let pendingTeamConfig = initialPendingTeamConfig;
@@ -19238,7 +19254,7 @@ async function pushCore(localConfig, teamConfig, options, initialPendingTeamConf
19238
19254
  const pendingPushes = pushState.pendingPushes;
19239
19255
  if (allItems.length === 0) {
19240
19256
  if (pendingTeamConfig !== null) {
19241
- await pushTeamConfigOnly(localConfig, teamConfig, options);
19257
+ await pushTeamConfigOnly(localConfig, teamConfig, options, result);
19242
19258
  return;
19243
19259
  }
19244
19260
  log.info("No new or modified resources to push");
@@ -19386,19 +19402,23 @@ async function pushCore(localConfig, teamConfig, options, initialPendingTeamConf
19386
19402
  }
19387
19403
  }
19388
19404
  let configRider = pendingTeamConfig !== null;
19405
+ let anyPushed = false;
19406
+ let anyPrFailed = false;
19389
19407
  for (const group of groups) {
19390
- const ok = await pushGroup({
19408
+ const outcome = await pushGroup({
19391
19409
  group,
19392
19410
  teamConfig,
19393
19411
  localConfig,
19394
19412
  pushState,
19395
19413
  includeTeamConfig: configRider
19396
19414
  });
19397
- if (!ok) {
19415
+ if (outcome === "failed") {
19398
19416
  await saveStateForScope(pushState, localConfig);
19399
19417
  process.exitCode = 1;
19400
19418
  return;
19401
19419
  }
19420
+ if (outcome === "pushed") anyPushed = true;
19421
+ if (outcome === "pr-failed") anyPrFailed = true;
19402
19422
  configRider = false;
19403
19423
  }
19404
19424
  const state = pushState;
@@ -19415,8 +19435,9 @@ async function pushCore(localConfig, teamConfig, options, initialPendingTeamConf
19415
19435
  }
19416
19436
  }
19417
19437
  await saveStateForScope(state, localConfig);
19438
+ if (result && anyPushed && !anyPrFailed) result.completed = true;
19418
19439
  }
19419
- async function pushTeamConfigOnly(localConfig, teamConfig, options) {
19440
+ async function pushTeamConfigOnly(localConfig, teamConfig, options, result) {
19420
19441
  console.log("");
19421
19442
  console.log("Found team config change to push:");
19422
19443
  console.log(" - teamai.yaml");
@@ -19449,6 +19470,8 @@ async function pushTeamConfigOnly(localConfig, teamConfig, options) {
19449
19470
  );
19450
19471
  if (!prUrl) {
19451
19472
  process.exitCode = 1;
19473
+ } else if (result) {
19474
+ result.completed = true;
19452
19475
  }
19453
19476
  await checkoutMaster(localConfig.repo.localPath);
19454
19477
  } catch (e) {
@@ -21263,6 +21286,270 @@ var init_commands = __esm({
21263
21286
  }
21264
21287
  });
21265
21288
 
21289
+ // src/webhook-formatters.ts
21290
+ function formatFeishuMessage(payload) {
21291
+ const eventLabel = formatEventLabel(payload.event);
21292
+ const time = new Date(payload.timestamp).toLocaleString("zh-CN", {
21293
+ timeZone: "Asia/Shanghai",
21294
+ year: "numeric",
21295
+ month: "2-digit",
21296
+ day: "2-digit",
21297
+ hour: "2-digit",
21298
+ minute: "2-digit",
21299
+ second: "2-digit"
21300
+ });
21301
+ const lines = [
21302
+ `\u{1F916} TeamAI Notification`,
21303
+ `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
21304
+ `Event: ${eventLabel}`
21305
+ ];
21306
+ if (payload.username) {
21307
+ lines.push(`User: ${payload.username}`);
21308
+ }
21309
+ lines.push(`Time: ${time}`);
21310
+ if (payload.tool) {
21311
+ lines.push(`Tool: ${payload.tool}`);
21312
+ }
21313
+ if (payload.data) {
21314
+ const dataStr = formatEventData(payload.event, payload.data);
21315
+ if (dataStr) {
21316
+ lines.push(dataStr);
21317
+ }
21318
+ }
21319
+ lines.push(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`);
21320
+ return {
21321
+ msg_type: "text",
21322
+ content: {
21323
+ text: lines.join("\n")
21324
+ }
21325
+ };
21326
+ }
21327
+ function formatWecomMessage(payload) {
21328
+ const eventLabel = formatEventLabel(payload.event);
21329
+ const time = new Date(payload.timestamp).toLocaleString("zh-CN", {
21330
+ timeZone: "Asia/Shanghai",
21331
+ year: "numeric",
21332
+ month: "2-digit",
21333
+ day: "2-digit",
21334
+ hour: "2-digit",
21335
+ minute: "2-digit",
21336
+ second: "2-digit"
21337
+ });
21338
+ const lines = [
21339
+ `\u{1F916} TeamAI Notification`,
21340
+ `Event: ${eventLabel}`
21341
+ ];
21342
+ if (payload.username) {
21343
+ lines.push(`User: ${payload.username}`);
21344
+ }
21345
+ lines.push(`Time: ${time}`);
21346
+ if (payload.tool) {
21347
+ lines.push(`Tool: ${payload.tool}`);
21348
+ }
21349
+ if (payload.data) {
21350
+ const dataStr = formatEventData(payload.event, payload.data);
21351
+ if (dataStr) {
21352
+ lines.push(dataStr);
21353
+ }
21354
+ }
21355
+ return {
21356
+ msgtype: "text",
21357
+ text: {
21358
+ content: lines.join("\n")
21359
+ }
21360
+ };
21361
+ }
21362
+ function formatGenericJson(payload) {
21363
+ return {
21364
+ event: payload.event,
21365
+ timestamp: payload.timestamp,
21366
+ tool: payload.tool,
21367
+ sessionId: payload.sessionId,
21368
+ cwd: payload.cwd,
21369
+ team: payload.team,
21370
+ username: payload.username,
21371
+ data: payload.data
21372
+ };
21373
+ }
21374
+ function formatEventLabel(event) {
21375
+ const labels = {
21376
+ "push": "Push Complete",
21377
+ "pull": "Pull Complete",
21378
+ "skill-use": "Skill Used",
21379
+ "session-start": "Session Started",
21380
+ "session-stop": "Session Ended",
21381
+ "webhook-test": "Webhook Test"
21382
+ };
21383
+ return labels[event] ?? event;
21384
+ }
21385
+ function formatEventData(event, data) {
21386
+ switch (event) {
21387
+ case "push":
21388
+ case "pull":
21389
+ if (data.resources && typeof data.resources === "object") {
21390
+ const resources = data.resources;
21391
+ const parts = Object.entries(resources).filter(([, count]) => count > 0).map(([type, count]) => `${type}: ${count}`);
21392
+ return parts.length > 0 ? `Resources: ${parts.join(", ")}` : null;
21393
+ }
21394
+ return null;
21395
+ case "skill-use":
21396
+ if (data.skillName && typeof data.skillName === "string") {
21397
+ return `Skill: ${data.skillName}`;
21398
+ }
21399
+ return null;
21400
+ case "session-start":
21401
+ case "session-stop":
21402
+ if (data.sessionId && typeof data.sessionId === "string") {
21403
+ return `Session: ${data.sessionId.slice(0, 8)}...`;
21404
+ }
21405
+ return null;
21406
+ default:
21407
+ return null;
21408
+ }
21409
+ }
21410
+ var init_webhook_formatters = __esm({
21411
+ "src/webhook-formatters.ts"() {
21412
+ "use strict";
21413
+ }
21414
+ });
21415
+
21416
+ // src/webhook.ts
21417
+ var webhook_exports = {};
21418
+ __export(webhook_exports, {
21419
+ listWebhooks: () => listWebhooks,
21420
+ loadWebhookConfig: () => loadWebhookConfig,
21421
+ sendWebhook: () => sendWebhook,
21422
+ testWebhook: () => testWebhook
21423
+ });
21424
+ import { createHmac } from "crypto";
21425
+ async function sendWebhook(event, payload, config) {
21426
+ if (!config) {
21427
+ const { teamConfig } = await autoDetectInit();
21428
+ config = getWebhookSharing(teamConfig);
21429
+ }
21430
+ if (!config.enabled || config.endpoints.length === 0) return;
21431
+ const fullPayload = {
21432
+ event,
21433
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
21434
+ tool: payload.tool ?? "unknown",
21435
+ sessionId: payload.sessionId,
21436
+ cwd: payload.cwd,
21437
+ team: payload.team,
21438
+ username: payload.username,
21439
+ data: payload.data ?? {}
21440
+ };
21441
+ const matchingEndpoints = config.endpoints.filter(
21442
+ (ep) => ep.events.includes(event) || ep.events.includes("*")
21443
+ );
21444
+ if (matchingEndpoints.length === 0) return;
21445
+ await Promise.allSettled(
21446
+ matchingEndpoints.map((ep) => sendToEndpoint(ep, fullPayload))
21447
+ );
21448
+ }
21449
+ async function sendToEndpoint(endpoint, payload) {
21450
+ const { url, type, secret, timeout, retries } = endpoint;
21451
+ const body = formatMessage(type, payload);
21452
+ const serialized = typeof body === "string" ? body : JSON.stringify(body);
21453
+ const outbound = redactWithEnv(serialized);
21454
+ const headers = {
21455
+ "Content-Type": type === "json" ? "application/json" : "text/plain; charset=utf-8"
21456
+ };
21457
+ if (secret) {
21458
+ const signature = createHmac("sha256", secret).update(outbound).digest("hex");
21459
+ headers["X-TeamAI-Signature"] = `sha256=${signature}`;
21460
+ }
21461
+ for (let attempt = 0; attempt <= retries; attempt++) {
21462
+ try {
21463
+ const controller = new AbortController();
21464
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
21465
+ const response = await fetch(url, {
21466
+ method: "POST",
21467
+ headers,
21468
+ body: outbound,
21469
+ signal: controller.signal
21470
+ });
21471
+ clearTimeout(timeoutId);
21472
+ if (response.ok) {
21473
+ log.debug(`Webhook sent successfully to ${url}`);
21474
+ return;
21475
+ }
21476
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
21477
+ log.warn(`Webhook to ${url} failed with status ${response.status} (not retrying)`);
21478
+ return;
21479
+ }
21480
+ if (attempt < retries) {
21481
+ const delay = Math.pow(2, attempt) * 1e3;
21482
+ log.debug(`Webhook to ${url} failed, retrying in ${delay}ms...`);
21483
+ await new Promise((resolve) => setTimeout(resolve, delay));
21484
+ }
21485
+ } catch (error) {
21486
+ if (error instanceof Error && error.name === "AbortError") {
21487
+ log.warn(`Webhook to ${url} timed out after ${timeout}ms`);
21488
+ } else if (attempt < retries) {
21489
+ const delay = Math.pow(2, attempt) * 1e3;
21490
+ log.debug(`Webhook to ${url} failed, retrying in ${delay}ms...`);
21491
+ await new Promise((resolve) => setTimeout(resolve, delay));
21492
+ } else {
21493
+ log.warn(`Webhook to ${url} failed after ${retries + 1} attempts: ${error.message}`);
21494
+ }
21495
+ }
21496
+ }
21497
+ }
21498
+ function formatMessage(type, payload) {
21499
+ switch (type) {
21500
+ case "feishu":
21501
+ return formatFeishuMessage(payload);
21502
+ case "wecom":
21503
+ return formatWecomMessage(payload);
21504
+ case "json":
21505
+ default:
21506
+ return formatGenericJson(payload);
21507
+ }
21508
+ }
21509
+ async function loadWebhookConfig() {
21510
+ const { teamConfig } = await autoDetectInit();
21511
+ return getWebhookSharing(teamConfig);
21512
+ }
21513
+ async function listWebhooks() {
21514
+ const config = await loadWebhookConfig();
21515
+ return config.endpoints;
21516
+ }
21517
+ async function testWebhook(url) {
21518
+ const config = await loadWebhookConfig();
21519
+ const endpoints = url ? config.endpoints.filter((ep) => ep.url === url) : config.endpoints;
21520
+ if (endpoints.length === 0) {
21521
+ log.warn("No webhook endpoints configured.");
21522
+ return;
21523
+ }
21524
+ const testPayload = {
21525
+ event: "webhook-test",
21526
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
21527
+ tool: "teamai-cli",
21528
+ data: {
21529
+ message: "This is a test webhook from TeamAI CLI"
21530
+ }
21531
+ };
21532
+ for (const endpoint of endpoints) {
21533
+ log.info(`Testing webhook to ${endpoint.url}...`);
21534
+ try {
21535
+ await sendToEndpoint(endpoint, testPayload);
21536
+ log.success(`Webhook test successful: ${endpoint.url}`);
21537
+ } catch (error) {
21538
+ log.error(`Webhook test failed: ${endpoint.url} - ${error.message}`);
21539
+ }
21540
+ }
21541
+ }
21542
+ var init_webhook = __esm({
21543
+ "src/webhook.ts"() {
21544
+ "use strict";
21545
+ init_config();
21546
+ init_logger();
21547
+ init_redact();
21548
+ init_types();
21549
+ init_webhook_formatters();
21550
+ }
21551
+ });
21552
+
21266
21553
  // src/migrate.ts
21267
21554
  var migrate_exports = {};
21268
21555
  __export(migrate_exports, {
@@ -23139,6 +23426,7 @@ __export(usage_tracker_exports, {
23139
23426
  isValidSkillName: () => isValidSkillName,
23140
23427
  readKnownSkills: () => readKnownSkills,
23141
23428
  readUsageEvents: () => readUsageEvents,
23429
+ resolveSkillUse: () => resolveSkillUse,
23142
23430
  skillExistsOnDisk: () => skillExistsOnDisk,
23143
23431
  track: () => track,
23144
23432
  trackFromStdin: () => trackFromStdin,
@@ -23175,6 +23463,23 @@ function extractSkillName(toolInput) {
23175
23463
  function isValidSkillName(name) {
23176
23464
  return SKILL_NAME_REGEX.test(name);
23177
23465
  }
23466
+ function resolveSkillUse(toolName, toolInput) {
23467
+ let skillName = null;
23468
+ let source = null;
23469
+ if (toolName === "Skill") {
23470
+ skillName = extractSkillName(toolInput);
23471
+ } else if (toolName === "Read") {
23472
+ const filePath = (typeof toolInput.file_path === "string" ? toolInput.file_path : null) ?? (typeof toolInput.filePath === "string" ? toolInput.filePath : null) ?? (typeof toolInput.path === "string" ? toolInput.path : null);
23473
+ if (filePath && /\/SKILL\.md$/i.test(filePath)) {
23474
+ skillName = extractSkillName({ skill: filePath });
23475
+ source = "cursor";
23476
+ }
23477
+ } else {
23478
+ return null;
23479
+ }
23480
+ if (!skillName || !isValidSkillName(skillName)) return null;
23481
+ return { skillName, source };
23482
+ }
23178
23483
  async function skillExistsOnDisk(skillName) {
23179
23484
  const home = getUserHome();
23180
23485
  const userSkillDirs = [
@@ -26063,7 +26368,7 @@ async function warnIfEnvYamlShapeIsWrong(freshConfig, localConfig) {
26063
26368
  log.debug(`env.yaml shape check skipped: ${e.message}`);
26064
26369
  }
26065
26370
  }
26066
- async function pullForScope(localConfig, options, reported, policy = {}) {
26371
+ async function pullForScope(localConfig, options, reported, policy = {}, result) {
26067
26372
  const scopeLabel = localConfig.scope;
26068
26373
  const revisionField = policy.revisionField ?? "lastPullRev";
26069
26374
  const targetsField = revisionField === "lastPullRev" ? "lastPullTargets" : "lastInheritedPullTargets";
@@ -26110,6 +26415,111 @@ async function pullForScope(localConfig, options, reported, policy = {}) {
26110
26415
  return;
26111
26416
  }
26112
26417
  const resourceTypes = policy.resourceTypes ?? ["skills", "rules", "docs", "env", "agents"];
26418
+ let reportsReadRoot;
26419
+ const resolveReportsReadRoot = () => {
26420
+ reportsReadRoot ??= (async () => {
26421
+ if (!usesBranchWorktree(localConfig)) return localConfig.repo.localPath;
26422
+ try {
26423
+ const { ensureReportsWorktree: ensureReportsWorktree2, refreshReportsWorktree: refreshReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
26424
+ await refreshReportsWorktree2(localConfig, { pushIfCreated: false });
26425
+ return await ensureReportsWorktree2(localConfig, { pushIfCreated: false });
26426
+ } catch (e) {
26427
+ log.debug(`reports worktree unavailable: ${e.message}`);
26428
+ return void 0;
26429
+ }
26430
+ })();
26431
+ return reportsReadRoot;
26432
+ };
26433
+ const syncLearningsAndRebuildIndex = async () => {
26434
+ if (options.dryRun) return;
26435
+ try {
26436
+ try {
26437
+ const { learningsBranch: learningsBranch2 } = await Promise.resolve().then(() => (init_learnings_branch(), learnings_branch_exports));
26438
+ await learningsBranch2.refresh(localConfig, { pushIfCreated: false });
26439
+ } catch (e) {
26440
+ log.debug(`learnings worktree unavailable: ${e.message}`);
26441
+ }
26442
+ const roots = learningsRoots(localConfig);
26443
+ const publishedRoots = roots.read;
26444
+ const docsRepoDir = path75.join(localConfig.repo.localPath, "docs");
26445
+ const rulesRepoDir = path75.join(localConfig.repo.localPath, "rules");
26446
+ const skillsRepoDir = path75.join(localConfig.repo.localPath, "skills");
26447
+ const reportsRoot = await resolveReportsReadRoot();
26448
+ const votesDir = reportsRoot ? path75.join(reportsRoot, "votes") : void 0;
26449
+ const activeLearningsNamespaces = roleContext?.activeNamespaces.learnings ?? [];
26450
+ const countLearnings = async (baseDir) => {
26451
+ if (!await pathExists(baseDir)) return [];
26452
+ const names = (await listFiles(baseDir)).filter((f) => f.endsWith(".md"));
26453
+ for (const ns of activeLearningsNamespaces) {
26454
+ const nsDir = path75.join(baseDir, ns);
26455
+ if (await pathExists(nsDir)) {
26456
+ names.push(
26457
+ ...(await listFilesRecursive(nsDir)).filter((f) => f.endsWith(".md")).map((f) => path75.join(ns, f))
26458
+ );
26459
+ }
26460
+ }
26461
+ return names;
26462
+ };
26463
+ let learningsCount = 0;
26464
+ let effectiveLearningsDir;
26465
+ const mirrorSources = publishedRoots.filter((dir) => dir !== getUserLearningsDir());
26466
+ const counted = /* @__PURE__ */ new Set();
26467
+ for (const dir of publishedRoots) {
26468
+ for (const name of await countLearnings(dir)) counted.add(name);
26469
+ }
26470
+ learningsCount = counted.size;
26471
+ if (localConfig.scope === "user") {
26472
+ await mirrorLearnings(
26473
+ mirrorSources,
26474
+ getUserLearningsDir(),
26475
+ activeLearningsNamespaces
26476
+ );
26477
+ effectiveLearningsDir = await pathExists(getUserLearningsDir()) ? getUserLearningsDir() : void 0;
26478
+ } else {
26479
+ for (const dir of publishedRoots) {
26480
+ if (await pathExists(dir)) {
26481
+ effectiveLearningsDir = dir;
26482
+ break;
26483
+ }
26484
+ }
26485
+ }
26486
+ const hasAnySource = effectiveLearningsDir || await pathExists(docsRepoDir) || await pathExists(rulesRepoDir) || await pathExists(skillsRepoDir);
26487
+ const repoCodebaseDir = path75.join(localConfig.repo.localPath, "docs", "team-codebase");
26488
+ const effectiveCodebaseDir = await pathExists(repoCodebaseDir) ? repoCodebaseDir : void 0;
26489
+ if (hasAnySource || effectiveCodebaseDir) {
26490
+ const votesExist = votesDir ? await pathExists(votesDir) : false;
26491
+ const teamaiHome = getDataHome(localConfig);
26492
+ const indexPath = path75.join(teamaiHome, "search-index.json");
26493
+ const { buildIndex: buildIndex2 } = await Promise.resolve().then(() => (init_search_index(), search_index_exports));
26494
+ const elapsed = await buildIndex2({
26495
+ // The queue comes first: a contribution that could not be published
26496
+ // yet stays recallable, and a queued edit wins over the published copy.
26497
+ // Then every published root, so nothing is indexed from one directory
26498
+ // that happened to be picked.
26499
+ learningsDirs: [
26500
+ pendingLearningsDir(localConfig),
26501
+ ...effectiveLearningsDir ? [effectiveLearningsDir] : [],
26502
+ ...publishedRoots
26503
+ ],
26504
+ learningsNamespaces: activeLearningsNamespaces,
26505
+ docsDir: await pathExists(docsRepoDir) ? docsRepoDir : void 0,
26506
+ rulesDir: await pathExists(rulesRepoDir) ? rulesRepoDir : void 0,
26507
+ skillsDir: await pathExists(skillsRepoDir) ? skillsRepoDir : void 0,
26508
+ codebaseDir: void 0,
26509
+ // codebase now served by teamwiki/ graph engine
26510
+ votesDir: votesExist ? votesDir : void 0,
26511
+ indexPath
26512
+ });
26513
+ if (learningsCount > 0) {
26514
+ log.success(`Synced ${learningsCount} learnings (index: ${elapsed}ms)`);
26515
+ } else {
26516
+ log.debug(`[${scopeLabel}] Built multi-category search index in ${elapsed}ms`);
26517
+ }
26518
+ }
26519
+ } catch (e) {
26520
+ log.debug(`Learnings/index sync skipped: ${e.message}`);
26521
+ }
26522
+ };
26113
26523
  let currentTargets = null;
26114
26524
  if (!options.force && !options.dryRun && !submodulesChanged) {
26115
26525
  try {
@@ -26143,6 +26553,7 @@ async function pullForScope(localConfig, options, reported, policy = {}) {
26143
26553
  if (resourceTypes.includes("env")) {
26144
26554
  await warnIfEnvYamlShapeIsWrong(freshConfig, localConfig);
26145
26555
  }
26556
+ await syncLearningsAndRebuildIndex();
26146
26557
  return;
26147
26558
  }
26148
26559
  log.debug(`[${scopeLabel}] Repo unchanged; resource target set changed, syncing`);
@@ -26302,110 +26713,7 @@ async function pullForScope(localConfig, options, reported, policy = {}) {
26302
26713
  if (totalSynced === 0) {
26303
26714
  log.info(`[${scopeLabel}] No resources to sync`);
26304
26715
  }
26305
- let reportsReadRoot;
26306
- const resolveReportsReadRoot = () => {
26307
- reportsReadRoot ??= (async () => {
26308
- if (!usesBranchWorktree(localConfig)) return localConfig.repo.localPath;
26309
- try {
26310
- const { ensureReportsWorktree: ensureReportsWorktree2, refreshReportsWorktree: refreshReportsWorktree2 } = await Promise.resolve().then(() => (init_reports_branch(), reports_branch_exports));
26311
- await refreshReportsWorktree2(localConfig, { pushIfCreated: false });
26312
- return await ensureReportsWorktree2(localConfig, { pushIfCreated: false });
26313
- } catch (e) {
26314
- log.debug(`reports worktree unavailable: ${e.message}`);
26315
- return void 0;
26316
- }
26317
- })();
26318
- return reportsReadRoot;
26319
- };
26320
- if (!options.dryRun) {
26321
- try {
26322
- try {
26323
- const { learningsBranch: learningsBranch2 } = await Promise.resolve().then(() => (init_learnings_branch(), learnings_branch_exports));
26324
- await learningsBranch2.refresh(localConfig, { pushIfCreated: false });
26325
- } catch (e) {
26326
- log.debug(`learnings worktree unavailable: ${e.message}`);
26327
- }
26328
- const roots = learningsRoots(localConfig);
26329
- const publishedRoots = roots.read;
26330
- const docsRepoDir = path75.join(localConfig.repo.localPath, "docs");
26331
- const rulesRepoDir = path75.join(localConfig.repo.localPath, "rules");
26332
- const skillsRepoDir = path75.join(localConfig.repo.localPath, "skills");
26333
- const reportsRoot = await resolveReportsReadRoot();
26334
- const votesDir = reportsRoot ? path75.join(reportsRoot, "votes") : void 0;
26335
- const activeLearningsNamespaces = roleContext?.activeNamespaces.learnings ?? [];
26336
- const countLearnings = async (baseDir) => {
26337
- if (!await pathExists(baseDir)) return [];
26338
- const names = (await listFiles(baseDir)).filter((f) => f.endsWith(".md"));
26339
- for (const ns of activeLearningsNamespaces) {
26340
- const nsDir = path75.join(baseDir, ns);
26341
- if (await pathExists(nsDir)) {
26342
- names.push(
26343
- ...(await listFilesRecursive(nsDir)).filter((f) => f.endsWith(".md")).map((f) => path75.join(ns, f))
26344
- );
26345
- }
26346
- }
26347
- return names;
26348
- };
26349
- let learningsCount = 0;
26350
- let effectiveLearningsDir;
26351
- const mirrorSources = publishedRoots.filter((dir) => dir !== getUserLearningsDir());
26352
- const counted = /* @__PURE__ */ new Set();
26353
- for (const dir of publishedRoots) {
26354
- for (const name of await countLearnings(dir)) counted.add(name);
26355
- }
26356
- learningsCount = counted.size;
26357
- if (localConfig.scope === "user") {
26358
- await mirrorLearnings(
26359
- mirrorSources,
26360
- getUserLearningsDir(),
26361
- activeLearningsNamespaces
26362
- );
26363
- effectiveLearningsDir = await pathExists(getUserLearningsDir()) ? getUserLearningsDir() : void 0;
26364
- } else {
26365
- for (const dir of publishedRoots) {
26366
- if (await pathExists(dir)) {
26367
- effectiveLearningsDir = dir;
26368
- break;
26369
- }
26370
- }
26371
- }
26372
- const hasAnySource = effectiveLearningsDir || await pathExists(docsRepoDir) || await pathExists(rulesRepoDir) || await pathExists(skillsRepoDir);
26373
- const repoCodebaseDir = path75.join(localConfig.repo.localPath, "docs", "team-codebase");
26374
- const effectiveCodebaseDir = await pathExists(repoCodebaseDir) ? repoCodebaseDir : void 0;
26375
- if (hasAnySource || effectiveCodebaseDir) {
26376
- const votesExist = votesDir ? await pathExists(votesDir) : false;
26377
- const teamaiHome = getDataHome(localConfig);
26378
- const indexPath = path75.join(teamaiHome, "search-index.json");
26379
- const { buildIndex: buildIndex2 } = await Promise.resolve().then(() => (init_search_index(), search_index_exports));
26380
- const elapsed = await buildIndex2({
26381
- // The queue comes first: a contribution that could not be published
26382
- // yet stays recallable, and a queued edit wins over the published copy.
26383
- // Then every published root, so nothing is indexed from one directory
26384
- // that happened to be picked.
26385
- learningsDirs: [
26386
- pendingLearningsDir(localConfig),
26387
- ...effectiveLearningsDir ? [effectiveLearningsDir] : [],
26388
- ...publishedRoots
26389
- ],
26390
- learningsNamespaces: activeLearningsNamespaces,
26391
- docsDir: await pathExists(docsRepoDir) ? docsRepoDir : void 0,
26392
- rulesDir: await pathExists(rulesRepoDir) ? rulesRepoDir : void 0,
26393
- skillsDir: await pathExists(skillsRepoDir) ? skillsRepoDir : void 0,
26394
- codebaseDir: void 0,
26395
- // codebase now served by teamwiki/ graph engine
26396
- votesDir: votesExist ? votesDir : void 0,
26397
- indexPath
26398
- });
26399
- if (learningsCount > 0) {
26400
- log.success(`Synced ${learningsCount} learnings (index: ${elapsed}ms)`);
26401
- } else {
26402
- log.debug(`[${scopeLabel}] Built multi-category search index in ${elapsed}ms`);
26403
- }
26404
- }
26405
- } catch (e) {
26406
- log.debug(`Learnings/index sync skipped: ${e.message}`);
26407
- }
26408
- }
26716
+ await syncLearningsAndRebuildIndex();
26409
26717
  if (!options.dryRun) {
26410
26718
  await syncManagedInstructions(freshConfig, localConfig, roleContext, scopeLabel);
26411
26719
  }
@@ -26493,6 +26801,7 @@ async function pullForScope(localConfig, options, reported, policy = {}) {
26493
26801
  } catch {
26494
26802
  }
26495
26803
  }
26804
+ if (result && !options.dryRun) result.completed = true;
26496
26805
  }
26497
26806
  function compileCulture(raw) {
26498
26807
  let parsed;
@@ -26777,7 +27086,7 @@ async function reinjectLegacyHooks(localConfig) {
26777
27086
  await injectHooksToAllTools2(teamConfig.toolPaths, baseDir, hookFilter);
26778
27087
  log.debug("Hooks migrated to dispatch format");
26779
27088
  }
26780
- async function pull(options) {
27089
+ async function pull(options, result) {
26781
27090
  const reported = /* @__PURE__ */ new Set();
26782
27091
  const needsHookMigration = await legacyHooksNeedReinject().catch(() => false);
26783
27092
  const contended = /* @__PURE__ */ new Set();
@@ -26819,12 +27128,12 @@ async function pull(options) {
26819
27128
  await pullForScope(inheritedUserConfig, options, reported, {
26820
27129
  resourceTypes: ["skills", "rules", "docs", "agents"],
26821
27130
  revisionField: "lastInheritedPullRev"
26822
- });
27131
+ }, result);
26823
27132
  }
26824
27133
  } else {
26825
27134
  activeUserConfig = loadedUserConfig;
26826
27135
  if (await lockScope(activeUserConfig)) {
26827
- await pullForScope(activeUserConfig, options, reported);
27136
+ await pullForScope(activeUserConfig, options, reported, {}, result);
26828
27137
  }
26829
27138
  }
26830
27139
  } else if (inheritUserScope) {
@@ -26839,7 +27148,7 @@ async function pull(options) {
26839
27148
  if (projectConfig) {
26840
27149
  try {
26841
27150
  if (await lockScope(projectConfig)) {
26842
- await pullForScope(projectConfig, options, reported);
27151
+ await pullForScope(projectConfig, options, reported, {}, result);
26843
27152
  }
26844
27153
  } catch (e) {
26845
27154
  log.warn(`Project-scope pull error: ${e.message}`);
@@ -29679,267 +29988,6 @@ var init_mcp_cmd = __esm({
29679
29988
  }
29680
29989
  });
29681
29990
 
29682
- // src/webhook-formatters.ts
29683
- function formatFeishuMessage(payload) {
29684
- const eventLabel = formatEventLabel(payload.event);
29685
- const time = new Date(payload.timestamp).toLocaleString("zh-CN", {
29686
- timeZone: "Asia/Shanghai",
29687
- year: "numeric",
29688
- month: "2-digit",
29689
- day: "2-digit",
29690
- hour: "2-digit",
29691
- minute: "2-digit",
29692
- second: "2-digit"
29693
- });
29694
- const lines = [
29695
- `\u{1F916} TeamAI Notification`,
29696
- `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
29697
- `Event: ${eventLabel}`
29698
- ];
29699
- if (payload.username) {
29700
- lines.push(`User: ${payload.username}`);
29701
- }
29702
- lines.push(`Time: ${time}`);
29703
- if (payload.tool) {
29704
- lines.push(`Tool: ${payload.tool}`);
29705
- }
29706
- if (payload.data) {
29707
- const dataStr = formatEventData(payload.event, payload.data);
29708
- if (dataStr) {
29709
- lines.push(dataStr);
29710
- }
29711
- }
29712
- lines.push(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`);
29713
- return {
29714
- msg_type: "text",
29715
- content: {
29716
- text: lines.join("\n")
29717
- }
29718
- };
29719
- }
29720
- function formatWecomMessage(payload) {
29721
- const eventLabel = formatEventLabel(payload.event);
29722
- const time = new Date(payload.timestamp).toLocaleString("zh-CN", {
29723
- timeZone: "Asia/Shanghai",
29724
- year: "numeric",
29725
- month: "2-digit",
29726
- day: "2-digit",
29727
- hour: "2-digit",
29728
- minute: "2-digit",
29729
- second: "2-digit"
29730
- });
29731
- const lines = [
29732
- `\u{1F916} TeamAI Notification`,
29733
- `Event: ${eventLabel}`
29734
- ];
29735
- if (payload.username) {
29736
- lines.push(`User: ${payload.username}`);
29737
- }
29738
- lines.push(`Time: ${time}`);
29739
- if (payload.tool) {
29740
- lines.push(`Tool: ${payload.tool}`);
29741
- }
29742
- if (payload.data) {
29743
- const dataStr = formatEventData(payload.event, payload.data);
29744
- if (dataStr) {
29745
- lines.push(dataStr);
29746
- }
29747
- }
29748
- return {
29749
- msgtype: "text",
29750
- text: {
29751
- content: lines.join("\n")
29752
- }
29753
- };
29754
- }
29755
- function formatGenericJson(payload) {
29756
- return {
29757
- event: payload.event,
29758
- timestamp: payload.timestamp,
29759
- tool: payload.tool,
29760
- sessionId: payload.sessionId,
29761
- cwd: payload.cwd,
29762
- team: payload.team,
29763
- username: payload.username,
29764
- data: payload.data
29765
- };
29766
- }
29767
- function formatEventLabel(event) {
29768
- const labels = {
29769
- "push": "Push Complete",
29770
- "pull": "Pull Complete",
29771
- "skill-use": "Skill Used",
29772
- "session-start": "Session Started",
29773
- "session-stop": "Session Ended",
29774
- "webhook-test": "Webhook Test"
29775
- };
29776
- return labels[event] ?? event;
29777
- }
29778
- function formatEventData(event, data) {
29779
- switch (event) {
29780
- case "push":
29781
- case "pull":
29782
- if (data.resources && typeof data.resources === "object") {
29783
- const resources = data.resources;
29784
- const parts = Object.entries(resources).filter(([, count]) => count > 0).map(([type, count]) => `${type}: ${count}`);
29785
- return parts.length > 0 ? `Resources: ${parts.join(", ")}` : null;
29786
- }
29787
- return null;
29788
- case "skill-use":
29789
- if (data.skillName && typeof data.skillName === "string") {
29790
- return `Skill: ${data.skillName}`;
29791
- }
29792
- return null;
29793
- case "session-start":
29794
- case "session-stop":
29795
- if (data.sessionId && typeof data.sessionId === "string") {
29796
- return `Session: ${data.sessionId.slice(0, 8)}...`;
29797
- }
29798
- return null;
29799
- default:
29800
- return null;
29801
- }
29802
- }
29803
- var init_webhook_formatters = __esm({
29804
- "src/webhook-formatters.ts"() {
29805
- "use strict";
29806
- }
29807
- });
29808
-
29809
- // src/webhook.ts
29810
- var webhook_exports = {};
29811
- __export(webhook_exports, {
29812
- listWebhooks: () => listWebhooks,
29813
- loadWebhookConfig: () => loadWebhookConfig,
29814
- sendWebhook: () => sendWebhook,
29815
- testWebhook: () => testWebhook
29816
- });
29817
- import { createHmac } from "crypto";
29818
- async function sendWebhook(event, payload, config) {
29819
- if (!config) {
29820
- const { teamConfig } = await autoDetectInit();
29821
- config = getWebhookSharing(teamConfig);
29822
- }
29823
- if (!config.enabled || config.endpoints.length === 0) return;
29824
- const fullPayload = {
29825
- event,
29826
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
29827
- tool: payload.tool ?? "unknown",
29828
- sessionId: payload.sessionId,
29829
- cwd: payload.cwd,
29830
- team: payload.team,
29831
- username: payload.username,
29832
- data: payload.data ?? {}
29833
- };
29834
- const matchingEndpoints = config.endpoints.filter(
29835
- (ep) => ep.events.includes(event) || ep.events.includes("*")
29836
- );
29837
- if (matchingEndpoints.length === 0) return;
29838
- await Promise.allSettled(
29839
- matchingEndpoints.map((ep) => sendToEndpoint(ep, fullPayload))
29840
- );
29841
- }
29842
- async function sendToEndpoint(endpoint, payload) {
29843
- const { url, type, secret, timeout, retries } = endpoint;
29844
- const body = formatMessage(type, payload);
29845
- const headers = {
29846
- "Content-Type": type === "json" ? "application/json" : "text/plain; charset=utf-8"
29847
- };
29848
- if (secret) {
29849
- const signature = createHmac("sha256", secret).update(JSON.stringify(body)).digest("hex");
29850
- headers["X-TeamAI-Signature"] = `sha256=${signature}`;
29851
- }
29852
- for (let attempt = 0; attempt <= retries; attempt++) {
29853
- try {
29854
- const controller = new AbortController();
29855
- const timeoutId = setTimeout(() => controller.abort(), timeout);
29856
- const response = await fetch(url, {
29857
- method: "POST",
29858
- headers,
29859
- body: typeof body === "string" ? body : JSON.stringify(body),
29860
- signal: controller.signal
29861
- });
29862
- clearTimeout(timeoutId);
29863
- if (response.ok) {
29864
- log.debug(`Webhook sent successfully to ${url}`);
29865
- return;
29866
- }
29867
- if (response.status >= 400 && response.status < 500 && response.status !== 429) {
29868
- log.warn(`Webhook to ${url} failed with status ${response.status} (not retrying)`);
29869
- return;
29870
- }
29871
- if (attempt < retries) {
29872
- const delay = Math.pow(2, attempt) * 1e3;
29873
- log.debug(`Webhook to ${url} failed, retrying in ${delay}ms...`);
29874
- await new Promise((resolve) => setTimeout(resolve, delay));
29875
- }
29876
- } catch (error) {
29877
- if (error instanceof Error && error.name === "AbortError") {
29878
- log.warn(`Webhook to ${url} timed out after ${timeout}ms`);
29879
- } else if (attempt < retries) {
29880
- const delay = Math.pow(2, attempt) * 1e3;
29881
- log.debug(`Webhook to ${url} failed, retrying in ${delay}ms...`);
29882
- await new Promise((resolve) => setTimeout(resolve, delay));
29883
- } else {
29884
- log.warn(`Webhook to ${url} failed after ${retries + 1} attempts: ${error.message}`);
29885
- }
29886
- }
29887
- }
29888
- }
29889
- function formatMessage(type, payload) {
29890
- switch (type) {
29891
- case "feishu":
29892
- return formatFeishuMessage(payload);
29893
- case "wecom":
29894
- return formatWecomMessage(payload);
29895
- case "json":
29896
- default:
29897
- return formatGenericJson(payload);
29898
- }
29899
- }
29900
- async function loadWebhookConfig() {
29901
- const { teamConfig } = await autoDetectInit();
29902
- return getWebhookSharing(teamConfig);
29903
- }
29904
- async function listWebhooks() {
29905
- const config = await loadWebhookConfig();
29906
- return config.endpoints;
29907
- }
29908
- async function testWebhook(url) {
29909
- const config = await loadWebhookConfig();
29910
- const endpoints = url ? config.endpoints.filter((ep) => ep.url === url) : config.endpoints;
29911
- if (endpoints.length === 0) {
29912
- log.warn("No webhook endpoints configured.");
29913
- return;
29914
- }
29915
- const testPayload = {
29916
- event: "webhook-test",
29917
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
29918
- tool: "teamai-cli",
29919
- data: {
29920
- message: "This is a test webhook from TeamAI CLI"
29921
- }
29922
- };
29923
- for (const endpoint of endpoints) {
29924
- log.info(`Testing webhook to ${endpoint.url}...`);
29925
- try {
29926
- await sendToEndpoint(endpoint, testPayload);
29927
- log.success(`Webhook test successful: ${endpoint.url}`);
29928
- } catch (error) {
29929
- log.error(`Webhook test failed: ${endpoint.url} - ${error.message}`);
29930
- }
29931
- }
29932
- }
29933
- var init_webhook = __esm({
29934
- "src/webhook.ts"() {
29935
- "use strict";
29936
- init_config();
29937
- init_logger();
29938
- init_types();
29939
- init_webhook_formatters();
29940
- }
29941
- });
29942
-
29943
29991
  // src/session-collector.ts
29944
29992
  import fs28 from "fs";
29945
29993
  import path86 from "path";
@@ -33953,6 +34001,24 @@ async function contributeHintAllowed() {
33953
34001
  return isContributeHintEnabled2({}, {});
33954
34002
  }
33955
34003
  }
34004
+ async function buildWebhookData(event, stdin) {
34005
+ if (event === "skill-use") {
34006
+ const rawToolName = stdin.tool_name;
34007
+ const toolInput = stdin.tool_input;
34008
+ if (typeof rawToolName !== "string" || !toolInput || typeof toolInput !== "object") return {};
34009
+ const { resolveSkillUse: resolveSkillUse2 } = await Promise.resolve().then(() => (init_usage_tracker(), usage_tracker_exports));
34010
+ const resolved = resolveSkillUse2(
34011
+ normalizeToolName(rawToolName),
34012
+ toolInput
34013
+ );
34014
+ return resolved ? { skillName: resolved.skillName } : {};
34015
+ }
34016
+ if (event === "session-start" || event === "session-stop") {
34017
+ const sessionId = deriveSessionId(stdin);
34018
+ return sessionId ? { sessionId } : {};
34019
+ }
34020
+ return {};
34021
+ }
33956
34022
  function buildHandlerRegistry() {
33957
34023
  return [
33958
34024
  // ─── SessionStart ─────────────────────────────────
@@ -33965,9 +34031,11 @@ function buildHandlerRegistry() {
33965
34031
  { event: "session-start", matcher: "*", handler: packageHintHandler, timeoutMs: FOREGROUND_HOOK_TIMEOUT_MS },
33966
34032
  { event: "session-start", matcher: "*", handler: localAgentHandler, timeoutMs: FOREGROUND_HOOK_TIMEOUT_MS },
33967
34033
  { event: "session-start", matcher: "*", handler: webhookHandler, timeoutMs: FOREGROUND_HOOK_TIMEOUT_MS, background: true },
33968
- // Copilot emits SessionEnd after its final turn. Only the dashboard needs
33969
- // this lifecycle event; detaching it avoids delaying CLI shutdown.
34034
+ // Copilot emits SessionEnd after its final turn (not Stop), so the webhook
34035
+ // handler must run here too or those sessions emit no session-stop
34036
+ // notification (#702). Detached, mirroring the stop registration.
33970
34037
  { event: "session-end", matcher: "*", handler: dashboardReportHandler, timeoutMs: FOREGROUND_HOOK_TIMEOUT_MS, background: true },
34038
+ { event: "session-end", matcher: "*", handler: webhookHandler, timeoutMs: FOREGROUND_HOOK_TIMEOUT_MS, background: true },
33971
34039
  // ─── Stop ─────────────────────────────────────────
33972
34040
  // votes-sync and contribute-check may return a hint the host injects back
33973
34041
  // into the session, so they run inline (capped at FOREGROUND_HOOK_TIMEOUT_MS).
@@ -34001,7 +34069,7 @@ function filterHandlersForConfig(registry, localConfig) {
34001
34069
  }
34002
34070
  return registry;
34003
34071
  }
34004
- var FOREGROUND_HOOK_TIMEOUT_MS, TODOWRITE_HINT_TIMEOUT_MS, UPDATE_TIMEOUT_MS2, LOCAL_AGENT_TIMEOUT_MS, PULL_TIMEOUT_MS, pullHandler, updateHandler, dashboardReportHandler, trackHandler, trackSlashHandler, contributeCheckHandler, pendingHintHandler, packagePendingHintHandler, votesSyncHandler, todowriteHintHandler, mrHintHandler, packageHintHandler, localAgentHandler, webhookHandler;
34072
+ var FOREGROUND_HOOK_TIMEOUT_MS, TODOWRITE_HINT_TIMEOUT_MS, UPDATE_TIMEOUT_MS2, LOCAL_AGENT_TIMEOUT_MS, PULL_TIMEOUT_MS, pullHandler, updateHandler, dashboardReportHandler, trackHandler, trackSlashHandler, contributeCheckHandler, pendingHintHandler, packagePendingHintHandler, votesSyncHandler, todowriteHintHandler, mrHintHandler, packageHintHandler, localAgentHandler, WEBHOOK_EVENT_BY_HOOK, webhookHandler;
34005
34073
  var init_hook_handlers = __esm({
34006
34074
  "src/hook-handlers.ts"() {
34007
34075
  "use strict";
@@ -34065,29 +34133,20 @@ var init_hook_handlers = __esm({
34065
34133
  trackHandler = {
34066
34134
  name: "track",
34067
34135
  async execute(stdin, tool) {
34068
- const { extractSkillName: extractSkillName2, isValidSkillName: isValidSkillName2, appendUsageEvent: appendUsageEvent2, updateKnownSkills: updateKnownSkills2 } = await Promise.resolve().then(() => (init_usage_tracker(), usage_tracker_exports));
34136
+ const { resolveSkillUse: resolveSkillUse2, appendUsageEvent: appendUsageEvent2, updateKnownSkills: updateKnownSkills2 } = await Promise.resolve().then(() => (init_usage_tracker(), usage_tracker_exports));
34069
34137
  const rawToolName = stdin.tool_name;
34070
34138
  if (typeof rawToolName !== "string") return null;
34071
34139
  const toolName = normalizeToolName(rawToolName);
34072
34140
  const toolInput = stdin.tool_input;
34073
34141
  if (!toolInput || typeof toolInput !== "object") return null;
34074
- let skillName = null;
34075
- let toolSource = tool;
34076
- if (toolName === "Skill") {
34077
- skillName = extractSkillName2(toolInput);
34078
- } else if (toolName === "Read") {
34079
- const input = toolInput;
34080
- const filePath = (typeof input.file_path === "string" ? input.file_path : null) ?? (typeof input.filePath === "string" ? input.filePath : null) ?? (typeof input.path === "string" ? input.path : null);
34081
- if (typeof filePath === "string" && /\/SKILL\.md$/i.test(filePath)) {
34082
- skillName = extractSkillName2({ skill: filePath });
34083
- toolSource = "cursor";
34084
- }
34085
- } else {
34086
- return null;
34087
- }
34088
- if (!skillName || !isValidSkillName2(skillName)) return null;
34089
- await appendUsageEvent2({ skill: skillName, timestamp: (/* @__PURE__ */ new Date()).toISOString(), tool: toolSource });
34090
- await updateKnownSkills2(skillName);
34142
+ const resolved = resolveSkillUse2(toolName, toolInput);
34143
+ if (!resolved) return null;
34144
+ await appendUsageEvent2({
34145
+ skill: resolved.skillName,
34146
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
34147
+ tool: resolved.source ?? tool
34148
+ });
34149
+ await updateKnownSkills2(resolved.skillName);
34091
34150
  return null;
34092
34151
  }
34093
34152
  };
@@ -34278,6 +34337,12 @@ var init_hook_handlers = __esm({
34278
34337
  return reportAndSyncFromHook2(stdin, tool);
34279
34338
  }
34280
34339
  };
34340
+ WEBHOOK_EVENT_BY_HOOK = {
34341
+ sessionstart: "session-start",
34342
+ stop: "session-stop",
34343
+ sessionend: "session-stop",
34344
+ posttooluse: "skill-use"
34345
+ };
34281
34346
  webhookHandler = {
34282
34347
  name: "webhook-dispatch",
34283
34348
  async execute(stdin, tool) {
@@ -34285,13 +34350,15 @@ var init_hook_handlers = __esm({
34285
34350
  try {
34286
34351
  const config = await loadWebhookConfig2();
34287
34352
  if (!config.enabled || config.endpoints.length === 0) return null;
34288
- const event = typeof stdin.event === "string" ? stdin.event : "unknown";
34353
+ const hookEventName = typeof stdin.hook_event_name === "string" ? stdin.hook_event_name : "";
34354
+ const event = WEBHOOK_EVENT_BY_HOOK[hookEventName.toLowerCase()];
34355
+ if (!event) return null;
34289
34356
  const payload = {
34290
34357
  tool,
34291
34358
  sessionId: deriveSessionId(stdin),
34292
34359
  cwd: resolveHookCwd(stdin),
34293
34360
  username: typeof stdin.username === "string" ? stdin.username : void 0,
34294
- data: stdin
34361
+ data: await buildWebhookData(event, stdin)
34295
34362
  };
34296
34363
  await sendWebhook2(event, payload, config);
34297
34364
  } catch (error) {
@@ -34671,6 +34738,13 @@ async function contribute(options) {
34671
34738
  log.debug(`contribute: index rebuild skipped: ${e.message}`);
34672
34739
  }
34673
34740
  const report = await publishQueuedLearnings(localConfig, username);
34741
+ if (report.published.length > 0) {
34742
+ try {
34743
+ await rebuildIndexAfterContribute(localConfig);
34744
+ } catch (e) {
34745
+ log.debug(`contribute: post-publish index rebuild skipped: ${e.message}`);
34746
+ }
34747
+ }
34674
34748
  const sessionId = options.sessionId || process.env.CLAUDE_SESSION_ID || "";
34675
34749
  if (sessionId) {
34676
34750
  await markContributed(sessionId);
@@ -43206,6 +43280,7 @@ async function importFromRepo(opts) {
43206
43280
  if (!dryRun) {
43207
43281
  const cacheWiki = path127.join(cacheDir, "teamwiki");
43208
43282
  try {
43283
+ let extractIncremental = incremental;
43209
43284
  if (incremental) {
43210
43285
  const destIndices = path127.join(teamwikiRoot, ".indices");
43211
43286
  const cacheIndices = path127.join(cacheDir, "teamwiki", ".indices");
@@ -43220,13 +43295,18 @@ async function importFromRepo(opts) {
43220
43295
  if (await fs48.pathExists(existingManifest)) {
43221
43296
  await fs48.copy(existingManifest, path127.join(cacheDir, "teamwiki", "source-manifest.json"));
43222
43297
  }
43298
+ const cacheManifest = path127.join(cacheWiki, "source-manifest.json");
43299
+ if (await fs48.pathExists(cacheManifest)) {
43300
+ const manifest = await fs48.readJson(cacheManifest).catch(() => null);
43301
+ if (!lastSync || manifest?.headSha !== lastSync.sha) extractIncremental = false;
43302
+ }
43223
43303
  }
43224
43304
  await extractCodebase({
43225
43305
  path: cacheDir,
43226
43306
  project: slug,
43227
43307
  json: false,
43228
43308
  skipEnrich,
43229
- incremental,
43309
+ incremental: extractIncremental,
43230
43310
  repoUrl: url,
43231
43311
  branch: cloneBranch === "HEAD" ? void 0 : cloneBranch,
43232
43312
  sourceMrUrl
@@ -43305,7 +43385,7 @@ ${aiNarrative}`;
43305
43385
  }
43306
43386
  log.info(chalk4.green(`\u2713 teamwiki/ knowledge graph updated: ${slug}`));
43307
43387
  } catch (err) {
43308
- log.debug(`[wiki-engine] Graph generation failed (non-blocking): ${err instanceof Error ? err.message : err}`);
43388
+ throw new Error(`Knowledge extraction failed: ${err instanceof Error ? err.message : String(err)}`);
43309
43389
  } finally {
43310
43390
  await fs48.remove(cacheWiki).catch(() => {
43311
43391
  });
@@ -45285,7 +45365,9 @@ async function findGitHubComment(owner, repo, prNumber, marker) {
45285
45365
  `/repos/${owner}/${repo}/issues/${prNumber}/comments?per_page=100`,
45286
45366
  "GET"
45287
45367
  );
45288
- if (!resp.ok) return null;
45368
+ if (!resp.ok) {
45369
+ throw new Error(`GitHub comment list request failed (${resp.status})`);
45370
+ }
45289
45371
  const comments = await resp.json();
45290
45372
  return comments.find((c) => c.body.includes(marker)) ?? null;
45291
45373
  }
@@ -45341,7 +45423,9 @@ async function findTGitComment(projectId, mrGlobalId, marker) {
45341
45423
  `/projects/${projectId}/merge_requests/${mrGlobalId}/notes?per_page=100`,
45342
45424
  "GET"
45343
45425
  );
45344
- if (!resp.ok) return null;
45426
+ if (!resp.ok) {
45427
+ throw new Error(`TGit note list request failed (${resp.status})`);
45428
+ }
45345
45429
  const notes = await resp.json();
45346
45430
  return notes.find((n) => n.body.includes(marker)) ?? null;
45347
45431
  }
@@ -46073,6 +46157,13 @@ function registerPackagesCommand(program2, run = async (target, opts) => {
46073
46157
  var MIGRATION_TRIGGER_COMMANDS = /* @__PURE__ */ new Set(["init", "pull", "push"]);
46074
46158
  var require4 = createRequire4(import.meta.url);
46075
46159
  var { version } = require4("../package.json");
46160
+ async function notifyWebhook(event) {
46161
+ try {
46162
+ const { sendWebhook: sendWebhook2 } = await Promise.resolve().then(() => (init_webhook(), webhook_exports));
46163
+ await sendWebhook2(event, { tool: "teamai-cli", data: {} });
46164
+ } catch {
46165
+ }
46166
+ }
46076
46167
  var program = new Command();
46077
46168
  program.name("teamai").description("TeamAI \u2014 Make Every Team AI Native").version(version).option("--dry-run", "Preview mode, no changes made").option("-v, --verbose", "Verbose output").hook("preAction", async (thisCommand, actionCommand) => {
46078
46169
  const opts = thisCommand.opts();
@@ -46097,13 +46188,17 @@ program.command("init").description("Initialize teamai (configure Git provider,
46097
46188
  program.command("push").description("Push local resources to team repo").option("--all", "Push all without confirmation").option("--skill <path>", "Push a specific skill by path (e.g., ~/.claude/skills/hai/my-skill or skills/hai_dev/my-skill)").option("--role <id>", "Target role namespace for pushed project skills").option("--project <id>", "Target a project: push skills into the project's skills namespace (from manifest/projects.yaml)").action(async (cmdOpts) => {
46098
46189
  const globalOpts = program.opts();
46099
46190
  const { push: push2 } = await Promise.resolve().then(() => (init_push(), push_exports));
46100
- await push2({ ...globalOpts, ...cmdOpts });
46191
+ const outcome = { completed: false };
46192
+ await push2({ ...globalOpts, ...cmdOpts }, outcome);
46193
+ if (outcome.completed) await notifyWebhook("push");
46101
46194
  });
46102
46195
  program.command("pull").description("Pull team resources and inject into local AI tools").option("--silent", "Silent mode (for hooks)").option("--force", "Force full sync even if repo is unchanged").action(async (cmdOpts) => {
46103
46196
  const globalOpts = program.opts();
46104
46197
  if (cmdOpts.silent) setSilent(true);
46105
46198
  const { pull: pull2 } = await Promise.resolve().then(() => (init_pull(), pull_exports));
46106
- await pull2({ ...globalOpts, ...cmdOpts, interactive: !cmdOpts.silent });
46199
+ const outcome = { completed: false };
46200
+ await pull2({ ...globalOpts, ...cmdOpts, interactive: !cmdOpts.silent }, outcome);
46201
+ if (!cmdOpts.silent && outcome.completed) await notifyWebhook("pull");
46107
46202
  });
46108
46203
  program.command("status").description("Show local vs team repo diff").option("--all", "List every project data partition under ~/.teamai/projects (flags stale/orphan ones)").action(async (cmdOpts) => {
46109
46204
  const globalOpts = program.opts();