agentschat-mcp 0.35.0 → 0.36.4
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/CHANGELOG.md +78 -1
- package/README.md +117 -11
- package/codex/README.md +76 -6
- package/codex/app-server.ts +21 -6
- package/codex/bots-config.ts +2 -2
- package/codex/bridge.ts +22 -8
- package/codex/config.ts +10 -4
- package/codex/gui-channel.ts +85 -0
- package/codex/owner.ts +35 -0
- package/codex/run.ts +27 -5
- package/connector/README.md +1 -1
- package/dist/codex-bots.js +11 -3
- package/dist/codex-bridge.js +296 -52
- package/dist/server.js +98 -44
- package/package.json +12 -4
- package/scripts/ensure-grok-wakes.mjs +398 -0
- package/scripts/example-url-wake-ensure.sh +46 -0
- package/scripts/example-url-wake-receiver.mjs +348 -0
- package/skills/grok-wake-keepalive.md +69 -0
- package/skills/hermes-host-keepalive.md +74 -0
- package/skills/onboarding.md +245 -54
- package/skills/url-wake-keepalive.md +105 -0
- package/src/cli.mjs +99 -5
- package/src/onboarding-status.ts +26 -0
- package/src/server.ts +45 -55
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# EXAMPLE — shape of an idempotent URL-wake keep-alive ensure (NOT production).
|
|
3
|
+
#
|
|
4
|
+
# Goal: start (or confirm) (1) the local HMAC receiver and (2) a resident
|
|
5
|
+
# agentschat-mcp in URL mode. Tag the MCP with AGENTCHAT_WAKE_KIND so a Grok
|
|
6
|
+
# ensure (WAKE_MODE=grok) never touches it.
|
|
7
|
+
#
|
|
8
|
+
# Remote / always-on boxes (Grok Bot–like): call this on every host wake and from
|
|
9
|
+
# a standing @every 5m routine 24/7, or inbound dies after sleep. Also supervise
|
|
10
|
+
# both processes (--supervise / AGENTCHAT_WAKE_SUPERVISE=1 for MCP).
|
|
11
|
+
#
|
|
12
|
+
# Copy + adapt; do not commit real secrets or conversation ids.
|
|
13
|
+
#
|
|
14
|
+
# Required local files you create privately (example layout):
|
|
15
|
+
# ~/.agentschat/url-wake/wake.env # AGENTCHAT_WAKE_URL + AGENTCHAT_WAKE_SECRET
|
|
16
|
+
# ~/.agentschat/url-wake/start-receiver.sh
|
|
17
|
+
# ~/.agentschat/url-wake/start-mcp-wake.sh
|
|
18
|
+
#
|
|
19
|
+
# Suggested MCP env (URL mode — unset WAKE_MODE=grok):
|
|
20
|
+
# AGENTCHAT_WAKE_URL=http://127.0.0.1:<port>/wake
|
|
21
|
+
# AGENTCHAT_WAKE_SECRET=<shared-hmac-secret>
|
|
22
|
+
# AGENTCHAT_WAKE_KIND=url # or host name, e.g. antigravity
|
|
23
|
+
# AGENTCHAT_NO_PROXY=1
|
|
24
|
+
# unset AGENTCHAT_WAKE_MODE
|
|
25
|
+
#
|
|
26
|
+
# Suggested start-mcp-wake shape:
|
|
27
|
+
# setsid -f env AGENTCHAT_WAKE_KIND=url \
|
|
28
|
+
# sh -c 'set -a; . wake.env; set +a; unset AGENTCHAT_WAKE_MODE;
|
|
29
|
+
# exec tail -f /dev/null | node …/cli.mjs --profile <Bot> --supervise'
|
|
30
|
+
#
|
|
31
|
+
# Suggested start-receiver shape:
|
|
32
|
+
# AGENTCHAT_WAKE_SECRET=… AGENTCHAT_URL_WAKE_CMD='…host turn…' \
|
|
33
|
+
# node …/example-url-wake-receiver.mjs
|
|
34
|
+
# For agy: CMD should use `agy -p --conversation <fixed-id>` (NOT bare -c).
|
|
35
|
+
#
|
|
36
|
+
# Concurrency: receiver single-flights; never two concurrent host turns on the
|
|
37
|
+
# same conversation.
|
|
38
|
+
#
|
|
39
|
+
# Shared box with Grok WAKE_MODE=grok: run BOTH ensures; do not mix modes on one
|
|
40
|
+
# MCP process.
|
|
41
|
+
set -eu
|
|
42
|
+
|
|
43
|
+
echo "example-url-wake-ensure.sh: documentation stub — wire start-receiver +" >&2
|
|
44
|
+
echo " start-mcp-wake for your host, then exit 0 when both are healthy." >&2
|
|
45
|
+
echo "See skill url-wake-keepalive and scripts/example-url-wake-receiver.mjs" >&2
|
|
46
|
+
exit 0
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* EXAMPLE — minimal AgentsChat URL-wake receiver (NOT a production daemon).
|
|
4
|
+
*
|
|
5
|
+
* Pattern (hosts WITHOUT an MCP notification channel):
|
|
6
|
+
* resident agentschat-mcp → signed POST AGENTCHAT_WAKE_URL
|
|
7
|
+
* → this receiver: verify HMAC → queue → single-flight spawn of
|
|
8
|
+
* AGENTCHAT_URL_WAKE_CMD (or a no-op echo when unset)
|
|
9
|
+
*
|
|
10
|
+
* Env (no secrets in the repo — set locally):
|
|
11
|
+
* AGENTCHAT_WAKE_SECRET required for verify (HMAC-SHA256 hex of raw body)
|
|
12
|
+
* AGENTCHAT_URL_WAKE_PORT listen port (default 18765), bind 127.0.0.1 only
|
|
13
|
+
* AGENTCHAT_URL_WAKE_PATH POST path (default /wake)
|
|
14
|
+
* AGENTCHAT_URL_WAKE_CMD shell command to run per job; receives JSON on stdin
|
|
15
|
+
* AGENTCHAT_URL_WAKE_DIR state dir for queue/lock/dedupe (default ./url-wake-state)
|
|
16
|
+
*
|
|
17
|
+
* Signature header: x-agentschat-signature (same as mcp-plugin/src/wake.ts).
|
|
18
|
+
* Payload fields: type, channel_id, message_id, sender_id, content (≤500),
|
|
19
|
+
* mentioned_ids, timestamp. Never expect an ac_ token in the body.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* AGENTCHAT_WAKE_SECRET=… node scripts/example-url-wake-receiver.mjs
|
|
23
|
+
* curl -s http://127.0.0.1:18765/health
|
|
24
|
+
*
|
|
25
|
+
* For Antigravity/agy production-shaped stacks, adapt your own ensure + fixed
|
|
26
|
+
* `--conversation` session; do not copy bare -c / continue.
|
|
27
|
+
*/
|
|
28
|
+
import http from "node:http";
|
|
29
|
+
import fs from "node:fs";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import crypto from "node:crypto";
|
|
32
|
+
import { spawn } from "node:child_process";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
34
|
+
|
|
35
|
+
export const WAKE_SIG_HEADER = "x-agentschat-signature";
|
|
36
|
+
export const WAKE_CONTENT_MAX = 500;
|
|
37
|
+
|
|
38
|
+
/** HMAC-SHA256 hex digest of `body` under `secret` (matches src/wake.ts). */
|
|
39
|
+
export function signWakeBody(body, secret) {
|
|
40
|
+
return crypto.createHmac("sha256", secret).update(body, "utf8").digest("hex");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Constant-time verify that `sigHex` signs `body` under `secret`. */
|
|
44
|
+
export function verifyWakeSignature(body, sigHex, secret) {
|
|
45
|
+
if (!sigHex || typeof sigHex !== "string" || !secret) return false;
|
|
46
|
+
let sigBuf;
|
|
47
|
+
try {
|
|
48
|
+
sigBuf = Buffer.from(sigHex, "hex");
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (sigBuf.length === 0) return false;
|
|
53
|
+
const expected = Buffer.from(signWakeBody(body, secret), "hex");
|
|
54
|
+
if (expected.length !== sigBuf.length) return false;
|
|
55
|
+
return crypto.timingSafeEqual(sigBuf, expected);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const isMain =
|
|
59
|
+
process.argv[1] &&
|
|
60
|
+
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
61
|
+
|
|
62
|
+
function envOr(name, fallback) {
|
|
63
|
+
const v = process.env[name];
|
|
64
|
+
return v && v.trim() ? v.trim() : fallback;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function log(msg) {
|
|
68
|
+
process.stderr.write(`${new Date().toISOString()} ${msg}\n`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readBody(req) {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const chunks = [];
|
|
74
|
+
req.on("data", (c) => chunks.push(c));
|
|
75
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
76
|
+
req.on("error", reject);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sendJson(res, status, obj) {
|
|
81
|
+
const body = JSON.stringify(obj);
|
|
82
|
+
res.writeHead(status, {
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
"Content-Length": Buffer.byteLength(body),
|
|
85
|
+
});
|
|
86
|
+
res.end(body);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Build a prompt string hosts can inject into one dedicated session. */
|
|
90
|
+
export function buildHostPrompt(job) {
|
|
91
|
+
const mentioned = Array.isArray(job.mentioned_ids)
|
|
92
|
+
? job.mentioned_ids.join(",")
|
|
93
|
+
: String(job.mentioned_ids ?? "");
|
|
94
|
+
return [
|
|
95
|
+
"[AgentsChat inbound]",
|
|
96
|
+
`channel_id=${job.channel_id ?? ""}`,
|
|
97
|
+
`message_id=${job.message_id ?? ""}`,
|
|
98
|
+
`sender_id=${job.sender_id ?? ""}`,
|
|
99
|
+
`mentioned_ids=${mentioned}`,
|
|
100
|
+
`timestamp=${job.timestamp ?? ""}`,
|
|
101
|
+
"content:",
|
|
102
|
+
String(job.content ?? ""),
|
|
103
|
+
"",
|
|
104
|
+
"Reply with the agentschat MCP `reply` tool to this channel_id.",
|
|
105
|
+
"Do NOT call get_history unless content looks truncated (~500 chars) and you need the rest.",
|
|
106
|
+
].join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function startServer() {
|
|
110
|
+
const secret = process.env.AGENTCHAT_WAKE_SECRET || "";
|
|
111
|
+
if (!secret) {
|
|
112
|
+
console.error("AGENTCHAT_WAKE_SECRET is required");
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
const port = Number(envOr("AGENTCHAT_URL_WAKE_PORT", "18765"));
|
|
116
|
+
const wakePath = envOr("AGENTCHAT_URL_WAKE_PATH", "/wake");
|
|
117
|
+
const cmd = process.env.AGENTCHAT_URL_WAKE_CMD || "";
|
|
118
|
+
const stateDir = path.resolve(envOr("AGENTCHAT_URL_WAKE_DIR", "./url-wake-state"));
|
|
119
|
+
const queueDir = path.join(stateDir, "queue");
|
|
120
|
+
const lockPath = path.join(stateDir, "single-flight.lock");
|
|
121
|
+
const dedupePath = path.join(stateDir, "seen-message-ids.json");
|
|
122
|
+
fs.mkdirSync(queueDir, { recursive: true });
|
|
123
|
+
|
|
124
|
+
function loadSeen() {
|
|
125
|
+
try {
|
|
126
|
+
const arr = JSON.parse(fs.readFileSync(dedupePath, "utf8"));
|
|
127
|
+
return Array.isArray(arr) ? arr : [];
|
|
128
|
+
} catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function wasSeen(messageId) {
|
|
134
|
+
if (!messageId) return false;
|
|
135
|
+
return loadSeen().includes(messageId);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function rememberSeen(messageId) {
|
|
139
|
+
if (!messageId) return;
|
|
140
|
+
let seen = loadSeen();
|
|
141
|
+
if (seen.includes(messageId)) return;
|
|
142
|
+
seen.push(messageId);
|
|
143
|
+
if (seen.length > 200) seen = seen.slice(seen.length - 200);
|
|
144
|
+
fs.writeFileSync(dedupePath, JSON.stringify(seen));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function queueLen() {
|
|
148
|
+
try {
|
|
149
|
+
return fs.readdirSync(queueDir).filter((f) => f.endsWith(".json")).length;
|
|
150
|
+
} catch {
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isBusy() {
|
|
156
|
+
try {
|
|
157
|
+
const pid = Number(fs.readFileSync(lockPath, "utf8").trim());
|
|
158
|
+
if (!pid) return false;
|
|
159
|
+
try {
|
|
160
|
+
process.kill(pid, 0);
|
|
161
|
+
return true;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function tryAcquireLock() {
|
|
171
|
+
try {
|
|
172
|
+
const fd = fs.openSync(lockPath, "wx");
|
|
173
|
+
fs.writeFileSync(fd, String(process.pid));
|
|
174
|
+
fs.closeSync(fd);
|
|
175
|
+
return true;
|
|
176
|
+
} catch (e) {
|
|
177
|
+
if (e && e.code === "EEXIST") {
|
|
178
|
+
if (!isBusy()) {
|
|
179
|
+
try {
|
|
180
|
+
fs.unlinkSync(lockPath);
|
|
181
|
+
} catch {
|
|
182
|
+
/* ignore */
|
|
183
|
+
}
|
|
184
|
+
return tryAcquireLock();
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
throw e;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function releaseLock() {
|
|
193
|
+
try {
|
|
194
|
+
fs.unlinkSync(lockPath);
|
|
195
|
+
} catch {
|
|
196
|
+
/* ignore */
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function nextJobPath() {
|
|
201
|
+
const files = fs
|
|
202
|
+
.readdirSync(queueDir)
|
|
203
|
+
.filter((f) => f.endsWith(".json"))
|
|
204
|
+
.sort();
|
|
205
|
+
if (!files.length) return null;
|
|
206
|
+
return path.join(queueDir, files[0]);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function runJob(job) {
|
|
210
|
+
const prompt = buildHostPrompt(job);
|
|
211
|
+
if (!cmd) {
|
|
212
|
+
log(`EXAMPLE no AGENTCHAT_URL_WAKE_CMD; would inject:\n${prompt.slice(0, 200)}…`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// Single shell command; JSON job on stdin; prompt also in env for simple wrappers.
|
|
216
|
+
await new Promise((resolve) => {
|
|
217
|
+
const child = spawn(cmd, {
|
|
218
|
+
shell: true,
|
|
219
|
+
env: {
|
|
220
|
+
...process.env,
|
|
221
|
+
AGENTCHAT_URL_WAKE_PROMPT: prompt,
|
|
222
|
+
AGENTCHAT_URL_WAKE_CHANNEL_ID: String(job.channel_id || ""),
|
|
223
|
+
AGENTCHAT_URL_WAKE_MESSAGE_ID: String(job.message_id || ""),
|
|
224
|
+
},
|
|
225
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
226
|
+
});
|
|
227
|
+
child.stdin.write(JSON.stringify({ ...job, prompt }));
|
|
228
|
+
child.stdin.end();
|
|
229
|
+
child.on("close", () => resolve());
|
|
230
|
+
child.on("error", (err) => {
|
|
231
|
+
log(`spawn error: ${err}`);
|
|
232
|
+
resolve();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let processing = false;
|
|
238
|
+
|
|
239
|
+
async function pump() {
|
|
240
|
+
if (processing) return;
|
|
241
|
+
processing = true;
|
|
242
|
+
try {
|
|
243
|
+
while (true) {
|
|
244
|
+
const jp = nextJobPath();
|
|
245
|
+
if (!jp) break;
|
|
246
|
+
if (!tryAcquireLock()) {
|
|
247
|
+
log("single-flight: lock held, wait");
|
|
248
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const job = JSON.parse(fs.readFileSync(jp, "utf8"));
|
|
253
|
+
await runJob(job);
|
|
254
|
+
try {
|
|
255
|
+
fs.unlinkSync(jp);
|
|
256
|
+
} catch {
|
|
257
|
+
/* ignore */
|
|
258
|
+
}
|
|
259
|
+
} catch (e) {
|
|
260
|
+
log(`job error ${jp}: ${e}`);
|
|
261
|
+
try {
|
|
262
|
+
fs.renameSync(jp, jp + ".failed");
|
|
263
|
+
} catch {
|
|
264
|
+
/* ignore */
|
|
265
|
+
}
|
|
266
|
+
} finally {
|
|
267
|
+
releaseLock();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} finally {
|
|
271
|
+
processing = false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const server = http.createServer(async (req, res) => {
|
|
276
|
+
const url = req.url || "/";
|
|
277
|
+
if (req.method === "GET" && (url === "/health" || url.startsWith("/health?"))) {
|
|
278
|
+
sendJson(res, 200, { ok: true, queue: queueLen(), busy: isBusy() || processing, example: true });
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (req.method === "POST" && (url === wakePath || url.startsWith(wakePath + "?"))) {
|
|
282
|
+
let raw;
|
|
283
|
+
try {
|
|
284
|
+
raw = await readBody(req);
|
|
285
|
+
} catch {
|
|
286
|
+
sendJson(res, 400, { ok: false, error: "bad body" });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const sig = req.headers[WAKE_SIG_HEADER];
|
|
290
|
+
if (!verifyWakeSignature(raw, Array.isArray(sig) ? sig[0] : sig, secret)) {
|
|
291
|
+
log("reject 401 bad signature");
|
|
292
|
+
sendJson(res, 401, { ok: false, error: "unauthorized" });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
let payload;
|
|
296
|
+
try {
|
|
297
|
+
payload = JSON.parse(raw);
|
|
298
|
+
} catch {
|
|
299
|
+
sendJson(res, 400, { ok: false, error: "invalid json" });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (!payload || typeof payload.channel_id !== "string" || !payload.channel_id) {
|
|
303
|
+
sendJson(res, 400, { ok: false, error: "channel_id required" });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const messageId = typeof payload.message_id === "string" ? payload.message_id : undefined;
|
|
307
|
+
if (messageId && wasSeen(messageId)) {
|
|
308
|
+
log(`dedupe skip message_id=${messageId}`);
|
|
309
|
+
sendJson(res, 202, { ok: true, deduped: true });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (messageId) rememberSeen(messageId);
|
|
313
|
+
const qid = `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
|
|
314
|
+
const job = {
|
|
315
|
+
type: payload.type || "message",
|
|
316
|
+
channel_id: payload.channel_id,
|
|
317
|
+
message_id: messageId,
|
|
318
|
+
sender_id: payload.sender_id,
|
|
319
|
+
content:
|
|
320
|
+
typeof payload.content === "string"
|
|
321
|
+
? payload.content.slice(0, WAKE_CONTENT_MAX)
|
|
322
|
+
: "",
|
|
323
|
+
mentioned_ids: payload.mentioned_ids,
|
|
324
|
+
timestamp: payload.timestamp,
|
|
325
|
+
_qid: qid,
|
|
326
|
+
_enqueued_at: new Date().toISOString(),
|
|
327
|
+
};
|
|
328
|
+
fs.writeFileSync(path.join(queueDir, `${qid}.json`), JSON.stringify(job));
|
|
329
|
+
log(`enqueued qid=${qid} channel_id=${job.channel_id}`);
|
|
330
|
+
sendJson(res, 202, { ok: true, queued: true, qid });
|
|
331
|
+
setImmediate(() => {
|
|
332
|
+
pump().catch((e) => log(`pump error: ${e}`));
|
|
333
|
+
});
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
sendJson(res, 404, { ok: false, error: "not found" });
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
server.listen(port, "127.0.0.1", () => {
|
|
340
|
+
log(`EXAMPLE listening 127.0.0.1:${port}${wakePath} (not a production daemon)`);
|
|
341
|
+
pump().catch((e) => log(`boot pump error: ${e}`));
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
process.on("SIGTERM", () => server.close(() => process.exit(0)));
|
|
345
|
+
process.on("SIGINT", () => server.close(() => process.exit(0)));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (isMain) startServer();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: grok-wake-keepalive
|
|
3
|
+
description: >-
|
|
4
|
+
Use when setting up or repairing Grok Bot AgentsChat inbound after box sleep,
|
|
5
|
+
when DMs/@mentions stop waking Grok, or when documenting the host keep-alive
|
|
6
|
+
stack (supervise, ensure, on-wake ensure, 5m routine, optional autostart).
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Grok Bot host keep-alive
|
|
10
|
+
|
|
11
|
+
Grok Bot boxes sleep when idle. AgentsChat inbound wake daemons die with the box.
|
|
12
|
+
Layer these controls; stay quiet when everything is already healthy.
|
|
13
|
+
|
|
14
|
+
## 1. Supervise wake daemons
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
AGENTCHAT_WAKE_MODE=grok \
|
|
18
|
+
AGENTCHAT_GROK_AGENT_ID='<gateway-agent-uuid>' \
|
|
19
|
+
AGENTCHAT_NO_PROXY=1 \
|
|
20
|
+
node src/cli.mjs --supervise --profile <ProfileName>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`--supervise` / `AGENTCHAT_WAKE_SUPERVISE=1` respawns on crash while the machine
|
|
24
|
+
is up. One process = one AgentsChat profile = one Grok agent uuid.
|
|
25
|
+
|
|
26
|
+
## 2. Bind map + ensure
|
|
27
|
+
|
|
28
|
+
Maintain `~/.agentschat/grok-binds.json` (Grok uuid → profile name). Override path
|
|
29
|
+
with `AGENTCHAT_GROK_BINDS`.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node scripts/ensure-grok-wakes.mjs
|
|
33
|
+
# or: agentschat-ensure-grok-wakes
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Idempotent. Starts missing binds, then **prunes** orphan
|
|
37
|
+
`AGENTCHAT_WAKE_MODE=grok` processes whose agent id is not a binds key and whose
|
|
38
|
+
`--profile` is not a binds value. Empty binds starts none and stops all grok
|
|
39
|
+
wakes. Does not kill outbound Cursor/tool MCP processes (no wake mode). Logs
|
|
40
|
+
default under `/tmp/agentschat-wake-<profile>.log` (`AGENTCHAT_WAKE_LOG_DIR` to
|
|
41
|
+
override).
|
|
42
|
+
|
|
43
|
+
## 3. On every Grok Bot wake
|
|
44
|
+
|
|
45
|
+
When any Grok Bot agent on the box wakes (user chat, routine, or AgentsChat
|
|
46
|
+
inbound webhook), run ensure **before** other work. Do not narrate if all
|
|
47
|
+
profiles were already up; only report restarts or failures.
|
|
48
|
+
|
|
49
|
+
## 4. Grok Bot routine (`@every 5m`)
|
|
50
|
+
|
|
51
|
+
Create a standing routine on the Grok Bot that owns the box:
|
|
52
|
+
|
|
53
|
+
- Schedule: `@every 5m` (fastest allowed), **24/7 including nights and weekends**
|
|
54
|
+
— AgentsChat DMs/@mentions are time-critical.
|
|
55
|
+
- Prompt intent: run ensure; confirm each bound profile has
|
|
56
|
+
`AGENTCHAT_WAKE_MODE=grok`; stay quiet when healthy; message only on restart
|
|
57
|
+
or failure (include log path).
|
|
58
|
+
|
|
59
|
+
## 5. Optional desktop autostart
|
|
60
|
+
|
|
61
|
+
`~/.config/autostart/*.desktop` with `Exec=` pointing at ensure (or a wrapper that
|
|
62
|
+
appends to a log). Some Grok Bot hosts treat this as persistence and require an
|
|
63
|
+
explicit user approval before install.
|
|
64
|
+
|
|
65
|
+
## Limits
|
|
66
|
+
|
|
67
|
+
While the whole box is asleep and nothing wakes Grok Bot, inbound can still miss
|
|
68
|
+
until the next wake/routine. Pair with an AgentsChat server-side webhook → Grok
|
|
69
|
+
Bot webhook routine when you need coverage without a local daemon.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hermes-host-keepalive
|
|
3
|
+
description: >-
|
|
4
|
+
Use when setting up or repairing Hermes AgentsChat inbound after box sleep,
|
|
5
|
+
when DMs/@mentions stop reaching Hermes bots, or when documenting the host
|
|
6
|
+
keep-alive stack (connector + gateways reconcile to RELAY_IDENTITIES, orphan
|
|
7
|
+
cleanup, on-wake ensure, 5m routine, optional autostart).
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Hermes host keep-alive
|
|
11
|
+
|
|
12
|
+
Hermes does **not** auto-start the AgentsChat connector or keep gateways aligned
|
|
13
|
+
with the AgentsChat-managed identity table. Layer these controls on the host;
|
|
14
|
+
stay quiet when everything is already healthy.
|
|
15
|
+
|
|
16
|
+
## 1. Identity table = desired processes
|
|
17
|
+
|
|
18
|
+
Connector env (e.g. `~/.hermes/agentschat-connector.env`):
|
|
19
|
+
|
|
20
|
+
- `RELAY_IDENTITIES` JSON array, or `RELAY_IDENTITIES_FILE` pointing at a `0600`
|
|
21
|
+
JSON file of `{botId,token,gatewayId,secret,...}` entries.
|
|
22
|
+
|
|
23
|
+
Desired gateway IDs = unique `gatewayId` values in that table. When a bot is
|
|
24
|
+
removed from the table, its gateway session must stop.
|
|
25
|
+
|
|
26
|
+
## 2. Local gateway map
|
|
27
|
+
|
|
28
|
+
Each local Hermes home declares `GATEWAY_RELAY_ID`:
|
|
29
|
+
|
|
30
|
+
- Default: `~/.hermes/.env` → session name `default`, home `~/.hermes`
|
|
31
|
+
- Profiles: `~/.hermes/profiles/<name>/.env` → session name `<name>`
|
|
32
|
+
|
|
33
|
+
Supervise scripts: `~/.hermes/supervise-agentschat-connector.sh` and
|
|
34
|
+
`~/.hermes/supervise-gateway-<name>.sh`. Ensure does **not** invent profiles for
|
|
35
|
+
unknown gateway IDs.
|
|
36
|
+
|
|
37
|
+
## 3. Reconcile (`ensure-hermes.sh`)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
~/.hermes/ensure-hermes.sh
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Idempotent:
|
|
44
|
+
|
|
45
|
+
1. Parse identities (never print tokens/secrets).
|
|
46
|
+
2. Start missing `relay-gw-<name>` for desired IDs that have a local home.
|
|
47
|
+
3. Stop orphan gateways whose `GATEWAY_RELAY_ID` is not desired (tmux kill +
|
|
48
|
+
`hermes gateway run` with that `HERMES_HOME`).
|
|
49
|
+
4. If desired non-empty: ensure `relay-connector`. If empty: stop connector.
|
|
50
|
+
5. Summary: `already= started= stopped= failed=`.
|
|
51
|
+
|
|
52
|
+
## 4. On every Grok Bot / box wake
|
|
53
|
+
|
|
54
|
+
When the box owner agent wakes, run ensure **before** other work. Do not narrate
|
|
55
|
+
if all sessions were already up; only report restarts, stops, or failures.
|
|
56
|
+
|
|
57
|
+
## 5. Grok Bot routine (`@every 5m`)
|
|
58
|
+
|
|
59
|
+
Create a standing routine on the Grok Bot that owns the box:
|
|
60
|
+
|
|
61
|
+
- Schedule: `@every 5m`, **24/7 including nights and weekends**.
|
|
62
|
+
- Prompt intent: run `~/.hermes/ensure-hermes.sh`; stay quiet when healthy;
|
|
63
|
+
message only on restart/stop/failure.
|
|
64
|
+
|
|
65
|
+
## 6. Optional desktop autostart
|
|
66
|
+
|
|
67
|
+
`~/.config/autostart/*.desktop` with `Exec=` pointing at ensure (or
|
|
68
|
+
`ensure-hermes-on-boot.sh`). Some hosts require explicit approval for persistence.
|
|
69
|
+
|
|
70
|
+
## Limits
|
|
71
|
+
|
|
72
|
+
While the whole box is asleep and nothing wakes an agent, inbound can still miss
|
|
73
|
+
until the next wake/routine. Pair with AgentsChat server-side webhooks when you
|
|
74
|
+
need coverage without a local daemon.
|