agentschat-mcp 0.33.3 → 0.33.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/README.md +50 -1
- package/connector/README.md +1 -0
- package/connector/backfill.ts +41 -0
- package/connector/ingest.ts +38 -0
- package/connector/run.ts +78 -11
- package/dist/connector.js +151 -11
- package/dist/server.js +229 -34
- package/package.json +5 -2
- package/src/grok-bind.ts +193 -0
- package/src/identity.ts +4 -1
- package/src/server.ts +148 -30
package/dist/server.js
CHANGED
|
@@ -175,6 +175,107 @@ function shouldMigrateDevToken(i) {
|
|
|
175
175
|
return i.source !== "default";
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
// src/grok-bind.ts
|
|
179
|
+
import { join } from "node:path";
|
|
180
|
+
var DEFAULT_GROK_BINDS_FILENAME = "grok-binds.json";
|
|
181
|
+
function parseGrokBinds(raw) {
|
|
182
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
183
|
+
return {};
|
|
184
|
+
const out = {};
|
|
185
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
186
|
+
if (typeof v !== "string")
|
|
187
|
+
continue;
|
|
188
|
+
const name = v.trim();
|
|
189
|
+
if (!name)
|
|
190
|
+
continue;
|
|
191
|
+
const key = k.trim();
|
|
192
|
+
if (!key)
|
|
193
|
+
continue;
|
|
194
|
+
out[key] = name;
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
function parseGrokBindsText(text) {
|
|
199
|
+
try {
|
|
200
|
+
return { binds: parseGrokBinds(JSON.parse(text)), malformed: false };
|
|
201
|
+
} catch {
|
|
202
|
+
return { binds: {}, malformed: true };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function resolveGrokBindsPath(configDir, envOverride) {
|
|
206
|
+
if (envOverride && envOverride.length > 0)
|
|
207
|
+
return envOverride;
|
|
208
|
+
return join(configDir, DEFAULT_GROK_BINDS_FILENAME);
|
|
209
|
+
}
|
|
210
|
+
function decideGrokBind(input) {
|
|
211
|
+
if (input.explicitIdentity || input.hasToken)
|
|
212
|
+
return { kind: "skip" };
|
|
213
|
+
const id = input.conversationId?.trim();
|
|
214
|
+
if (!id)
|
|
215
|
+
return { kind: "skip" };
|
|
216
|
+
const profileName = input.binds[id];
|
|
217
|
+
if (typeof profileName === "string" && profileName.length > 0) {
|
|
218
|
+
return { kind: "hit", conversationId: id, profileName };
|
|
219
|
+
}
|
|
220
|
+
return { kind: "miss", conversationId: id };
|
|
221
|
+
}
|
|
222
|
+
function boundProfileForConversation(conversationId, binds) {
|
|
223
|
+
const id = conversationId?.trim();
|
|
224
|
+
if (!id)
|
|
225
|
+
return null;
|
|
226
|
+
const name = binds[id];
|
|
227
|
+
return typeof name === "string" && name.length > 0 ? name : null;
|
|
228
|
+
}
|
|
229
|
+
function profileNameFromPath(profilePath) {
|
|
230
|
+
if (!profilePath)
|
|
231
|
+
return null;
|
|
232
|
+
const base = profilePath.replace(/\\/g, "/").split("/").pop() || "";
|
|
233
|
+
const name = base.replace(/\.json$/i, "");
|
|
234
|
+
return name || null;
|
|
235
|
+
}
|
|
236
|
+
function gateSwitchProfile(input) {
|
|
237
|
+
const req = typeof input.requestedProfileName === "string" ? input.requestedProfileName.trim() : "";
|
|
238
|
+
if (!req)
|
|
239
|
+
return { kind: "allow" };
|
|
240
|
+
const bindValues = Object.values(input.binds).filter((v) => typeof v === "string" && v.length > 0);
|
|
241
|
+
if (bindValues.length === 0)
|
|
242
|
+
return { kind: "allow" };
|
|
243
|
+
const lockedMessage = (lockedName) => `Outbound identity is locked to grok-bind profile "${lockedName}" on this shared MCP; ` + `use a separate MCP process / wake daemon, do not switch_profile.`;
|
|
244
|
+
const bound = boundProfileForConversation(input.conversationId, input.binds);
|
|
245
|
+
if (bound) {
|
|
246
|
+
if (req === bound)
|
|
247
|
+
return { kind: "allow" };
|
|
248
|
+
return {
|
|
249
|
+
kind: "locked",
|
|
250
|
+
boundProfileName: bound,
|
|
251
|
+
requestedProfileName: req,
|
|
252
|
+
message: lockedMessage(bound)
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const current = typeof input.currentProfileName === "string" ? input.currentProfileName.trim() : "";
|
|
256
|
+
if (current && bindValues.includes(current)) {
|
|
257
|
+
if (req === current)
|
|
258
|
+
return { kind: "allow" };
|
|
259
|
+
return {
|
|
260
|
+
kind: "locked",
|
|
261
|
+
boundProfileName: current,
|
|
262
|
+
requestedProfileName: req,
|
|
263
|
+
message: lockedMessage(current)
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return { kind: "allow" };
|
|
267
|
+
}
|
|
268
|
+
function shouldHealBoundIdentity(input) {
|
|
269
|
+
if (!input.boundProfileName)
|
|
270
|
+
return false;
|
|
271
|
+
if (input.liveProfileName !== input.boundProfileName)
|
|
272
|
+
return true;
|
|
273
|
+
if (typeof input.boundAgentId === "string" && input.boundAgentId.length > 0 && input.liveAgentId !== input.boundAgentId) {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
178
279
|
// src/terms.ts
|
|
179
280
|
var TERMS_URL = "https://agents-chat.com/terms";
|
|
180
281
|
var TERMS_VERSION = "2026-05-29";
|
|
@@ -361,7 +462,7 @@ async function fireGrokWake(msg, cfg) {
|
|
|
361
462
|
var package_default = {
|
|
362
463
|
name: "agentschat-mcp",
|
|
363
464
|
mcpName: "io.github.swswordholy-tech/agentschat-mcp",
|
|
364
|
-
version: "0.33.
|
|
465
|
+
version: "0.33.7",
|
|
365
466
|
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.",
|
|
366
467
|
type: "module",
|
|
367
468
|
bin: {
|
|
@@ -427,6 +528,7 @@ var package_default = {
|
|
|
427
528
|
"src/timestamps.ts",
|
|
428
529
|
"src/argcheck.ts",
|
|
429
530
|
"src/identity.ts",
|
|
531
|
+
"src/grok-bind.ts",
|
|
430
532
|
"src/terms.ts",
|
|
431
533
|
"src/wake.ts",
|
|
432
534
|
"src/profile-store.ts",
|
|
@@ -437,6 +539,8 @@ var package_default = {
|
|
|
437
539
|
"connector/identities.ts",
|
|
438
540
|
"connector/server.ts",
|
|
439
541
|
"connector/run.ts",
|
|
542
|
+
"connector/backfill.ts",
|
|
543
|
+
"connector/ingest.ts",
|
|
440
544
|
"connector/README.md",
|
|
441
545
|
"skills/onboarding.md",
|
|
442
546
|
"dist/server.js",
|
|
@@ -451,7 +555,7 @@ import {
|
|
|
451
555
|
ListToolsRequestSchema
|
|
452
556
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
453
557
|
import { readFileSync as readFileSync2, existsSync as existsSync2, writeFileSync as writeFileSync3, mkdirSync, readdirSync } from "fs";
|
|
454
|
-
import { join, dirname } from "path";
|
|
558
|
+
import { join as join2, dirname } from "path";
|
|
455
559
|
|
|
456
560
|
// src/profile-store.ts
|
|
457
561
|
import { existsSync, writeFileSync, renameSync, chmodSync, unlinkSync, statSync } from "fs";
|
|
@@ -653,6 +757,13 @@ Wake a host that has no channel-notification surface (Grok Bot, generic MCP clie
|
|
|
653
757
|
~/.grok/gateway.json, /home/box/sand-data/gateway.json)
|
|
654
758
|
AGENTCHAT_GROK_AGENT_ID the Grok gateway agent uuid to wake (1:1 binding)
|
|
655
759
|
|
|
760
|
+
Grok multi-bot identity bind (Cursor / Grok Bot, no --profile):
|
|
761
|
+
~/.agentschat/grok-binds.json maps CURSOR_CONVERSATION_ID (Grok uuid) \u2192 profile name.
|
|
762
|
+
AGENTCHAT_GROK_BINDS overrides that path. Profile names resolve like --profile.
|
|
763
|
+
Auto-bind does NOT set AGENTCHAT_WAKE_MODE \u2014 a Cursor-tool MCP should unset it
|
|
764
|
+
(per-identity wake daemons already POST sendPrompt). If the operator set
|
|
765
|
+
WAKE_MODE, it is left as-is.
|
|
766
|
+
|
|
656
767
|
Hermes relay connector (no Hermes patch): run with --connector. See --connector --help.
|
|
657
768
|
|
|
658
769
|
Identity is never created implicitly: with no --name/--profile/AGENTSCHAT_PROFILE and
|
|
@@ -664,14 +775,14 @@ Docs: https://github.com/swswordholy-tech/AgentsChatProtocol`);
|
|
|
664
775
|
}
|
|
665
776
|
var cliArgs = parseArgs();
|
|
666
777
|
var homeDir = process.env.HOME || process.env.USERPROFILE || ".";
|
|
667
|
-
var configDir =
|
|
668
|
-
var legacyConfigDir =
|
|
778
|
+
var configDir = join2(homeDir, ".agentschat");
|
|
779
|
+
var legacyConfigDir = join2(homeDir, ".agentchat");
|
|
669
780
|
var profileDirs = [configDir, legacyConfigDir];
|
|
670
781
|
function profileNameToPaths(name) {
|
|
671
782
|
if (name.includes("/") || name.includes("\\"))
|
|
672
783
|
return [name];
|
|
673
784
|
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
674
|
-
return profileDirs.map((dir) =>
|
|
785
|
+
return profileDirs.map((dir) => join2(dir, `${safeName}.json`));
|
|
675
786
|
}
|
|
676
787
|
function nameToPath(name) {
|
|
677
788
|
const candidates = profileNameToPaths(name);
|
|
@@ -686,11 +797,13 @@ function listProfileFiles() {
|
|
|
686
797
|
files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
687
798
|
} catch {}
|
|
688
799
|
for (const file of files) {
|
|
800
|
+
if (file === DEFAULT_GROK_BINDS_FILENAME)
|
|
801
|
+
continue;
|
|
689
802
|
const name = file.replace(/\.json$/, "");
|
|
690
803
|
if (seen.has(name))
|
|
691
804
|
continue;
|
|
692
805
|
seen.add(name);
|
|
693
|
-
profiles.push({ name, path:
|
|
806
|
+
profiles.push({ name, path: join2(dir, file) });
|
|
694
807
|
}
|
|
695
808
|
}
|
|
696
809
|
return profiles;
|
|
@@ -704,6 +817,39 @@ function resolveProfile() {
|
|
|
704
817
|
return { path: nameToPath(cliArgs.profile), source: "flag-profile", declaredName: cliArgs.profile };
|
|
705
818
|
if (cliArgs.name)
|
|
706
819
|
return { path: nameToPath(cliArgs.name), source: "flag-name", declaredName: cliArgs.name };
|
|
820
|
+
const grokToken = !!(cliArgs.token || process.env.AGENTCHAT_TOKEN);
|
|
821
|
+
const conversationId = process.env.CURSOR_CONVERSATION_ID;
|
|
822
|
+
let binds = {};
|
|
823
|
+
if (!grokToken && conversationId) {
|
|
824
|
+
const bindPath = resolveGrokBindsPath(configDir, process.env.AGENTCHAT_GROK_BINDS);
|
|
825
|
+
if (existsSync2(bindPath)) {
|
|
826
|
+
try {
|
|
827
|
+
const parsed = parseGrokBindsText(readFileSync2(bindPath, "utf-8"));
|
|
828
|
+
binds = parsed.binds;
|
|
829
|
+
if (parsed.malformed) {
|
|
830
|
+
process.stderr.write(`[agentchat] WARNING: grok-binds file is malformed (${bindPath}); ignoring.
|
|
831
|
+
`);
|
|
832
|
+
}
|
|
833
|
+
} catch {
|
|
834
|
+
binds = {};
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
const grok = decideGrokBind({
|
|
839
|
+
explicitIdentity: false,
|
|
840
|
+
hasToken: grokToken,
|
|
841
|
+
conversationId,
|
|
842
|
+
binds
|
|
843
|
+
});
|
|
844
|
+
if (grok.kind === "hit") {
|
|
845
|
+
process.stderr.write(`[agentchat] grok-bind: ${grok.conversationId} \u2192 profile "${grok.profileName}"
|
|
846
|
+
`);
|
|
847
|
+
return { path: nameToPath(grok.profileName), source: "grok-bind", declaredName: grok.profileName };
|
|
848
|
+
}
|
|
849
|
+
if (grok.kind === "miss") {
|
|
850
|
+
process.stderr.write(`[agentchat] no grok-bind matched CURSOR_CONVERSATION_ID=${grok.conversationId}
|
|
851
|
+
`);
|
|
852
|
+
}
|
|
707
853
|
return { path: nameToPath("profile"), source: "default" };
|
|
708
854
|
}
|
|
709
855
|
var { path: profileFile, source: profileSource, declaredName } = resolveProfile();
|
|
@@ -2339,6 +2485,70 @@ HANDLERS.set("transcribe", async (args) => {
|
|
|
2339
2485
|
return { content: [{ type: "text", text: `Error transcribing: ${String(e?.message || e).slice(0, 120)}` }], isError: true };
|
|
2340
2486
|
}
|
|
2341
2487
|
});
|
|
2488
|
+
function loadGrokBinds() {
|
|
2489
|
+
const bindPath = resolveGrokBindsPath(configDir, process.env.AGENTCHAT_GROK_BINDS);
|
|
2490
|
+
if (!existsSync2(bindPath))
|
|
2491
|
+
return {};
|
|
2492
|
+
try {
|
|
2493
|
+
return parseGrokBindsText(readFileSync2(bindPath, "utf-8")).binds;
|
|
2494
|
+
} catch {
|
|
2495
|
+
return {};
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
function applyIdentityFromProfile(newProfile, targetFile) {
|
|
2499
|
+
heartbeat.stop();
|
|
2500
|
+
if (backfillTimer) {
|
|
2501
|
+
clearTimeout(backfillTimer);
|
|
2502
|
+
backfillTimer = null;
|
|
2503
|
+
}
|
|
2504
|
+
if (reconnectTimer) {
|
|
2505
|
+
clearTimeout(reconnectTimer);
|
|
2506
|
+
reconnectTimer = null;
|
|
2507
|
+
}
|
|
2508
|
+
if (ws) {
|
|
2509
|
+
ws.onclose = null;
|
|
2510
|
+
try {
|
|
2511
|
+
ws.close();
|
|
2512
|
+
} catch {}
|
|
2513
|
+
ws = null;
|
|
2514
|
+
}
|
|
2515
|
+
sessionId = null;
|
|
2516
|
+
AGENT_ID = newProfile.agent_id;
|
|
2517
|
+
TOKEN = newProfile.token || "dev-token";
|
|
2518
|
+
CAPABILITIES = newProfile.capabilities || ["claude-code", "coding", "chat"];
|
|
2519
|
+
profile = newProfile;
|
|
2520
|
+
activeProfileFile = targetFile;
|
|
2521
|
+
anonymousMode = false;
|
|
2522
|
+
wsReconnectAttempt = 0;
|
|
2523
|
+
heartbeat.start();
|
|
2524
|
+
connectWS();
|
|
2525
|
+
}
|
|
2526
|
+
function ensureGrokBoundIdentity() {
|
|
2527
|
+
const boundName = boundProfileForConversation(process.env.CURSOR_CONVERSATION_ID, loadGrokBinds());
|
|
2528
|
+
if (!boundName)
|
|
2529
|
+
return;
|
|
2530
|
+
const boundPath = nameToPath(boundName);
|
|
2531
|
+
if (!existsSync2(boundPath))
|
|
2532
|
+
return;
|
|
2533
|
+
let boundProfile;
|
|
2534
|
+
try {
|
|
2535
|
+
boundProfile = JSON.parse(readFileSync2(boundPath, "utf-8"));
|
|
2536
|
+
} catch {
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
const liveName = profileNameFromPath(activeProfileFile);
|
|
2540
|
+
if (!shouldHealBoundIdentity({
|
|
2541
|
+
boundProfileName: boundName,
|
|
2542
|
+
liveProfileName: liveName,
|
|
2543
|
+
liveAgentId: AGENT_ID,
|
|
2544
|
+
boundAgentId: boundProfile?.agent_id
|
|
2545
|
+
})) {
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
process.stderr.write(`[agentchat] grok-bind heal: live profile=${liveName ?? "?"} agent=${AGENT_ID || "?"} \u2192 bound "${boundName}" (${boundProfile.agent_id || "?"})
|
|
2549
|
+
`);
|
|
2550
|
+
applyIdentityFromProfile(boundProfile, boundPath);
|
|
2551
|
+
}
|
|
2342
2552
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2343
2553
|
let { name, arguments: args } = request.params;
|
|
2344
2554
|
let viaExtendedCompat = false;
|
|
@@ -2348,6 +2558,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2348
2558
|
if (argErr)
|
|
2349
2559
|
return { content: [{ type: "text", text: `${name}: ${argErr}` }], isError: true };
|
|
2350
2560
|
}
|
|
2561
|
+
ensureGrokBoundIdentity();
|
|
2351
2562
|
if (name === "list_skills") {
|
|
2352
2563
|
const { chat_id } = args || {};
|
|
2353
2564
|
const out = {
|
|
@@ -3444,37 +3655,21 @@ ${list}` }] };
|
|
|
3444
3655
|
Available profiles:
|
|
3445
3656
|
${list}` }] };
|
|
3446
3657
|
}
|
|
3658
|
+
const switchGate = gateSwitchProfile({
|
|
3659
|
+
conversationId: process.env.CURSOR_CONVERSATION_ID,
|
|
3660
|
+
binds: loadGrokBinds(),
|
|
3661
|
+
requestedProfileName: profile_name,
|
|
3662
|
+
currentProfileName: profileNameFromPath(activeProfileFile)
|
|
3663
|
+
});
|
|
3664
|
+
if (switchGate.kind === "locked") {
|
|
3665
|
+
return { content: [{ type: "text", text: switchGate.message }], isError: true };
|
|
3666
|
+
}
|
|
3447
3667
|
const targetFile = nameToPath(profile_name);
|
|
3448
3668
|
if (!existsSync2(targetFile)) {
|
|
3449
3669
|
return { content: [{ type: "text", text: `Profile "${profile_name}" not found. Available: ${available.join(", ")}` }], isError: true };
|
|
3450
3670
|
}
|
|
3451
3671
|
const newProfile = JSON.parse(readFileSync2(targetFile, "utf-8"));
|
|
3452
|
-
|
|
3453
|
-
if (backfillTimer) {
|
|
3454
|
-
clearTimeout(backfillTimer);
|
|
3455
|
-
backfillTimer = null;
|
|
3456
|
-
}
|
|
3457
|
-
if (reconnectTimer) {
|
|
3458
|
-
clearTimeout(reconnectTimer);
|
|
3459
|
-
reconnectTimer = null;
|
|
3460
|
-
}
|
|
3461
|
-
if (ws) {
|
|
3462
|
-
ws.onclose = null;
|
|
3463
|
-
try {
|
|
3464
|
-
ws.close();
|
|
3465
|
-
} catch {}
|
|
3466
|
-
ws = null;
|
|
3467
|
-
}
|
|
3468
|
-
sessionId = null;
|
|
3469
|
-
AGENT_ID = newProfile.agent_id;
|
|
3470
|
-
TOKEN = newProfile.token || "dev-token";
|
|
3471
|
-
CAPABILITIES = newProfile.capabilities || ["claude-code", "coding", "chat"];
|
|
3472
|
-
profile = newProfile;
|
|
3473
|
-
activeProfileFile = targetFile;
|
|
3474
|
-
anonymousMode = false;
|
|
3475
|
-
wsReconnectAttempt = 0;
|
|
3476
|
-
heartbeat.start();
|
|
3477
|
-
connectWS();
|
|
3672
|
+
applyIdentityFromProfile(newProfile, targetFile);
|
|
3478
3673
|
return { content: [{ type: "text", text: `Switched to profile "${profile_name}" (${AGENT_ID}). Reconnecting...` }] };
|
|
3479
3674
|
}
|
|
3480
3675
|
if (name === "list_channel_docs") {
|
|
@@ -3824,7 +4019,7 @@ ${list}` }] };
|
|
|
3824
4019
|
return { content: [{ type: "text", text: `${name} failed: ${String(e?.message || e).slice(0, 300)}` }], isError: true };
|
|
3825
4020
|
}
|
|
3826
4021
|
});
|
|
3827
|
-
var mentionTsFile =
|
|
4022
|
+
var mentionTsFile = join2(configDir, `mention-ts-${AGENT_ID}.json`);
|
|
3828
4023
|
function loadMentionTimestamps() {
|
|
3829
4024
|
return loadCursor(mentionTsFile, safeStderrWrite);
|
|
3830
4025
|
}
|
|
@@ -3832,7 +4027,7 @@ function saveMentionTimestamps(m) {
|
|
|
3832
4027
|
persistCursor(mentionTsFile, m, safeStderrWrite);
|
|
3833
4028
|
}
|
|
3834
4029
|
var lastMentionTimestamp = loadMentionTimestamps();
|
|
3835
|
-
var lastSeenMessageTsFile =
|
|
4030
|
+
var lastSeenMessageTsFile = join2(configDir, `last-seen-msg-ts-${AGENT_ID}.json`);
|
|
3836
4031
|
function loadLastSeenMessageTs() {
|
|
3837
4032
|
return loadCursor(lastSeenMessageTsFile, safeStderrWrite);
|
|
3838
4033
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentschat-mcp",
|
|
3
3
|
"mcpName": "io.github.swswordholy-tech/agentschat-mcp",
|
|
4
|
-
"version": "0.33.
|
|
5
|
-
"description": "Connect Claude Code to AgentsChat
|
|
4
|
+
"version": "0.33.7",
|
|
5
|
+
"description": "Connect Claude Code to AgentsChat \u2014 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",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"src/timestamps.ts",
|
|
68
68
|
"src/argcheck.ts",
|
|
69
69
|
"src/identity.ts",
|
|
70
|
+
"src/grok-bind.ts",
|
|
70
71
|
"src/terms.ts",
|
|
71
72
|
"src/wake.ts",
|
|
72
73
|
"src/profile-store.ts",
|
|
@@ -77,6 +78,8 @@
|
|
|
77
78
|
"connector/identities.ts",
|
|
78
79
|
"connector/server.ts",
|
|
79
80
|
"connector/run.ts",
|
|
81
|
+
"connector/backfill.ts",
|
|
82
|
+
"connector/ingest.ts",
|
|
80
83
|
"connector/README.md",
|
|
81
84
|
"skills/onboarding.md",
|
|
82
85
|
"dist/server.js",
|
package/src/grok-bind.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok outbound identity bind — map CURSOR_CONVERSATION_ID (the Grok agent uuid)
|
|
3
|
+
* to an AgentsChat profile name.
|
|
4
|
+
*
|
|
5
|
+
* Pure: parse the bind map and decide whether it applies. File I/O and
|
|
6
|
+
* `nameToPath` live at the call site (`resolveProfile` in server.ts) so this
|
|
7
|
+
* stays unit-testable.
|
|
8
|
+
*
|
|
9
|
+
* Auto-bind does NOT imply AGENTCHAT_WAKE_MODE. A Cursor-tool MCP should unset
|
|
10
|
+
* WAKE_MODE because per-identity wake daemons already POST sendPrompt. If the
|
|
11
|
+
* operator set WAKE_MODE, leave it — this module never reads or writes it.
|
|
12
|
+
*
|
|
13
|
+
* Shared Cursor MCP safety: when a conversation id is bound OR the live profile
|
|
14
|
+
* is already a grok-bound identity (e.g. Cursor stdio started with `--profile Jack`
|
|
15
|
+
* and no CURSOR_CONVERSATION_ID), `switch_profile` must not steal the live
|
|
16
|
+
* identity (gateSwitchProfile). Outbound mutators should heal back to the bound
|
|
17
|
+
* profile if something already switched (shouldHealBoundIdentity).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_GROK_BINDS_FILENAME = "grok-binds.json";
|
|
23
|
+
export const GROK_BINDS_ENV = "AGENTCHAT_GROK_BINDS";
|
|
24
|
+
export const CURSOR_CONVERSATION_ENV = "CURSOR_CONVERSATION_ID";
|
|
25
|
+
|
|
26
|
+
/** Clear error returned when switch_profile would leave a grok-bound identity. */
|
|
27
|
+
export const GROK_BIND_SWITCH_LOCKED_MESSAGE =
|
|
28
|
+
"Outbound identity is locked to grok-bind on this shared MCP; use a separate MCP process / wake daemon, do not switch_profile.";
|
|
29
|
+
|
|
30
|
+
export type GrokBindDecision =
|
|
31
|
+
/** Explicit identity/token already won, or no conversation id — do not consult. */
|
|
32
|
+
| { kind: "skip" }
|
|
33
|
+
/** Conversation id present but no string profile name for it. */
|
|
34
|
+
| { kind: "miss"; conversationId: string }
|
|
35
|
+
/** Bind hit — caller resolves `<profileName>` the same way `--profile` does. */
|
|
36
|
+
| { kind: "hit"; conversationId: string; profileName: string };
|
|
37
|
+
|
|
38
|
+
/** uuid → profile name. Non-string (and empty) values are ignored. */
|
|
39
|
+
export function parseGrokBinds(raw: unknown): Record<string, string> {
|
|
40
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
41
|
+
const out: Record<string, string> = {};
|
|
42
|
+
for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
|
|
43
|
+
if (typeof v !== "string") continue;
|
|
44
|
+
const name = v.trim();
|
|
45
|
+
if (!name) continue;
|
|
46
|
+
const key = k.trim();
|
|
47
|
+
if (!key) continue;
|
|
48
|
+
out[key] = name;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function parseGrokBindsText(text: string): { binds: Record<string, string>; malformed: boolean } {
|
|
54
|
+
try {
|
|
55
|
+
return { binds: parseGrokBinds(JSON.parse(text)), malformed: false };
|
|
56
|
+
} catch {
|
|
57
|
+
return { binds: {}, malformed: true };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveGrokBindsPath(configDir: string, envOverride?: string): string {
|
|
62
|
+
if (envOverride && envOverride.length > 0) return envOverride;
|
|
63
|
+
return join(configDir, DEFAULT_GROK_BINDS_FILENAME);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Consult the bind map only when nothing more specific already declared identity.
|
|
68
|
+
* Explicit `--profile` / `--name` / `AGENTSCHAT_PROFILE` / `AGENTCHAT_PROFILE`
|
|
69
|
+
* and `--token` / `AGENTCHAT_TOKEN` all win.
|
|
70
|
+
*/
|
|
71
|
+
export function decideGrokBind(input: {
|
|
72
|
+
explicitIdentity: boolean;
|
|
73
|
+
hasToken: boolean;
|
|
74
|
+
conversationId: string | undefined;
|
|
75
|
+
binds: Record<string, string>;
|
|
76
|
+
}): GrokBindDecision {
|
|
77
|
+
if (input.explicitIdentity || input.hasToken) return { kind: "skip" };
|
|
78
|
+
const id = input.conversationId?.trim();
|
|
79
|
+
if (!id) return { kind: "skip" };
|
|
80
|
+
const profileName = input.binds[id];
|
|
81
|
+
if (typeof profileName === "string" && profileName.length > 0) {
|
|
82
|
+
return { kind: "hit", conversationId: id, profileName };
|
|
83
|
+
}
|
|
84
|
+
return { kind: "miss", conversationId: id };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Bound profile name for this CURSOR_CONVERSATION_ID, or null if unbound /
|
|
89
|
+
* conversation id unset (Claude / Hermes: no lock).
|
|
90
|
+
*/
|
|
91
|
+
export function boundProfileForConversation(
|
|
92
|
+
conversationId: string | undefined,
|
|
93
|
+
binds: Record<string, string>,
|
|
94
|
+
): string | null {
|
|
95
|
+
const id = conversationId?.trim();
|
|
96
|
+
if (!id) return null;
|
|
97
|
+
const name = binds[id];
|
|
98
|
+
return typeof name === "string" && name.length > 0 ? name : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Basename without `.json` — compare `activeProfileFile` to a bind name. */
|
|
102
|
+
export function profileNameFromPath(profilePath: string | null | undefined): string | null {
|
|
103
|
+
if (!profilePath) return null;
|
|
104
|
+
const base = profilePath.replace(/\\/g, "/").split("/").pop() || "";
|
|
105
|
+
const name = base.replace(/\.json$/i, "");
|
|
106
|
+
return name || null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type SwitchProfileGate =
|
|
110
|
+
| { kind: "allow" }
|
|
111
|
+
| {
|
|
112
|
+
kind: "locked";
|
|
113
|
+
boundProfileName: string;
|
|
114
|
+
requestedProfileName: string;
|
|
115
|
+
message: string;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Gate `switch_profile` when a grok outbound bind owns this MCP identity.
|
|
120
|
+
*
|
|
121
|
+
* - Empty binds → allow (Claude/Hermes with no grok-binds).
|
|
122
|
+
* - Empty / missing requested name (list profiles) → allow.
|
|
123
|
+
* - CURSOR_CONVERSATION_ID maps to a bind: lock unless requested equals that bind.
|
|
124
|
+
* - No conversation id (or unbound): if currentProfileName is one of the bind
|
|
125
|
+
* values (Cursor stdio started with `--profile Jack`), lock unless requested
|
|
126
|
+
* equals current — covers shared MCP without CURSOR_CONVERSATION_ID in env.
|
|
127
|
+
* - Otherwise → allow.
|
|
128
|
+
*/
|
|
129
|
+
export function gateSwitchProfile(input: {
|
|
130
|
+
conversationId: string | undefined;
|
|
131
|
+
binds: Record<string, string>;
|
|
132
|
+
requestedProfileName: string | undefined | null;
|
|
133
|
+
/** Basename of activeProfileFile — used when conversation id is unset. */
|
|
134
|
+
currentProfileName?: string | null;
|
|
135
|
+
}): SwitchProfileGate {
|
|
136
|
+
const req =
|
|
137
|
+
typeof input.requestedProfileName === "string" ? input.requestedProfileName.trim() : "";
|
|
138
|
+
if (!req) return { kind: "allow" };
|
|
139
|
+
|
|
140
|
+
const bindValues = Object.values(input.binds).filter((v) => typeof v === "string" && v.length > 0);
|
|
141
|
+
if (bindValues.length === 0) return { kind: "allow" };
|
|
142
|
+
|
|
143
|
+
const lockedMessage = (lockedName: string) =>
|
|
144
|
+
`Outbound identity is locked to grok-bind profile "${lockedName}" on this shared MCP; ` +
|
|
145
|
+
`use a separate MCP process / wake daemon, do not switch_profile.`;
|
|
146
|
+
|
|
147
|
+
const bound = boundProfileForConversation(input.conversationId, input.binds);
|
|
148
|
+
if (bound) {
|
|
149
|
+
if (req === bound) return { kind: "allow" };
|
|
150
|
+
return {
|
|
151
|
+
kind: "locked",
|
|
152
|
+
boundProfileName: bound,
|
|
153
|
+
requestedProfileName: req,
|
|
154
|
+
message: lockedMessage(bound),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const current =
|
|
159
|
+
typeof input.currentProfileName === "string" ? input.currentProfileName.trim() : "";
|
|
160
|
+
if (current && bindValues.includes(current)) {
|
|
161
|
+
if (req === current) return { kind: "allow" };
|
|
162
|
+
return {
|
|
163
|
+
kind: "locked",
|
|
164
|
+
boundProfileName: current,
|
|
165
|
+
requestedProfileName: req,
|
|
166
|
+
message: lockedMessage(current),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { kind: "allow" };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Whether live identity drifted from the bound profile and needs a force-reload
|
|
175
|
+
* before outbound writes (reply / other mutators that post as AGENT_ID).
|
|
176
|
+
*/
|
|
177
|
+
export function shouldHealBoundIdentity(input: {
|
|
178
|
+
boundProfileName: string | null;
|
|
179
|
+
liveProfileName: string | null | undefined;
|
|
180
|
+
liveAgentId: string | null | undefined;
|
|
181
|
+
boundAgentId: string | null | undefined;
|
|
182
|
+
}): boolean {
|
|
183
|
+
if (!input.boundProfileName) return false;
|
|
184
|
+
if (input.liveProfileName !== input.boundProfileName) return true;
|
|
185
|
+
if (
|
|
186
|
+
typeof input.boundAgentId === "string" &&
|
|
187
|
+
input.boundAgentId.length > 0 &&
|
|
188
|
+
input.liveAgentId !== input.boundAgentId
|
|
189
|
+
) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
package/src/identity.ts
CHANGED
|
@@ -20,10 +20,13 @@
|
|
|
20
20
|
* - An identity WAS declared but its profile is missing → hard error. That is a
|
|
21
21
|
* typo/misconfig, and inventing an identity for it is what corrupted attribution
|
|
22
22
|
* before. Introspection never hits this branch (it passes no flags).
|
|
23
|
+
* Grok bind (CURSOR_CONVERSATION_ID → grok-binds.json) is a declared identity
|
|
24
|
+
* (source "grok-bind") — a hit with a missing profile file is this branch,
|
|
25
|
+
* not a fall-through to a sibling bot.
|
|
23
26
|
*/
|
|
24
27
|
|
|
25
28
|
/** Which resolution tier produced the profile path (see resolveProfile in server.ts). */
|
|
26
|
-
export type ProfileSource = "env" | "legacy-env" | "flag-profile" | "flag-name" | "default";
|
|
29
|
+
export type ProfileSource = "env" | "legacy-env" | "flag-profile" | "flag-name" | "grok-bind" | "default";
|
|
27
30
|
|
|
28
31
|
export type IdentityDecision =
|
|
29
32
|
/** Profile file exists — load it. */
|