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/src/server.ts
CHANGED
|
@@ -42,6 +42,16 @@ import { normalizeTimestampForCursor } from "./timestamps.ts";
|
|
|
42
42
|
import { validateToolArgs } from "./argcheck.ts";
|
|
43
43
|
import { decideIdentity, shouldMigrateDevToken } from "./identity.ts";
|
|
44
44
|
import type { ProfileSource } from "./identity.ts";
|
|
45
|
+
import {
|
|
46
|
+
decideGrokBind,
|
|
47
|
+
parseGrokBindsText,
|
|
48
|
+
resolveGrokBindsPath,
|
|
49
|
+
DEFAULT_GROK_BINDS_FILENAME,
|
|
50
|
+
boundProfileForConversation,
|
|
51
|
+
gateSwitchProfile,
|
|
52
|
+
profileNameFromPath,
|
|
53
|
+
shouldHealBoundIdentity,
|
|
54
|
+
} from "./grok-bind.ts";
|
|
45
55
|
import { decideTermsConsent, TERMS_URL } from "./terms.ts";
|
|
46
56
|
import { fireWake, fireGrokWake, resolveGrokAgentId, resolveGrokGatewayPath, grokBearerFromGatewayConfig, grokPortFromGatewayConfig } from "./wake.ts";
|
|
47
57
|
import pkg from "../package.json";
|
|
@@ -107,6 +117,13 @@ Wake a host that has no channel-notification surface (Grok Bot, generic MCP clie
|
|
|
107
117
|
~/.grok/gateway.json, /home/box/sand-data/gateway.json)
|
|
108
118
|
AGENTCHAT_GROK_AGENT_ID the Grok gateway agent uuid to wake (1:1 binding)
|
|
109
119
|
|
|
120
|
+
Grok multi-bot identity bind (Cursor / Grok Bot, no --profile):
|
|
121
|
+
~/.agentschat/grok-binds.json maps CURSOR_CONVERSATION_ID (Grok uuid) → profile name.
|
|
122
|
+
AGENTCHAT_GROK_BINDS overrides that path. Profile names resolve like --profile.
|
|
123
|
+
Auto-bind does NOT set AGENTCHAT_WAKE_MODE — a Cursor-tool MCP should unset it
|
|
124
|
+
(per-identity wake daemons already POST sendPrompt). If the operator set
|
|
125
|
+
WAKE_MODE, it is left as-is.
|
|
126
|
+
|
|
110
127
|
Hermes relay connector (no Hermes patch): run with --connector. See --connector --help.
|
|
111
128
|
|
|
112
129
|
Identity is never created implicitly: with no --name/--profile/AGENTSCHAT_PROFILE and
|
|
@@ -124,7 +141,9 @@ const cliArgs = parseArgs();
|
|
|
124
141
|
// 2. AGENTCHAT_PROFILE env var (legacy singular)
|
|
125
142
|
// 3. --profile <name> CLI arg
|
|
126
143
|
// 4. --name <name> CLI arg (also used as profile name)
|
|
127
|
-
// 5.
|
|
144
|
+
// 5. Grok bind: CURSOR_CONVERSATION_ID → ~/.agentschat/grok-binds.json
|
|
145
|
+
// (skipped if an explicit token is set; does NOT imply AGENTCHAT_WAKE_MODE)
|
|
146
|
+
// 6. default ~/.agentschat/profile.json, falling back to ~/.agentchat/profile.json
|
|
128
147
|
const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
|
|
129
148
|
const configDir = join(homeDir, ".agentschat");
|
|
130
149
|
const legacyConfigDir = join(homeDir, ".agentchat");
|
|
@@ -148,6 +167,7 @@ function listProfileFiles(): Array<{ name: string; path: string }> {
|
|
|
148
167
|
let files: string[] = [];
|
|
149
168
|
try { files = readdirSync(dir).filter((f: string) => f.endsWith(".json")); } catch {}
|
|
150
169
|
for (const file of files) {
|
|
170
|
+
if (file === DEFAULT_GROK_BINDS_FILENAME) continue; // bind map, not a profile
|
|
151
171
|
const name = file.replace(/\.json$/, "");
|
|
152
172
|
if (seen.has(name)) continue;
|
|
153
173
|
seen.add(name);
|
|
@@ -168,7 +188,45 @@ function resolveProfile(): { path: string; source: ProfileSource; declaredName?:
|
|
|
168
188
|
if (cliArgs.profile) return { path: nameToPath(cliArgs.profile), source: "flag-profile", declaredName: cliArgs.profile };
|
|
169
189
|
// 4. --name <name>
|
|
170
190
|
if (cliArgs.name) return { path: nameToPath(cliArgs.name), source: "flag-name", declaredName: cliArgs.name };
|
|
171
|
-
|
|
191
|
+
|
|
192
|
+
// 5. Grok outbound bind. Explicit env/flags already returned above. An
|
|
193
|
+
// explicit --token/AGENTCHAT_TOKEN also wins: do not steal identity from
|
|
194
|
+
// operator-supplied creds. CURSOR_CONVERSATION_ID unset → skip entirely
|
|
195
|
+
// (Claude Code / Hermes: zero change). A hit with a missing profile file
|
|
196
|
+
// is a declared identity (source grok-bind) so decideIdentity hard-errors
|
|
197
|
+
// rather than falling through to a sibling bot.
|
|
198
|
+
const grokToken = !!(cliArgs.token || process.env.AGENTCHAT_TOKEN);
|
|
199
|
+
const conversationId = process.env.CURSOR_CONVERSATION_ID;
|
|
200
|
+
let binds: Record<string, string> = {};
|
|
201
|
+
if (!grokToken && conversationId) {
|
|
202
|
+
const bindPath = resolveGrokBindsPath(configDir, process.env.AGENTCHAT_GROK_BINDS);
|
|
203
|
+
if (existsSync(bindPath)) {
|
|
204
|
+
try {
|
|
205
|
+
const parsed = parseGrokBindsText(readFileSync(bindPath, "utf-8"));
|
|
206
|
+
binds = parsed.binds;
|
|
207
|
+
if (parsed.malformed) {
|
|
208
|
+
process.stderr.write(`[agentchat] WARNING: grok-binds file is malformed (${bindPath}); ignoring.\n`);
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
binds = {};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const grok = decideGrokBind({
|
|
216
|
+
explicitIdentity: false,
|
|
217
|
+
hasToken: grokToken,
|
|
218
|
+
conversationId,
|
|
219
|
+
binds,
|
|
220
|
+
});
|
|
221
|
+
if (grok.kind === "hit") {
|
|
222
|
+
process.stderr.write(`[agentchat] grok-bind: ${grok.conversationId} → profile "${grok.profileName}"\n`);
|
|
223
|
+
return { path: nameToPath(grok.profileName), source: "grok-bind", declaredName: grok.profileName };
|
|
224
|
+
}
|
|
225
|
+
if (grok.kind === "miss") {
|
|
226
|
+
process.stderr.write(`[agentchat] no grok-bind matched CURSOR_CONVERSATION_ID=${grok.conversationId}\n`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 6. default — nothing was declared. NOT a licence to invent an identity.
|
|
172
230
|
return { path: nameToPath("profile"), source: "default" };
|
|
173
231
|
}
|
|
174
232
|
|
|
@@ -1938,6 +1996,75 @@ HANDLERS.set("transcribe", async (args) => {
|
|
|
1938
1996
|
}
|
|
1939
1997
|
});
|
|
1940
1998
|
|
|
1999
|
+
|
|
2000
|
+
/** Reload grok-binds.json (or AGENTCHAT_GROK_BINDS) for runtime lock/heal checks. */
|
|
2001
|
+
function loadGrokBinds(): Record<string, string> {
|
|
2002
|
+
const bindPath = resolveGrokBindsPath(configDir, process.env.AGENTCHAT_GROK_BINDS);
|
|
2003
|
+
if (!existsSync(bindPath)) return {};
|
|
2004
|
+
try {
|
|
2005
|
+
return parseGrokBindsText(readFileSync(bindPath, "utf-8")).binds;
|
|
2006
|
+
} catch {
|
|
2007
|
+
return {};
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
/**
|
|
2012
|
+
* Apply a profile file as the live identity and reconnect WS.
|
|
2013
|
+
* Shared by `switch_profile` and grok-bind heal.
|
|
2014
|
+
*/
|
|
2015
|
+
function applyIdentityFromProfile(newProfile: any, targetFile: string): void {
|
|
2016
|
+
heartbeat.stop();
|
|
2017
|
+
if (backfillTimer) { clearTimeout(backfillTimer); backfillTimer = null; }
|
|
2018
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
2019
|
+
if (ws) {
|
|
2020
|
+
ws.onclose = null;
|
|
2021
|
+
try { ws.close(); } catch {}
|
|
2022
|
+
ws = null;
|
|
2023
|
+
}
|
|
2024
|
+
sessionId = null;
|
|
2025
|
+
AGENT_ID = newProfile.agent_id;
|
|
2026
|
+
TOKEN = newProfile.token || "dev-token";
|
|
2027
|
+
CAPABILITIES = newProfile.capabilities || ["claude-code", "coding", "chat"];
|
|
2028
|
+
profile = newProfile;
|
|
2029
|
+
activeProfileFile = targetFile;
|
|
2030
|
+
anonymousMode = false;
|
|
2031
|
+
wsReconnectAttempt = 0;
|
|
2032
|
+
heartbeat.start();
|
|
2033
|
+
connectWS();
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
/**
|
|
2037
|
+
* If CURSOR_CONVERSATION_ID is bound and live identity drifted (e.g. another
|
|
2038
|
+
* agent called switch_profile on this shared MCP), force-reload the bound
|
|
2039
|
+
* profile before outbound writes. Logs to stderr when healing.
|
|
2040
|
+
*/
|
|
2041
|
+
function ensureGrokBoundIdentity(): void {
|
|
2042
|
+
const boundName = boundProfileForConversation(process.env.CURSOR_CONVERSATION_ID, loadGrokBinds());
|
|
2043
|
+
if (!boundName) return;
|
|
2044
|
+
const boundPath = nameToPath(boundName);
|
|
2045
|
+
if (!existsSync(boundPath)) return;
|
|
2046
|
+
let boundProfile: any;
|
|
2047
|
+
try {
|
|
2048
|
+
boundProfile = JSON.parse(readFileSync(boundPath, "utf-8"));
|
|
2049
|
+
} catch {
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
const liveName = profileNameFromPath(activeProfileFile);
|
|
2053
|
+
if (!shouldHealBoundIdentity({
|
|
2054
|
+
boundProfileName: boundName,
|
|
2055
|
+
liveProfileName: liveName,
|
|
2056
|
+
liveAgentId: AGENT_ID,
|
|
2057
|
+
boundAgentId: boundProfile?.agent_id,
|
|
2058
|
+
})) {
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
process.stderr.write(
|
|
2062
|
+
`[agentchat] grok-bind heal: live profile=${liveName ?? "?"} agent=${AGENT_ID || "?"} → bound "${boundName}" (${boundProfile.agent_id || "?"})\n`,
|
|
2063
|
+
);
|
|
2064
|
+
applyIdentityFromProfile(boundProfile, boundPath);
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
|
|
1941
2068
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1942
2069
|
let { name, arguments: args } = request.params;
|
|
1943
2070
|
let viaExtendedCompat = false;
|
|
@@ -1954,6 +2081,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1954
2081
|
if (argErr) return { content: [{ type: "text", text: `${name}: ${argErr}` }], isError: true };
|
|
1955
2082
|
}
|
|
1956
2083
|
|
|
2084
|
+
// Shared Cursor MCP: heal back to grok-bind identity if another agent
|
|
2085
|
+
// stole the live profile via switch_profile. No-op when unbound.
|
|
2086
|
+
ensureGrokBoundIdentity();
|
|
2087
|
+
|
|
1957
2088
|
if (name === "list_skills") {
|
|
1958
2089
|
const { chat_id } = (args || {}) as { chat_id?: string };
|
|
1959
2090
|
const out: any = {
|
|
@@ -3054,6 +3185,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3054
3185
|
return { content: [{ type: "text", text: `Current: ${current}\nAvailable profiles:\n${list}` }] };
|
|
3055
3186
|
}
|
|
3056
3187
|
|
|
3188
|
+
// Shared Cursor MCP: grok-binds lock outbound identity — via conversation id
|
|
3189
|
+
// when set, or via current profile when Cursor stdio started with --profile
|
|
3190
|
+
// and no CURSOR_CONVERSATION_ID. Refuse switches that leave the locked
|
|
3191
|
+
// identity (including other Grok bots or Hermes/Spiral). No-op allowed.
|
|
3192
|
+
const switchGate = gateSwitchProfile({
|
|
3193
|
+
conversationId: process.env.CURSOR_CONVERSATION_ID,
|
|
3194
|
+
binds: loadGrokBinds(),
|
|
3195
|
+
requestedProfileName: profile_name,
|
|
3196
|
+
currentProfileName: profileNameFromPath(activeProfileFile),
|
|
3197
|
+
});
|
|
3198
|
+
if (switchGate.kind === "locked") {
|
|
3199
|
+
return { content: [{ type: "text", text: switchGate.message }], isError: true };
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3057
3202
|
// Find and load the profile
|
|
3058
3203
|
const targetFile = nameToPath(profile_name);
|
|
3059
3204
|
if (!existsSync(targetFile)) {
|
|
@@ -3061,34 +3206,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3061
3206
|
}
|
|
3062
3207
|
|
|
3063
3208
|
const newProfile = JSON.parse(readFileSync(targetFile, "utf-8"));
|
|
3064
|
-
|
|
3065
|
-
// Stop heartbeat first to prevent race with old connection
|
|
3066
|
-
heartbeat.stop();
|
|
3067
|
-
// Cancel any pending backfill / reconnect from the old identity.
|
|
3068
|
-
if (backfillTimer) { clearTimeout(backfillTimer); backfillTimer = null; }
|
|
3069
|
-
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
3070
|
-
|
|
3071
|
-
// Close old connection, disable its reconnect handler
|
|
3072
|
-
if (ws) {
|
|
3073
|
-
ws.onclose = null;
|
|
3074
|
-
try { ws.close(); } catch {}
|
|
3075
|
-
ws = null;
|
|
3076
|
-
}
|
|
3077
|
-
sessionId = null;
|
|
3078
|
-
|
|
3079
|
-
// Update identity. activeProfileFile must move too — whoami reports it, and a
|
|
3080
|
-
// stale value there makes the identity probe lie about which profile is live.
|
|
3081
|
-
AGENT_ID = newProfile.agent_id;
|
|
3082
|
-
TOKEN = newProfile.token || "dev-token";
|
|
3083
|
-
CAPABILITIES = newProfile.capabilities || ["claude-code", "coding", "chat"];
|
|
3084
|
-
profile = newProfile;
|
|
3085
|
-
activeProfileFile = targetFile;
|
|
3086
|
-
anonymousMode = false;
|
|
3087
|
-
|
|
3088
|
-
// Restart heartbeat and connect with new identity
|
|
3089
|
-
wsReconnectAttempt = 0;
|
|
3090
|
-
heartbeat.start();
|
|
3091
|
-
connectWS();
|
|
3209
|
+
applyIdentityFromProfile(newProfile, targetFile);
|
|
3092
3210
|
|
|
3093
3211
|
return { content: [{ type: "text", text: `Switched to profile "${profile_name}" (${AGENT_ID}). Reconnecting...` }] };
|
|
3094
3212
|
}
|