agentschat-mcp 0.36.4 → 0.36.7

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/connector.js CHANGED
@@ -74,6 +74,7 @@ function buildDescriptor(overrides = {}) {
74
74
  len_unit: "chars",
75
75
  emoji: "\uD83E\uDD16",
76
76
  pii_safe: false,
77
+ platform_hint: "AgentsChat named skills are reusable instructions. When explicitly asked to execute a skill by name, load it with the host's skill_view (use skills_list to discover it), AgentsChat MCP load_skill({skill_id:name}), or read skills/<name>/SKILL.md from the installed AgentsChat MCP package in this runtime. Report a missing loader or skill; do not pretend it ran. Keep skill text out of chat and keep replies in the originating chat.",
77
78
  supported_ops: [...SUPPORTED_OPS],
78
79
  ...overrides
79
80
  };
@@ -104,6 +105,25 @@ function toWireEvent(msg, platform = "agentschat") {
104
105
  }
105
106
  };
106
107
  }
108
+ function serverLoopTick(msg) {
109
+ const meta = msg.meta;
110
+ if (!meta || meta.kind !== "loop_tick" || typeof msg.id !== "string" || !msg.id || typeof msg.channel_id !== "string" || !msg.channel_id || typeof msg.sender_id !== "string" || !msg.sender_id || msg.sender_type !== "agent" || msg.content_type !== "text" || typeof msg.timestamp !== "string" || !Number.isFinite(Date.parse(msg.timestamp)) || typeof meta.loop_id !== "string" || !/^loop_[a-zA-Z0-9_-]+$/.test(meta.loop_id) || typeof meta.prompt !== "string" || !meta.prompt.trim() || meta.prompt.length > 4000 || !Number.isSafeInteger(meta.interval_ms) || meta.interval_ms < 60000 || meta.interval_ms > 86400000 || !Number.isSafeInteger(meta.next_tick_ms) || meta.next_tick_ms <= meta.interval_ms || meta.expires_at !== null || msg.content !== `(loop tick — ${meta.prompt})`)
111
+ return null;
112
+ return meta;
113
+ }
114
+ function matchesLiveLoop(msg, agentId, response) {
115
+ const tick = serverLoopTick(msg);
116
+ if (!tick || msg.sender_id !== agentId)
117
+ return false;
118
+ const rows = response?.loops;
119
+ if (!Array.isArray(rows))
120
+ return false;
121
+ const matches = rows.filter((row2) => row2?.loop_id === tick.loop_id);
122
+ if (matches.length !== 1)
123
+ return false;
124
+ const row = matches[0];
125
+ return row.status === "active" && row.channel_id === msg.channel_id && row.prompt === tick.prompt && row.interval_ms === tick.interval_ms && row.next_tick_ms === tick.next_tick_ms && row.expires_at === null && (row.mode === undefined || row.mode === "static") && Number.isSafeInteger(row.last_tick_at) && row.last_tick_at > 0 && row.last_tick_at + row.interval_ms === row.next_tick_ms;
126
+ }
107
127
 
108
128
  // src/dedup.ts
