@projectstar/agxp-openclaw 0.0.1

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 ADDED
@@ -0,0 +1,2819 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ default: () => index_default
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+ var os4 = __toESM(require("os"));
37
+ var import_plugin_entry = require("openclaw/plugin-sdk/plugin-entry");
38
+
39
+ // src/cli-executor.ts
40
+ var import_child_process = require("child_process");
41
+ var EXIT_SESSION_REQUIRED = 4;
42
+ var DEFAULT_TIMEOUT_MS = 3e4;
43
+ function execAgxp(bin, args, options) {
44
+ const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
45
+ const logger = options?.logger;
46
+ return new Promise((resolve) => {
47
+ logger?.debug(`execAgxp: ${bin} ${args.join(" ")}`);
48
+ (0, import_child_process.execFile)(
49
+ bin,
50
+ args,
51
+ {
52
+ timeout,
53
+ maxBuffer: 10 * 1024 * 1024,
54
+ encoding: "utf-8",
55
+ ...options?.cwd ? { cwd: options.cwd } : {}
56
+ },
57
+ (error, stdout, stderr) => {
58
+ if (error) {
59
+ const exitCode = error.code;
60
+ if (exitCode === "ENOENT") {
61
+ logger?.warn(`execAgxp: binary not found: ${bin}`);
62
+ resolve({ kind: "not_installed", bin });
63
+ return;
64
+ }
65
+ const numericExit = typeof exitCode === "number" ? exitCode : error.killed ? null : error.status ?? null;
66
+ if (numericExit === EXIT_SESSION_REQUIRED) {
67
+ logger?.warn(`execAgxp session required: ${stderr.trim()}`);
68
+ resolve({ kind: "session_required", stderr: stderr.trim() });
69
+ return;
70
+ }
71
+ logger?.error(`execAgxp failed (exit=${numericExit}): ${stderr.trim() || error.message}`);
72
+ resolve({
73
+ kind: "error",
74
+ error: new Error(stderr.trim() || error.message),
75
+ exitCode: numericExit,
76
+ stderr: stderr.trim()
77
+ });
78
+ return;
79
+ }
80
+ const trimmed = stdout.trim();
81
+ if (!trimmed) {
82
+ resolve({
83
+ kind: "success",
84
+ data: void 0
85
+ });
86
+ return;
87
+ }
88
+ if (options?.parseJson === false) {
89
+ resolve({ kind: "success", data: trimmed });
90
+ return;
91
+ }
92
+ try {
93
+ const data = JSON.parse(trimmed);
94
+ resolve({ kind: "success", data });
95
+ } catch (parseError) {
96
+ logger?.error(`execAgxp JSON parse error: ${parseError.message}`);
97
+ resolve({
98
+ kind: "error",
99
+ error: new Error(`Failed to parse CLI output: ${parseError.message}`),
100
+ exitCode: 0,
101
+ stderr: ""
102
+ });
103
+ }
104
+ }
105
+ );
106
+ });
107
+ }
108
+
109
+ // src/timeline-client.ts
110
+ var POLL_INTERVAL_CONFIG_KEY = "timeline_poll_interval";
111
+ var DEFAULT_POLL_INTERVAL_SEC = 600;
112
+ var MIN_POLL_INTERVAL_SEC = 10;
113
+ var MAX_POLL_INTERVAL_SEC = 24 * 60 * 60;
114
+ async function readPollIntervalSec(agxpBin, serverName, logger) {
115
+ const result = await execAgxp(
116
+ agxpBin,
117
+ ["config", "get", "--key", POLL_INTERVAL_CONFIG_KEY, "--server", serverName, "--output", "json"],
118
+ { logger }
119
+ );
120
+ if (result.kind !== "success" || result.data === void 0 || result.data === null) {
121
+ return DEFAULT_POLL_INTERVAL_SEC;
122
+ }
123
+ let numeric;
124
+ if (typeof result.data === "number" && Number.isFinite(result.data)) {
125
+ numeric = result.data;
126
+ } else if (typeof result.data === "string") {
127
+ const parsed = Number(result.data.trim());
128
+ if (Number.isFinite(parsed)) {
129
+ numeric = parsed;
130
+ }
131
+ }
132
+ if (numeric === void 0) {
133
+ logger.warn(
134
+ `Ignoring non-numeric pollInterval from agxp config (server=${serverName}, value=${JSON.stringify(result.data)}); using ${DEFAULT_POLL_INTERVAL_SEC}s`
135
+ );
136
+ return DEFAULT_POLL_INTERVAL_SEC;
137
+ }
138
+ const floored = Math.floor(numeric);
139
+ if (floored < MIN_POLL_INTERVAL_SEC || floored > MAX_POLL_INTERVAL_SEC) {
140
+ logger.warn(
141
+ `pollInterval ${floored}s from agxp config (server=${serverName}) is outside [${MIN_POLL_INTERVAL_SEC}s, ${MAX_POLL_INTERVAL_SEC}s]; using ${DEFAULT_POLL_INTERVAL_SEC}s`
142
+ );
143
+ return DEFAULT_POLL_INTERVAL_SEC;
144
+ }
145
+ return floored;
146
+ }
147
+ var AGXPTimelineClient = class {
148
+ constructor(config) {
149
+ this.timeoutId = null;
150
+ this.isRunning = false;
151
+ this.activePoll = null;
152
+ this.config = config;
153
+ }
154
+ async start() {
155
+ if (this.isRunning) {
156
+ this.config.logger.warn("Timeline client already running");
157
+ return;
158
+ }
159
+ this.isRunning = true;
160
+ this.config.logger.info(
161
+ `Starting timeline client for server=${this.config.serverName}`
162
+ );
163
+ await this.pollOnce();
164
+ this.scheduleNext();
165
+ }
166
+ stop() {
167
+ if (!this.isRunning) {
168
+ return;
169
+ }
170
+ this.config.logger.info(`Stopping timeline client for server=${this.config.serverName}`);
171
+ this.isRunning = false;
172
+ if (this.timeoutId) {
173
+ clearTimeout(this.timeoutId);
174
+ this.timeoutId = null;
175
+ }
176
+ }
177
+ async scheduleNext() {
178
+ if (!this.isRunning) {
179
+ return;
180
+ }
181
+ let intervalSec;
182
+ try {
183
+ intervalSec = await this.config.resolvePollIntervalSec();
184
+ } catch (error) {
185
+ this.config.logger.warn(
186
+ `Failed to resolve pollInterval for server=${this.config.serverName}: ${error instanceof Error ? error.message : String(error)}; using ${DEFAULT_POLL_INTERVAL_SEC}s`
187
+ );
188
+ intervalSec = DEFAULT_POLL_INTERVAL_SEC;
189
+ }
190
+ if (!this.isRunning) {
191
+ return;
192
+ }
193
+ this.config.logger.debug(
194
+ `Scheduling next timeline poll for server=${this.config.serverName} in ${intervalSec}s`
195
+ );
196
+ this.timeoutId = setTimeout(() => {
197
+ this.timeoutId = null;
198
+ this.pollOnce().catch((err) => {
199
+ this.config.logger.error(
200
+ `Polling error: ${err instanceof Error ? err.message : String(err)}`
201
+ );
202
+ }).finally(() => {
203
+ this.scheduleNext();
204
+ });
205
+ }, intervalSec * 1e3);
206
+ }
207
+ async pollOnce(options = {}) {
208
+ if (this.activePoll) {
209
+ this.config.logger.warn("Skipping timeline poll because a previous poll is still in progress");
210
+ return this.activePoll;
211
+ }
212
+ const run = async () => {
213
+ const notifyTimeline = options.notifyTimeline ?? true;
214
+ const notifySessionRequired = options.notifySessionRequired ?? true;
215
+ try {
216
+ this.config.logger.info(`Polling timeline via CLI for server=${this.config.serverName}`);
217
+ const result = await execAgxp(
218
+ this.config.agxpBin,
219
+ ["timeline", "pull", "--limit", "20", "--action", "refresh", "-s", this.config.serverName, "-f", "json"],
220
+ { logger: this.config.logger }
221
+ );
222
+ if (result.kind === "session_required") {
223
+ const sessionEvent = { reason: "session_required" };
224
+ if (notifySessionRequired) {
225
+ await this.config.onSessionRequired(sessionEvent);
226
+ }
227
+ return { kind: "session_required", sessionEvent };
228
+ }
229
+ if (result.kind === "not_installed") {
230
+ return {
231
+ kind: "error",
232
+ error: new Error(`agxp CLI not installed (bin=${result.bin})`)
233
+ };
234
+ }
235
+ if (result.kind === "error") {
236
+ return { kind: "error", error: result.error };
237
+ }
238
+ const timelineResponse = {
239
+ result: result.data,
240
+ meta: {
241
+ next: null,
242
+ has_more: result.data?.has_more ?? false
243
+ }
244
+ };
245
+ const items = timelineResponse.result.items ?? [];
246
+ const notifications = timelineResponse.result.notifications ?? [];
247
+ this.config.logger.info(
248
+ `Polled timeline: ${items.length} posts, notifications=${notifications.length}, has_more=${timelineResponse.result.has_more}`
249
+ );
250
+ if (notifyTimeline && (items.length > 0 || notifications.length > 0)) {
251
+ await this.config.onTimelinePolled(timelineResponse);
252
+ }
253
+ return { kind: "success", payload: timelineResponse };
254
+ } catch (error) {
255
+ const normalized = error instanceof Error ? error : new Error(String(error));
256
+ this.config.logger.error(
257
+ `Failed to poll timeline for server=${this.config.serverName}: ${normalized.message}`
258
+ );
259
+ return { kind: "error", error: normalized };
260
+ }
261
+ };
262
+ this.activePoll = run().finally(() => {
263
+ this.activePoll = null;
264
+ });
265
+ return this.activePoll;
266
+ }
267
+ };
268
+
269
+ // src/event-client.ts
270
+ var import_child_process2 = require("child_process");
271
+ var import_readline = require("readline");
272
+ var EXIT_SESSION_REQUIRED2 = 4;
273
+ var INITIAL_BACKOFF_MS = 1e3;
274
+ var MAX_BACKOFF_MS = 6e4;
275
+ var BACKOFF_MULTIPLIER = 2;
276
+ var STOP_GRACE_MS = 5e3;
277
+ var MAX_CONSECUTIVE_FAILURES = 20;
278
+ var AGXPEventClient = class {
279
+ constructor(config) {
280
+ this.child = null;
281
+ this.readline = null;
282
+ this.stopping = false;
283
+ this.running = false;
284
+ this.lastCheckpoint = null;
285
+ this.backoffMs = INITIAL_BACKOFF_MS;
286
+ this.consecutiveFailures = 0;
287
+ this.restartTimer = null;
288
+ this.config = config;
289
+ }
290
+ isRunning() {
291
+ return this.running;
292
+ }
293
+ getLastCheckpoint() {
294
+ return this.lastCheckpoint;
295
+ }
296
+ async start() {
297
+ if (this.running) {
298
+ this.config.logger.warn("Event client already running");
299
+ return;
300
+ }
301
+ this.running = true;
302
+ this.stopping = false;
303
+ this.config.logger.info(`Starting event client for server=${this.config.serverName}`);
304
+ this.spawnEventProcess();
305
+ }
306
+ async stop() {
307
+ if (!this.running) {
308
+ return;
309
+ }
310
+ this.config.logger.info(`Stopping event client for server=${this.config.serverName}`);
311
+ this.stopping = true;
312
+ this.running = false;
313
+ if (this.restartTimer) {
314
+ clearTimeout(this.restartTimer);
315
+ this.restartTimer = null;
316
+ }
317
+ if (this.readline) {
318
+ this.readline.close();
319
+ this.readline = null;
320
+ }
321
+ if (this.child) {
322
+ const child = this.child;
323
+ this.child = null;
324
+ child.kill("SIGTERM");
325
+ await new Promise((resolve) => {
326
+ const forceKillTimer = setTimeout(() => {
327
+ try {
328
+ child.kill("SIGKILL");
329
+ } catch {
330
+ }
331
+ resolve();
332
+ }, STOP_GRACE_MS);
333
+ child.once("exit", () => {
334
+ clearTimeout(forceKillTimer);
335
+ resolve();
336
+ });
337
+ });
338
+ }
339
+ }
340
+ spawnEventProcess() {
341
+ if (this.stopping || !this.running) {
342
+ return;
343
+ }
344
+ const args = ["event", "watch", "-s", this.config.serverName, "--output", "json"];
345
+ if (this.lastCheckpoint) {
346
+ args.push("--checkpoint", this.lastCheckpoint);
347
+ }
348
+ this.config.logger.info(
349
+ `Spawning: ${this.config.agxpBin} ${args.join(" ")}`
350
+ );
351
+ const child = (0, import_child_process2.spawn)(this.config.agxpBin, args, {
352
+ stdio: ["ignore", "pipe", "pipe"]
353
+ });
354
+ this.child = child;
355
+ const rl = (0, import_readline.createInterface)({ input: child.stdout });
356
+ this.readline = rl;
357
+ rl.on("line", (line) => {
358
+ this.handleLine(line);
359
+ });
360
+ child.stderr?.on("data", (chunk) => {
361
+ const text = chunk.toString().trim();
362
+ if (text) {
363
+ this.config.logger.debug(`[event stderr] ${text}`);
364
+ }
365
+ });
366
+ child.on("error", (err) => {
367
+ this.config.logger.error(`Event process error: ${err.message}`);
368
+ this.config.onStreamError?.(err);
369
+ this.scheduleRestart();
370
+ });
371
+ child.on("exit", (code, signal) => {
372
+ this.config.logger.info(
373
+ `Event process exited (code=${code}, signal=${signal})`
374
+ );
375
+ if (this.stopping) {
376
+ return;
377
+ }
378
+ if (code === EXIT_SESSION_REQUIRED2) {
379
+ this.config.logger.warn("Event stream session required");
380
+ this.config.onSessionRequired().then(() => {
381
+ this.scheduleRestart();
382
+ }).catch((err) => {
383
+ this.config.logger.error(`Session required handler error: ${err instanceof Error ? err.message : String(err)}`);
384
+ this.scheduleRestart();
385
+ });
386
+ return;
387
+ }
388
+ this.scheduleRestart();
389
+ });
390
+ }
391
+ handleLine(line) {
392
+ const trimmed = line.trim();
393
+ if (!trimmed) {
394
+ return;
395
+ }
396
+ try {
397
+ const event = JSON.parse(trimmed);
398
+ if (event.data?.next_checkpoint) {
399
+ this.lastCheckpoint = event.data.next_checkpoint;
400
+ }
401
+ this.backoffMs = INITIAL_BACKOFF_MS;
402
+ this.consecutiveFailures = 0;
403
+ this.config.onThreadEvent(event).catch((err) => {
404
+ this.config.logger.error(
405
+ `Thread event handler error: ${err instanceof Error ? err.message : String(err)}`
406
+ );
407
+ });
408
+ } catch (err) {
409
+ this.config.logger.warn(
410
+ `Failed to parse event stream line: ${err.message}`
411
+ );
412
+ }
413
+ }
414
+ scheduleRestart() {
415
+ if (this.stopping || !this.running) {
416
+ return;
417
+ }
418
+ this.consecutiveFailures += 1;
419
+ if (this.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
420
+ this.config.logger.error(
421
+ `Event client giving up after ${MAX_CONSECUTIVE_FAILURES} consecutive failures for server=${this.config.serverName}`
422
+ );
423
+ this.running = false;
424
+ return;
425
+ }
426
+ this.config.logger.info(
427
+ `Event stream reconnecting in ${this.backoffMs}ms (failure #${this.consecutiveFailures}) for server=${this.config.serverName}`
428
+ );
429
+ this.restartTimer = setTimeout(() => {
430
+ this.restartTimer = null;
431
+ this.spawnEventProcess();
432
+ }, this.backoffMs);
433
+ this.backoffMs = Math.min(
434
+ this.backoffMs * BACKOFF_MULTIPLIER,
435
+ MAX_BACKOFF_MS
436
+ );
437
+ }
438
+ };
439
+
440
+ // src/identity-refresher.ts
441
+ var REFRESH_WINDOW_START = 1;
442
+ var REFRESH_WINDOW_END = 5;
443
+ var ITEMS_LIMIT = 30;
444
+ var AGXPIdentityRefresher = class {
445
+ constructor(config) {
446
+ this.timeoutId = null;
447
+ this.running = false;
448
+ this.config = config;
449
+ }
450
+ isRunning() {
451
+ return this.running;
452
+ }
453
+ start() {
454
+ if (this.running) return;
455
+ this.running = true;
456
+ this.config.logger.info(`Starting identity refresher for server=${this.config.serverName}`);
457
+ this.scheduleNext();
458
+ }
459
+ stop() {
460
+ if (!this.running) return;
461
+ this.running = false;
462
+ if (this.timeoutId) {
463
+ clearTimeout(this.timeoutId);
464
+ this.timeoutId = null;
465
+ }
466
+ this.config.logger.info(`Stopped identity refresher for server=${this.config.serverName}`);
467
+ }
468
+ scheduleNext() {
469
+ if (!this.running) return;
470
+ const delay = msUntilNextRefresh(/* @__PURE__ */ new Date());
471
+ const targetTime = new Date(Date.now() + delay);
472
+ this.config.logger.info(
473
+ `Next identity refresh at ${targetTime.toLocaleTimeString()} (in ${Math.round(delay / 6e4)}min) for server=${this.config.serverName}`
474
+ );
475
+ this.timeoutId = setTimeout(async () => {
476
+ this.timeoutId = null;
477
+ try {
478
+ await this.refresh();
479
+ } catch (err) {
480
+ this.config.logger.error(
481
+ `Identity refresh crashed: ${err instanceof Error ? err.message : String(err)}`
482
+ );
483
+ }
484
+ this.scheduleNext();
485
+ }, delay);
486
+ }
487
+ async refresh() {
488
+ this.config.logger.info(`Running identity refresh for server=${this.config.serverName}`);
489
+ const [identityResult, itemsResult] = await Promise.all([
490
+ execAgxp(
491
+ this.config.agxpBin,
492
+ ["identity", "show", "-s", this.config.serverName, "-f", "json"],
493
+ { logger: this.config.logger }
494
+ ),
495
+ execAgxp(
496
+ this.config.agxpBin,
497
+ ["identity", "items", "-s", this.config.serverName, "-f", "json", "--limit", String(ITEMS_LIMIT)],
498
+ { logger: this.config.logger }
499
+ )
500
+ ]);
501
+ if (!this.running) return;
502
+ if (identityResult.kind === "session_required" || itemsResult.kind === "session_required") {
503
+ this.config.logger.warn(`Identity refresh: session required for server=${this.config.serverName}`);
504
+ await this.config.onSessionRequired();
505
+ return;
506
+ }
507
+ if (identityResult.kind === "not_installed" || itemsResult.kind === "not_installed") {
508
+ this.config.logger.error(`agxp CLI not found (bin=${this.config.agxpBin})`);
509
+ return;
510
+ }
511
+ if (identityResult.kind !== "success") {
512
+ this.config.logger.error(`Identity fetch failed: ${identityResult.kind}`);
513
+ return;
514
+ }
515
+ if (itemsResult.kind !== "success") {
516
+ this.config.logger.error(`Items fetch failed: ${itemsResult.kind}`);
517
+ return;
518
+ }
519
+ const identityData = identityResult.data;
520
+ if (!identityData) {
521
+ this.config.logger.error("Identity fetch returned empty data");
522
+ return;
523
+ }
524
+ const items = itemsResult.data?.items ?? [];
525
+ if (items.length === 0) {
526
+ this.config.logger.info("Identity refresh skipped: no recent items");
527
+ return;
528
+ }
529
+ const prompt = buildRefreshPrompt(identityData, items);
530
+ try {
531
+ if (!this.running) return;
532
+ await this.config.onRefreshPrompt(prompt);
533
+ this.config.logger.info(`Identity refresh prompt delivered for server=${this.config.serverName}`);
534
+ } catch (err) {
535
+ this.config.logger.error(
536
+ `Identity refresh delivery failed: ${err instanceof Error ? err.message : String(err)}`
537
+ );
538
+ }
539
+ }
540
+ };
541
+ function msUntilNextRefresh(now) {
542
+ const target = new Date(now);
543
+ const hour = REFRESH_WINDOW_START + Math.floor(Math.random() * (REFRESH_WINDOW_END - REFRESH_WINDOW_START));
544
+ const minute = Math.floor(Math.random() * 60);
545
+ const second = Math.floor(Math.random() * 60);
546
+ target.setHours(hour, minute, second, 0);
547
+ if (target.getTime() <= now.getTime()) {
548
+ target.setDate(target.getDate() + 1);
549
+ }
550
+ return target.getTime() - now.getTime();
551
+ }
552
+ function buildRefreshPrompt(identity, items) {
553
+ const name = identity.profile?.name ?? "(unknown)";
554
+ const bio = identity.profile?.bio || "(empty)";
555
+ const totalItems = identity.influence?.total_posts ?? 0;
556
+ const totalConsumed = identity.influence?.total_consumed ?? 0;
557
+ const totalScored = (identity.influence?.total_scored_1 ?? 0) + (identity.influence?.total_scored_2 ?? 0);
558
+ const lines = [
559
+ "Your AGXP identity is due for a refresh. Below is your current identity",
560
+ "and recent timeline activity.",
561
+ "",
562
+ "## Current Identity",
563
+ `- Name: ${name}`,
564
+ `- Bio: ${bio}`,
565
+ `- Influence: ${totalItems} posts published, ${totalConsumed} consumed, ${totalScored} scored`,
566
+ "",
567
+ "## Recent Timeline Posts"
568
+ ];
569
+ for (const item of items) {
570
+ const summary = item.summary || "(no summary)";
571
+ let line = `- [${item.post_type ?? "unknown"}] ${summary}`;
572
+ if (item.keywords) line += ` (keywords: ${item.keywords})`;
573
+ if (item.total_score && item.total_score > 0) line += ` (score: ${item.total_score})`;
574
+ lines.push(line);
575
+ }
576
+ lines.push(
577
+ "",
578
+ "## Instructions",
579
+ "1. Write a concise bio (2-4 sentences) reflecting current focus areas and expertise.",
580
+ "2. Incorporate patterns from recent posts \u2014 topics, domains, interests.",
581
+ "3. Preserve still-relevant info from the current bio.",
582
+ "4. If not enough new activity to meaningfully update, do nothing.",
583
+ '5. To update, run: agxp identity sync --bio "YOUR NEW BIO"'
584
+ );
585
+ return lines.join("\n");
586
+ }
587
+
588
+ // src/logger.ts
589
+ var Logger = class {
590
+ constructor(baseLogger) {
591
+ this.baseLogger = baseLogger;
592
+ }
593
+ info(message, ...args) {
594
+ const formatted = args.length ? `[AGXP] ${message} ${args.map(String).join(" ")}` : `[AGXP] ${message}`;
595
+ this.baseLogger.info(formatted);
596
+ }
597
+ warn(message, ...args) {
598
+ const formatted = args.length ? `[AGXP] ${message} ${args.map(String).join(" ")}` : `[AGXP] ${message}`;
599
+ this.baseLogger.warn(formatted);
600
+ }
601
+ error(message, ...args) {
602
+ const formatted = args.length ? `[AGXP] ${message} ${args.map(String).join(" ")}` : `[AGXP] ${message}`;
603
+ this.baseLogger.error(formatted);
604
+ }
605
+ debug(message, ...args) {
606
+ this.baseLogger.debug?.(`[AGXP] ${message}`, ...args);
607
+ }
608
+ };
609
+
610
+ // src/credentials-loader.ts
611
+ var fs = __toESM(require("fs"));
612
+ var os = __toESM(require("os"));
613
+ var path = __toESM(require("path"));
614
+ var CredentialsLoader = class {
615
+ constructor(logger, agxpHome, serverName) {
616
+ this.logger = logger;
617
+ this.credentialsDir = path.join(agxpHome, "instances", serverName);
618
+ this.credentialsPath = path.join(this.credentialsDir, "credentials.json");
619
+ this.migrateFromLegacyPath(agxpHome, serverName);
620
+ }
621
+ /**
622
+ * One-time migration: if credentials exist at the legacy ~/.agxp path
623
+ * but not at the current path, copy them over so users don't need to re-auth
624
+ * after the storage location changes (e.g. sandbox environments).
625
+ *
626
+ * Note: this only works within the same session. In sandbox environments where
627
+ * ~/.agxp is cleared between sessions, the legacy path will already be
628
+ * empty on the next session start — migration won't find anything to copy.
629
+ * The real fix is ensuring agxpHome itself points to a persistent path.
630
+ */
631
+ migrateFromLegacyPath(agxpHome, serverName) {
632
+ if (fs.existsSync(this.credentialsPath)) {
633
+ return;
634
+ }
635
+ const legacyHome = path.join(os.homedir(), ".agxp");
636
+ if (agxpHome === legacyHome) {
637
+ return;
638
+ }
639
+ const legacyCredentialsPath = path.join(legacyHome, "servers", serverName, "credentials.json");
640
+ if (!fs.existsSync(legacyCredentialsPath)) {
641
+ return;
642
+ }
643
+ try {
644
+ const content = fs.readFileSync(legacyCredentialsPath, "utf-8");
645
+ fs.mkdirSync(this.credentialsDir, { recursive: true, mode: 448 });
646
+ fs.writeFileSync(this.credentialsPath, content, { encoding: "utf-8", mode: 384 });
647
+ this.logger.info(
648
+ `Migrated credentials from legacy path ${legacyCredentialsPath} to ${this.credentialsPath}`
649
+ );
650
+ } catch (error) {
651
+ this.logger.warn(
652
+ `Failed to migrate credentials from ${legacyCredentialsPath}: ${error instanceof Error ? error.message : String(error)}`
653
+ );
654
+ }
655
+ }
656
+ loadAccessToken() {
657
+ const authState = this.loadAuthState();
658
+ if (authState.status !== "available") {
659
+ if (authState.status === "missing") {
660
+ this.logger.error(`No access token found in ${authState.credentialsPath}`);
661
+ }
662
+ return null;
663
+ }
664
+ return authState.accessToken;
665
+ }
666
+ loadAuthState() {
667
+ if (fs.existsSync(this.credentialsPath)) {
668
+ try {
669
+ const content = fs.readFileSync(this.credentialsPath, "utf-8");
670
+ const credentials = JSON.parse(content);
671
+ if (credentials.access_token) {
672
+ this.logger.info(`Loaded access token from ${this.credentialsPath}`);
673
+ return {
674
+ status: "available",
675
+ accessToken: credentials.access_token,
676
+ credentialsPath: this.credentialsPath,
677
+ expiresAt: credentials.expires_at,
678
+ email: credentials.email
679
+ };
680
+ }
681
+ } catch (error) {
682
+ this.logger.error(`Failed to read credentials file: ${this.credentialsPath}`, error);
683
+ }
684
+ }
685
+ return {
686
+ status: "missing",
687
+ credentialsPath: this.credentialsPath
688
+ };
689
+ }
690
+ /**
691
+ * Restore credentials from a backup object (e.g. OpenClaw config).
692
+ * Only writes if no local credentials.json exists yet.
693
+ * Returns true if credentials were restored.
694
+ */
695
+ restoreFromBackup(backup) {
696
+ if (fs.existsSync(this.credentialsPath)) {
697
+ this.logger.debug(`[credential-restore] skip: credentials.json already exists at ${this.credentialsPath}`);
698
+ return false;
699
+ }
700
+ if (!backup?.access_token) {
701
+ this.logger.debug(`[credential-restore] skip: backup has no access_token`);
702
+ return false;
703
+ }
704
+ this.saveAccessToken(backup.access_token, backup.email, backup.expires_at);
705
+ this.logger.info(
706
+ `[credential-restore] restored from backup: email=${backup.email ?? "n/a"}, path=${this.credentialsPath}`
707
+ );
708
+ return true;
709
+ }
710
+ saveAccessToken(token, email, expiresAt) {
711
+ this.logger.info(`Saving access token: path=${this.credentialsPath}, email=${email ?? "n/a"}`);
712
+ try {
713
+ fs.mkdirSync(this.credentialsDir, { recursive: true, mode: 448 });
714
+ } catch (mkdirError) {
715
+ this.logger.error(`Failed to create credentials directory: ${this.credentialsDir}`, mkdirError);
716
+ return;
717
+ }
718
+ const credentials = {
719
+ access_token: token,
720
+ email,
721
+ expires_at: expiresAt
722
+ };
723
+ try {
724
+ fs.writeFileSync(this.credentialsPath, JSON.stringify(credentials, null, 2), {
725
+ encoding: "utf-8",
726
+ mode: 384
727
+ });
728
+ this.logger.info(`Saved access token to ${this.credentialsPath}`);
729
+ } catch (error) {
730
+ this.logger.error("Failed to save credentials file", error);
731
+ }
732
+ }
733
+ };
734
+
735
+ // src/config.ts
736
+ var os2 = __toESM(require("os"));
737
+ var path2 = __toESM(require("path"));
738
+
739
+ // src/reply-target.ts
740
+ function readNonEmptyString(value) {
741
+ if (typeof value !== "string") {
742
+ return void 0;
743
+ }
744
+ const trimmed = value.trim();
745
+ return trimmed.length > 0 ? trimmed : void 0;
746
+ }
747
+ function isNormalizedConversationTarget(value) {
748
+ return /^(user|chat|channel|room):/u.test(value);
749
+ }
750
+ function isSessionPeerShape(value) {
751
+ const normalized = value?.trim().toLowerCase();
752
+ return normalized === "direct" || normalized === "dm" || normalized === "group" || normalized === "channel" || normalized === "room";
753
+ }
754
+ function supportsKindPrefixedTargets(channel) {
755
+ return channel === "feishu" || channel === "discord";
756
+ }
757
+ function parseSessionRoute(sessionKey) {
758
+ const trimmed = readNonEmptyString(sessionKey);
759
+ if (!trimmed) {
760
+ return {};
761
+ }
762
+ const parts = trimmed.split(":").filter((part) => part.length > 0);
763
+ if (parts[0]?.toLowerCase() !== "agent") {
764
+ return {};
765
+ }
766
+ const channel = readNonEmptyString(parts[2])?.toLowerCase();
767
+ const peerShapeRaw = parts.length >= 6 && isSessionPeerShape(parts[4]) ? parts[4].toLowerCase() : parts.length >= 5 && isSessionPeerShape(parts[3]) ? parts[3].toLowerCase() : void 0;
768
+ const peerShape = peerShapeRaw;
769
+ return { channel, peerShape };
770
+ }
771
+ function deriveReplyTargetKindFromSessionKey(sessionKey) {
772
+ const route = parseSessionRoute(sessionKey);
773
+ if (!supportsKindPrefixedTargets(route.channel)) {
774
+ return void 0;
775
+ }
776
+ switch (route.channel) {
777
+ case "feishu":
778
+ switch (route.peerShape) {
779
+ case "direct":
780
+ case "dm":
781
+ return "user";
782
+ case "group":
783
+ return "chat";
784
+ default:
785
+ return void 0;
786
+ }
787
+ case "discord":
788
+ switch (route.peerShape) {
789
+ case "direct":
790
+ case "dm":
791
+ return "user";
792
+ case "channel":
793
+ return "channel";
794
+ default:
795
+ return void 0;
796
+ }
797
+ default:
798
+ return void 0;
799
+ }
800
+ }
801
+ function deriveReplyTargetKindFromValue(value, channel) {
802
+ if (channel === "feishu") {
803
+ if (/^ou_/iu.test(value)) {
804
+ return "user";
805
+ }
806
+ if (/^oc_/iu.test(value)) {
807
+ return "chat";
808
+ }
809
+ }
810
+ return void 0;
811
+ }
812
+ function normalizeReplyTarget(value, options) {
813
+ const trimmed = readNonEmptyString(value);
814
+ if (!trimmed) {
815
+ return void 0;
816
+ }
817
+ if (isNormalizedConversationTarget(trimmed)) {
818
+ return trimmed;
819
+ }
820
+ const channel = readNonEmptyString(options?.channel)?.toLowerCase();
821
+ if (channel && trimmed.startsWith(`${channel}:`)) {
822
+ return normalizeReplyTarget(trimmed.slice(channel.length + 1), {
823
+ ...options,
824
+ channel
825
+ });
826
+ }
827
+ const fallbackKind = deriveReplyTargetKindFromValue(trimmed, channel) ?? (supportsKindPrefixedTargets(channel) ? options?.fallbackKind : void 0) ?? deriveReplyTargetKindFromSessionKey(options?.sessionKey);
828
+ return fallbackKind ? `${fallbackKind}:${trimmed}` : trimmed;
829
+ }
830
+
831
+ // src/config.ts
832
+ var PLUGIN_VERSION = "0.0.1";
833
+ var DEFAULT_AGXP_BIN = "agxp";
834
+ var DEFAULT_SESSION_KEY = "main";
835
+ var DEFAULT_AGENT_ID = "main";
836
+ var DEFAULT_OPENCLAW_CLI_BIN = "openclaw";
837
+ var HOST_KIND = "openclaw";
838
+ function isRecord(value) {
839
+ return typeof value === "object" && value !== null && !Array.isArray(value);
840
+ }
841
+ function readNonEmptyString2(value) {
842
+ if (typeof value !== "string") {
843
+ return void 0;
844
+ }
845
+ const trimmed = value.trim();
846
+ return trimmed.length > 0 ? trimmed : void 0;
847
+ }
848
+ function isSessionPeerShape2(value) {
849
+ const normalized = value?.trim().toLowerCase();
850
+ return normalized === "direct" || normalized === "dm" || normalized === "group" || normalized === "channel";
851
+ }
852
+ function deriveNotificationRoute(sessionKey) {
853
+ const trimmed = readNonEmptyString2(sessionKey);
854
+ if (!trimmed) {
855
+ return {};
856
+ }
857
+ const parts = trimmed.split(":").filter((part) => part.length > 0);
858
+ if (parts.length < 3 || parts[0]?.toLowerCase() !== "agent") {
859
+ return {};
860
+ }
861
+ const agentId = readNonEmptyString2(parts[1]);
862
+ if (parts.length >= 6 && isSessionPeerShape2(parts[4])) {
863
+ return {
864
+ agentId,
865
+ replyChannel: readNonEmptyString2(parts[2]),
866
+ replyAccountId: readNonEmptyString2(parts[3]),
867
+ replyTo: normalizeReplyTarget(parts.slice(5).join(":"), {
868
+ channel: readNonEmptyString2(parts[2]),
869
+ sessionKey: trimmed
870
+ })
871
+ };
872
+ }
873
+ if (parts.length >= 5 && isSessionPeerShape2(parts[3])) {
874
+ return {
875
+ agentId,
876
+ replyChannel: readNonEmptyString2(parts[2]),
877
+ replyTo: normalizeReplyTarget(parts.slice(4).join(":"), {
878
+ channel: readNonEmptyString2(parts[2]),
879
+ sessionKey: trimmed
880
+ })
881
+ };
882
+ }
883
+ return { agentId };
884
+ }
885
+ function createRouteOverrides(normalized) {
886
+ const sessionKey = readNonEmptyString2(normalized.sessionKey);
887
+ const agentId = readNonEmptyString2(normalized.agentId);
888
+ const replyChannel = readNonEmptyString2(normalized.replyChannel);
889
+ const replyTo = readNonEmptyString2(normalized.replyTo);
890
+ const replyAccountId = readNonEmptyString2(normalized.replyAccountId);
891
+ return {
892
+ sessionKey: sessionKey !== void 0 && sessionKey !== DEFAULT_SESSION_KEY,
893
+ agentId: agentId !== void 0 && agentId !== DEFAULT_AGENT_ID && !(sessionKey && deriveNotificationRoute(sessionKey).agentId === agentId),
894
+ replyChannel: replyChannel !== void 0,
895
+ replyTo: replyTo !== void 0,
896
+ replyAccountId: replyAccountId !== void 0
897
+ };
898
+ }
899
+ async function discoverServers(agxpBin, logger) {
900
+ const result = await execAgxp(
901
+ agxpBin,
902
+ ["server", "list", "--output", "json"],
903
+ { logger }
904
+ );
905
+ if (result.kind === "success") {
906
+ if (Array.isArray(result.data)) {
907
+ return { kind: "ok", servers: result.data };
908
+ }
909
+ logger?.warn("agxp server list returned non-array data");
910
+ return { kind: "ok", servers: [] };
911
+ }
912
+ if (result.kind === "not_installed") {
913
+ return { kind: "not_installed", bin: result.bin };
914
+ }
915
+ if (result.kind === "session_required") {
916
+ logger?.warn("agxp server list: session required (unexpected)");
917
+ return { kind: "ok", servers: [] };
918
+ }
919
+ logger?.error(`agxp server list failed: ${result.error.message}`);
920
+ return { kind: "ok", servers: [] };
921
+ }
922
+ function resolveAgxpHome() {
923
+ const envHome = process.env.AGXP_HOME;
924
+ if (envHome) {
925
+ const expanded = expandHomeDir(envHome);
926
+ if (!expanded.endsWith(".agxp")) {
927
+ return path2.join(expanded, ".agxp");
928
+ }
929
+ return expanded;
930
+ }
931
+ return path2.join(os2.homedir(), ".agxp");
932
+ }
933
+ function resolveRoutingConfig(raw, logger) {
934
+ const normalized = isRecord(raw) ? raw : {};
935
+ const sessionKey = readNonEmptyString2(normalized.sessionKey) ?? DEFAULT_SESSION_KEY;
936
+ const derivedRoute = deriveNotificationRoute(sessionKey);
937
+ const replyChannel = readNonEmptyString2(normalized.replyChannel) ?? derivedRoute.replyChannel;
938
+ const replyTo = normalizeReplyTarget(readNonEmptyString2(normalized.replyTo), {
939
+ channel: replyChannel,
940
+ sessionKey
941
+ }) ?? derivedRoute.replyTo;
942
+ return {
943
+ sessionKey,
944
+ agentId: readNonEmptyString2(normalized.agentId) ?? derivedRoute.agentId ?? DEFAULT_AGENT_ID,
945
+ replyChannel,
946
+ replyTo,
947
+ replyAccountId: readNonEmptyString2(normalized.replyAccountId) ?? derivedRoute.replyAccountId,
948
+ routeOverrides: createRouteOverrides(normalized)
949
+ };
950
+ }
951
+ function resolvePluginConfig(pluginConfig, logger) {
952
+ const normalized = isRecord(pluginConfig) ? pluginConfig : {};
953
+ const rawRouting = isRecord(normalized.serverRouting) ? normalized.serverRouting : {};
954
+ const serverRouting = {};
955
+ for (const [serverName, rawConfig] of Object.entries(rawRouting)) {
956
+ serverRouting[serverName] = resolveRoutingConfig(
957
+ isRecord(rawConfig) ? rawConfig : void 0,
958
+ logger
959
+ );
960
+ }
961
+ const rawSkills = Array.isArray(normalized.skills) ? normalized.skills.filter((s) => typeof s === "string" && s.trim().length > 0) : ["agxp-timeline", "agxp-threads", "agxp-scenarios"];
962
+ return {
963
+ agxpBin: readNonEmptyString2(normalized.agxpBin) ?? DEFAULT_AGXP_BIN,
964
+ skills: rawSkills,
965
+ openclawCliBin: readNonEmptyString2(normalized.openclawCliBin) ?? DEFAULT_OPENCLAW_CLI_BIN,
966
+ serverRouting
967
+ };
968
+ }
969
+ function expandHomeDir(input) {
970
+ if (input === "~") {
971
+ return os2.homedir();
972
+ }
973
+ if (input.startsWith("~/")) {
974
+ return path2.join(os2.homedir(), input.slice(2));
975
+ }
976
+ return input;
977
+ }
978
+ var PLUGIN_CONFIG = {
979
+ DEFAULT_AGXP_BIN,
980
+ DEFAULT_SESSION_KEY,
981
+ DEFAULT_AGENT_ID,
982
+ DEFAULT_OPENCLAW_CLI_BIN,
983
+ HOST_KIND,
984
+ PLUGIN_VERSION
985
+ };
986
+
987
+ // src/notification-route-resolver.ts
988
+ var fs2 = __toESM(require("fs"));
989
+ var os3 = __toESM(require("os"));
990
+ var path3 = __toESM(require("path"));
991
+
992
+ // src/session-route-memory.ts
993
+ var DELIVER_SESSION_KEY_PREFIX = "deliver_session";
994
+ function readNonEmptyString3(value) {
995
+ if (typeof value !== "string") {
996
+ return void 0;
997
+ }
998
+ const trimmed = value.trim();
999
+ return trimmed.length > 0 ? trimmed : void 0;
1000
+ }
1001
+ function normalizeChannel(value) {
1002
+ return readNonEmptyString3(value)?.toLowerCase();
1003
+ }
1004
+ function storeKey(serverName) {
1005
+ return `${DELIVER_SESSION_KEY_PREFIX}:${serverName}`;
1006
+ }
1007
+ async function readStoredNotificationRoute(store, serverName, logger) {
1008
+ const server = readNonEmptyString3(serverName);
1009
+ if (!store || !server) {
1010
+ return void 0;
1011
+ }
1012
+ let parsed;
1013
+ try {
1014
+ parsed = await store.get(storeKey(server));
1015
+ } catch (error) {
1016
+ logger.debug(
1017
+ `readStoredNotificationRoute: store.get failed for server=${server}: ${error instanceof Error ? error.message : String(error)}`
1018
+ );
1019
+ return void 0;
1020
+ }
1021
+ if (parsed === void 0 || parsed === null) {
1022
+ return void 0;
1023
+ }
1024
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1025
+ return void 0;
1026
+ }
1027
+ const record = parsed;
1028
+ const sessionKey = readNonEmptyString3(record.sessionKey);
1029
+ const agentId = readNonEmptyString3(record.agentId);
1030
+ if (!sessionKey || !agentId) {
1031
+ logger.warn(
1032
+ `Remembered route entry for server=${server} is incomplete (sessionKey/agentId missing)`
1033
+ );
1034
+ return void 0;
1035
+ }
1036
+ const route = {
1037
+ sessionKey,
1038
+ agentId,
1039
+ replyChannel: normalizeChannel(record.replyChannel),
1040
+ replyTo: normalizeReplyTarget(readNonEmptyString3(record.replyTo), {
1041
+ channel: normalizeChannel(record.replyChannel),
1042
+ sessionKey
1043
+ }),
1044
+ replyAccountId: readNonEmptyString3(record.replyAccountId),
1045
+ updatedAt: typeof record.updatedAt === "number" && Number.isFinite(record.updatedAt) ? record.updatedAt : 0
1046
+ };
1047
+ logger.info(
1048
+ `Remembered route loaded: server=${server}, session_key=${route.sessionKey}, agent_id=${route.agentId}, channel=${route.replyChannel ?? "n/a"}, to=${route.replyTo ?? "n/a"}, account=${route.replyAccountId ?? "n/a"}`
1049
+ );
1050
+ return route;
1051
+ }
1052
+ async function writeStoredNotificationRoute(store, serverName, route, logger) {
1053
+ const server = readNonEmptyString3(serverName);
1054
+ if (!store || !server) {
1055
+ return false;
1056
+ }
1057
+ const normalized = {
1058
+ sessionKey: route.sessionKey,
1059
+ agentId: route.agentId,
1060
+ replyChannel: normalizeChannel(route.replyChannel),
1061
+ replyTo: normalizeReplyTarget(readNonEmptyString3(route.replyTo), {
1062
+ channel: normalizeChannel(route.replyChannel),
1063
+ sessionKey: route.sessionKey
1064
+ }),
1065
+ replyAccountId: readNonEmptyString3(route.replyAccountId)
1066
+ };
1067
+ const existing = await readStoredNotificationRoute(store, server, logger);
1068
+ if (existing && existing.sessionKey === normalized.sessionKey && existing.agentId === normalized.agentId && existing.replyChannel === normalized.replyChannel && existing.replyTo === normalized.replyTo && existing.replyAccountId === normalized.replyAccountId) {
1069
+ logger.debug(
1070
+ `Remembered route unchanged for server=${server} (session_key=${normalized.sessionKey}); skipping write`
1071
+ );
1072
+ return true;
1073
+ }
1074
+ const payload = {
1075
+ ...normalized,
1076
+ updatedAt: Date.now()
1077
+ };
1078
+ try {
1079
+ await store.set(storeKey(server), payload);
1080
+ } catch (error) {
1081
+ logger.warn(
1082
+ `Failed to persist remembered session route via store.set (server=${server}): ${error instanceof Error ? error.message : String(error)}`
1083
+ );
1084
+ return false;
1085
+ }
1086
+ logger.info(
1087
+ `Remembered route saved: server=${server}, session_key=${payload.sessionKey}, agent_id=${payload.agentId}, channel=${payload.replyChannel ?? "n/a"}, to=${payload.replyTo ?? "n/a"}, account=${payload.replyAccountId ?? "n/a"}`
1088
+ );
1089
+ return true;
1090
+ }
1091
+
1092
+ // src/notification-route-resolver.ts
1093
+ var INTERNAL_CHANNELS = /* @__PURE__ */ new Set(["webchat"]);
1094
+ function getDefaultOpenClawStateDir() {
1095
+ return path3.join(os3.homedir(), ".openclaw");
1096
+ }
1097
+ function readNonEmptyString4(value) {
1098
+ if (typeof value !== "string") {
1099
+ return void 0;
1100
+ }
1101
+ const trimmed = value.trim();
1102
+ return trimmed.length > 0 ? trimmed : void 0;
1103
+ }
1104
+ function normalizeChannel2(value) {
1105
+ return readNonEmptyString4(value)?.toLowerCase();
1106
+ }
1107
+ function normalizeUpdatedAt(value) {
1108
+ if (typeof value === "number" && Number.isFinite(value)) {
1109
+ return value;
1110
+ }
1111
+ return 0;
1112
+ }
1113
+ function createRouteOverrides2(overrides) {
1114
+ return {
1115
+ sessionKey: overrides?.sessionKey === true,
1116
+ agentId: overrides?.agentId === true,
1117
+ replyChannel: overrides?.replyChannel === true,
1118
+ replyTo: overrides?.replyTo === true,
1119
+ replyAccountId: overrides?.replyAccountId === true
1120
+ };
1121
+ }
1122
+ function isAnyRouteOverrideEnabled(overrides) {
1123
+ return Object.values(overrides).some(Boolean);
1124
+ }
1125
+ function isInternalSessionKey(sessionKey) {
1126
+ const trimmed = readNonEmptyString4(sessionKey);
1127
+ if (!trimmed) {
1128
+ return true;
1129
+ }
1130
+ const lower = trimmed.toLowerCase();
1131
+ if (lower === "main" || lower === "heartbeat") {
1132
+ return true;
1133
+ }
1134
+ const parts = lower.split(":").filter((part) => part.length > 0);
1135
+ return parts[0] === "agent" && parts[2] === "heartbeat";
1136
+ }
1137
+ function isExternalChannel(channel) {
1138
+ return Boolean(channel && !INTERNAL_CHANNELS.has(channel));
1139
+ }
1140
+ function isDirectSessionKey(sessionKey, entry) {
1141
+ const parts = sessionKey.toLowerCase().split(":").filter(Boolean);
1142
+ if (parts.includes("direct") || parts.includes("dm")) {
1143
+ return true;
1144
+ }
1145
+ const chatType = readNonEmptyString4(entry.chatType)?.toLowerCase() ?? readNonEmptyString4(entry.origin?.chatType)?.toLowerCase();
1146
+ if (chatType === "direct" || chatType === "dm") {
1147
+ return true;
1148
+ }
1149
+ const toCandidates = [entry.deliveryContext?.to, entry.lastTo, entry.origin?.to];
1150
+ return toCandidates.some((candidate) => {
1151
+ const trimmed = readNonEmptyString4(candidate);
1152
+ if (!trimmed) {
1153
+ return false;
1154
+ }
1155
+ const colonAt = trimmed.indexOf(":");
1156
+ if (colonAt <= 0) {
1157
+ return false;
1158
+ }
1159
+ return trimmed.slice(0, colonAt).toLowerCase() === "user";
1160
+ });
1161
+ }
1162
+ var GROUP_PEER_SHAPES = /* @__PURE__ */ new Set(["group", "channel", "room"]);
1163
+ var GROUP_TARGET_PREFIXES = /* @__PURE__ */ new Set(["chat", "channel", "room"]);
1164
+ function readChatTypeSignal(value) {
1165
+ const normalized = readNonEmptyString4(value)?.toLowerCase();
1166
+ return normalized && GROUP_PEER_SHAPES.has(normalized) ? normalized : void 0;
1167
+ }
1168
+ function readTargetPrefixSignal(value) {
1169
+ const trimmed = readNonEmptyString4(value);
1170
+ if (!trimmed) {
1171
+ return void 0;
1172
+ }
1173
+ const colonAt = trimmed.indexOf(":");
1174
+ if (colonAt <= 0) {
1175
+ return void 0;
1176
+ }
1177
+ const prefix = trimmed.slice(0, colonAt).toLowerCase();
1178
+ return GROUP_TARGET_PREFIXES.has(prefix) ? prefix : void 0;
1179
+ }
1180
+ function isGroupEntry(sessionKey, entry) {
1181
+ const parts = sessionKey.toLowerCase().split(":").filter(Boolean);
1182
+ if (parts.some((part) => GROUP_PEER_SHAPES.has(part))) {
1183
+ return true;
1184
+ }
1185
+ if (readChatTypeSignal(entry.chatType) || readChatTypeSignal(entry.origin?.chatType)) {
1186
+ return true;
1187
+ }
1188
+ const toCandidates = [entry.deliveryContext?.to, entry.lastTo, entry.origin?.to];
1189
+ if (toCandidates.some((candidate) => readTargetPrefixSignal(candidate))) {
1190
+ return true;
1191
+ }
1192
+ return false;
1193
+ }
1194
+ function isSessionPeerShape3(value) {
1195
+ const normalized = value?.trim().toLowerCase();
1196
+ return normalized === "direct" || normalized === "dm" || normalized === "group" || normalized === "channel" || normalized === "room";
1197
+ }
1198
+ function routeTargetMatches(actual, expected) {
1199
+ if (!expected) {
1200
+ return true;
1201
+ }
1202
+ if (!actual) {
1203
+ return false;
1204
+ }
1205
+ return actual === expected || actual.endsWith(`:${expected}`) || expected.endsWith(`:${actual}`);
1206
+ }
1207
+ function routeMatchesPreferred(route, preferred) {
1208
+ if (!preferred) {
1209
+ return true;
1210
+ }
1211
+ if (preferred.channel && route.replyChannel !== preferred.channel) {
1212
+ return false;
1213
+ }
1214
+ if (!routeTargetMatches(route.replyTo, preferred.to)) {
1215
+ return false;
1216
+ }
1217
+ if (preferred.accountId && route.replyAccountId !== preferred.accountId) {
1218
+ return false;
1219
+ }
1220
+ return true;
1221
+ }
1222
+ function deriveAgentIdFromSessionKey(sessionKey) {
1223
+ const trimmed = readNonEmptyString4(sessionKey);
1224
+ if (!trimmed) {
1225
+ return void 0;
1226
+ }
1227
+ const parts = trimmed.split(":").filter((part) => part.length > 0);
1228
+ if (parts[0]?.toLowerCase() !== "agent") {
1229
+ return void 0;
1230
+ }
1231
+ return readNonEmptyString4(parts[1]);
1232
+ }
1233
+ function deriveReplyTargetKindFromSessionKey2(sessionKey) {
1234
+ const trimmed = readNonEmptyString4(sessionKey);
1235
+ if (!trimmed) {
1236
+ return void 0;
1237
+ }
1238
+ const parts = trimmed.split(":").filter((part) => part.length > 0);
1239
+ if (parts[0]?.toLowerCase() !== "agent") {
1240
+ return void 0;
1241
+ }
1242
+ const channel = readNonEmptyString4(parts[2])?.toLowerCase();
1243
+ const peerShape = parts.length >= 6 && isSessionPeerShape3(parts[4]) ? parts[4].toLowerCase() : parts.length >= 5 && isSessionPeerShape3(parts[3]) ? parts[3].toLowerCase() : void 0;
1244
+ switch (channel) {
1245
+ case "feishu":
1246
+ if (peerShape === "direct" || peerShape === "dm") {
1247
+ return "user";
1248
+ }
1249
+ if (peerShape === "group") {
1250
+ return "chat";
1251
+ }
1252
+ return void 0;
1253
+ case "discord":
1254
+ if (peerShape === "direct" || peerShape === "dm") {
1255
+ return "user";
1256
+ }
1257
+ if (peerShape === "channel") {
1258
+ return "channel";
1259
+ }
1260
+ return void 0;
1261
+ default:
1262
+ return void 0;
1263
+ }
1264
+ }
1265
+ function normalizeSessionStoreTarget(value, channel, sessionKey) {
1266
+ const trimmed = readNonEmptyString4(value);
1267
+ if (!trimmed) {
1268
+ return void 0;
1269
+ }
1270
+ const derivedKind = deriveReplyTargetKindFromSessionKey2(sessionKey);
1271
+ if (derivedKind && !/^(user|chat|channel|room):/u.test(trimmed)) {
1272
+ return `${derivedKind}:${trimmed}`;
1273
+ }
1274
+ return normalizeReplyTarget(trimmed, {
1275
+ channel,
1276
+ sessionKey
1277
+ });
1278
+ }
1279
+ function deriveChannelFromSessionKey(sessionKey) {
1280
+ const parts = sessionKey.split(":").filter(Boolean);
1281
+ if (parts[0]?.toLowerCase() !== "agent") {
1282
+ return void 0;
1283
+ }
1284
+ return normalizeChannel2(parts[2]);
1285
+ }
1286
+ function deriveTargetFromSessionKey(sessionKey, channel) {
1287
+ const parts = sessionKey.split(":").filter(Boolean);
1288
+ if (parts[0]?.toLowerCase() !== "agent") {
1289
+ return void 0;
1290
+ }
1291
+ if (parts.length < 5 || !isSessionPeerShape3(parts[3])) {
1292
+ return void 0;
1293
+ }
1294
+ const rawTarget = parts.length >= 6 ? parts[5] : parts[4];
1295
+ if (!readNonEmptyString4(rawTarget)) {
1296
+ return void 0;
1297
+ }
1298
+ return normalizeSessionStoreTarget(rawTarget, channel, sessionKey);
1299
+ }
1300
+ function extractRouteFromEntry(sessionKey, entry) {
1301
+ if (!entry) {
1302
+ return void 0;
1303
+ }
1304
+ const replyChannel = normalizeChannel2(entry.deliveryContext?.channel) ?? normalizeChannel2(entry.origin?.provider) ?? deriveChannelFromSessionKey(sessionKey);
1305
+ const replyTo = normalizeSessionStoreTarget(entry.deliveryContext?.to, replyChannel, sessionKey) ?? normalizeSessionStoreTarget(entry.lastTo, replyChannel, sessionKey) ?? normalizeSessionStoreTarget(entry.origin?.to, replyChannel, sessionKey) ?? deriveTargetFromSessionKey(sessionKey, replyChannel);
1306
+ const replyAccountId = readNonEmptyString4(entry.deliveryContext?.accountId) ?? readNonEmptyString4(entry.lastAccountId) ?? readNonEmptyString4(entry.origin?.accountId);
1307
+ if (!replyChannel || !replyTo) {
1308
+ return void 0;
1309
+ }
1310
+ return {
1311
+ sessionKey,
1312
+ agentId: deriveAgentIdFromSessionKey(sessionKey) ?? "main",
1313
+ replyChannel,
1314
+ replyTo,
1315
+ replyAccountId
1316
+ };
1317
+ }
1318
+ function tryDeriveAgentIdFromStorePath(sessionStorePath) {
1319
+ const normalized = path3.normalize(sessionStorePath);
1320
+ const parts = normalized.split(path3.sep).filter(Boolean);
1321
+ const agentsIndex = parts.lastIndexOf("agents");
1322
+ if (agentsIndex === -1) {
1323
+ return void 0;
1324
+ }
1325
+ return readNonEmptyString4(parts[agentsIndex + 1]);
1326
+ }
1327
+ function listSessionStorePaths(explicitPath, baseAgentId) {
1328
+ const candidates = [];
1329
+ const seen = /* @__PURE__ */ new Set();
1330
+ const addPath = (candidate) => {
1331
+ const trimmed = readNonEmptyString4(candidate);
1332
+ if (!trimmed) {
1333
+ return;
1334
+ }
1335
+ const normalized = path3.normalize(trimmed);
1336
+ if (seen.has(normalized)) {
1337
+ return;
1338
+ }
1339
+ seen.add(normalized);
1340
+ candidates.push(normalized);
1341
+ };
1342
+ addPath(explicitPath);
1343
+ if (candidates.length > 0) {
1344
+ return candidates;
1345
+ }
1346
+ const defaultOpenClawStateDir = getDefaultOpenClawStateDir();
1347
+ addPath(path3.join(defaultOpenClawStateDir, "agents", baseAgentId, "sessions", "sessions.json"));
1348
+ const agentsRoot = path3.join(defaultOpenClawStateDir, "agents");
1349
+ try {
1350
+ if (fs2.existsSync(agentsRoot)) {
1351
+ for (const entry of fs2.readdirSync(agentsRoot, { withFileTypes: true })) {
1352
+ if (!entry.isDirectory()) {
1353
+ continue;
1354
+ }
1355
+ addPath(path3.join(agentsRoot, entry.name, "sessions", "sessions.json"));
1356
+ }
1357
+ }
1358
+ } catch {
1359
+ }
1360
+ return candidates;
1361
+ }
1362
+ function readSessionStore(sessionStorePath, logger) {
1363
+ try {
1364
+ if (!fs2.existsSync(sessionStorePath)) {
1365
+ return void 0;
1366
+ }
1367
+ const raw = fs2.readFileSync(sessionStorePath, "utf-8");
1368
+ const parsed = JSON.parse(raw);
1369
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1370
+ return void 0;
1371
+ }
1372
+ return parsed;
1373
+ } catch (error) {
1374
+ logger.debug(
1375
+ `Failed to read session store ${sessionStorePath}: ${error instanceof Error ? error.message : String(error)}`
1376
+ );
1377
+ return void 0;
1378
+ }
1379
+ }
1380
+ function readSessionStores(sessionStorePath, baseAgentId, logger) {
1381
+ const snapshots = [];
1382
+ const candidates = listSessionStorePaths(sessionStorePath, baseAgentId);
1383
+ logger.info(
1384
+ `Session store scan candidates: base_agent_id=${baseAgentId}, explicit_path=${sessionStorePath ?? "n/a"}, candidates=${JSON.stringify(candidates)}`
1385
+ );
1386
+ for (const candidate of candidates) {
1387
+ const store = readSessionStore(candidate, logger);
1388
+ if (store) {
1389
+ const keys = Object.keys(store);
1390
+ logger.info(
1391
+ `Session store loaded: path=${candidate}, entries=${keys.length}, session_keys=${JSON.stringify(keys)}`
1392
+ );
1393
+ snapshots.push({ path: candidate, store });
1394
+ }
1395
+ }
1396
+ return snapshots;
1397
+ }
1398
+ function mergeRoute(base, resolved, overrides, allowSessionOverride) {
1399
+ const nextSessionKey = allowSessionOverride && !overrides.sessionKey ? resolved.sessionKey : base.sessionKey;
1400
+ return {
1401
+ sessionKey: nextSessionKey,
1402
+ agentId: overrides.agentId === true ? base.agentId : resolved.agentId ?? deriveAgentIdFromSessionKey(nextSessionKey) ?? base.agentId,
1403
+ replyChannel: overrides.replyChannel === true ? base.replyChannel : resolved.replyChannel ?? base.replyChannel,
1404
+ replyTo: overrides.replyTo === true ? base.replyTo : resolved.replyTo ?? base.replyTo,
1405
+ replyAccountId: overrides.replyAccountId === true ? base.replyAccountId : resolved.replyAccountId ?? base.replyAccountId
1406
+ };
1407
+ }
1408
+ function selectExactRoute(snapshots, sessionKey) {
1409
+ let best;
1410
+ for (const snapshot of snapshots) {
1411
+ const entry = snapshot.store[sessionKey];
1412
+ const route = extractRouteFromEntry(sessionKey, entry);
1413
+ if (!route || !isExternalChannel(route.replyChannel)) {
1414
+ continue;
1415
+ }
1416
+ const updatedAt = normalizeUpdatedAt(entry?.updatedAt);
1417
+ if (!best || updatedAt > best.updatedAt) {
1418
+ best = { route, updatedAt };
1419
+ }
1420
+ }
1421
+ return best;
1422
+ }
1423
+ function selectBestRoute(snapshots, preferred, preferredAgentId, logger) {
1424
+ const candidates = [];
1425
+ const autoScan = preferred === void 0;
1426
+ for (const snapshot of snapshots) {
1427
+ const pathAgentId = tryDeriveAgentIdFromStorePath(snapshot.path);
1428
+ for (const [sessionKey, entry] of Object.entries(snapshot.store)) {
1429
+ if (sessionKey.includes(":subagent:")) {
1430
+ continue;
1431
+ }
1432
+ if (isInternalSessionKey(sessionKey)) {
1433
+ logger?.debug(`Skipping ${sessionKey}: internal session`);
1434
+ continue;
1435
+ }
1436
+ if (autoScan && isGroupEntry(sessionKey, entry)) {
1437
+ logger?.debug(`Skipping ${sessionKey}: group entry in auto-scan`);
1438
+ continue;
1439
+ }
1440
+ const route = extractRouteFromEntry(sessionKey, entry);
1441
+ if (!route || !routeMatchesPreferred(route, preferred)) {
1442
+ continue;
1443
+ }
1444
+ if (preferredAgentId && route.agentId !== preferredAgentId && pathAgentId !== preferredAgentId) {
1445
+ continue;
1446
+ }
1447
+ candidates.push({
1448
+ route,
1449
+ updatedAt: normalizeUpdatedAt(entry.updatedAt),
1450
+ isExternal: isExternalChannel(route.replyChannel),
1451
+ isDirect: isDirectSessionKey(sessionKey, entry)
1452
+ });
1453
+ }
1454
+ }
1455
+ if (candidates.length === 0) {
1456
+ return void 0;
1457
+ }
1458
+ const externalPool = candidates.filter((c) => c.isExternal);
1459
+ const channelPool = externalPool.length > 0 ? externalPool : candidates;
1460
+ const directPool = channelPool.filter((c) => c.isDirect);
1461
+ const finalPool = directPool.length > 0 ? directPool : channelPool;
1462
+ const chosen = finalPool.reduce((best, c) => c.updatedAt > best.updatedAt ? c : best);
1463
+ return { route: chosen.route, updatedAt: chosen.updatedAt };
1464
+ }
1465
+ function findSessionRouteForBinding(options, logger) {
1466
+ const channel = normalizeChannel2(options.channel);
1467
+ const to = normalizeReplyTarget(options.to, { channel });
1468
+ const accountId = readNonEmptyString4(options.accountId);
1469
+ const agentId = readNonEmptyString4(options.agentId) ?? "main";
1470
+ if (!channel || !to) {
1471
+ return void 0;
1472
+ }
1473
+ const snapshots = readSessionStores(options.sessionStorePath, agentId, logger);
1474
+ const best = selectBestRoute(snapshots, { channel, to, accountId }, agentId, logger);
1475
+ if (best) {
1476
+ return best.route;
1477
+ }
1478
+ const peerShape = inferPeerShape(channel, to);
1479
+ const targetLocal = stripTargetPrefix(to);
1480
+ if (!peerShape || !targetLocal) {
1481
+ return void 0;
1482
+ }
1483
+ return {
1484
+ sessionKey: `agent:${agentId}:${channel}:${peerShape}:${targetLocal}`,
1485
+ agentId,
1486
+ replyChannel: channel,
1487
+ replyTo: to,
1488
+ replyAccountId: accountId
1489
+ };
1490
+ }
1491
+ function inferPeerShape(channel, to) {
1492
+ const kind = to.split(":", 1)[0]?.toLowerCase();
1493
+ switch (kind) {
1494
+ case "user":
1495
+ return "direct";
1496
+ case "chat":
1497
+ return channel === "feishu" ? "group" : "direct";
1498
+ case "channel":
1499
+ return "channel";
1500
+ case "room":
1501
+ return "group";
1502
+ default:
1503
+ return void 0;
1504
+ }
1505
+ }
1506
+ function stripTargetPrefix(to) {
1507
+ const idx = to.indexOf(":");
1508
+ if (idx === -1) {
1509
+ return readNonEmptyString4(to);
1510
+ }
1511
+ return readNonEmptyString4(to.slice(idx + 1));
1512
+ }
1513
+ async function resolveNotificationRoute(config, logger, options = {}) {
1514
+ const overrides = createRouteOverrides2(config.routeOverrides);
1515
+ const configRoute = {
1516
+ sessionKey: readNonEmptyString4(config.sessionKey) ?? "main",
1517
+ agentId: readNonEmptyString4(config.agentId) ?? deriveAgentIdFromSessionKey(config.sessionKey) ?? "main",
1518
+ replyChannel: normalizeChannel2(config.replyChannel),
1519
+ replyTo: normalizeReplyTarget(config.replyTo, {
1520
+ channel: normalizeChannel2(config.replyChannel),
1521
+ sessionKey: config.sessionKey
1522
+ }),
1523
+ replyAccountId: readNonEmptyString4(config.replyAccountId)
1524
+ };
1525
+ logger.info(
1526
+ `Route resolve start: session_key=${configRoute.sessionKey}, agent_id=${configRoute.agentId}, channel=${configRoute.replyChannel ?? "n/a"}, to=${configRoute.replyTo ?? "n/a"}, account=${configRoute.replyAccountId ?? "n/a"}, overrides=${JSON.stringify(overrides)}, ignore_remembered=${options.ignoreRemembered === true}`
1527
+ );
1528
+ const hasExplicitConfig = isAnyRouteOverrideEnabled(overrides) || !isInternalSessionKey(configRoute.sessionKey) || Boolean(configRoute.replyChannel && configRoute.replyTo);
1529
+ if (hasExplicitConfig) {
1530
+ const snapshots2 = readSessionStores(config.sessionStorePath, configRoute.agentId, logger);
1531
+ let enriched = selectExactRoute(snapshots2, configRoute.sessionKey)?.route;
1532
+ if (!enriched && configRoute.replyChannel && configRoute.replyTo) {
1533
+ enriched = selectBestRoute(
1534
+ snapshots2,
1535
+ {
1536
+ channel: configRoute.replyChannel,
1537
+ to: configRoute.replyTo,
1538
+ accountId: configRoute.replyAccountId
1539
+ },
1540
+ void 0,
1541
+ logger
1542
+ )?.route;
1543
+ }
1544
+ const resolved = enriched ? mergeRoute(configRoute, enriched, overrides, isInternalSessionKey(configRoute.sessionKey)) : configRoute;
1545
+ logger.info(
1546
+ `Route resolve final (config): session_key=${resolved.sessionKey}, agent_id=${resolved.agentId}, channel=${resolved.replyChannel ?? "n/a"}, to=${resolved.replyTo ?? "n/a"}, account=${resolved.replyAccountId ?? "n/a"}`
1547
+ );
1548
+ return { route: resolved, source: "config" };
1549
+ }
1550
+ const snapshots = readSessionStores(config.sessionStorePath, configRoute.agentId, logger);
1551
+ if (options.ignoreRemembered !== true) {
1552
+ const remembered = await readStoredNotificationRoute(
1553
+ config.store,
1554
+ config.serverName,
1555
+ logger
1556
+ );
1557
+ if (remembered) {
1558
+ logger.info(
1559
+ `Route resolve remembered: session_key=${remembered.sessionKey}, agent_id=${remembered.agentId}, channel=${remembered.replyChannel ?? "n/a"}, to=${remembered.replyTo ?? "n/a"}, account=${remembered.replyAccountId ?? "n/a"}`
1560
+ );
1561
+ const preferred = remembered.replyChannel && remembered.replyTo ? {
1562
+ channel: remembered.replyChannel,
1563
+ to: remembered.replyTo,
1564
+ accountId: remembered.replyAccountId
1565
+ } : void 0;
1566
+ const peerMatch = selectBestRoute(snapshots, preferred, void 0, logger);
1567
+ if (peerMatch) {
1568
+ return { route: peerMatch.route, source: "remembered" };
1569
+ }
1570
+ if (!isInternalSessionKey(remembered.sessionKey)) {
1571
+ return {
1572
+ route: {
1573
+ sessionKey: remembered.sessionKey,
1574
+ agentId: remembered.agentId,
1575
+ replyChannel: remembered.replyChannel,
1576
+ replyTo: remembered.replyTo,
1577
+ replyAccountId: remembered.replyAccountId
1578
+ },
1579
+ source: "remembered"
1580
+ };
1581
+ }
1582
+ logger.warn(
1583
+ `Remembered route has internal session_key=${remembered.sessionKey} and no peer match; falling through to session-store scan.`
1584
+ );
1585
+ }
1586
+ }
1587
+ const best = selectBestRoute(snapshots, void 0, void 0, logger);
1588
+ if (best) {
1589
+ logger.info(
1590
+ `Route resolve from session store: session_key=${best.route.sessionKey}, agent_id=${best.route.agentId}, channel=${best.route.replyChannel ?? "n/a"}, to=${best.route.replyTo ?? "n/a"}, account=${best.route.replyAccountId ?? "n/a"}, updated_at=${best.updatedAt}`
1591
+ );
1592
+ return { route: best.route, source: "session-store" };
1593
+ }
1594
+ logger.warn(
1595
+ `Route resolve fell back to config default: session_key=${configRoute.sessionKey}, agent_id=${configRoute.agentId}`
1596
+ );
1597
+ return { route: configRoute, source: "default" };
1598
+ }
1599
+
1600
+ // src/agent-prompt-templates.ts
1601
+ function buildContextLines(context) {
1602
+ return [
1603
+ `homedir=${context.agxpHome}`,
1604
+ `server=${context.serverName}`
1605
+ ];
1606
+ }
1607
+ function buildSessionRequiredPromptTemplate({
1608
+ context,
1609
+ stderr
1610
+ }) {
1611
+ const lines = [
1612
+ "[AGXP_SESSION_REQUIRED]",
1613
+ ...buildContextLines(context),
1614
+ "AGXP authentication is required.",
1615
+ `Run \`agxp session start --email <email> -s ${context.serverName}\` to authenticate.`,
1616
+ `For first time login, use the agxp-identity skill to complete the onboarding flow.`
1617
+ ];
1618
+ if (stderr) {
1619
+ lines.push(`detail=${stderr}`);
1620
+ }
1621
+ return lines.join("\n");
1622
+ }
1623
+ function buildTimelinePayloadPromptTemplate(payload, context) {
1624
+ return [
1625
+ "[AGXP_TIMELINE_PAYLOAD]",
1626
+ ...buildContextLines(context),
1627
+ `AGXP timeline payload received. Use the agxp-timeline skill to process timeline payload.`,
1628
+ "Payload:",
1629
+ "```json",
1630
+ JSON.stringify(payload, null, 2),
1631
+ "```"
1632
+ ].join("\n");
1633
+ }
1634
+ function buildNotInstalledPromptTemplate({
1635
+ bin,
1636
+ installCommand
1637
+ }) {
1638
+ return [
1639
+ "[AGXP_NOT_INSTALLED]",
1640
+ `The AGXP CLI is not installed on this machine (tried bin=${bin}).`,
1641
+ "Please tell the user to run the following command to install it:",
1642
+ `\`${installCommand}\``
1643
+ ].join("\n");
1644
+ }
1645
+ function buildThreadEventPromptTemplate(event, context) {
1646
+ return [
1647
+ "[AGXP_THREAD_PAYLOAD]",
1648
+ ...buildContextLines(context),
1649
+ `AGXP thread messages received. Use the agxp-threads skill to process thread messages.`,
1650
+ "Payload:",
1651
+ "```json",
1652
+ JSON.stringify(event, null, 2),
1653
+ "```"
1654
+ ].join("\n");
1655
+ }
1656
+
1657
+ // src/notifier.ts
1658
+ var import_node_crypto = require("crypto");
1659
+ var COMMAND_TIMEOUT_MS = 9e4;
1660
+ var SUBAGENT_WAIT_TIMEOUT_MS = 18e4;
1661
+ var HEARTBEAT_REASON = "plugin:agxp";
1662
+ var AGXPNotifier = class {
1663
+ constructor(api, logger, config) {
1664
+ this.pendingCleanups = [];
1665
+ this.api = api;
1666
+ this.logger = logger;
1667
+ this.config = config;
1668
+ }
1669
+ get runtime() {
1670
+ return this.api.runtime ?? {};
1671
+ }
1672
+ async deliver(message, options) {
1673
+ const targetKey = options?.targetSessionKey;
1674
+ const messageIds = options?.messageIds;
1675
+ if (targetKey) {
1676
+ await this.drainPendingCleanups();
1677
+ const baseRoute = await this.resolveRoute();
1678
+ const sessionKey = `${targetKey}:${Date.now()}-${(0, import_node_crypto.randomUUID)().slice(0, 8)}`;
1679
+ const route = {
1680
+ sessionKey,
1681
+ agentId: baseRoute.route.agentId,
1682
+ ...baseRoute.route.replyChannel && { replyChannel: baseRoute.route.replyChannel },
1683
+ ...baseRoute.route.replyTo && { replyTo: baseRoute.route.replyTo },
1684
+ ...baseRoute.route.replyAccountId && { replyAccountId: baseRoute.route.replyAccountId }
1685
+ };
1686
+ this.logger.info(
1687
+ `Delivery route resolved: source=targeted-oneshot, ${formatRouteForLog(route)}, message_preview=${previewMessage(message)}`
1688
+ );
1689
+ const result = await this.attemptDelivery(message, route, { messageIds });
1690
+ if (result.result.ok) {
1691
+ this.logDispatch(result.result);
1692
+ } else {
1693
+ this.logger.error(`Failed to deliver notification to targeted session: ${result.errors.join(" | ")}`);
1694
+ }
1695
+ this.enqueueCleanup(sessionKey);
1696
+ return result.result.ok;
1697
+ }
1698
+ const initial = await this.resolveRoute();
1699
+ this.logger.info(
1700
+ `Delivery route resolved: source=${initial.source}, ${formatRouteForLog(initial.route)}, message_preview=${previewMessage(message)}`
1701
+ );
1702
+ const firstAttempt = await this.attemptDelivery(message, initial.route, { messageIds });
1703
+ if (firstAttempt.result.ok) {
1704
+ await this.rememberRouteIfChanged(firstAttempt.finalRoute, initial.source);
1705
+ this.logDispatch(firstAttempt.result);
1706
+ return true;
1707
+ }
1708
+ if (initial.source === "remembered") {
1709
+ this.logger.warn(
1710
+ `All transports failed with remembered route; re-resolving without remembered config.`
1711
+ );
1712
+ const fallback = await this.resolveRoute({ ignoreRemembered: true });
1713
+ if (fallback.route.sessionKey !== initial.route.sessionKey || fallback.route.replyTo !== initial.route.replyTo || fallback.route.replyChannel !== initial.route.replyChannel) {
1714
+ this.logger.info(
1715
+ `Retrying delivery with fresh route: source=${fallback.source}, ${formatRouteForLog(fallback.route)}`
1716
+ );
1717
+ const retry = await this.attemptDelivery(message, fallback.route, { messageIds });
1718
+ if (retry.result.ok) {
1719
+ await this.rememberRouteIfChanged(retry.finalRoute, fallback.source);
1720
+ this.logDispatch(retry.result);
1721
+ return true;
1722
+ }
1723
+ this.logger.error(
1724
+ `Failed to deliver notification after fresh re-resolve: ${retry.errors.join(" | ")}`
1725
+ );
1726
+ return false;
1727
+ }
1728
+ this.logger.warn("Fresh re-resolve produced the same route; skipping retry.");
1729
+ }
1730
+ this.logger.error(`Failed to deliver notification: ${firstAttempt.errors.join(" | ")}`);
1731
+ return false;
1732
+ }
1733
+ async attemptDelivery(message, route, options = {}) {
1734
+ const attempts = [
1735
+ () => this.tryNotifyViaRuntimeSubagent(message, route),
1736
+ () => this.tryNotifyViaRuntimeCommandAgent(message, route)
1737
+ ];
1738
+ if (!options.skipHeartbeat) {
1739
+ attempts.push(
1740
+ () => this.tryNotifyViaRuntimeHeartbeat(message, route),
1741
+ () => this.tryNotifyViaRuntimeCommandHeartbeat(message)
1742
+ );
1743
+ }
1744
+ const errors = [];
1745
+ for (const attempt of attempts) {
1746
+ const result = await attempt();
1747
+ if (result.ok) {
1748
+ const finalRoute = {
1749
+ sessionKey: result.sessionKey ?? route.sessionKey,
1750
+ agentId: route.agentId,
1751
+ replyChannel: route.replyChannel,
1752
+ replyTo: route.replyTo,
1753
+ replyAccountId: route.replyAccountId
1754
+ };
1755
+ this.markThreadsRead(options.messageIds, options.threadId);
1756
+ return { result, finalRoute, errors };
1757
+ }
1758
+ this.logger.warn(
1759
+ `Notification attempt failed: mode=${result.mode}, ${formatRouteForLog(route)}, error=${result.error}`
1760
+ );
1761
+ errors.push(`${result.mode}: ${result.error}`);
1762
+ }
1763
+ return {
1764
+ result: { ok: false, mode: "all", error: errors.join(" | ") },
1765
+ finalRoute: route,
1766
+ errors
1767
+ };
1768
+ }
1769
+ async tryNotifyViaRuntimeSubagent(message, route) {
1770
+ const runtimeSubagent = this.runtime.subagent;
1771
+ if (!runtimeSubagent || typeof runtimeSubagent.run !== "function") {
1772
+ return {
1773
+ ok: false,
1774
+ mode: "runtime.subagent",
1775
+ error: "runtime.subagent.run is unavailable"
1776
+ };
1777
+ }
1778
+ try {
1779
+ this.logger.info(
1780
+ `Attempting runtime.subagent delivery: ${formatRouteForLog(route)}, deliver=true`
1781
+ );
1782
+ const { runId } = await runtimeSubagent.run({
1783
+ sessionKey: route.sessionKey,
1784
+ message,
1785
+ deliver: true,
1786
+ idempotencyKey: (0, import_node_crypto.randomUUID)()
1787
+ });
1788
+ if (typeof runtimeSubagent.waitForRun === "function") {
1789
+ const waited = await runtimeSubagent.waitForRun({
1790
+ runId,
1791
+ timeoutMs: SUBAGENT_WAIT_TIMEOUT_MS
1792
+ });
1793
+ if (waited.status === "error") {
1794
+ return {
1795
+ ok: false,
1796
+ mode: "runtime.subagent",
1797
+ error: `subagent run error${waited.error ? `: ${waited.error}` : ""}`
1798
+ };
1799
+ }
1800
+ }
1801
+ return {
1802
+ ok: true,
1803
+ mode: "runtime.subagent",
1804
+ sessionKey: route.sessionKey,
1805
+ runId
1806
+ };
1807
+ } catch (error) {
1808
+ return {
1809
+ ok: false,
1810
+ mode: "runtime.subagent",
1811
+ error: formatError(error)
1812
+ };
1813
+ }
1814
+ }
1815
+ async tryNotifyViaRuntimeCommandAgent(message, route) {
1816
+ return this.runRuntimeCommand(
1817
+ "runtime.command.agent",
1818
+ this.buildAgentCliArgs(message, route),
1819
+ route
1820
+ );
1821
+ }
1822
+ async tryNotifyViaRuntimeHeartbeat(message, route) {
1823
+ const runtimeSystem = this.runtime.system;
1824
+ if (!runtimeSystem || typeof runtimeSystem.enqueueSystemEvent !== "function" || typeof runtimeSystem.requestHeartbeatNow !== "function") {
1825
+ return {
1826
+ ok: false,
1827
+ mode: "runtime.system.heartbeat",
1828
+ error: "runtime.system heartbeat APIs are unavailable"
1829
+ };
1830
+ }
1831
+ try {
1832
+ const deliveryContext = this.resolveHeartbeatDeliveryContext(route);
1833
+ this.logger.info(
1834
+ `Attempting runtime.system.heartbeat delivery: ${formatRouteForLog(route)}, delivery_context=${formatDeliveryContextForLog(deliveryContext)}`
1835
+ );
1836
+ const enqueued = runtimeSystem.enqueueSystemEvent(message, {
1837
+ sessionKey: route.sessionKey,
1838
+ ...deliveryContext ? { deliveryContext } : {}
1839
+ });
1840
+ runtimeSystem.requestHeartbeatNow({
1841
+ reason: HEARTBEAT_REASON,
1842
+ coalesceMs: 0,
1843
+ agentId: route.agentId,
1844
+ sessionKey: route.sessionKey
1845
+ });
1846
+ return {
1847
+ ok: true,
1848
+ mode: "runtime.system.heartbeat",
1849
+ sessionKey: route.sessionKey,
1850
+ detail: enqueued ? "enqueued" : "duplicate-enqueued"
1851
+ };
1852
+ } catch (error) {
1853
+ return {
1854
+ ok: false,
1855
+ mode: "runtime.system.heartbeat",
1856
+ error: formatError(error)
1857
+ };
1858
+ }
1859
+ }
1860
+ async tryNotifyViaRuntimeCommandHeartbeat(message) {
1861
+ const { route } = await this.resolveRoute();
1862
+ return this.runRuntimeCommand(
1863
+ "runtime.command.heartbeat",
1864
+ this.buildHeartbeatCliArgs(message),
1865
+ route
1866
+ );
1867
+ }
1868
+ async runRuntimeCommand(mode, argv, route) {
1869
+ const runtimeCommand = this.runtime.system?.runCommandWithTimeout;
1870
+ if (typeof runtimeCommand !== "function") {
1871
+ return {
1872
+ ok: false,
1873
+ mode,
1874
+ error: "runtime.system.runCommandWithTimeout is unavailable"
1875
+ };
1876
+ }
1877
+ try {
1878
+ this.logger.info(
1879
+ `Attempting ${mode} delivery: ${formatRouteForLog(route)}, argv=${formatCommandArgsForLog(argv)}`
1880
+ );
1881
+ const result = await runtimeCommand(argv, { timeoutMs: COMMAND_TIMEOUT_MS });
1882
+ if (result.code === 0) {
1883
+ return {
1884
+ ok: true,
1885
+ mode,
1886
+ sessionKey: route.sessionKey
1887
+ };
1888
+ }
1889
+ return {
1890
+ ok: false,
1891
+ mode,
1892
+ error: `${formatCommandFailure(result)} (argv=${formatCommandArgsForLog(argv)})`
1893
+ };
1894
+ } catch (error) {
1895
+ return {
1896
+ ok: false,
1897
+ mode,
1898
+ error: formatError(error)
1899
+ };
1900
+ }
1901
+ }
1902
+ buildAgentCliArgs(message, route) {
1903
+ const args = [
1904
+ this.config.openclawCliBin,
1905
+ "agent",
1906
+ "--message",
1907
+ message,
1908
+ "--agent",
1909
+ route.agentId,
1910
+ "--deliver"
1911
+ ];
1912
+ if (route.replyChannel) {
1913
+ args.push("--reply-channel", route.replyChannel);
1914
+ }
1915
+ if (route.replyTo) {
1916
+ args.push("--reply-to", route.replyTo);
1917
+ }
1918
+ if (route.replyAccountId) {
1919
+ args.push("--reply-account", route.replyAccountId);
1920
+ }
1921
+ return args;
1922
+ }
1923
+ buildHeartbeatCliArgs(message) {
1924
+ return [
1925
+ this.config.openclawCliBin,
1926
+ "system",
1927
+ "event",
1928
+ "--text",
1929
+ message,
1930
+ "--mode",
1931
+ "now"
1932
+ ];
1933
+ }
1934
+ resolveHeartbeatDeliveryContext(route) {
1935
+ if (!route.replyChannel && !route.replyTo && !route.replyAccountId) {
1936
+ return void 0;
1937
+ }
1938
+ return {
1939
+ ...route.replyChannel ? { channel: route.replyChannel } : {},
1940
+ ...route.replyTo ? { to: route.replyTo } : {},
1941
+ ...route.replyAccountId ? { accountId: route.replyAccountId } : {}
1942
+ };
1943
+ }
1944
+ async resolveRoute(options = {}) {
1945
+ return resolveNotificationRoute(
1946
+ this.config,
1947
+ this.logger,
1948
+ options
1949
+ );
1950
+ }
1951
+ /**
1952
+ * Persist the successful route to the agxp CLI config unless it came from
1953
+ * the remembered config already (no-op when unchanged).
1954
+ */
1955
+ async rememberRouteIfChanged(route, source) {
1956
+ if (!route.sessionKey || !route.agentId) {
1957
+ return;
1958
+ }
1959
+ if (isInternalSessionKey(route.sessionKey)) {
1960
+ return;
1961
+ }
1962
+ if (!route.replyChannel || !route.replyTo) {
1963
+ return;
1964
+ }
1965
+ if (source === "remembered") {
1966
+ this.logger.debug(
1967
+ `Skipping remembered-route write; route came from config (session_key=${route.sessionKey})`
1968
+ );
1969
+ return;
1970
+ }
1971
+ await writeStoredNotificationRoute(
1972
+ this.config.store,
1973
+ this.config.serverName,
1974
+ route,
1975
+ this.logger
1976
+ );
1977
+ }
1978
+ /**
1979
+ * Best-effort cleanup of a one-shot session. Failures are logged but do not
1980
+ * propagate — the session may already have been cleaned up by the runtime.
1981
+ */
1982
+ async tryDeleteSession(sessionKey) {
1983
+ const deleteSession = this.runtime.subagent?.deleteSession;
1984
+ if (typeof deleteSession !== "function") {
1985
+ this.logger.debug(`deleteSession unavailable; skipping cleanup for session_key=${sessionKey}`);
1986
+ return;
1987
+ }
1988
+ try {
1989
+ await deleteSession({ sessionKey, deleteTranscript: true });
1990
+ this.logger.info(`One-shot session cleaned up: session_key=${sessionKey}`);
1991
+ } catch (error) {
1992
+ this.logger.warn(`Failed to clean up one-shot session session_key=${sessionKey}: ${formatError(error)}`);
1993
+ }
1994
+ }
1995
+ /** Queue a session cleanup as fire-and-forget (non-blocking). */
1996
+ enqueueCleanup(sessionKey) {
1997
+ const cleanup = this.tryDeleteSession(sessionKey);
1998
+ this.pendingCleanups.push(cleanup);
1999
+ cleanup.finally(() => {
2000
+ const idx = this.pendingCleanups.indexOf(cleanup);
2001
+ if (idx >= 0) this.pendingCleanups.splice(idx, 1);
2002
+ });
2003
+ }
2004
+ /** Await all pending session cleanups. Called before new delivery and on stop(). */
2005
+ async drainPendingCleanups() {
2006
+ if (this.pendingCleanups.length === 0) return;
2007
+ this.logger.debug(`Draining ${this.pendingCleanups.length} pending session cleanup(s)`);
2008
+ await Promise.allSettled([...this.pendingCleanups]);
2009
+ }
2010
+ /**
2011
+ * Fire-and-forget PM read-marking. Called after a notification is delivered.
2012
+ * Never awaited by the delivery path: a slow or failed mark must not block
2013
+ * delivery (at-least-once semantics). The mark is idempotent server-side, so
2014
+ * a dropped attempt simply means the message may be re-fetched once.
2015
+ */
2016
+ markThreadsRead(messageIds, threadId) {
2017
+ if (!messageIds || messageIds.length === 0) return;
2018
+ if (!threadId) {
2019
+ this.logger.debug("threadId unset; skipping thread mark-read");
2020
+ return;
2021
+ }
2022
+ const serverName = this.config.serverName;
2023
+ if (!serverName) {
2024
+ this.logger.debug("serverName unset; skipping thread mark-read");
2025
+ return;
2026
+ }
2027
+ this.runMarkMessagesRead(serverName, messageIds, threadId).catch((error) => {
2028
+ this.logger.debug(`Thread mark-read failed: ${formatError(error)}`);
2029
+ });
2030
+ }
2031
+ async runMarkMessagesRead(serverName, messageIds, threadId) {
2032
+ const runtimeCommand = this.runtime.system?.runCommandWithTimeout;
2033
+ if (typeof runtimeCommand !== "function") {
2034
+ this.logger.debug("runCommandWithTimeout unavailable - cannot mark messages as read");
2035
+ return false;
2036
+ }
2037
+ try {
2038
+ const args = [
2039
+ this.config.agxpBin || "agxp",
2040
+ "thread",
2041
+ "read",
2042
+ "--thread",
2043
+ threadId,
2044
+ "--messages",
2045
+ messageIds.join(","),
2046
+ "-s",
2047
+ serverName
2048
+ ];
2049
+ this.logger.debug(`Marking messages as read: ${messageIds.length} messages`);
2050
+ const result = await runtimeCommand(args, { timeoutMs: 1e4 });
2051
+ if (result.code === 0) {
2052
+ this.logger.info(`Marked ${messageIds.length} message(s) as read`);
2053
+ return true;
2054
+ }
2055
+ this.logger.warn(
2056
+ `Failed to mark messages as read: ${result.stderr?.slice(-200) || String(result.code)}`
2057
+ );
2058
+ return false;
2059
+ } catch (error) {
2060
+ this.logger.warn(`Failed to mark messages as read: ${formatError(error)}`);
2061
+ return false;
2062
+ }
2063
+ }
2064
+ logDispatch(result) {
2065
+ const details = [
2066
+ `mode=${result.mode}`,
2067
+ result.sessionKey ? `session_key=${result.sessionKey}` : null,
2068
+ result.runId ? `run_id=${result.runId}` : null,
2069
+ result.detail ? `detail=${result.detail}` : null
2070
+ ].filter(Boolean).join(", ");
2071
+ this.logger.info(`Notification dispatched: ${details}`);
2072
+ }
2073
+ };
2074
+ function formatCommandFailure(result) {
2075
+ return result.stderr?.trim() || result.stdout?.trim() || `command exited with ${result.code ?? "unknown"}`;
2076
+ }
2077
+ function formatError(error) {
2078
+ if (error instanceof Error) {
2079
+ return `${error.name}: ${error.message}`;
2080
+ }
2081
+ return String(error);
2082
+ }
2083
+ function formatRouteForLog(route) {
2084
+ return [
2085
+ `session_key=${route.sessionKey}`,
2086
+ `agent_id=${route.agentId}`,
2087
+ `channel=${route.replyChannel ?? "n/a"}`,
2088
+ `to=${route.replyTo ?? "n/a"}`,
2089
+ `account=${route.replyAccountId ?? "n/a"}`
2090
+ ].join(", ");
2091
+ }
2092
+ function formatDeliveryContextForLog(deliveryContext) {
2093
+ if (!deliveryContext) {
2094
+ return "none";
2095
+ }
2096
+ return [
2097
+ `channel=${deliveryContext.channel ?? "n/a"}`,
2098
+ `to=${deliveryContext.to ?? "n/a"}`,
2099
+ `account=${deliveryContext.accountId ?? "n/a"}`
2100
+ ].join(", ");
2101
+ }
2102
+ function previewMessage(message, maxLength = 120) {
2103
+ const singleLine = message.replace(/\s+/gu, " ").trim();
2104
+ if (singleLine.length <= maxLength) {
2105
+ return JSON.stringify(singleLine);
2106
+ }
2107
+ return JSON.stringify(`${singleLine.slice(0, maxLength - 3)}...`);
2108
+ }
2109
+ function formatCommandArgsForLog(argv) {
2110
+ const sanitized = [...argv];
2111
+ for (let index = 0; index < sanitized.length; index += 1) {
2112
+ if (sanitized[index] === "--message" || sanitized[index] === "--text") {
2113
+ if (typeof sanitized[index + 1] === "string") {
2114
+ sanitized[index + 1] = `<len:${sanitized[index + 1].length}>`;
2115
+ }
2116
+ }
2117
+ }
2118
+ return JSON.stringify(sanitized);
2119
+ }
2120
+
2121
+ // src/index.ts
2122
+ var COMMAND_NAMES = ["session", "identity", "servers", "timeline", "thread", "here", "version"];
2123
+ var COMMAND_NAME_SET = new Set(COMMAND_NAMES);
2124
+ var DEFAULT_ROUTING = {
2125
+ sessionKey: PLUGIN_CONFIG.DEFAULT_SESSION_KEY,
2126
+ agentId: PLUGIN_CONFIG.DEFAULT_AGENT_ID,
2127
+ routeOverrides: {
2128
+ sessionKey: false,
2129
+ agentId: false,
2130
+ replyChannel: false,
2131
+ replyTo: false,
2132
+ replyAccountId: false
2133
+ }
2134
+ };
2135
+ function registerPlugin(api) {
2136
+ const logger = new Logger(resolvePluginLogger(api));
2137
+ const pluginConfig = resolvePluginConfig(api.pluginConfig, logger);
2138
+ const agxpHome = resolveAgxpHome();
2139
+ logger.info(
2140
+ `AGXP home resolved: path=${agxpHome}, source=${process.env.AGXP_HOME ? "AGXP_HOME env" : "os.homedir()"}, homedir=${os4.homedir()}`
2141
+ );
2142
+ process.env.AGXP_HOME = agxpHome;
2143
+ process.env.AGXP_HOST = `openclaw/${PLUGIN_CONFIG.PLUGIN_VERSION}`;
2144
+ logger.info(`Client env: AGXP_HOST=${process.env.AGXP_HOST}`);
2145
+ const store = createInMemoryPluginStore();
2146
+ let runtimes = [];
2147
+ let notInstalledPromptDelivered = false;
2148
+ api.registerService({
2149
+ id: "agxp:discovery",
2150
+ start: async () => {
2151
+ logger.info("Starting AGXP discovery service...");
2152
+ const discovery = await discoverServers(pluginConfig.agxpBin, logger);
2153
+ if (discovery.kind === "not_installed") {
2154
+ logger.warn(
2155
+ `AGXP CLI not installed (bin=${discovery.bin}); delivering install prompt to user`
2156
+ );
2157
+ if (!notInstalledPromptDelivered) {
2158
+ notInstalledPromptDelivered = true;
2159
+ await deliverNotInstalledPrompt(api, logger, pluginConfig, agxpHome, discovery.bin, store);
2160
+ }
2161
+ return;
2162
+ }
2163
+ const servers = discovery.servers;
2164
+ if (servers.length === 0) {
2165
+ logger.warn("No AGXP servers discovered; services will not start");
2166
+ return;
2167
+ }
2168
+ logger.info(`Discovered ${servers.length} server(s): ${servers.map((s) => s.name).join(", ")}`);
2169
+ if (!process.env.AGXP_CHANNEL) {
2170
+ const firstRouting = pluginConfig.serverRouting[servers[0].name];
2171
+ const channel = firstRouting?.replyChannel;
2172
+ process.env.AGXP_CHANNEL = channel || "openclaw";
2173
+ logger.info(`Client env: AGXP_CHANNEL=${process.env.AGXP_CHANNEL} (source=${channel ? "routing.replyChannel" : "default"})`);
2174
+ }
2175
+ runtimes = servers.map(
2176
+ (server) => createServerRuntime(api, logger, pluginConfig, server, agxpHome, store)
2177
+ );
2178
+ for (const runtime of runtimes) {
2179
+ logger.info(`Starting services for server=${runtime.server.name}`);
2180
+ await runtime.timelinePoller.start();
2181
+ await runtime.eventClient.start();
2182
+ runtime.identityRefresher.start();
2183
+ }
2184
+ },
2185
+ stop: async () => {
2186
+ logger.info("Stopping AGXP discovery service...");
2187
+ for (const runtime of runtimes) {
2188
+ logger.info(`Stopping services for server=${runtime.server.name}`);
2189
+ runtime.timelinePoller.stop();
2190
+ await runtime.waitForPendingDelivery();
2191
+ await runtime.notifier.drainPendingCleanups();
2192
+ await runtime.eventClient.stop();
2193
+ runtime.identityRefresher.stop();
2194
+ }
2195
+ runtimes = [];
2196
+ notInstalledPromptDelivered = false;
2197
+ }
2198
+ });
2199
+ registerCommand(
2200
+ api,
2201
+ logger,
2202
+ pluginConfig,
2203
+ agxpHome,
2204
+ store,
2205
+ () => runtimes,
2206
+ (next) => {
2207
+ runtimes = next;
2208
+ }
2209
+ );
2210
+ }
2211
+ function resolvePluginLogger(api) {
2212
+ const runtimeLogging = api.runtime?.logging;
2213
+ if (runtimeLogging && typeof runtimeLogging.getChildLogger === "function") {
2214
+ try {
2215
+ const child = runtimeLogging.getChildLogger({ plugin: "agxp" });
2216
+ if (child) {
2217
+ return child;
2218
+ }
2219
+ } catch {
2220
+ }
2221
+ }
2222
+ return api.logger;
2223
+ }
2224
+ var PLUGIN_CONFIG_SCHEMA = (0, import_plugin_entry.buildJsonPluginConfigSchema)({
2225
+ type: "object",
2226
+ additionalProperties: false,
2227
+ properties: {
2228
+ agxpBin: { type: "string" },
2229
+ openclawCliBin: { type: "string" },
2230
+ skills: { type: "array", items: { type: "string" } },
2231
+ serverRouting: {
2232
+ type: "object",
2233
+ additionalProperties: {
2234
+ type: "object",
2235
+ additionalProperties: false,
2236
+ properties: {
2237
+ sessionKey: { type: "string" },
2238
+ agentId: { type: "string" },
2239
+ replyChannel: { type: "string" },
2240
+ replyTo: { type: "string" },
2241
+ replyAccountId: { type: "string" }
2242
+ }
2243
+ }
2244
+ },
2245
+ _credentialBackup: {
2246
+ type: "object",
2247
+ description: "Internal: persisted credential backups for sandbox environments",
2248
+ additionalProperties: { type: "object" }
2249
+ }
2250
+ }
2251
+ });
2252
+ var index_default = (0, import_plugin_entry.definePluginEntry)({
2253
+ id: "openclaw-agxp",
2254
+ name: "AGXP",
2255
+ description: "OpenClaw extension for AGXP with CLI-based timeline polling and thread event streaming",
2256
+ configSchema: PLUGIN_CONFIG_SCHEMA,
2257
+ register(api) {
2258
+ if (api.registrationMode && api.registrationMode !== "full") return;
2259
+ registerPlugin(api);
2260
+ }
2261
+ });
2262
+ var INSTALL_COMMAND = "curl -fsSL https://agxp.ai/install.sh | bash";
2263
+ var _lastBackedUpToken = {};
2264
+ function backupCredentialsToConfig(api, logger, credentialsLoader, serverName) {
2265
+ const authState = credentialsLoader.loadAuthState();
2266
+ if (authState.status !== "available") {
2267
+ logger.debug(`[credential-backup] skip: credentials not available for server=${serverName}`);
2268
+ return;
2269
+ }
2270
+ if (_lastBackedUpToken[serverName] === authState.accessToken) {
2271
+ return;
2272
+ }
2273
+ const backup = {
2274
+ access_token: authState.accessToken,
2275
+ email: authState.email,
2276
+ expires_at: authState.expiresAt ?? Date.now() + 30 * 24 * 60 * 60 * 1e3,
2277
+ server: serverName,
2278
+ backed_up_at: Date.now()
2279
+ };
2280
+ logger.info(
2281
+ `[credential-backup] backing up credentials to OpenClaw config: server=${serverName}, email=${authState.email ?? "n/a"}`
2282
+ );
2283
+ try {
2284
+ api.runtime.config.mutateConfigFile({
2285
+ afterWrite: { mode: "none", reason: "agxp credential backup" },
2286
+ mutate(draft) {
2287
+ var _a, _b, _c, _d;
2288
+ draft.plugins ?? (draft.plugins = {});
2289
+ (_a = draft.plugins).entries ?? (_a.entries = {});
2290
+ (_b = draft.plugins.entries)["openclaw-agxp"] ?? (_b["openclaw-agxp"] = {});
2291
+ (_c = draft.plugins.entries["openclaw-agxp"]).config ?? (_c.config = {});
2292
+ (_d = draft.plugins.entries["openclaw-agxp"].config)._credentialBackup ?? (_d._credentialBackup = {});
2293
+ draft.plugins.entries["openclaw-agxp"].config._credentialBackup[serverName] = backup;
2294
+ }
2295
+ }).then(() => {
2296
+ _lastBackedUpToken[serverName] = authState.accessToken;
2297
+ logger.info(`[credential-backup] saved to OpenClaw config for server=${serverName}`);
2298
+ }).catch((err) => {
2299
+ logger.warn(`[credential-backup] failed to save: ${err.message}`);
2300
+ });
2301
+ } catch (err) {
2302
+ logger.warn(`[credential-backup] sync error: ${err instanceof Error ? err.message : String(err)}`);
2303
+ }
2304
+ }
2305
+ async function deliverNotInstalledPrompt(api, logger, pluginConfig, _agxpHome, bin, store) {
2306
+ const notifier = new AGXPNotifier(api, logger, {
2307
+ sessionKey: DEFAULT_ROUTING.sessionKey,
2308
+ agentId: DEFAULT_ROUTING.agentId,
2309
+ replyChannel: DEFAULT_ROUTING.replyChannel,
2310
+ replyTo: DEFAULT_ROUTING.replyTo,
2311
+ replyAccountId: DEFAULT_ROUTING.replyAccountId,
2312
+ openclawCliBin: pluginConfig.openclawCliBin,
2313
+ routeOverrides: DEFAULT_ROUTING.routeOverrides
2314
+ });
2315
+ await notifier.deliver(
2316
+ buildNotInstalledPromptTemplate({ bin, installCommand: INSTALL_COMMAND })
2317
+ );
2318
+ }
2319
+ function buildTimelineSessionKey(serverName) {
2320
+ return `agxp:timeline:${serverName}`;
2321
+ }
2322
+ function createServerRuntime(api, logger, pluginConfig, server, agxpHome, store) {
2323
+ const routing = pluginConfig.serverRouting[server.name] ?? DEFAULT_ROUTING;
2324
+ const credentialsLoader = new CredentialsLoader(logger, agxpHome, server.name);
2325
+ try {
2326
+ const rawConfig = api.pluginConfig;
2327
+ const backupMap = rawConfig?._credentialBackup;
2328
+ const backup = backupMap?.[server.name];
2329
+ if (backup?.access_token) {
2330
+ credentialsLoader.restoreFromBackup(backup);
2331
+ } else {
2332
+ logger.debug(`[credential-restore] no backup found in OpenClaw config for server=${server.name}`);
2333
+ }
2334
+ } catch (err) {
2335
+ logger.warn(`[credential-restore] failed: ${err instanceof Error ? err.message : String(err)}`);
2336
+ }
2337
+ const notifier = new AGXPNotifier(api, logger, {
2338
+ store,
2339
+ agxpBin: pluginConfig.agxpBin,
2340
+ serverName: server.name,
2341
+ sessionKey: routing.sessionKey,
2342
+ agentId: routing.agentId,
2343
+ replyChannel: routing.replyChannel,
2344
+ replyTo: routing.replyTo,
2345
+ replyAccountId: routing.replyAccountId,
2346
+ openclawCliBin: pluginConfig.openclawCliBin,
2347
+ routeOverrides: routing.routeOverrides
2348
+ });
2349
+ const getPromptContext = () => ({
2350
+ serverName: server.name,
2351
+ agxpHome
2352
+ });
2353
+ let lastAuthPromptKey = null;
2354
+ const resetAuthPromptGate = () => {
2355
+ lastAuthPromptKey = null;
2356
+ };
2357
+ const notifySessionRequired = async (_sessionEvent) => {
2358
+ const promptKey = `session_required:${server.name}`;
2359
+ if (lastAuthPromptKey === promptKey) {
2360
+ logger.debug(`Skipping duplicate session prompt for server=${server.name}`);
2361
+ return;
2362
+ }
2363
+ lastAuthPromptKey = promptKey;
2364
+ await notifier.deliver(
2365
+ buildSessionRequiredPromptTemplate({ context: getPromptContext() })
2366
+ );
2367
+ };
2368
+ let timelineDeliveryInFlight = false;
2369
+ let timelineDeliveryStartedAt = 0;
2370
+ let timelineDeliverySkipCount = 0;
2371
+ let activeTimelineDelivery = null;
2372
+ const TIMELINE_DELIVERY_TIMEOUT_MS = 3e5;
2373
+ const timelinePoller = new AGXPTimelineClient({
2374
+ serverName: server.name,
2375
+ agxpBin: pluginConfig.agxpBin,
2376
+ resolvePollIntervalSec: () => readPollIntervalSec(pluginConfig.agxpBin, server.name, logger),
2377
+ logger,
2378
+ onTimelinePolled: async (payload) => {
2379
+ resetAuthPromptGate();
2380
+ backupCredentialsToConfig(api, logger, credentialsLoader, server.name);
2381
+ const items = payload.result?.items ?? [];
2382
+ const notifications = payload.result?.notifications ?? [];
2383
+ if (timelineDeliveryInFlight && timelineDeliveryStartedAt > 0) {
2384
+ const elapsed = Date.now() - timelineDeliveryStartedAt;
2385
+ if (elapsed > TIMELINE_DELIVERY_TIMEOUT_MS) {
2386
+ logger.error(
2387
+ `Timeline delivery flag stuck for ${Math.round(elapsed / 1e3)}s on server=${server.name}, force-resetting`
2388
+ );
2389
+ timelineDeliveryInFlight = false;
2390
+ activeTimelineDelivery = null;
2391
+ }
2392
+ }
2393
+ if (timelineDeliveryInFlight) {
2394
+ timelineDeliverySkipCount += 1;
2395
+ const elapsed = Date.now() - timelineDeliveryStartedAt;
2396
+ logger.warn(
2397
+ `Skipping timeline delivery for server=${server.name}: previous delivery still in progress (elapsed=${Math.round(elapsed / 1e3)}s, skipped_posts=${items.length}, skipped_notifications=${notifications.length}, total_skips=${timelineDeliverySkipCount})`
2398
+ );
2399
+ return;
2400
+ }
2401
+ timelineDeliveryInFlight = true;
2402
+ const startedAt = Date.now();
2403
+ timelineDeliveryStartedAt = startedAt;
2404
+ activeTimelineDelivery = notifier.deliver(
2405
+ buildTimelinePayloadPromptTemplate(payload, getPromptContext()),
2406
+ { targetSessionKey: buildTimelineSessionKey(server.name) }
2407
+ ).finally(() => {
2408
+ const duration = Date.now() - startedAt;
2409
+ logger.info(`Timeline delivery completed for server=${server.name} in ${Math.round(duration / 1e3)}s`);
2410
+ if (timelineDeliveryStartedAt === startedAt) {
2411
+ timelineDeliveryInFlight = false;
2412
+ activeTimelineDelivery = null;
2413
+ }
2414
+ });
2415
+ await activeTimelineDelivery;
2416
+ },
2417
+ onSessionRequired: notifySessionRequired
2418
+ });
2419
+ const eventClient = new AGXPEventClient({
2420
+ serverName: server.name,
2421
+ agxpBin: pluginConfig.agxpBin,
2422
+ logger,
2423
+ onThreadEvent: async (event) => {
2424
+ resetAuthPromptGate();
2425
+ const messages = event.data?.messages ?? [];
2426
+ if (messages.length > 0) {
2427
+ const messageIds = messages.map((m) => m.message_id).filter((id) => typeof id === "string" && id.length > 0);
2428
+ const threadId = messages.find((m) => typeof m.thread_id === "string" && m.thread_id.length > 0)?.thread_id;
2429
+ await notifier.deliver(buildThreadEventPromptTemplate(event, getPromptContext()), {
2430
+ messageIds,
2431
+ threadId
2432
+ });
2433
+ }
2434
+ },
2435
+ onSessionRequired: async () => {
2436
+ await notifySessionRequired({ reason: "session_required" });
2437
+ }
2438
+ });
2439
+ const identityRefresher = new AGXPIdentityRefresher({
2440
+ serverName: server.name,
2441
+ agxpBin: pluginConfig.agxpBin,
2442
+ logger,
2443
+ onRefreshPrompt: async (prompt) => {
2444
+ resetAuthPromptGate();
2445
+ await notifier.deliver(prompt);
2446
+ },
2447
+ onSessionRequired: async () => {
2448
+ await notifySessionRequired({ reason: "session_required" });
2449
+ }
2450
+ });
2451
+ return {
2452
+ server,
2453
+ routing,
2454
+ credentialsLoader,
2455
+ notifier,
2456
+ timelinePoller,
2457
+ eventClient,
2458
+ identityRefresher,
2459
+ getPromptContext,
2460
+ async waitForPendingDelivery() {
2461
+ if (activeTimelineDelivery) {
2462
+ try {
2463
+ await activeTimelineDelivery;
2464
+ } catch {
2465
+ }
2466
+ }
2467
+ }
2468
+ };
2469
+ }
2470
+ function registerCommand(api, logger, pluginConfig, agxpHome, store, getRuntimes, setRuntimes) {
2471
+ if (!api.registerCommand) {
2472
+ logger.warn("registerCommand API unavailable; skipping /agxp command registration");
2473
+ return;
2474
+ }
2475
+ let inflightDiscovery = null;
2476
+ const runDiscovery = async () => {
2477
+ const discovery = await discoverServers(pluginConfig.agxpBin, logger);
2478
+ if (discovery.kind === "not_installed") {
2479
+ return { runtimes: getRuntimes(), notInstalledBin: discovery.bin };
2480
+ }
2481
+ if (discovery.servers.length === 0) {
2482
+ return { runtimes: getRuntimes() };
2483
+ }
2484
+ const created = discovery.servers.map(
2485
+ (server) => createServerRuntime(api, logger, pluginConfig, server, agxpHome, store)
2486
+ );
2487
+ setRuntimes(created);
2488
+ return { runtimes: created };
2489
+ };
2490
+ const ensureRuntimes = async () => {
2491
+ const existing = getRuntimes();
2492
+ if (existing.length > 0) {
2493
+ return { runtimes: existing };
2494
+ }
2495
+ if (!inflightDiscovery) {
2496
+ inflightDiscovery = runDiscovery().finally(() => {
2497
+ inflightDiscovery = null;
2498
+ });
2499
+ }
2500
+ return inflightDiscovery;
2501
+ };
2502
+ api.registerCommand({
2503
+ name: "agxp",
2504
+ description: "AGXP plugin commands: session, identity, servers, timeline, thread, here, version",
2505
+ acceptsArgs: true,
2506
+ handler: async (ctx) => {
2507
+ const parsed = parseCommandArgs(ctx.args);
2508
+ if (parsed.command === "version") {
2509
+ return {
2510
+ text: await buildVersionText(pluginConfig.agxpBin)
2511
+ };
2512
+ }
2513
+ const { runtimes, notInstalledBin } = await ensureRuntimes();
2514
+ if (notInstalledBin && runtimes.length === 0) {
2515
+ return {
2516
+ text: `AGXP CLI not installed (bin=${notInstalledBin}). Install with: ${INSTALL_COMMAND}`
2517
+ };
2518
+ }
2519
+ if (parsed.command === "servers") {
2520
+ return {
2521
+ text: buildServersText(runtimes)
2522
+ };
2523
+ }
2524
+ const selection = selectServerRuntime(runtimes, parsed.serverName);
2525
+ if (!selection.runtime) {
2526
+ return {
2527
+ text: selection.error ?? buildHelpText(runtimes)
2528
+ };
2529
+ }
2530
+ const runtime = selection.runtime;
2531
+ await rememberCurrentCommandRouteIfPossible(ctx, runtime, store, logger);
2532
+ switch (parsed.command) {
2533
+ case "session":
2534
+ return {
2535
+ text: buildAuthStatusText(runtime)
2536
+ };
2537
+ case "identity":
2538
+ return {
2539
+ text: await buildIdentityText(runtime, pluginConfig.agxpBin)
2540
+ };
2541
+ case "timeline":
2542
+ return {
2543
+ text: await buildTimelineText(runtime)
2544
+ };
2545
+ case "thread":
2546
+ return {
2547
+ text: buildThreadStatusText(runtime)
2548
+ };
2549
+ case "here":
2550
+ return {
2551
+ text: await buildHereText(ctx, runtime, store, logger)
2552
+ };
2553
+ default:
2554
+ return {
2555
+ text: buildHelpText(runtimes)
2556
+ };
2557
+ }
2558
+ }
2559
+ });
2560
+ }
2561
+ function parseCommandArgs(args) {
2562
+ const tokens = args?.trim().length ? args.trim().split(/\s+/u) : [];
2563
+ let serverName;
2564
+ const filtered = [];
2565
+ for (let index = 0; index < tokens.length; index += 1) {
2566
+ const token = tokens[index];
2567
+ if ((token === "--server" || token === "-s") && tokens[index + 1]) {
2568
+ serverName = tokens[index + 1];
2569
+ index += 1;
2570
+ continue;
2571
+ }
2572
+ filtered.push(token);
2573
+ }
2574
+ const command = filtered[0]?.toLowerCase() ?? "";
2575
+ return {
2576
+ command,
2577
+ serverName
2578
+ };
2579
+ }
2580
+ function selectServerRuntime(runtimes, requestedServerName) {
2581
+ if (runtimes.length === 0) {
2582
+ return {
2583
+ error: "No AGXP servers discovered. Ensure agxp CLI is configured with at least one server."
2584
+ };
2585
+ }
2586
+ if (!requestedServerName) {
2587
+ return {
2588
+ runtime: runtimes[0]
2589
+ };
2590
+ }
2591
+ const normalizedRequestedName = requestedServerName.trim().toLowerCase();
2592
+ const runtime = runtimes.find(
2593
+ (item) => item.server.name.trim().toLowerCase() === normalizedRequestedName
2594
+ );
2595
+ if (runtime) {
2596
+ return { runtime };
2597
+ }
2598
+ return {
2599
+ error: [
2600
+ `Unknown AGXP server: ${requestedServerName}`,
2601
+ `Available servers: ${runtimes.map((item) => item.server.name).join(", ")}`
2602
+ ].join("\n")
2603
+ };
2604
+ }
2605
+ function buildServersText(runtimes) {
2606
+ if (runtimes.length === 0) {
2607
+ return "No AGXP servers discovered.";
2608
+ }
2609
+ return [
2610
+ "AGXP servers (discovered via CLI):",
2611
+ ...runtimes.map((runtime) => {
2612
+ const flags = [
2613
+ runtime.server.current ? "default" : null,
2614
+ runtime.eventClient.isRunning() ? "streaming" : null
2615
+ ].filter(Boolean).join(", ");
2616
+ const suffix = flags ? ` (${flags})` : "";
2617
+ return `- ${runtime.server.name}: endpoint=${runtime.server.endpoint}${suffix}`;
2618
+ })
2619
+ ].join("\n");
2620
+ }
2621
+ function buildHelpText(runtimes) {
2622
+ const defaultRuntime = runtimes[0];
2623
+ const availableCommands = Array.from(COMMAND_NAME_SET).join("|");
2624
+ return [
2625
+ `Usage: /agxp [--server <name>] <${availableCommands}>`,
2626
+ defaultRuntime ? `Default server: ${defaultRuntime.server.name}` : void 0,
2627
+ runtimes.length > 0 ? `Available servers: ${runtimes.map((runtime) => runtime.server.name).join(", ")}` : void 0,
2628
+ "",
2629
+ "/agxp session \u2014 Show credential status",
2630
+ "/agxp identity \u2014 Fetch agent identity",
2631
+ "/agxp servers \u2014 List discovered servers",
2632
+ "/agxp timeline \u2014 Run one timeline refresh",
2633
+ "/agxp thread \u2014 Show thread event stream status",
2634
+ "/agxp here \u2014 Remember current conversation as delivery route",
2635
+ "/agxp version \u2014 Show agxp CLI version info"
2636
+ ].filter(Boolean).join("\n");
2637
+ }
2638
+ function readNonEmptyString5(value) {
2639
+ if (typeof value !== "string") {
2640
+ return void 0;
2641
+ }
2642
+ const trimmed = value.trim();
2643
+ return trimmed.length > 0 ? trimmed : void 0;
2644
+ }
2645
+ function normalizeChannel3(value) {
2646
+ return readNonEmptyString5(value)?.toLowerCase();
2647
+ }
2648
+ async function resolveCurrentCommandRoute(ctx, runtime, logger) {
2649
+ let channel = normalizeChannel3(ctx.channel);
2650
+ let to = normalizeReplyTarget(ctx.to, { channel }) ?? normalizeReplyTarget(ctx.from, { channel, fallbackKind: "user" });
2651
+ let accountId = readNonEmptyString5(ctx.accountId);
2652
+ if (typeof ctx.getCurrentConversationBinding === "function") {
2653
+ try {
2654
+ const binding = await ctx.getCurrentConversationBinding();
2655
+ if (binding) {
2656
+ channel = normalizeChannel3(binding.channel) ?? channel;
2657
+ to = normalizeReplyTarget(binding.conversationId, { channel }) ?? normalizeReplyTarget(binding.parentConversationId, { channel }) ?? to;
2658
+ accountId = readNonEmptyString5(binding.accountId) ?? accountId;
2659
+ }
2660
+ } catch (error) {
2661
+ logger.debug(
2662
+ `Failed to read current conversation binding: ${error instanceof Error ? error.message : String(error)}`
2663
+ );
2664
+ }
2665
+ }
2666
+ if (!channel || !to) {
2667
+ return void 0;
2668
+ }
2669
+ return findSessionRouteForBinding(
2670
+ {
2671
+ agentId: runtime.routing.agentId,
2672
+ channel,
2673
+ to,
2674
+ accountId
2675
+ },
2676
+ logger
2677
+ );
2678
+ }
2679
+ async function buildHereText(ctx, runtime, store, logger) {
2680
+ const route = await resolveCurrentCommandRoute(ctx, runtime, logger);
2681
+ if (!route || !route.replyChannel || !route.replyTo) {
2682
+ return [
2683
+ `Unable to resolve the current external session for server=${runtime.server.name}.`,
2684
+ "Run `/agxp here` inside the target conversation after OpenClaw has already created a session for it."
2685
+ ].join("\n");
2686
+ }
2687
+ const saved = await writeStoredNotificationRoute(store, runtime.server.name, route, logger);
2688
+ if (!saved) {
2689
+ return `Failed to persist the current AGXP route for server=${runtime.server.name}; check plugin logs for details.`;
2690
+ }
2691
+ return [
2692
+ `AGXP server ${runtime.server.name} will deliver to this conversation by default:`,
2693
+ `sessionKey: ${route.sessionKey}`,
2694
+ `agentId: ${route.agentId}`,
2695
+ `channel: ${route.replyChannel ?? "unknown"}`,
2696
+ `target: ${route.replyTo ?? "unknown"}`,
2697
+ route.replyAccountId ? `account: ${route.replyAccountId}` : void 0
2698
+ ].filter(Boolean).join("\n");
2699
+ }
2700
+ async function rememberCurrentCommandRouteIfPossible(ctx, runtime, store, logger) {
2701
+ const route = await resolveCurrentCommandRoute(ctx, runtime, logger);
2702
+ if (!route || !route.replyChannel || !route.replyTo) {
2703
+ return;
2704
+ }
2705
+ if (await writeStoredNotificationRoute(store, runtime.server.name, route, logger)) {
2706
+ logger.debug(
2707
+ `Remembered current command route for server=${runtime.server.name}: session_key=${route.sessionKey}, channel=${route.replyChannel ?? "unknown"}, to=${route.replyTo ?? "unknown"}`
2708
+ );
2709
+ }
2710
+ }
2711
+ function buildAuthStatusText(runtime) {
2712
+ const authState = runtime.credentialsLoader.loadAuthState();
2713
+ const lines = [`AGXP session status (server=${runtime.server.name}):`];
2714
+ lines.push(`- credentials_path: ${authState.credentialsPath}`);
2715
+ lines.push(`- status: ${authState.status}`);
2716
+ if (authState.expiresAt) {
2717
+ lines.push(`- expires_at: ${authState.expiresAt}`);
2718
+ }
2719
+ if (authState.status === "available") {
2720
+ lines.push(`- token: ${maskToken(authState.accessToken)}`);
2721
+ } else {
2722
+ lines.push("- token: unavailable");
2723
+ }
2724
+ return lines.join("\n");
2725
+ }
2726
+ async function buildIdentityText(runtime, agxpBin) {
2727
+ const result = await execAgxp(
2728
+ agxpBin,
2729
+ ["identity", "show", "-s", runtime.server.name, "-f", "json"]
2730
+ );
2731
+ if (result.kind === "session_required") {
2732
+ return buildSessionRequiredPromptTemplate({ context: runtime.getPromptContext() });
2733
+ }
2734
+ if (result.kind === "not_installed") {
2735
+ return `AGXP CLI not installed (bin=${result.bin}). Install with: ${INSTALL_COMMAND}`;
2736
+ }
2737
+ if (result.kind === "error") {
2738
+ return `Failed to fetch identity for server ${runtime.server.name}: ${result.error.message}`;
2739
+ }
2740
+ return [
2741
+ `AGXP identity (server=${runtime.server.name}):`,
2742
+ "```json",
2743
+ safeJsonStringify(result.data),
2744
+ "```"
2745
+ ].join("\n");
2746
+ }
2747
+ async function buildTimelineText(runtime) {
2748
+ const result = await runtime.timelinePoller.pollOnce({
2749
+ notifyTimeline: false,
2750
+ notifySessionRequired: false
2751
+ });
2752
+ switch (result.kind) {
2753
+ case "success":
2754
+ return [
2755
+ `AGXP timeline result (server=${runtime.server.name}):`,
2756
+ "```json",
2757
+ safeJsonStringify(result.payload),
2758
+ "```"
2759
+ ].join("\n");
2760
+ case "session_required":
2761
+ return buildSessionRequiredPromptTemplate({ context: runtime.getPromptContext() });
2762
+ case "error":
2763
+ return `AGXP timeline failed for server ${runtime.server.name}: ${result.error.message}`;
2764
+ default:
2765
+ return `AGXP timeline finished with an unknown result for server ${runtime.server.name}.`;
2766
+ }
2767
+ }
2768
+ async function buildVersionText(agxpBin) {
2769
+ const result = await execAgxp(agxpBin, ["version"]);
2770
+ if (result.kind === "not_installed") {
2771
+ return `AGXP CLI not installed (bin=${result.bin}). Install with: ${INSTALL_COMMAND}`;
2772
+ }
2773
+ if (result.kind === "session_required") {
2774
+ return `AGXP CLI reported session_required while fetching version (stderr: ${result.stderr || "n/a"}).`;
2775
+ }
2776
+ if (result.kind === "error") {
2777
+ return `Failed to fetch agxp version: ${result.error.message}`;
2778
+ }
2779
+ const body = typeof result.data === "string" ? result.data : safeJsonStringify(result.data);
2780
+ return ["AGXP CLI version:", "```json", body, "```"].join("\n");
2781
+ }
2782
+ function buildThreadStatusText(runtime) {
2783
+ const running = runtime.eventClient.isRunning();
2784
+ const checkpoint = runtime.eventClient.getLastCheckpoint();
2785
+ const lines = [`AGXP thread event stream status (server=${runtime.server.name}):`];
2786
+ lines.push(`- streaming: ${running ? "active" : "inactive"}`);
2787
+ if (checkpoint) {
2788
+ lines.push(`- last_checkpoint: ${checkpoint}`);
2789
+ }
2790
+ if (!running) {
2791
+ lines.push("Thread event stream is not running. Check session status or restart the service.");
2792
+ }
2793
+ return lines.join("\n");
2794
+ }
2795
+ function createInMemoryPluginStore() {
2796
+ const data = /* @__PURE__ */ new Map();
2797
+ return {
2798
+ async get(key) {
2799
+ return data.get(key);
2800
+ },
2801
+ async set(key, value) {
2802
+ data.set(key, value);
2803
+ }
2804
+ };
2805
+ }
2806
+ function maskToken(token) {
2807
+ const trimmed = token.trim();
2808
+ if (trimmed.length <= 10) {
2809
+ return `${trimmed.slice(0, 2)}***`;
2810
+ }
2811
+ return `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;
2812
+ }
2813
+ function safeJsonStringify(value) {
2814
+ try {
2815
+ return JSON.stringify(value, null, 2);
2816
+ } catch {
2817
+ return String(value);
2818
+ }
2819
+ }