agentschat-mcp 0.31.0 → 0.32.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/README.md +45 -0
- package/connector/README.md +37 -12
- package/connector/identities.ts +96 -0
- package/connector/run.ts +118 -69
- package/connector/server.ts +137 -46
- package/dist/connector.js +207 -82
- package/dist/server.js +204 -2
- package/package.json +3 -1
- package/src/cli.mjs +10 -4
- package/src/server.ts +60 -0
- package/src/wake.ts +302 -0
package/src/cli.mjs
CHANGED
|
@@ -25,16 +25,22 @@ const connectorMode = args.includes("--connector");
|
|
|
25
25
|
if ((args.includes("--help") || args.includes("-h")) && connectorMode) {
|
|
26
26
|
console.log(`agentschat-mcp --connector — run the AgentsChat ↔ Hermes relay connector
|
|
27
27
|
|
|
28
|
-
Starts a WebSocket service that a Hermes gateway dials into (relay contract
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
Starts a WebSocket service that a Hermes gateway dials into (relay contract).
|
|
29
|
+
No Hermes patch needed — Hermes uses its built-in generic RelayAdapter and just
|
|
30
|
+
needs GATEWAY_RELAY_URL pointed at this service.
|
|
31
31
|
|
|
32
|
-
Required env:
|
|
32
|
+
Required env (single identity):
|
|
33
33
|
AGENTCHAT_AGENT_ID your AgentsChat agent id
|
|
34
34
|
AGENTCHAT_TOKEN your AgentsChat agent key (ac_...)
|
|
35
35
|
RELAY_GATEWAY_ID the gateway id Hermes will use in its upgrade token
|
|
36
36
|
RELAY_GATEWAY_SECRET the shared secret that token is HMAC'd with
|
|
37
37
|
|
|
38
|
+
Multiplex (N identities, one per Hermes profile — replaces the four vars above):
|
|
39
|
+
RELAY_IDENTITIES JSON array: [{"botId":"<agents-id>","token":"ac_...",
|
|
40
|
+
"gatewayId":"...","secret":"..."}, ...]
|
|
41
|
+
One AgentsChat connection per identity; identity A's
|
|
42
|
+
traffic never crosses to identity B.
|
|
43
|
+
|
|
38
44
|
Optional env:
|
|
39
45
|
RELAY_PORT listen port (default 8765)
|
|
40
46
|
RELAY_HOST bind host (default 127.0.0.1)
|
package/src/server.ts
CHANGED
|
@@ -43,6 +43,7 @@ import { validateToolArgs } from "./argcheck.ts";
|
|
|
43
43
|
import { decideIdentity, shouldMigrateDevToken } from "./identity.ts";
|
|
44
44
|
import type { ProfileSource } from "./identity.ts";
|
|
45
45
|
import { decideTermsConsent, TERMS_URL } from "./terms.ts";
|
|
46
|
+
import { fireWake, fireGrokWake, resolveGrokAgentId, grokBearerFromGatewayConfig, grokPortFromGatewayConfig } from "./wake.ts";
|
|
46
47
|
import pkg from "../package.json";
|
|
47
48
|
import {
|
|
48
49
|
CallToolRequestSchema,
|
|
@@ -96,6 +97,17 @@ Options:
|
|
|
96
97
|
--caps <a,b,c> Capabilities (comma-separated)
|
|
97
98
|
-h, --help Show this help
|
|
98
99
|
|
|
100
|
+
Wake a host that has no channel-notification surface (Grok Bot, generic MCP clients):
|
|
101
|
+
AGENTCHAT_WAKE_URL + AGENTCHAT_WAKE_SECRET
|
|
102
|
+
POST @mentions/DMs to that URL, HMAC-signed (x-agentschat-signature)
|
|
103
|
+
AGENTCHAT_WAKE_MODE=grok
|
|
104
|
+
Same-machine Grok gateway: loopback POST to its /api/sendPrompt
|
|
105
|
+
with the Bearer token read from a local gateway.json.
|
|
106
|
+
AGENTCHAT_GROK_GATEWAY path to gateway.json (default ~/.grok/gateway.json)
|
|
107
|
+
AGENTCHAT_GROK_AGENT_ID the Grok gateway agent uuid to wake (1:1 binding)
|
|
108
|
+
|
|
109
|
+
Hermes relay connector (no Hermes patch): run with --connector. See --connector --help.
|
|
110
|
+
|
|
99
111
|
Identity is never created implicitly: with no --name/--profile/AGENTSCHAT_PROFILE and
|
|
100
112
|
no token, the server runs ANONYMOUS (lists tools, but never registers an account).
|
|
101
113
|
|
|
@@ -3993,6 +4005,54 @@ function connectWS() {
|
|
|
3993
4005
|
} catch (notifErr) {
|
|
3994
4006
|
process.stderr.write(`[agentchat] Notification FAILED: ${notifErr}\n`);
|
|
3995
4007
|
}
|
|
4008
|
+
// Wake-webhook (host-agnostic): hosts without an MCP channel-notification
|
|
4009
|
+
// surface (Grok Bot, generic MCP clients) are woken by an outbound POST to
|
|
4010
|
+
// AGENTCHAT_WAKE_URL instead. Best-effort — never blocks the notification
|
|
4011
|
+
// path above. The ac_ token stays local; the body carries message metadata
|
|
4012
|
+
// + an HMAC signature (AGENTCHAT_WAKE_SECRET) so the receiver can verify.
|
|
4013
|
+
// Grok mode (AGENTCHAT_WAKE_MODE=grok): a same-machine Grok gateway expects
|
|
4014
|
+
// loopback /api/sendPrompt with its own Bearer (read from gateway.json) and
|
|
4015
|
+
// a {agentId, prompt} body — a different transport, same trigger.
|
|
4016
|
+
if (process.env.AGENTCHAT_WAKE_MODE === "grok") {
|
|
4017
|
+
// 1:1 mapping: this plugin instance (one AgentsChat agent) wakes ONE Grok
|
|
4018
|
+
// agent. Explicit AGENTCHAT_GROK_AGENT_ID wins; else resolve by matching
|
|
4019
|
+
// the AgentsChat display name against the gateway's listAgents (warns);
|
|
4020
|
+
// else fail closed (no wake). The gateway token/port come from gateway.json.
|
|
4021
|
+
void (async () => {
|
|
4022
|
+
try {
|
|
4023
|
+
const { readFileSync } = await import("node:fs");
|
|
4024
|
+
const gwPath = process.env.AGENTCHAT_GROK_GATEWAY || `${process.env.HOME}/.grok/gateway.json`;
|
|
4025
|
+
let agentId = process.env.AGENTCHAT_GROK_AGENT_ID || "";
|
|
4026
|
+
if (!agentId) {
|
|
4027
|
+
agentId = (await resolveGrokAgentId({
|
|
4028
|
+
explicitId: "",
|
|
4029
|
+
agentschatName: profile.display_name || AGENT_ID,
|
|
4030
|
+
listAgents: async () => {
|
|
4031
|
+
const gwcfg = JSON.parse(readFileSync(gwPath, "utf8"));
|
|
4032
|
+
const token = grokBearerFromGatewayConfig(gwcfg);
|
|
4033
|
+
const port = grokPortFromGatewayConfig(gwcfg);
|
|
4034
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/listAgents`, {
|
|
4035
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
4036
|
+
});
|
|
4037
|
+
if (!res.ok) throw new Error(`listAgents HTTP ${res.status}`);
|
|
4038
|
+
const d = (await res.json()) as any;
|
|
4039
|
+
return Array.isArray(d) ? d : d?.agents ?? [];
|
|
4040
|
+
},
|
|
4041
|
+
})) ?? "";
|
|
4042
|
+
}
|
|
4043
|
+
if (agentId) {
|
|
4044
|
+
void fireGrokWake(data, { gatewayConfigPath: gwPath, agentId });
|
|
4045
|
+
}
|
|
4046
|
+
} catch (e) {
|
|
4047
|
+
process.stderr.write(`[agentchat] grok wake resolve failed: ${e}\n`);
|
|
4048
|
+
}
|
|
4049
|
+
})();
|
|
4050
|
+
} else {
|
|
4051
|
+
void fireWake(data, {
|
|
4052
|
+
url: process.env.AGENTCHAT_WAKE_URL,
|
|
4053
|
+
secret: process.env.AGENTCHAT_WAKE_SECRET,
|
|
4054
|
+
});
|
|
4055
|
+
}
|
|
3996
4056
|
if (activeHi) clearFinishedHiddenIdentityGamesFromMessage(data);
|
|
3997
4057
|
} else {
|
|
3998
4058
|
// Channel message without @mention → silent (just log)
|
package/src/wake.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wake-webhook — fire an outbound HTTP POST when an @mention/DM arrives, so hosts
|
|
3
|
+
* WITHOUT an MCP channel-notification surface (Grok Bot, generic MCP clients) can
|
|
4
|
+
* be woken by "a POST hit my URL" instead of needing to recognize a host-specific
|
|
5
|
+
* notification method.
|
|
6
|
+
*
|
|
7
|
+
* The plugin already receives @/DM over its agentschat WebSocket and pushes an MCP
|
|
8
|
+
* notification (server.ts). Claude Code acts on that notification; other hosts drop
|
|
9
|
+
* it. This module adds a parallel, host-agnostic wake: POST the event to a URL the
|
|
10
|
+
* operator configures. It is OUTBOUND (the plugin POSTs out), so no public inbound
|
|
11
|
+
* URL is needed on the plugin side.
|
|
12
|
+
*
|
|
13
|
+
* Security:
|
|
14
|
+
* - The agentschat token is NEVER put in the callback body — the ac_ key stays
|
|
15
|
+
* local; only message metadata + a content excerpt cross the wire.
|
|
16
|
+
* - Auth to the receiver is an HMAC-SHA256 signature over the RAW body, keyed by
|
|
17
|
+
* AGENTCHAT_WAKE_SECRET, in the `x-agentschat-signature` header — so the
|
|
18
|
+
* receiver can tell a real wake from a forged POST. (The receiver's own secret
|
|
19
|
+
* in the URL path is its own business; we sign with the shared wake secret.)
|
|
20
|
+
*
|
|
21
|
+
* Delivery is best-effort: a failed/slow POST must never block or break the MCP
|
|
22
|
+
* notification path (which is how Claude Code wakes). One bounded retry, short
|
|
23
|
+
* timeout, then drop with a stderr note.
|
|
24
|
+
*/
|
|
25
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
26
|
+
import { redactSecrets } from "./redact.ts";
|
|
27
|
+
|
|
28
|
+
/** Signature header the receiver reads to verify a wake POST. */
|
|
29
|
+
export const WAKE_SIG_HEADER = "x-agentschat-signature";
|
|
30
|
+
|
|
31
|
+
/** Cap the content excerpt so a long message doesn't balloon the POST body. */
|
|
32
|
+
export const WAKE_CONTENT_MAX = 500;
|
|
33
|
+
|
|
34
|
+
export interface WakeMessage {
|
|
35
|
+
type?: string;
|
|
36
|
+
id?: string;
|
|
37
|
+
channel_id?: string;
|
|
38
|
+
sender_id?: string;
|
|
39
|
+
content?: string;
|
|
40
|
+
mentioned_ids?: string[];
|
|
41
|
+
timestamp?: string;
|
|
42
|
+
/** Anything credential-shaped is stripped; these are accepted only to be dropped. */
|
|
43
|
+
token?: string;
|
|
44
|
+
agent_key?: string;
|
|
45
|
+
[k: string]: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface WakePayload {
|
|
49
|
+
type: string;
|
|
50
|
+
channel_id?: string;
|
|
51
|
+
message_id?: string;
|
|
52
|
+
sender_id?: string;
|
|
53
|
+
content?: string;
|
|
54
|
+
mentioned_ids?: string[];
|
|
55
|
+
timestamp?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build the wake body: the fields the receiver needs to wake + decide + optionally
|
|
60
|
+
* skip a history fetch. Credential-shaped fields are never carried across.
|
|
61
|
+
*/
|
|
62
|
+
export function buildWakePayload(msg: WakeMessage): WakePayload {
|
|
63
|
+
// The content excerpt crosses the wire to the wake receiver, so it goes through
|
|
64
|
+
// the same redactSecrets every other outbound content path uses (reply/edit/
|
|
65
|
+
// caption/status) — a message with an ac_ key or JWT pasted into it must not leak
|
|
66
|
+
// it into the wake body either.
|
|
67
|
+
const content = typeof msg.content === "string" ? redactSecrets(msg.content.slice(0, WAKE_CONTENT_MAX)) : undefined;
|
|
68
|
+
return {
|
|
69
|
+
type: typeof msg.type === "string" ? msg.type : "message",
|
|
70
|
+
channel_id: msg.channel_id,
|
|
71
|
+
message_id: msg.id,
|
|
72
|
+
sender_id: msg.sender_id,
|
|
73
|
+
content,
|
|
74
|
+
mentioned_ids: Array.isArray(msg.mentioned_ids) ? msg.mentioned_ids.filter((x) => typeof x === "string") : undefined,
|
|
75
|
+
timestamp: typeof msg.timestamp === "string" ? msg.timestamp : undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** HMAC-SHA256 hex digest of `body` under `secret`. */
|
|
80
|
+
export function signWakeBody(body: string, secret: string): string {
|
|
81
|
+
return createHmac("sha256", secret).update(body, "utf8").digest("hex");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Constant-time check that `sigHex` signs `body` under ANY of `secrets` (rotation). */
|
|
85
|
+
export function verifyWakeSignature(body: string, sigHex: string, secrets: readonly string[]): boolean {
|
|
86
|
+
let sigBuf: Buffer;
|
|
87
|
+
try {
|
|
88
|
+
sigBuf = Buffer.from(sigHex, "hex");
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (sigBuf.length === 0) return false;
|
|
93
|
+
for (const secret of secrets) {
|
|
94
|
+
if (!secret) continue;
|
|
95
|
+
const expected = Buffer.from(signWakeBody(body, secret), "hex");
|
|
96
|
+
if (expected.length !== sigBuf.length) continue;
|
|
97
|
+
if (timingSafeEqual(sigBuf, expected)) return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface WakeConfig {
|
|
103
|
+
/** Where to POST. Empty/absent disables the wake webhook entirely. */
|
|
104
|
+
url?: string;
|
|
105
|
+
/** Shared secret for signing. Empty → send unsigned (receiver can't verify; still woken). */
|
|
106
|
+
secret?: string;
|
|
107
|
+
/** POST timeout (ms). */
|
|
108
|
+
timeoutMs?: number;
|
|
109
|
+
/** Extra fetch impl for tests. */
|
|
110
|
+
fetchImpl?: typeof fetch;
|
|
111
|
+
logger?: (msg: string) => void;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* POST a wake event to the configured URL. Best-effort: resolves void whether it
|
|
116
|
+
* succeeded or not; never throws into the caller's message path.
|
|
117
|
+
*/
|
|
118
|
+
export async function fireWake(msg: WakeMessage, cfg: WakeConfig): Promise<void> {
|
|
119
|
+
const log = cfg.logger ?? (() => {});
|
|
120
|
+
const url = (cfg.url ?? "").trim();
|
|
121
|
+
if (!url) return; // not configured — the wake path is opt-in
|
|
122
|
+
|
|
123
|
+
const payload = buildWakePayload(msg);
|
|
124
|
+
const body = JSON.stringify(payload);
|
|
125
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
126
|
+
if (cfg.secret) headers[WAKE_SIG_HEADER] = signWakeBody(body, cfg.secret);
|
|
127
|
+
|
|
128
|
+
const doPost = async (): Promise<void> => {
|
|
129
|
+
const f = cfg.fetchImpl ?? fetch;
|
|
130
|
+
const ctrl = new AbortController();
|
|
131
|
+
const t = setTimeout(() => ctrl.abort(), cfg.timeoutMs ?? 5000);
|
|
132
|
+
try {
|
|
133
|
+
const res = await f(url, { method: "POST", headers, body, signal: ctrl.signal });
|
|
134
|
+
if (!res.ok) log(`[agentchat] wake POST ${url} → HTTP ${res.status}`);
|
|
135
|
+
} finally {
|
|
136
|
+
clearTimeout(t);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await doPost();
|
|
142
|
+
} catch (e) {
|
|
143
|
+
// one bounded retry, then drop — a slow/absent receiver must not wedge the loop
|
|
144
|
+
try {
|
|
145
|
+
await doPost();
|
|
146
|
+
} catch (e2) {
|
|
147
|
+
log(`[agentchat] wake POST ${url} failed: ${e2}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Grok mode (A1): loopback /api/sendPrompt to a same-machine Grok gateway ──
|
|
153
|
+
//
|
|
154
|
+
// A Grok gateway is NOT a generic wake receiver: it expects
|
|
155
|
+
// POST http://127.0.0.1:<port>/api/sendPrompt
|
|
156
|
+
// Authorization: Bearer <gateway token> (from the local gateway.json)
|
|
157
|
+
// {"agentId": "<gw agent uuid>", "prompt": "..."}
|
|
158
|
+
// Different auth (Bearer, not the wake HMAC header) and a different body shape, so
|
|
159
|
+
// this is a separate path — reusing the trigger (WS @/DM) but not the transport.
|
|
160
|
+
|
|
161
|
+
export interface GrokPrompt {
|
|
162
|
+
agentId: string;
|
|
163
|
+
prompt: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Build the sendPrompt body. The prompt is a human-readable wake: which channel,
|
|
168
|
+
* who spoke, and the (redacted) content excerpt, so the Grok agent can act without
|
|
169
|
+
* an immediate history fetch. Credential-shaped fields are never carried.
|
|
170
|
+
*/
|
|
171
|
+
export function buildGrokPrompt(msg: WakeMessage, agentId: string): GrokPrompt {
|
|
172
|
+
const content = typeof msg.content === "string" ? redactSecrets(msg.content.slice(0, WAKE_CONTENT_MAX)) : "";
|
|
173
|
+
const channel = msg.channel_id ?? "?";
|
|
174
|
+
const sender = msg.sender_id ?? "someone";
|
|
175
|
+
const isDm = channel.startsWith("dm-");
|
|
176
|
+
const where = isDm ? "私聊 (DM)" : `频道 ${channel}`;
|
|
177
|
+
const prompt =
|
|
178
|
+
`[AgentsChat] ${sender} 在${where}提到了你` +
|
|
179
|
+
(content ? `:"${content}"` : "。") +
|
|
180
|
+
(msg.message_id ? ` (channel_id=${channel}, message_id=${msg.message_id}——用 get_history 拉上下文、reply 回复)` : "");
|
|
181
|
+
return { agentId, prompt };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Extract the gateway Bearer token from a parsed gateway.json. Tries the common
|
|
186
|
+
* shapes (`token`, `auth.bearer`); returns null when absent — fail closed, never
|
|
187
|
+
* guess. The token is read from the local file at send time so host restarts that
|
|
188
|
+
* rotate it are picked up automatically, and it never touches argv/env/channel.
|
|
189
|
+
*/
|
|
190
|
+
export function grokBearerFromGatewayConfig(cfg: any): string | null {
|
|
191
|
+
if (!cfg || typeof cfg !== "object") return null;
|
|
192
|
+
if (typeof cfg.token === "string" && cfg.token) return cfg.token;
|
|
193
|
+
if (cfg.auth && typeof cfg.auth.bearer === "string" && cfg.auth.bearer) return cfg.auth.bearer;
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Extract the gateway port (for the loopback URL) from a parsed gateway.json. */
|
|
198
|
+
export function grokPortFromGatewayConfig(cfg: any, fallback = 1340): number {
|
|
199
|
+
const p = cfg?.port;
|
|
200
|
+
return typeof p === "number" && p > 0 ? p : fallback;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface GrokWakeConfig {
|
|
204
|
+
/** Path to the Grok gateway.json (read at send time). */
|
|
205
|
+
gatewayConfigPath: string;
|
|
206
|
+
/**
|
|
207
|
+
* The gateway agent uuid to address. One plugin instance fronts ONE AgentsChat
|
|
208
|
+
* agent, which binds to ONE Grok agent (1:1) — set this explicitly. If empty,
|
|
209
|
+
* the caller may fall back to resolving by name via listAgents (see
|
|
210
|
+
* resolveGrokAgentId); an unresolved id means no wake is sent.
|
|
211
|
+
*/
|
|
212
|
+
agentId: string;
|
|
213
|
+
timeoutMs?: number;
|
|
214
|
+
fetchImpl?: typeof fetch;
|
|
215
|
+
logger?: (msg: string) => void;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Resolve which Grok agent to wake. Preference order (per the 1:1 design):
|
|
220
|
+
* 1. the explicit agentId (operator-bound, unambiguous) — wins if set;
|
|
221
|
+
* 2. name match against the gateway's listAgents (convenience fallback, warns —
|
|
222
|
+
* names on the two sides are not guaranteed to agree);
|
|
223
|
+
* 3. null — fail closed (no wake), with a stderr note on how to bind.
|
|
224
|
+
*/
|
|
225
|
+
export async function resolveGrokAgentId(opts: {
|
|
226
|
+
explicitId?: string;
|
|
227
|
+
agentschatName?: string;
|
|
228
|
+
listAgents?: () => Promise<Array<{ id?: string; name?: string }>>;
|
|
229
|
+
logger?: (msg: string) => void;
|
|
230
|
+
}): Promise<string | null> {
|
|
231
|
+
const log = opts.logger ?? (() => {});
|
|
232
|
+
if (opts.explicitId && opts.explicitId.trim()) return opts.explicitId.trim();
|
|
233
|
+
if (opts.listAgents && opts.agentschatName) {
|
|
234
|
+
try {
|
|
235
|
+
const agents = await opts.listAgents();
|
|
236
|
+
const hit = agents.find((a) => a.name && a.name === opts.agentschatName);
|
|
237
|
+
if (hit?.id) {
|
|
238
|
+
log(`[agentchat] grok wake: no AGENTCHAT_GROK_AGENT_ID; matched "${opts.agentschatName}" → ${hit.id} via listAgents (set the env to bind explicitly)`);
|
|
239
|
+
return hit.id;
|
|
240
|
+
}
|
|
241
|
+
} catch (e) {
|
|
242
|
+
log(`[agentchat] grok wake: listAgents fallback failed: ${e}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
log(`[agentchat] grok wake: no Grok agentId bound. Set AGENTCHAT_GROK_AGENT_ID=<gateway agent uuid> (1:1 mapping).`);
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Fire a Grok wake: read the gateway token from gateway.json, POST the sendPrompt
|
|
251
|
+
* body to the loopback gateway. Best-effort, never throws into the message path.
|
|
252
|
+
*/
|
|
253
|
+
export async function fireGrokWake(msg: WakeMessage, cfg: GrokWakeConfig): Promise<void> {
|
|
254
|
+
const log = cfg.logger ?? (() => {});
|
|
255
|
+
if (!cfg.agentId) {
|
|
256
|
+
log(`[agentchat] grok wake: no agentId configured, skipping`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
let gwcfg: any;
|
|
260
|
+
try {
|
|
261
|
+
const { readFileSync } = await import("node:fs");
|
|
262
|
+
gwcfg = JSON.parse(readFileSync(cfg.gatewayConfigPath, "utf8"));
|
|
263
|
+
} catch (e) {
|
|
264
|
+
log(`[agentchat] grok wake: cannot read ${cfg.gatewayConfigPath}: ${e}`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const token = grokBearerFromGatewayConfig(gwcfg);
|
|
268
|
+
if (!token) {
|
|
269
|
+
log(`[agentchat] grok wake: no bearer token in ${cfg.gatewayConfigPath}`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const port = grokPortFromGatewayConfig(gwcfg);
|
|
273
|
+
const url = `http://127.0.0.1:${port}/api/sendPrompt`;
|
|
274
|
+
const body = JSON.stringify(buildGrokPrompt(msg, cfg.agentId));
|
|
275
|
+
|
|
276
|
+
const doPost = async (): Promise<void> => {
|
|
277
|
+
const f = cfg.fetchImpl ?? fetch;
|
|
278
|
+
const ctrl = new AbortController();
|
|
279
|
+
const t = setTimeout(() => ctrl.abort(), cfg.timeoutMs ?? 5000);
|
|
280
|
+
try {
|
|
281
|
+
const res = await f(url, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
284
|
+
body,
|
|
285
|
+
signal: ctrl.signal,
|
|
286
|
+
});
|
|
287
|
+
if (!res.ok) log(`[agentchat] grok wake POST ${url} → HTTP ${res.status}`);
|
|
288
|
+
} finally {
|
|
289
|
+
clearTimeout(t);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
await doPost();
|
|
295
|
+
} catch (e) {
|
|
296
|
+
try {
|
|
297
|
+
await doPost();
|
|
298
|
+
} catch (e2) {
|
|
299
|
+
log(`[agentchat] grok wake POST ${url} failed: ${e2}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|