omoclaw 2.4.0 → 3.0.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/dist/index.js +397 -18
- package/dist/project-config.d.ts +13 -0
- package/dist/registry.d.ts +57 -0
- package/dist/webhook.d.ts +6 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/index.ts
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
3
|
+
import fs5 from "fs";
|
|
4
|
+
import os3 from "os";
|
|
5
|
+
import path4 from "path";
|
|
6
6
|
|
|
7
7
|
// src/config.ts
|
|
8
8
|
import fs from "fs";
|
|
@@ -238,6 +238,344 @@ function handleSessionStatusEvent(event, dedup, state, timers, webhookFn, debugL
|
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
// src/project-config.ts
|
|
242
|
+
import fs2 from "fs";
|
|
243
|
+
import path2 from "path";
|
|
244
|
+
var PROJECT_CONFIG_FILE = ".omoclaw.local.json";
|
|
245
|
+
function asObject2(value) {
|
|
246
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
return value;
|
|
250
|
+
}
|
|
251
|
+
function asString(value) {
|
|
252
|
+
return typeof value === "string" ? value.trim() : "";
|
|
253
|
+
}
|
|
254
|
+
function parseSessionKey(sessionKey) {
|
|
255
|
+
const parts = sessionKey.split(":");
|
|
256
|
+
if (parts.length < 5 || parts[0] !== "agent") {
|
|
257
|
+
return { agentId: "", channelId: "" };
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
agentId: parts[1] ?? "",
|
|
261
|
+
channelId: parts.slice(4).join(":")
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function buildSessionKeyFromConfig(config) {
|
|
265
|
+
const webhook = config.webhook;
|
|
266
|
+
if (!webhook.channel) {
|
|
267
|
+
return "";
|
|
268
|
+
}
|
|
269
|
+
return `agent:${webhook.agentId}:${webhook.channelType}:${webhook.chatType}:${webhook.channel}`;
|
|
270
|
+
}
|
|
271
|
+
function loadProjectConfig(cwd = process.cwd()) {
|
|
272
|
+
const filePath = path2.join(cwd, PROJECT_CONFIG_FILE);
|
|
273
|
+
try {
|
|
274
|
+
const stats = fs2.statSync(filePath);
|
|
275
|
+
const mode = stats.mode & 511;
|
|
276
|
+
if (mode !== 384) {
|
|
277
|
+
try {
|
|
278
|
+
fs2.chmodSync(filePath, 384);
|
|
279
|
+
} catch {}
|
|
280
|
+
}
|
|
281
|
+
const raw = fs2.readFileSync(filePath, "utf8");
|
|
282
|
+
const parsed = JSON.parse(raw);
|
|
283
|
+
const root = asObject2(parsed);
|
|
284
|
+
if (!root) {
|
|
285
|
+
return { sessionKey: "", tmuxSession: "" };
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
sessionKey: asString(root.sessionKey),
|
|
289
|
+
tmuxSession: asString(root.tmuxSession)
|
|
290
|
+
};
|
|
291
|
+
} catch {
|
|
292
|
+
return { sessionKey: "", tmuxSession: "" };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function resolveSessionRouting(config, projectConfig) {
|
|
296
|
+
const envSessionKey = asString(process.env.OPENCLAW_SESSION_KEY);
|
|
297
|
+
if (envSessionKey) {
|
|
298
|
+
const parsed = parseSessionKey(envSessionKey);
|
|
299
|
+
return {
|
|
300
|
+
sessionKey: envSessionKey,
|
|
301
|
+
source: "env",
|
|
302
|
+
agentId: parsed.agentId || config.webhook.agentId,
|
|
303
|
+
channelId: parsed.channelId || config.webhook.channel
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (projectConfig.sessionKey) {
|
|
307
|
+
const parsed = parseSessionKey(projectConfig.sessionKey);
|
|
308
|
+
return {
|
|
309
|
+
sessionKey: projectConfig.sessionKey,
|
|
310
|
+
source: "project",
|
|
311
|
+
agentId: parsed.agentId || config.webhook.agentId,
|
|
312
|
+
channelId: parsed.channelId || config.webhook.channel
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
if (config.webhook.sessionKey) {
|
|
316
|
+
const parsed = parseSessionKey(config.webhook.sessionKey);
|
|
317
|
+
return {
|
|
318
|
+
sessionKey: config.webhook.sessionKey,
|
|
319
|
+
source: "config",
|
|
320
|
+
agentId: parsed.agentId || config.webhook.agentId,
|
|
321
|
+
channelId: parsed.channelId || config.webhook.channel
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
const derivedSessionKey = buildSessionKeyFromConfig(config);
|
|
325
|
+
if (derivedSessionKey) {
|
|
326
|
+
return {
|
|
327
|
+
sessionKey: derivedSessionKey,
|
|
328
|
+
source: "derived",
|
|
329
|
+
agentId: config.webhook.agentId,
|
|
330
|
+
channelId: config.webhook.channel
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
sessionKey: "",
|
|
335
|
+
source: "none",
|
|
336
|
+
agentId: config.webhook.agentId,
|
|
337
|
+
channelId: config.webhook.channel
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/registry.ts
|
|
342
|
+
import { createHash, randomUUID } from "crypto";
|
|
343
|
+
import fs3 from "fs";
|
|
344
|
+
import os2 from "os";
|
|
345
|
+
import path3 from "path";
|
|
346
|
+
var REGISTRY_VERSION = 1;
|
|
347
|
+
var DEFAULT_TTL_MS = 60000;
|
|
348
|
+
var DEFAULT_HEARTBEAT_MS = 20000;
|
|
349
|
+
var REGISTRY_DIR_PATH = path3.join(os2.homedir(), ".local", "state", "omoclaw");
|
|
350
|
+
var REGISTRY_FILE_PATH = path3.join(REGISTRY_DIR_PATH, "registry.json");
|
|
351
|
+
function asObject3(value) {
|
|
352
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
return value;
|
|
356
|
+
}
|
|
357
|
+
function asString2(value) {
|
|
358
|
+
return typeof value === "string" ? value : "";
|
|
359
|
+
}
|
|
360
|
+
function errorToString(error) {
|
|
361
|
+
if (error instanceof Error) {
|
|
362
|
+
return `${error.name}: ${error.message}`;
|
|
363
|
+
}
|
|
364
|
+
return String(error);
|
|
365
|
+
}
|
|
366
|
+
function parseRegistryEntry(value) {
|
|
367
|
+
const entry = asObject3(value);
|
|
368
|
+
if (!entry) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const pid = typeof entry.pid === "number" ? entry.pid : Number.NaN;
|
|
372
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const parsed = {
|
|
376
|
+
instanceId: asString2(entry.instanceId),
|
|
377
|
+
hostname: asString2(entry.hostname),
|
|
378
|
+
hostLabel: asString2(entry.hostLabel),
|
|
379
|
+
pid,
|
|
380
|
+
startedAt: asString2(entry.startedAt),
|
|
381
|
+
leaseExpiresAt: asString2(entry.leaseExpiresAt),
|
|
382
|
+
project: asString2(entry.project),
|
|
383
|
+
projectId: asString2(entry.projectId),
|
|
384
|
+
workspaceRoot: asString2(entry.workspaceRoot),
|
|
385
|
+
tmuxSession: asString2(entry.tmuxSession),
|
|
386
|
+
openclawSessionKey: asString2(entry.openclawSessionKey),
|
|
387
|
+
agentId: asString2(entry.agentId),
|
|
388
|
+
channelId: asString2(entry.channelId)
|
|
389
|
+
};
|
|
390
|
+
if (!parsed.instanceId || !parsed.hostname || !parsed.hostLabel || !parsed.startedAt || !parsed.leaseExpiresAt || !parsed.project || !parsed.projectId || !parsed.workspaceRoot) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
return parsed;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
class Registry {
|
|
397
|
+
instanceId = randomUUID();
|
|
398
|
+
hostname = os2.hostname();
|
|
399
|
+
workspaceRoot;
|
|
400
|
+
project;
|
|
401
|
+
projectId;
|
|
402
|
+
ttlMs;
|
|
403
|
+
heartbeatMs;
|
|
404
|
+
debugLog;
|
|
405
|
+
heartbeatTimer;
|
|
406
|
+
hooksInstalled = false;
|
|
407
|
+
entryTemplate;
|
|
408
|
+
stopOnProcessExit = () => {
|
|
409
|
+
this.stop();
|
|
410
|
+
};
|
|
411
|
+
constructor(options = {}) {
|
|
412
|
+
this.workspaceRoot = options.workspaceRoot ?? process.cwd();
|
|
413
|
+
this.project = path3.basename(this.workspaceRoot);
|
|
414
|
+
this.projectId = createHash("sha256").update(this.workspaceRoot).digest("hex");
|
|
415
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
416
|
+
this.heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
|
|
417
|
+
this.debugLog = options.debugLog;
|
|
418
|
+
}
|
|
419
|
+
start(input) {
|
|
420
|
+
const startedAt = new Date().toISOString();
|
|
421
|
+
this.entryTemplate = {
|
|
422
|
+
instanceId: this.instanceId,
|
|
423
|
+
hostname: this.hostname,
|
|
424
|
+
hostLabel: input.hostLabel,
|
|
425
|
+
pid: process.pid,
|
|
426
|
+
startedAt,
|
|
427
|
+
project: this.project,
|
|
428
|
+
projectId: this.projectId,
|
|
429
|
+
workspaceRoot: this.workspaceRoot,
|
|
430
|
+
tmuxSession: input.tmuxSession,
|
|
431
|
+
openclawSessionKey: input.openclawSessionKey,
|
|
432
|
+
agentId: input.agentId,
|
|
433
|
+
channelId: input.channelId
|
|
434
|
+
};
|
|
435
|
+
this.upsert();
|
|
436
|
+
this.installProcessHooks();
|
|
437
|
+
this.startHeartbeat();
|
|
438
|
+
}
|
|
439
|
+
stop() {
|
|
440
|
+
if (this.heartbeatTimer) {
|
|
441
|
+
clearInterval(this.heartbeatTimer);
|
|
442
|
+
this.heartbeatTimer = undefined;
|
|
443
|
+
}
|
|
444
|
+
this.remove();
|
|
445
|
+
}
|
|
446
|
+
readSessions() {
|
|
447
|
+
const registry = this.readAndGc();
|
|
448
|
+
return registry.sessions;
|
|
449
|
+
}
|
|
450
|
+
startHeartbeat() {
|
|
451
|
+
if (this.heartbeatTimer) {
|
|
452
|
+
clearInterval(this.heartbeatTimer);
|
|
453
|
+
}
|
|
454
|
+
this.heartbeatTimer = setInterval(() => {
|
|
455
|
+
this.upsert();
|
|
456
|
+
}, this.heartbeatMs);
|
|
457
|
+
this.heartbeatTimer.unref?.();
|
|
458
|
+
}
|
|
459
|
+
installProcessHooks() {
|
|
460
|
+
if (this.hooksInstalled) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
this.hooksInstalled = true;
|
|
464
|
+
process.once("beforeExit", this.stopOnProcessExit);
|
|
465
|
+
process.once("exit", this.stopOnProcessExit);
|
|
466
|
+
}
|
|
467
|
+
upsert() {
|
|
468
|
+
if (!this.entryTemplate) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const leaseExpiresAt = new Date(Date.now() + this.ttlMs).toISOString();
|
|
472
|
+
const entry = {
|
|
473
|
+
...this.entryTemplate,
|
|
474
|
+
leaseExpiresAt
|
|
475
|
+
};
|
|
476
|
+
this.updateSessions((sessions) => {
|
|
477
|
+
const existingIndex = sessions.findIndex((session) => session.instanceId === this.instanceId);
|
|
478
|
+
if (existingIndex === -1) {
|
|
479
|
+
return [...sessions, entry];
|
|
480
|
+
}
|
|
481
|
+
const next = [...sessions];
|
|
482
|
+
next[existingIndex] = entry;
|
|
483
|
+
return next;
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
remove() {
|
|
487
|
+
this.updateSessions((sessions) => {
|
|
488
|
+
return sessions.filter((session) => session.instanceId !== this.instanceId);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
updateSessions(updater) {
|
|
492
|
+
try {
|
|
493
|
+
const current = this.readAndGc();
|
|
494
|
+
const updatedSessions = updater(current.sessions);
|
|
495
|
+
this.writeRegistryFile({
|
|
496
|
+
version: REGISTRY_VERSION,
|
|
497
|
+
sessions: updatedSessions
|
|
498
|
+
});
|
|
499
|
+
} catch (error) {
|
|
500
|
+
this.debugLog?.(`registry update failed: ${errorToString(error)}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
readAndGc() {
|
|
504
|
+
const registry = this.readRegistryFile();
|
|
505
|
+
const now = Date.now();
|
|
506
|
+
const sessions = registry.sessions.filter((session) => {
|
|
507
|
+
const expiresAtMs = Date.parse(session.leaseExpiresAt);
|
|
508
|
+
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now) {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
return this.isPidAlive(session.pid);
|
|
512
|
+
});
|
|
513
|
+
if (sessions.length !== registry.sessions.length) {
|
|
514
|
+
this.writeRegistryFile({
|
|
515
|
+
version: REGISTRY_VERSION,
|
|
516
|
+
sessions
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
version: REGISTRY_VERSION,
|
|
521
|
+
sessions
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
readRegistryFile() {
|
|
525
|
+
this.ensureRegistryDir();
|
|
526
|
+
try {
|
|
527
|
+
const raw = fs3.readFileSync(REGISTRY_FILE_PATH, "utf8");
|
|
528
|
+
const parsed = JSON.parse(raw);
|
|
529
|
+
const root = asObject3(parsed);
|
|
530
|
+
const items = Array.isArray(root?.sessions) ? root.sessions : [];
|
|
531
|
+
const sessions = items.map((item) => parseRegistryEntry(item)).filter((item) => item !== undefined);
|
|
532
|
+
return {
|
|
533
|
+
version: REGISTRY_VERSION,
|
|
534
|
+
sessions
|
|
535
|
+
};
|
|
536
|
+
} catch {
|
|
537
|
+
return {
|
|
538
|
+
version: REGISTRY_VERSION,
|
|
539
|
+
sessions: []
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
writeRegistryFile(registry) {
|
|
544
|
+
this.ensureRegistryDir();
|
|
545
|
+
const payload = JSON.stringify({
|
|
546
|
+
version: REGISTRY_VERSION,
|
|
547
|
+
sessions: registry.sessions
|
|
548
|
+
}, null, 2);
|
|
549
|
+
const tmpPath = `${REGISTRY_FILE_PATH}.${process.pid}.${Date.now()}.tmp`;
|
|
550
|
+
fs3.writeFileSync(tmpPath, payload, { mode: 384 });
|
|
551
|
+
fs3.renameSync(tmpPath, REGISTRY_FILE_PATH);
|
|
552
|
+
try {
|
|
553
|
+
fs3.chmodSync(REGISTRY_FILE_PATH, 384);
|
|
554
|
+
} catch {}
|
|
555
|
+
}
|
|
556
|
+
ensureRegistryDir() {
|
|
557
|
+
fs3.mkdirSync(REGISTRY_DIR_PATH, { recursive: true, mode: 448 });
|
|
558
|
+
try {
|
|
559
|
+
fs3.chmodSync(REGISTRY_DIR_PATH, 448);
|
|
560
|
+
} catch {}
|
|
561
|
+
}
|
|
562
|
+
isPidAlive(pid) {
|
|
563
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
if (pid === process.pid) {
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
process.kill(pid, 0);
|
|
571
|
+
return true;
|
|
572
|
+
} catch (error) {
|
|
573
|
+
const code = error.code;
|
|
574
|
+
return code === "EPERM";
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
241
579
|
// src/state.ts
|
|
242
580
|
class SessionStateTracker {
|
|
243
581
|
states = new Map;
|
|
@@ -403,26 +741,54 @@ class TimerManager {
|
|
|
403
741
|
}
|
|
404
742
|
|
|
405
743
|
// src/webhook.ts
|
|
406
|
-
import
|
|
744
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
745
|
+
import fs4 from "fs";
|
|
407
746
|
var LOG_PATH = "/tmp/opencode-monitor-debug.log";
|
|
408
747
|
function wlog(msg) {
|
|
409
748
|
try {
|
|
410
|
-
|
|
749
|
+
fs4.appendFileSync(LOG_PATH, `[${new Date().toISOString()}] WEBHOOK_IO: ${msg}
|
|
411
750
|
`);
|
|
412
751
|
} catch {}
|
|
413
752
|
}
|
|
414
|
-
function
|
|
415
|
-
const { webhook } = config;
|
|
416
|
-
const body = { text, mode: "now" };
|
|
753
|
+
function resolveFallbackSessionKey(config) {
|
|
417
754
|
const envSessionKey = process.env.OPENCLAW_SESSION_KEY;
|
|
418
755
|
if (envSessionKey) {
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
756
|
+
return envSessionKey;
|
|
757
|
+
}
|
|
758
|
+
if (config.webhook.sessionKey) {
|
|
759
|
+
return config.webhook.sessionKey;
|
|
760
|
+
}
|
|
761
|
+
if (config.webhook.channel) {
|
|
762
|
+
return `agent:${config.webhook.agentId}:${config.webhook.channelType}:${config.webhook.chatType}:${config.webhook.channel}`;
|
|
763
|
+
}
|
|
764
|
+
return "";
|
|
765
|
+
}
|
|
766
|
+
function buildCorrelationId(text, provided) {
|
|
767
|
+
if (provided && provided.trim()) {
|
|
768
|
+
return provided.trim();
|
|
424
769
|
}
|
|
425
|
-
|
|
770
|
+
const sessionMatch = text.match(/\((ses_[A-Za-z0-9_-]+)\)/);
|
|
771
|
+
if (sessionMatch?.[1]) {
|
|
772
|
+
return `${sessionMatch[1]}:${Date.now()}`;
|
|
773
|
+
}
|
|
774
|
+
return randomUUID2();
|
|
775
|
+
}
|
|
776
|
+
function sendWebhook(config, text, options = {}) {
|
|
777
|
+
const { webhook } = config;
|
|
778
|
+
const envSessionKey = process.env.OPENCLAW_SESSION_KEY?.trim();
|
|
779
|
+
const sessionKey = envSessionKey || options.sessionKey?.trim() || resolveFallbackSessionKey(config);
|
|
780
|
+
const correlationId = buildCorrelationId(text, options.correlationId);
|
|
781
|
+
const body = {
|
|
782
|
+
text,
|
|
783
|
+
mode: "now",
|
|
784
|
+
metadata: {
|
|
785
|
+
correlationId
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
if (sessionKey) {
|
|
789
|
+
body.sessionKey = sessionKey;
|
|
790
|
+
}
|
|
791
|
+
wlog(`Sending /hooks/wake to ${webhook.url} (session: ${sessionKey || "main"}, correlationId: ${correlationId}): ${text}`);
|
|
426
792
|
fetch(webhook.url, {
|
|
427
793
|
method: "POST",
|
|
428
794
|
headers: {
|
|
@@ -442,26 +808,37 @@ function sendWebhook(config, text) {
|
|
|
442
808
|
// src/index.ts
|
|
443
809
|
var DEBUG_LOG_PATH = "/tmp/opencode-monitor-debug.log";
|
|
444
810
|
function buildPrefix(config) {
|
|
445
|
-
const host = config.hostLabel ||
|
|
446
|
-
const project =
|
|
811
|
+
const host = config.hostLabel || os3.hostname();
|
|
812
|
+
const project = path4.basename(process.cwd());
|
|
447
813
|
return `[OpenCode@${host}:${project}]`;
|
|
448
814
|
}
|
|
449
815
|
var SessionMonitorPlugin = async (_ctx) => {
|
|
450
816
|
const config = loadConfig();
|
|
817
|
+
const projectConfig = loadProjectConfig();
|
|
818
|
+
const sessionRouting = resolveSessionRouting(config, projectConfig);
|
|
451
819
|
const prefix = buildPrefix(config);
|
|
452
820
|
const debugLog = config.debug ? (msg) => {
|
|
453
821
|
const line = `[${new Date().toISOString()}] ${msg}
|
|
454
822
|
`;
|
|
455
823
|
try {
|
|
456
|
-
|
|
824
|
+
fs5.appendFileSync(DEBUG_LOG_PATH, line);
|
|
457
825
|
} catch {}
|
|
458
826
|
} : undefined;
|
|
459
827
|
debugLog?.("Plugin initializing...");
|
|
460
828
|
debugLog?.(`Config loaded: enabled=${config.enabled}, url=${config.webhook.url}`);
|
|
829
|
+
debugLog?.(`Routing session source=${sessionRouting.source}`);
|
|
461
830
|
if (!config.enabled) {
|
|
462
831
|
debugLog?.("Plugin disabled, exiting.");
|
|
463
832
|
return {};
|
|
464
833
|
}
|
|
834
|
+
const registry = new Registry({ debugLog });
|
|
835
|
+
registry.start({
|
|
836
|
+
hostLabel: config.hostLabel || os3.hostname(),
|
|
837
|
+
tmuxSession: projectConfig.tmuxSession,
|
|
838
|
+
openclawSessionKey: sessionRouting.sessionKey,
|
|
839
|
+
agentId: sessionRouting.agentId,
|
|
840
|
+
channelId: sessionRouting.channelId
|
|
841
|
+
});
|
|
465
842
|
if (!config.webhook.url || !config.webhook.token) {
|
|
466
843
|
console.log("[monitor] Webhook not configured. Set webhook.url and webhook.token in ~/.config/opencode/opencode-monitor.json");
|
|
467
844
|
return {};
|
|
@@ -471,7 +848,9 @@ var SessionMonitorPlugin = async (_ctx) => {
|
|
|
471
848
|
const webhookFn = (text) => {
|
|
472
849
|
const tagged = text.startsWith("[OpenCode]") ? prefix + text.slice("[OpenCode]".length) : `${prefix} ${text}`;
|
|
473
850
|
debugLog?.(`WEBHOOK: ${tagged}`);
|
|
474
|
-
sendWebhook(config, tagged
|
|
851
|
+
sendWebhook(config, tagged, {
|
|
852
|
+
sessionKey: sessionRouting.sessionKey
|
|
853
|
+
});
|
|
475
854
|
};
|
|
476
855
|
const timers = new TimerManager(config, webhookFn);
|
|
477
856
|
debugLog?.("Plugin loaded successfully. Waiting for events...");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MonitorConfig } from "./config";
|
|
2
|
+
export interface ProjectConfig {
|
|
3
|
+
sessionKey: string;
|
|
4
|
+
tmuxSession: string;
|
|
5
|
+
}
|
|
6
|
+
export interface SessionRouting {
|
|
7
|
+
sessionKey: string;
|
|
8
|
+
source: "env" | "project" | "config" | "derived" | "none";
|
|
9
|
+
agentId: string;
|
|
10
|
+
channelId: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function loadProjectConfig(cwd?: string): ProjectConfig;
|
|
13
|
+
export declare function resolveSessionRouting(config: MonitorConfig, projectConfig: ProjectConfig): SessionRouting;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export declare const REGISTRY_FILE_PATH: string;
|
|
2
|
+
export interface RegistryEntry {
|
|
3
|
+
instanceId: string;
|
|
4
|
+
hostname: string;
|
|
5
|
+
hostLabel: string;
|
|
6
|
+
pid: number;
|
|
7
|
+
startedAt: string;
|
|
8
|
+
leaseExpiresAt: string;
|
|
9
|
+
project: string;
|
|
10
|
+
projectId: string;
|
|
11
|
+
workspaceRoot: string;
|
|
12
|
+
tmuxSession: string;
|
|
13
|
+
openclawSessionKey: string;
|
|
14
|
+
agentId: string;
|
|
15
|
+
channelId: string;
|
|
16
|
+
}
|
|
17
|
+
export interface RegistryStartInput {
|
|
18
|
+
hostLabel: string;
|
|
19
|
+
tmuxSession: string;
|
|
20
|
+
openclawSessionKey: string;
|
|
21
|
+
agentId: string;
|
|
22
|
+
channelId: string;
|
|
23
|
+
}
|
|
24
|
+
export interface RegistryOptions {
|
|
25
|
+
workspaceRoot?: string;
|
|
26
|
+
ttlMs?: number;
|
|
27
|
+
heartbeatMs?: number;
|
|
28
|
+
debugLog?: (msg: string) => void;
|
|
29
|
+
}
|
|
30
|
+
export declare class Registry {
|
|
31
|
+
private readonly instanceId;
|
|
32
|
+
private readonly hostname;
|
|
33
|
+
private readonly workspaceRoot;
|
|
34
|
+
private readonly project;
|
|
35
|
+
private readonly projectId;
|
|
36
|
+
private readonly ttlMs;
|
|
37
|
+
private readonly heartbeatMs;
|
|
38
|
+
private readonly debugLog;
|
|
39
|
+
private heartbeatTimer;
|
|
40
|
+
private hooksInstalled;
|
|
41
|
+
private entryTemplate;
|
|
42
|
+
private readonly stopOnProcessExit;
|
|
43
|
+
constructor(options?: RegistryOptions);
|
|
44
|
+
start(input: RegistryStartInput): void;
|
|
45
|
+
stop(): void;
|
|
46
|
+
readSessions(): RegistryEntry[];
|
|
47
|
+
private startHeartbeat;
|
|
48
|
+
private installProcessHooks;
|
|
49
|
+
private upsert;
|
|
50
|
+
private remove;
|
|
51
|
+
private updateSessions;
|
|
52
|
+
private readAndGc;
|
|
53
|
+
private readRegistryFile;
|
|
54
|
+
private writeRegistryFile;
|
|
55
|
+
private ensureRegistryDir;
|
|
56
|
+
private isPidAlive;
|
|
57
|
+
}
|
package/dist/webhook.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import type { MonitorConfig } from "./config";
|
|
2
|
-
|
|
2
|
+
interface SendWebhookOptions {
|
|
3
|
+
sessionKey?: string;
|
|
4
|
+
correlationId?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function sendWebhook(config: MonitorConfig, text: string, options?: SendWebhookOptions): void;
|
|
7
|
+
export {};
|
package/package.json
CHANGED