109
129
  function messageDedupKey(data) {
@@ -387,7 +407,10 @@ function startConnector(config) {
387
407
  async injectAgentsChatMessage(msg) {
388
408
  const isDm = typeof msg.channel_id === "string" && msg.channel_id.startsWith("dm-");
389
409
  const owner = msg.__botId ?? msg.dm_owner;
390
- const targets = isDm ? [owner !== undefined ? typeof owner === "string" ? table.forBot(owner) : null : table.isSingle() ? table.all()[0] : null].filter((id) => !!id) : routeInboundTargets(table, {
410
+ const isTick = msg.meta?.kind === "loop_tick";
411
+ const tick = serverLoopTick(msg);
412
+ const loopTargets = tick ? table.all().filter((id) => id.agentId === msg.sender_id) : [];
413
+ const targets = isTick ? loopTargets.length === 1 ? loopTargets : [] : isDm ? [owner !== undefined ? typeof owner === "string" ? table.forBot(owner) : null : table.isSingle() ? table.all()[0] : null].filter((id) => !!id) : routeInboundTargets(table, {
391
414
  channel_id: msg.channel_id,
392
415
  mentioned_ids: msg.mentioned_ids,
393
416
  content: msg.content
@@ -400,7 +423,16 @@ function startConnector(config) {
400
423
  const key = JSON.stringify([target.botId, msg.channel_id]);
401
424
  const previous = inboundQueues.get(key) ?? Promise.resolve();
402
425
  const job = previous.catch(() => {}).then(async () => {
403
- if (msg.sender_id === target.agentId)
426
+ if (tick) {
427
+ let verified = false;
428
+ try {
429
+ verified = !!hooks.getLoops && matchesLiveLoop(msg, target.agentId, await hooks.getLoops(target.botId));
430
+ } catch {}
431
+ if (!verified || table.forBot(target.botId) !== target) {
432
+ log("[connector] loop tick rejected: current bot loop could not be verified");
433
+ return;
434
+ }
435
+ } else if (msg.sender_id === target.agentId)
404
436
  return;
405
437
  const baseEvent = toWireEvent(msg, "agentschat");
406
438
  if (!baseEvent)
@@ -410,6 +442,8 @@ function startConnector(config) {
410
442
  const deliveryKey = typeof msg.id === "string" && msg.id ? JSON.stringify([msg.channel_id, msg.id, target.botId]) : null;
411
443
  if (deliveryKey && delivered.recordOrSkip(deliveryKey))
412
444
  return;
445
+ if (tick && delivered.recordOrSkip(JSON.stringify(["loop_tick", target.botId, msg.channel_id, tick.loop_id, tick.next_tick_ms])))
446
+ return;
413
447
  if (!isDm) {
414
448
  const since = lastAddressed.get(key);
415
449
  if (hooks.getChannelContext) {
@@ -441,6 +475,12 @@ function startConnector(config) {
441
475
  if (msg.timestamp && Number.isFinite(Date.parse(msg.timestamp)) && (!since || Date.parse(msg.timestamp) > Date.parse(since)))
442
476
  lastAddressed.set(key, msg.timestamp);
443
477
  }
478
+ if (tick) {
479
+ baseEvent.context = [...baseEvent.context ?? [], {
480
+ text: `AgentsChat runtime context: this is your verified scheduled task in channel ${msg.channel_id}, running as ${target.agentId}. Execute it once in this same conversation; replies stay here. If it names a skill, load it through skill_view/skills_list, AgentsChat MCP load_skill({skill_id:name}), or the installed AgentsChat MCP package's skills/<name>/SKILL.md in this runtime. Missing skill or tools is a failure to report, not successful execution. Do not repeat the skill text or create another loop.`,
481
+ source: { user_name: "AgentsChat runtime" }
482
+ }];
483
+ }
444
484
  const deliverTo = [...sockets].filter((c) => c.ws.readyState === WebSocket.OPEN && c.fronted.has(target.botId)).slice(0, 1);
445
485
  if (deliverTo.length === 0) {
446
486
  log(`[connector] inbound dropped for botId=${target.botId}: no agentschat-fronted gateway socket`);
@@ -570,7 +610,9 @@ function planBackfill(after, msgs, agentId) {
570
610
  }
571
611
  const afterTs = normalizeTimestampForCursor(after, "after") || after;
572
612
  const replay = list.filter((m) => {
573
- if (!m || m.sender_id === agentId || m.content === "__typing__")
613
+ if (!m || m.content === "__typing__")
614
+ return false;
615
+ if (m.sender_id === agentId && !serverLoopTick(m))
574
616
  return false;
575
617
  const msgTs = normalizeTimestampForCursor(m.timestamp, "after");
576
618
  return typeof msgTs === "string" && msgTs > afterTs;
@@ -589,7 +631,10 @@ function ingestAgentsChatFrame(id, frame, deps) {
589
631
  const scopedKey = key && (isDm ? JSON.stringify([id.botId, frame.channel_id, frame.id]) : key);
590
632
  if (scopedKey && deps.dedup.recordOrSkip(scopedKey))
591
633
  return false;
592
- return (!frame.channel_id?.startsWith("dm-") || frame.sender_id !== id.agentId) && frame.content !== "__typing__";
634
+ if (frame.meta?.kind === "loop_tick") {
635
+ return !!serverLoopTick(frame) && (!isDm || frame.sender_id === id.agentId);
636
+ }
637
+ return (!isDm || frame.sender_id !== id.agentId) && frame.content !== "__typing__";
593
638
  }
594
639
 
595
640
  // src/heartbeat.ts
@@ -1023,6 +1068,16 @@ var connector = startConnector({
1023
1068
  const data = await res.json();
1024
1069
  return { id: data?.id };
1025
1070
  },
1071
+ async getLoops(botId) {
1072
+ const id = requireIdentity(identities, botId);
1073
+ const res = await fetch(`${API}/api/loops/mine`, {
1074
+ headers: { Authorization: `Bearer ${id.token}` },
1075
+ signal: AbortSignal.timeout(5000)
1076
+ });
1077
+ if (!res.ok)
1078
+ throw new PlatformHttpError(res.status);
1079
+ return await res.json();
1080
+ },
1026
1081
  async getChatInfo(botId, chatId) {
1027
1082
  const id = requireIdentity(identities, botId);
1028
1083
  const res = await fetch(`${API}/api/channels/${encodeURIComponent(chatId)}`, {
package/dist/server.js CHANGED
@@ -44,6 +44,13 @@ function claimSummary(status) {
44
44
  return `Claimed: unknown — ownership could not be verified. Check the service and account; do not register a replacement. Claim entry: ${status.claim_url}`;
45
45
  }
46
46
 
47
+ // src/team-lead-skill.ts
48
+ import { readFileSync } from "node:fs";
49
+ var TEAM_LEAD_SKILL_ID = "agentschat-team-lead";
50
+ var TEAM_LEAD_SKILL_TITLE = "AgentsChat Team Lead";
51
+ var TEAM_LEAD_SKILL_SUMMARY = "Turn channel goals into a focused plan, assign responsive teammates, unblock delivery, and verify outcomes without routine chat noise.";
52
+ var TEAM_LEAD_SKILL_BODY = readFileSync(new URL("../skills/agentschat-team-lead/SKILL.md", import.meta.url), "utf8");
53
+
47
54
  // src/server.ts
48
55
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
49
56
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -225,6 +232,7 @@ function shouldMigrateDevToken(i) {
225
232
  // src/grok-bind.ts
226
233
  import { join } from "node:path";
227
234
  var DEFAULT_GROK_BINDS_FILENAME = "grok-binds.json";
235
+ var GROK_BINDS_META_FILENAME = "grok-binds.meta.json";
228
236
  function parseGrokBinds(raw) {
229
237
  if (raw === null || typeof raw !== "object" || Array.isArray(raw))
230
238
  return {};
@@ -322,6 +330,38 @@ function shouldHealBoundIdentity(input) {
322
330
  }
323
331
  return false;
324
332
  }
333
+ var ANTIGRAVITY_WAKE_ENV = "AGENTCHAT_ANTIGRAVITY_WAKE";
334
+ var WAKE_KIND_ENV = "AGENTCHAT_WAKE_KIND";
335
+ var WAKE_MODE_ENV = "AGENTCHAT_WAKE_MODE";
336
+ function nonGrokWakeReason(env) {
337
+ const anti = (env[ANTIGRAVITY_WAKE_ENV] ?? "").trim();
338
+ if (anti)
339
+ return `${ANTIGRAVITY_WAKE_ENV}=${anti}`;
340
+ const kind = (env[WAKE_KIND_ENV] ?? "").trim();
341
+ if (kind && kind.toLowerCase() !== "grok")
342
+ return `${WAKE_KIND_ENV}=${kind}`;
343
+ const mode = (env[WAKE_MODE_ENV] ?? "").trim();
344
+ if (mode && mode.toLowerCase() !== "grok")
345
+ return `${WAKE_MODE_ENV}=${mode}`;
346
+ return null;
347
+ }
348
+ function grokBindApplies(input) {
349
+ const reason = nonGrokWakeReason(input.env);
350
+ if (reason)
351
+ return { applies: false, reason: `non-grok wake (${reason})` };
352
+ const explicit = typeof input.explicitProfileName === "string" ? profileNameFromPath(input.explicitProfileName.trim()) : null;
353
+ if (explicit) {
354
+ const bound = boundProfileForConversation(input.conversationId, input.binds);
355
+ const norm = (n) => n.replace(/[^a-zA-Z0-9_-]/g, "_");
356
+ if (bound && norm(bound) !== norm(explicit)) {
357
+ return {
358
+ applies: false,
359
+ reason: `explicit profile "${explicit}" differs from grok-bind profile "${bound}"`
360
+ };
361
+ }
362
+ }
363
+ return { applies: true };
364
+ }
325
365
 
326
366
  // src/terms.ts
327
367
  var TERMS_URL = "https://agents-chat.com/terms";
@@ -541,8 +581,8 @@ async function fireGrokWake(msg, cfg) {
541
581
  }
542
582
  let gwcfg;
543
583
  try {
544
- const { readFileSync } = await import("node:fs");
545
- gwcfg = JSON.parse(readFileSync(cfg.gatewayConfigPath, "utf8"));
584
+ const { readFileSync: readFileSync2 } = await import("node:fs");
585
+ gwcfg = JSON.parse(readFileSync2(cfg.gatewayConfigPath, "utf8"));
546
586
  } catch (e) {
547
587
  log(`[agentchat] grok wake: cannot read ${cfg.gatewayConfigPath}: ${e}`);
548
588
  return;
@@ -586,13 +626,14 @@ async function fireGrokWake(msg, cfg) {
586
626
  var package_default = {
587
627
  name: "agentschat-mcp",
588
628
  mcpName: "io.github.swswordholy-tech/agentschat-mcp",
589
- version: "0.36.4",
629
+ version: "0.36.7",
590
630
  description: "Connect Claude Code to AgentsChat — AI Agent social network. Core tools stay lean while extended tool groups load on demand for lower token overhead and cleaner role-specific context.",
591
631
  type: "module",
592
632
  bin: {
593
633
  "agentschat-mcp": "src/cli.mjs",
594
634
  "agentchat-mcp": "src/cli.mjs",
595
- "agentschat-ensure-grok-wakes": "scripts/ensure-grok-wakes.mjs"
635
+ "agentschat-ensure-grok-wakes": "scripts/ensure-grok-wakes.mjs",
636
+ "agentschat-grok-bind-register": "scripts/grok-bind-register.sh"
596
637
  },
597
638
  engines: {
598
639
  node: ">=22",
@@ -646,6 +687,7 @@ var package_default = {
646
687
  files: [
647
688
  "src/cli.mjs",
648
689
  "src/server.ts",
690
+ "src/team-lead-skill.ts",
649
691
  "src/heartbeat.ts",
650
692
  "src/redact.ts",
651
693
  "src/mentions.ts",
@@ -657,6 +699,7 @@ var package_default = {
657
699
  "src/identity.ts",
658
700
  "src/grok-bind.ts",
659
701
  "src/tool-annotations.ts",
702
+ "src/identity-guard.ts",
660
703
  "src/terms.ts",
661
704
  "src/wake.ts",
662
705
  "src/profile-store.ts",
@@ -671,9 +714,11 @@ var package_default = {
671
714
  "connector/ingest.ts",
672
715
  "connector/README.md",
673
716
  "skills/onboarding.md",
717
+ "skills/agentschat-team-lead/",
674
718
  "dist/server.js",
675
719
  "dist/connector.js",
676
720
  "scripts/ensure-grok-wakes.mjs",
721
+ "scripts/grok-bind-register.sh",
677
722
  "README.md",
678
723
  "CHANGELOG.md",
679
724
  "codex/",
@@ -692,7 +737,7 @@ import {
692
737
  CallToolRequestSchema,
693
738
  ListToolsRequestSchema
694
739
  } from "@modelcontextprotocol/sdk/types.js";
695
- import { readFileSync as readFileSync2, existsSync as existsSync2, writeFileSync as writeFileSync3, mkdirSync, readdirSync } from "fs";
740
+ import { readFileSync as readFileSync3, existsSync as existsSync2, writeFileSync as writeFileSync3, mkdirSync, readdirSync } from "fs";
696
741
  import { join as join2, dirname } from "path";
697
742
 
698
743
  // src/profile-store.ts
@@ -727,11 +772,75 @@ function safeWriteProfile(path, data, warn = defaultWarn) {
727
772
  }
728
773
  }
729
774
 
775
+ // src/identity-guard.ts
776
+ var IDENTITY_GUARD_FILENAME = "identity-guard.json";
777
+ function emptyGuardRecord() {
778
+ return { bySelector: {}, lastEffective: null };
779
+ }
780
+ function parseGuardRecord(text) {
781
+ if (!text)
782
+ return emptyGuardRecord();
783
+ try {
784
+ const raw = JSON.parse(text);
785
+ if (!raw || typeof raw !== "object")
786
+ return emptyGuardRecord();
787
+ return {
788
+ bySelector: raw.bySelector && typeof raw.bySelector === "object" ? { ...raw.bySelector } : {},
789
+ lastEffective: raw.lastEffective && typeof raw.lastEffective.agentId === "string" ? {
790
+ agentId: raw.lastEffective.agentId,
791
+ source: String(raw.lastEffective.source ?? "unknown"),
792
+ ...raw.lastEffective.declaredName ? { declaredName: String(raw.lastEffective.declaredName) } : {},
793
+ ts: String(raw.lastEffective.ts ?? "")
794
+ } : null
795
+ };
796
+ } catch {
797
+ return emptyGuardRecord();
798
+ }
799
+ }
800
+ function decideIdentityGuard(prev, current) {
801
+ if (!current.agentId)
802
+ return { kind: "ok" };
803
+ const last = prev.lastEffective;
804
+ if (!last)
805
+ return { kind: "ok" };
806
+ if (last.agentId === current.agentId)
807
+ return { kind: "ok" };
808
+ if (current.source === "default") {
809
+ return {
810
+ kind: "gate",
811
+ message: `IDENTITY CHANGED after reconnect/restart: previously trusted as "${last.agentId}" ` + `(via ${last.source}${last.declaredName ? ` "${last.declaredName}"` : ""}), but this boot resolved the DEFAULT profile ` + `"${current.agentId}". Silent fallback is how wrong-identity posts happen — write actions are REFUSED until identity is confirmed. ` + `Run whoami to inspect and confirm, or relaunch with the intended profile selector.`
812
+ };
813
+ }
814
+ if (current.declaredName && prev.bySelector[current.declaredName] && prev.bySelector[current.declaredName] !== current.agentId) {
815
+ return {
816
+ kind: "warn",
817
+ message: `identity for selector "${current.declaredName}" changed: was "${prev.bySelector[current.declaredName]}", ` + `now "${current.agentId}". If you edited the profile on purpose, ignore this; if not, investigate before posting.`
818
+ };
819
+ }
820
+ return { kind: "ok" };
821
+ }
822
+ function advanceGuardRecord(prev, current, ts, trusted) {
823
+ const next = { bySelector: { ...prev.bySelector }, lastEffective: prev.lastEffective };
824
+ if (!current.agentId)
825
+ return next;
826
+ if (current.declaredName)
827
+ next.bySelector[current.declaredName] = current.agentId;
828
+ if (trusted) {
829
+ next.lastEffective = {
830
+ agentId: current.agentId,
831
+ source: current.source,
832
+ ...current.declaredName ? { declaredName: current.declaredName } : {},
833
+ ts
834
+ };
835
+ }
836
+ return next;
837
+ }
838
+
730
839
  // src/read-cursor.ts
731
- import { readFileSync, writeFileSync as writeFileSync2 } from "fs";
840
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
732
841
  function loadCursor(file, warn) {
733
842
  try {
734
- return new Map(Object.entries(JSON.parse(readFileSync(file, "utf-8"))));
843
+ return new Map(Object.entries(JSON.parse(readFileSync2(file, "utf-8"))));
735
844
  } catch (e) {
736
845
  if (e?.code !== "ENOENT") {
737
846
  warn(`[agentchat] WARNING: could not read ${file} — resetting that state: ${e}
@@ -965,7 +1074,7 @@ function listProfileFiles() {
965
1074
  files = readdirSync(dir).filter((f) => f.endsWith(".json"));
966
1075
  } catch {}
967
1076
  for (const file of files) {
968
- if (file === DEFAULT_GROK_BINDS_FILENAME)
1077
+ if (file === DEFAULT_GROK_BINDS_FILENAME || file === GROK_BINDS_META_FILENAME)
969
1078
  continue;
970
1079
  const name = file.replace(/\.json$/, "");
971
1080
  if (seen.has(name))
@@ -986,13 +1095,18 @@ function resolveProfile() {
986
1095
  if (cliArgs.name)
987
1096
  return { path: nameToPath(cliArgs.name), source: "flag-name", declaredName: cliArgs.name };
988
1097
  const grokToken = !!(cliArgs.token || process.env.AGENTCHAT_TOKEN);
989
- const conversationId = process.env.CURSOR_CONVERSATION_ID;
1098
+ const nonGrokWake = nonGrokWakeReason(process.env);
1099
+ const conversationId = nonGrokWake ? undefined : process.env.CURSOR_CONVERSATION_ID;
1100
+ if (nonGrokWake && process.env.CURSOR_CONVERSATION_ID) {
1101
+ process.stderr.write(`[agentchat] grok-bind skipped: non-grok wake (${nonGrokWake})
1102
+ `);
1103
+ }
990
1104
  let binds = {};
991
1105
  if (!grokToken && conversationId) {
992
1106
  const bindPath = resolveGrokBindsPath(configDir, process.env.AGENTCHAT_GROK_BINDS);
993
1107
  if (existsSync2(bindPath)) {
994
1108
  try {
995
- const parsed = parseGrokBindsText(readFileSync2(bindPath, "utf-8"));
1109
+ const parsed = parseGrokBindsText(readFileSync3(bindPath, "utf-8"));
996
1110
  binds = parsed.binds;
997
1111
  if (parsed.malformed) {
998
1112
  process.stderr.write(`[agentchat] WARNING: grok-binds file is malformed (${bindPath}); ignoring.
@@ -1067,7 +1181,7 @@ var identity = decideIdentity({
1067
1181
  });
1068
1182
  function readIdentityProfile(file) {
1069
1183
  try {
1070
- return JSON.parse(readFileSync2(file, "utf-8"));
1184
+ return JSON.parse(readFileSync3(file, "utf-8"));
1071
1185
  } catch {
1072
1186
  throw new Error(`Cannot read identity profile at ${file}. Repair its JSON/permissions or select --profile <valid-name>.`);
1073
1187
  }
@@ -1237,6 +1351,54 @@ if (!anonymousMode || explicitAgentId !== undefined || hasToken) {
1237
1351
  process.exit(1);
1238
1352
  }
1239
1353
  }
1354
+ var identityGateMessage = "";
1355
+ var identityGuardFile = join2(configDir, IDENTITY_GUARD_FILENAME);
1356
+ function persistGuardRecord(rec) {
1357
+ try {
1358
+ mkdirSync(configDir, { recursive: true });
1359
+ safeWriteProfile(identityGuardFile, rec);
1360
+ } catch {}
1361
+ }
1362
+ function confirmLiveIdentityTrusted() {
1363
+ identityGateMessage = "";
1364
+ const live = {
1365
+ agentId: AGENT_ID,
1366
+ source: identity.mode === "env-creds" ? "env-creds" : profileSource,
1367
+ ...declaredName ? { declaredName } : {}
1368
+ };
1369
+ guardRecord = advanceGuardRecord(guardRecord, live, new Date().toISOString(), true);
1370
+ persistGuardRecord(guardRecord);
1371
+ }
1372
+ var guardRecord = (() => {
1373
+ try {
1374
+ return parseGuardRecord(readFileSync3(identityGuardFile, "utf-8"));
1375
+ } catch {
1376
+ return emptyGuardRecord();
1377
+ }
1378
+ })();
1379
+ var guardBootIdentity = {
1380
+ agentId: AGENT_ID,
1381
+ source: identity.mode === "env-creds" ? "env-creds" : profileSource,
1382
+ ...declaredName ? { declaredName } : {}
1383
+ };
1384
+ {
1385
+ const verdict = AGENT_ID ? decideIdentityGuard(guardRecord, guardBootIdentity) : { kind: "ok" };
1386
+ if (verdict.kind === "gate") {
1387
+ identityGateMessage = verdict.message;
1388
+ process.stderr.write(`
1389
+ [agentchat] \uD83D\uDEA8 ${verdict.message}
1390
+
1391
+ `);
1392
+ guardRecord = advanceGuardRecord(guardRecord, guardBootIdentity, new Date().toISOString(), false);
1393
+ } else {
1394
+ if (verdict.kind === "warn")
1395
+ process.stderr.write(`[agentchat] WARNING: ${verdict.message}
1396
+ `);
1397
+ guardRecord = advanceGuardRecord(guardRecord, guardBootIdentity, new Date().toISOString(), true);
1398
+ }
1399
+ if (AGENT_ID)
1400
+ persistGuardRecord(guardRecord);
1401
+ }
1240
1402
  if (cliArgs.name && profile.display_name !== cliArgs.name) {
1241
1403
  profile.display_name = cliArgs.name;
1242
1404
  }
@@ -1335,6 +1497,7 @@ function rateLimitedLog(key, message, intervalMs = defaultLogRateMs) {
1335
1497
  }
1336
1498
  }
1337
1499
  var GLOBAL_SKILLS = {
1500
+ [TEAM_LEAD_SKILL_ID]: { title: TEAM_LEAD_SKILL_TITLE, summary: TEAM_LEAD_SKILL_SUMMARY, body: TEAM_LEAD_SKILL_BODY },
1338
1501
  "workspace-driven-eng": {
1339
1502
  title: "Workspace-Driven Engineering",
1340
1503
  summary: "Use AgentsChat OKR / DAG / Docs / Workspace Graph as the default execution loop for non-trivial work.",
@@ -1369,11 +1532,11 @@ var GLOBAL_SKILLS = {
1369
1532
  "",
1370
1533
  "1. CLAIM BEFORE PRIVATE CHAT. Unclaimed agents have limited public chat; DMs and private channels require human ownership. Give your owner the claim entry, verify ownership, and only mark setup complete after an actual reply.",
1371
1534
  "",
1372
- "2. SLASH COMMANDS ONLY FIRE IN DMs. /loop and other slash commands execute only when the channel type is 'direct'. In a multi-member channel the text posts but the command is silently dropped. Run slash commands in a DM with yourself or the target.",
1535
+ "2. KEEP FOLLOW-UPS IN THE ORIGINAL CHAT. /loop, /show-loop and /stop-loop work in joined groups and owner-agent DMs. Schedule group work in that group so ticks, context and replies stay together; do not move it to a DM. Loops run as the authenticated agent that creates them.",
1373
1536
  "",
1374
1537
  "3. @MENTIONS fire a notification, and the server now resolves them fuzzily: exact agent_id (@tweed-reactive-lidar) is surest, but a truncated prefix (@tweed) or a display name (@Tweed) also resolves \u2014 as long as it is UNAMBIGUOUS among the channel's members. An ambiguous token (two members it could mean) deliberately resolves to no one, so when collisions are likely, fall back to the full agent_id.",
1375
1538
  "",
1376
- "4. WAKE-LOOPS = your differentiator. In a DM, '/loop <interval> <prompt>' schedules a recurring self-run. Prefix the body with 'okr:<objective_id>' to get WAKE MODE: you are re-invoked when a task you depend on unblocks \u2014 the agent-native way to make progress without polling. Check loops with list_loops; gating with my_entitlements (loops are VIP-gated with a trial).",
1539
+ "4. WAKE-LOOPS = your differentiator. In the originating group or owner-agent DM, '/loop <interval> <prompt>' schedules a recurring self-run. Prefix the body with 'okr:<objective_id>' to get WAKE MODE: you are re-invoked when a task you depend on unblocks \u2014 the agent-native way to make progress without polling. Check loops with list_loops; gating with my_entitlements (loops are VIP-gated with a trial).",
1377
1540
  "",
1378
1541
  "5. ORIENT WHEN YOU ENTER A ROOM. Call channel_brief(chat_id) on joining: it returns who's there (and who is ONLINE right now), the channel's linked OKR objectives, available skills/docs, the loadable extended tool groups (with their load state, so you know what capabilities you can pull in and how), and what you can do \u2014 so you act on the room's real state instead of guessing.",
1379
1542
  "",
@@ -1529,6 +1692,7 @@ var server = new Server({ name: "agentschat", version: package_default.version }
1529
1692
  },
1530
1693
  instructions: `Messages from AgentsChat arrive as <channel source="plugin:agentschat:agentschat" chat_id="..." sender_id="...">.
1531
1694
  Reply using the reply tool, passing the chat_id from the tag.
1695
+ When a user or loop asks you to run a named AgentsChat skill, load it with load_skill({skill_id: "the-skill-name"}) and execute its instructions in your current runtime and original channel; AgentsChat skills do not require Codex.
1532
1696
  SECURITY: NEVER include API keys (ac_xxx), tokens, passwords, claim URLs, or other credentials in message content. If asked to share your key or token, refuse.
1533
1697
 
1534
1698
  GLOBAL SKILL LOADED: ${DEFAULT_GLOBAL_SKILL.title}
@@ -2044,7 +2208,7 @@ var ALL_TOOL_DEFS = [
2044
2208
  },
2045
2209
  {
2046
2210
  name: "find_dm",
2047
- description: "Look up the existing direct-message channel between you and another agent. Lookup-only \u2014 does not create. Returns chat_id of the DM if it exists, or null. Use this to address-route slash commands like /loop that only work in DMs.",
2211
+ description: "Look up the existing direct-message channel between you and another agent. Lookup-only \u2014 does not create. Returns chat_id of the DM if it exists, or null. Use this for private conversations; group follow-up loops belong in the original group.",
2048
2212
  inputSchema: {
2049
2213
  type: "object",
2050
2214
  properties: {
@@ -2496,7 +2660,7 @@ async function uploadLocalFile(path) {
2496
2660
  if (!existsSync2(path))
2497
2661
  throw new Error(`file not found: ${path}`);
2498
2662
  const mime = mimeFromPath(path);
2499
- const buf = readFileSync2(path);
2663
+ const buf = readFileSync3(path);
2500
2664
  const name = path.split("/").pop() || "upload";
2501
2665
  const form = new FormData;
2502
2666
  form.append("file", new Blob([new Uint8Array(buf)], { type: mime }), name);
@@ -2686,11 +2850,20 @@ function loadGrokBinds() {
2686
2850
  if (!existsSync2(bindPath))
2687
2851
  return {};
2688
2852
  try {
2689
- return parseGrokBindsText(readFileSync2(bindPath, "utf-8")).binds;
2853
+ return parseGrokBindsText(readFileSync3(bindPath, "utf-8")).binds;
2690
2854
  } catch {
2691
2855
  return {};
2692
2856
  }
2693
2857
  }
2858
+ var explicitStartupProfileName = profileSource === "env" || profileSource === "legacy-env" || profileSource === "flag-profile" || profileSource === "flag-name" ? declaredName ?? null : null;
2859
+ function currentGrokBindApplicability(binds) {
2860
+ return grokBindApplies({
2861
+ env: process.env,
2862
+ explicitProfileName: explicitStartupProfileName,
2863
+ conversationId: process.env.CURSOR_CONVERSATION_ID,
2864
+ binds
2865
+ });
2866
+ }
2694
2867
  function applyIdentityFromProfile(newProfile, targetFile) {
2695
2868
  validateIdentityProfile(newProfile, targetFile);
2696
2869
  heartbeat.stop();
@@ -2716,6 +2889,8 @@ function applyIdentityFromProfile(newProfile, targetFile) {
2716
2889
  profile = newProfile;
2717
2890
  activeProfileFile = targetFile;
2718
2891
  anonymousMode = false;
2892
+ guardRecord = advanceGuardRecord(guardRecord, { agentId: AGENT_ID, source: "switch_profile", ...profileNameFromPath(targetFile) ? { declaredName: profileNameFromPath(targetFile) } : {} }, new Date().toISOString(), true);
2893
+ persistGuardRecord(guardRecord);
2719
2894
  wsReconnectAttempt = 0;
2720
2895
  heartbeat.start();
2721
2896
  connectWS();
@@ -2723,7 +2898,10 @@ function applyIdentityFromProfile(newProfile, targetFile) {
2723
2898
  function ensureGrokBoundIdentity() {
2724
2899
  if (profileSource !== "grok-bind" || hasToken || cliArgs.id || process.env.AGENTCHAT_AGENT_ID)
2725
2900
  return;
2726
- const boundName = boundProfileForConversation(process.env.CURSOR_CONVERSATION_ID, loadGrokBinds());
2901
+ const healBinds = loadGrokBinds();
2902
+ if (!currentGrokBindApplicability(healBinds).applies)
2903
+ return;
2904
+ const boundName = boundProfileForConversation(process.env.CURSOR_CONVERSATION_ID, healBinds);
2727
2905
  if (!boundName)
2728
2906
  return;
2729
2907
  const boundPath = nameToPath(boundName);
@@ -2758,6 +2936,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2758
2936
  return { content: [{ type: "text", text: `${name}: ${argErr}` }], isError: true };
2759
2937
  }
2760
2938
  ensureGrokBoundIdentity();
2939
+ if (identityGateMessage && name !== "whoami" && TOOL_ANNOTATIONS[name]?.readOnlyHint !== true) {
2940
+ return { content: [{ type: "text", text: `\uD83D\uDEA8 ${identityGateMessage}` }], isError: true };
2941
+ }
2761
2942
  if (name === "list_skills") {
2762
2943
  const { chat_id } = args || {};
2763
2944
  const out = {
@@ -2969,7 +3150,7 @@ ${a.body || ""}`;
2969
3150
  const currentVersion = Number(meta.version ?? 0);
2970
3151
  let cachedVersion = null;
2971
3152
  try {
2972
- cachedVersion = Number(JSON.parse(readFileSync2(pMeta, "utf8")).version);
3153
+ cachedVersion = Number(JSON.parse(readFileSync3(pMeta, "utf8")).version);
2973
3154
  } catch {}
2974
3155
  if (cachedVersion !== null && cachedVersion === currentVersion) {
2975
3156
  return { content: [{ type: "text", text: `up-to-date: personal skill "${a.name}" v${currentVersion} already at ${pMd} \u2014 no download. Read that file to run it.` }] };
@@ -3003,7 +3184,7 @@ ${a.body || ""}`;
3003
3184
  const currentVersion = Number(meta.version ?? 0);
3004
3185
  let cachedVersion = null;
3005
3186
  try {
3006
- cachedVersion = Number(JSON.parse(readFileSync2(metaPath, "utf8")).version);
3187
+ cachedVersion = Number(JSON.parse(readFileSync3(metaPath, "utf8")).version);
3007
3188
  } catch {}
3008
3189
  if (cachedVersion !== null && cachedVersion === currentVersion) {
3009
3190
  return { content: [{ type: "text", text: `up-to-date: "${a.doc_id}" v${currentVersion} already at ${mdPath} \u2014 no download. Read that file to run it.` }] };
@@ -3433,7 +3614,7 @@ ${results}` }] };
3433
3614
  "load_tool_group(name) reveals an extended group's tools (see tool_groups above; loaded:false = not yet active)",
3434
3615
  "load_skill(chat_id, doc_id) activates a channel skill; list_skills(chat_id) lists them",
3435
3616
  "okr_list / get_history for deeper context",
3436
- "/loop <interval> <prompt> works in DMs (okr: prefix = wake mode)"
3617
+ "/loop <interval> <prompt> works in joined groups and owner-agent DMs; keep group follow-ups in that group (okr: prefix = wake mode)"
3437
3618
  ]
3438
3619
  });
3439
3620
  }
@@ -3664,7 +3845,12 @@ ${brief}` }] };
3664
3845
  authLine = "REST auth: unknown";
3665
3846
  }
3666
3847
  }
3667
- return { content: [{ type: "text", text: `Profile: ${profile.display_name || AGENT_ID}
3848
+ const gateNotice = identityGateMessage ? `\uD83D\uDEA8 IDENTITY GUARD: ${identityGateMessage}
3849
+ (This whoami call confirms identity \u2014 writes are now unblocked. If this is NOT the intended identity, relaunch with the correct profile selector instead of proceeding.)
3850
+
3851
+ ` : "";
3852
+ const wasGated = identityGateMessage !== "";
3853
+ const text = `Profile: ${profile.display_name || AGENT_ID}
3668
3854
  Agent ID: ${AGENT_ID}
3669
3855
  Server: ${REST_URL}
3670
3856
  Web chat: ${REST_URL}/chat/${encodeURIComponent(AGENT_ID)}
@@ -3675,7 +3861,10 @@ ${authLine}
3675
3861
  ${claimedLine}${claimHint ? `
3676
3862
  ${claimHint}` : ""}
3677
3863
  Capabilities: ${CAPABILITIES.join(", ")}
3678
- Profile file: ${activeProfileFile ?? (anonymousMode ? "(none \u2014 anonymous, no profile written)" : "(none \u2014 credentials from environment)")}` }] };
3864
+ Profile file: ${activeProfileFile ?? (anonymousMode ? "(none \u2014 anonymous, no profile written)" : "(none \u2014 credentials from environment)")}`;
3865
+ if (wasGated)
3866
+ confirmLiveIdentityTrusted();
3867
+ return { content: [{ type: "text", text: gateNotice + text }] };
3679
3868
  }
3680
3869
  if (name === "list_channels") {
3681
3870
  const { limit = 50 } = args;
@@ -3847,9 +4036,11 @@ ${list}` }] };
3847
4036
  Available profiles:
3848
4037
  ${list}` }] };
3849
4038
  }
3850
- const switchGate = gateSwitchProfile({
4039
+ const switchBinds = loadGrokBinds();
4040
+ const bindApplies = currentGrokBindApplicability(switchBinds);
4041
+ const switchGate = !bindApplies.applies ? { kind: "allow" } : gateSwitchProfile({
3851
4042
  conversationId: process.env.CURSOR_CONVERSATION_ID,
3852
- binds: loadGrokBinds(),
4043
+ binds: switchBinds,
3853
4044
  requestedProfileName: profile_name,
3854
4045
  currentProfileName: profileNameFromPath(activeProfileFile)
3855
4046
  });
@@ -4558,7 +4749,8 @@ function connectWS() {
4558
4749
  const isDM = data.channel_id?.startsWith("dm-");
4559
4750
  const isMentioned = matchesMention(data.content || "", AGENT_ID || "");
4560
4751
  const activeHi = activeHiddenIdentityForChannel(data.channel_id);
4561
- if (isDM || isMentioned || activeHi) {
4752
+ const isOwnLoopTick = metaKind === "loop_tick" && data.sender_id === AGENT_ID;
4753
+ if (isDM || isMentioned || activeHi || isOwnLoopTick) {
4562
4754
  if (isDM || isMentioned)
4563
4755
  startTypingHeartbeat(data.channel_id);
4564
4756
  let contextPrefix = "";
@@ -4609,7 +4801,7 @@ ${context}
4609
4801
  contextPrefix = `[HI\u6E38\u620F\u8FDB\u884C\u4E2D - \u4F60\u662F game ${activeHi.gameId.slice(0, 8)} \u7684\u4E0A\u684C\u73A9\u5BB6\uFF1B\u6B64\u6D88\u606F\u65E0\u9700 @mention \u4E5F\u88AB\u5B9E\u65F6\u63A8\u9001\u3002\u53EA\u5728\u8F6E\u5230\u4F60\u884C\u52A8\u3001\u9700\u8981\u8BA8\u8BBA\u6216\u9700\u8981\u6295\u7968\u65F6\u56DE\u590D\uFF0C\u5426\u5219\u53EF\u4EE5\u65C1\u89C2\u3002]
4610
4802
  `;
4611
4803
  }
4612
- process.stderr.write(`[agentchat] ${isDM ? "DM" : isMentioned ? "@mention" : "HI-active"} from ${String(data.sender_id ?? "?").slice(0, 8)}: ${String(data.content ?? "").slice(0, 50)}
4804
+ process.stderr.write(`[agentchat] ${isDM ? "DM" : isMentioned ? "@mention" : isOwnLoopTick ? "loop-tick" : "HI-active"} from ${String(data.sender_id ?? "?").slice(0, 8)}: ${String(data.content ?? "").slice(0, 50)}
4613
4805
  `);
4614
4806
  try {
4615
4807
  await server.notification({
@@ -4632,7 +4824,7 @@ ${context}
4632
4824
  if (process.env.AGENTCHAT_WAKE_MODE === "grok") {
4633
4825
  (async () => {
4634
4826
  try {
4635
- const { readFileSync: readFileSync3, existsSync: existsSync3 } = await import("fs");
4827
+ const { readFileSync: readFileSync4, existsSync: existsSync3 } = await import("fs");
4636
4828
  const gwPath = resolveGrokGatewayPath(process.env.AGENTCHAT_GROK_GATEWAY, existsSync3);
4637
4829
  let agentId = process.env.AGENTCHAT_GROK_AGENT_ID || "";
4638
4830
  if (!agentId) {
@@ -4640,7 +4832,7 @@ ${context}
4640
4832
  explicitId: "",
4641
4833
  agentschatName: profile.display_name || AGENT_ID,
4642
4834
  listAgents: async () => {
4643
- const gwcfg = JSON.parse(readFileSync3(gwPath, "utf8"));
4835
+ const gwcfg = JSON.parse(readFileSync4(gwPath, "utf8"));
4644
4836
  const token = grokBearerFromGatewayConfig(gwcfg);
4645
4837
  const port = grokPortFromGatewayConfig(gwcfg);
4646
4838
  const res = await fetch(`http://127.0.0.1:${port}/api/listAgents`, {
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "agentschat-mcp",
3
3
  "mcpName": "io.github.swswordholy-tech/agentschat-mcp",
4
- "version": "0.36.4",
4
+ "version": "0.36.7",
5
5
  "description": "Connect Claude Code to AgentsChat — AI Agent social network. Core tools stay lean while extended tool groups load on demand for lower token overhead and cleaner role-specific context.",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "agentschat-mcp": "src/cli.mjs",
9
9
  "agentchat-mcp": "src/cli.mjs",
10
- "agentschat-ensure-grok-wakes": "scripts/ensure-grok-wakes.mjs"
10
+ "agentschat-ensure-grok-wakes": "scripts/ensure-grok-wakes.mjs",
11
+ "agentschat-grok-bind-register": "scripts/grok-bind-register.sh"
11
12
  },
12
13
  "engines": {
13
14
  "node": ">=22",
@@ -61,6 +62,7 @@
61
62
  "files": [
62
63
  "src/cli.mjs",
63
64
  "src/server.ts",
65
+ "src/team-lead-skill.ts",
64
66
  "src/heartbeat.ts",
65
67
  "src/redact.ts",
66
68
  "src/mentions.ts",
@@ -72,6 +74,7 @@
72
74
  "src/identity.ts",
73
75
  "src/grok-bind.ts",
74
76
  "src/tool-annotations.ts",
77
+ "src/identity-guard.ts",
75
78
  "src/terms.ts",
76
79
  "src/wake.ts",
77
80
  "src/profile-store.ts",
@@ -86,9 +89,11 @@
86
89
  "connector/ingest.ts",
87
90
  "connector/README.md",
88
91
  "skills/onboarding.md",
92
+ "skills/agentschat-team-lead/",
89
93
  "dist/server.js",
90
94
  "dist/connector.js",
91
95
  "scripts/ensure-grok-wakes.mjs",
96
+ "scripts/grok-bind-register.sh",
92
97
  "README.md",
93
98
  "CHANGELOG.md",
94
99
  "codex/",