@tloncorp/openclaw 0.4.3 → 0.6.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.
Files changed (112) hide show
  1. package/README.md +130 -141
  2. package/dist/index.js +703 -152
  3. package/dist/index.js.map +1 -1
  4. package/dist/setup-api.js +2 -2
  5. package/dist/setup-entry.js +2 -2
  6. package/dist/setup-entry.js.map +1 -1
  7. package/dist/src/account-fields.js +7 -3
  8. package/dist/src/account-fields.js.map +1 -1
  9. package/dist/src/actions.js +73 -52
  10. package/dist/src/actions.js.map +1 -1
  11. package/dist/src/channel.js +63 -39
  12. package/dist/src/channel.js.map +1 -1
  13. package/dist/src/channel.runtime.js +61 -32
  14. package/dist/src/channel.runtime.js.map +1 -1
  15. package/dist/src/config-schema.js +24 -4
  16. package/dist/src/config-schema.js.map +1 -1
  17. package/dist/src/diagnostic-subscriptions.js +49 -0
  18. package/dist/src/diagnostic-subscriptions.js.map +1 -0
  19. package/dist/src/effective-owner.js.map +1 -1
  20. package/dist/src/gateway-status.js +55 -7
  21. package/dist/src/gateway-status.js.map +1 -1
  22. package/dist/src/monitor/approval.js +71 -62
  23. package/dist/src/monitor/approval.js.map +1 -1
  24. package/dist/src/monitor/command-auth.js +7 -7
  25. package/dist/src/monitor/command-auth.js.map +1 -1
  26. package/dist/src/monitor/command-bridge.js +3 -2
  27. package/dist/src/monitor/command-bridge.js.map +1 -1
  28. package/dist/src/monitor/computing-presence.js +76 -12
  29. package/dist/src/monitor/computing-presence.js.map +1 -1
  30. package/dist/src/monitor/discovery.js +16 -9
  31. package/dist/src/monitor/discovery.js.map +1 -1
  32. package/dist/src/monitor/history.js +58 -26
  33. package/dist/src/monitor/history.js.map +1 -1
  34. package/dist/src/monitor/index.js +3018 -2496
  35. package/dist/src/monitor/index.js.map +1 -1
  36. package/dist/src/monitor/media.js +106 -78
  37. package/dist/src/monitor/media.js.map +1 -1
  38. package/dist/src/monitor/nudge-runner.js +36 -27
  39. package/dist/src/monitor/nudge-runner.js.map +1 -1
  40. package/dist/src/monitor/nudge-state.js +7 -11
  41. package/dist/src/monitor/nudge-state.js.map +1 -1
  42. package/dist/src/monitor/owner-reply-persistence.js +27 -26
  43. package/dist/src/monitor/owner-reply-persistence.js.map +1 -1
  44. package/dist/src/monitor/processed-messages.js.map +1 -1
  45. package/dist/src/monitor/session-routing.js +261 -0
  46. package/dist/src/monitor/session-routing.js.map +1 -0
  47. package/dist/src/monitor/settings-sync.js +1 -8
  48. package/dist/src/monitor/settings-sync.js.map +1 -1
  49. package/dist/src/monitor/utils.js +77 -71
  50. package/dist/src/monitor/utils.js.map +1 -1
  51. package/dist/src/nudge-decision.js +40 -43
  52. package/dist/src/nudge-decision.js.map +1 -1
  53. package/dist/src/nudge-messages.js +9 -9
  54. package/dist/src/nudge-scheduler.js.map +1 -1
  55. package/dist/src/owner-listen-command.js +38 -28
  56. package/dist/src/owner-listen-command.js.map +1 -1
  57. package/dist/src/pending-nudge.js.map +1 -1
  58. package/dist/src/runtime.js +10 -6
  59. package/dist/src/runtime.js.map +1 -1
  60. package/dist/src/session-roles.js +2 -1
  61. package/dist/src/session-roles.js.map +1 -1
  62. package/dist/src/session-route.js +44 -0
  63. package/dist/src/session-route.js.map +1 -0
  64. package/dist/src/settings.js +233 -102
  65. package/dist/src/settings.js.map +1 -1
  66. package/dist/src/setup-core.js +32 -32
  67. package/dist/src/setup-core.js.map +1 -1
  68. package/dist/src/setup-surface.js +19 -19
  69. package/dist/src/setup-surface.js.map +1 -1
  70. package/dist/src/shared-state.js +46 -0
  71. package/dist/src/shared-state.js.map +1 -0
  72. package/dist/src/targets.js +17 -10
  73. package/dist/src/targets.js.map +1 -1
  74. package/dist/src/telemetry.js +764 -34
  75. package/dist/src/telemetry.js.map +1 -1
  76. package/dist/src/tlon-binary.js +20 -12
  77. package/dist/src/tlon-binary.js.map +1 -1
  78. package/dist/src/tlon-tool-guard.js +5 -5
  79. package/dist/src/tool-trace.js +17 -13
  80. package/dist/src/tool-trace.js.map +1 -1
  81. package/dist/src/types.js +30 -12
  82. package/dist/src/types.js.map +1 -1
  83. package/dist/src/urbit/api-client.js +16 -12
  84. package/dist/src/urbit/api-client.js.map +1 -1
  85. package/dist/src/urbit/auth.js +9 -9
  86. package/dist/src/urbit/auth.js.map +1 -1
  87. package/dist/src/urbit/base-url.js +11 -11
  88. package/dist/src/urbit/base-url.js.map +1 -1
  89. package/dist/src/urbit/channel-ops.js +25 -19
  90. package/dist/src/urbit/channel-ops.js.map +1 -1
  91. package/dist/src/urbit/context.js +8 -8
  92. package/dist/src/urbit/context.js.map +1 -1
  93. package/dist/src/urbit/errors.js +33 -7
  94. package/dist/src/urbit/errors.js.map +1 -1
  95. package/dist/src/urbit/fetch.js +3 -3
  96. package/dist/src/urbit/fetch.js.map +1 -1
  97. package/dist/src/urbit/http-poke.js +10 -10
  98. package/dist/src/urbit/http-poke.js.map +1 -1
  99. package/dist/src/urbit/send.js +27 -23
  100. package/dist/src/urbit/send.js.map +1 -1
  101. package/dist/src/urbit/sse-client.js +45 -41
  102. package/dist/src/urbit/sse-client.js.map +1 -1
  103. package/dist/src/urbit/story.js +31 -30
  104. package/dist/src/urbit/story.js.map +1 -1
  105. package/dist/src/urbit/upload.js +8 -8
  106. package/dist/src/urbit/upload.js.map +1 -1
  107. package/dist/src/version.generated.js +2 -1
  108. package/dist/src/version.generated.js.map +1 -1
  109. package/dist/src/version.js +134 -0
  110. package/dist/src/version.js.map +1 -0
  111. package/openclaw.plugin.json +37 -0
  112. package/package.json +9 -15
package/dist/index.js CHANGED
@@ -1,59 +1,54 @@
1
- import { gatewayStop } from "@tloncorp/api";
2
- import { spawn } from "node:child_process";
3
- import { readFileSync } from "node:fs";
4
- import { createRequire } from "node:module";
5
- import { dirname } from "node:path";
6
- import { fileURLToPath } from "node:url";
7
- import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
8
- import { tlonPlugin } from "./src/channel.js";
9
- import { createGatewayStatusManager, setGatewayStatusManager } from "./src/gateway-status.js";
10
- import { resolveBridgeForCommand } from "./src/monitor/command-auth.js";
11
- import { handleOwnerListenCommand } from "./src/owner-listen-command.js";
12
- import { setTlonRuntime } from "./src/runtime.js";
13
- import { getSessionRole } from "./src/session-roles.js";
14
- import { recordToolCall } from "./src/telemetry.js";
15
- import { resolveTlonBinary } from "./src/tlon-binary.js";
16
- import { checkBlockedSendOperation } from "./src/tlon-tool-guard.js";
17
- import { formatToolTraceEvent, liveToolTraceContentsEnabled, shouldLogAfterToolTrace, } from "./src/tool-trace.js";
18
- import { resolveTlonAccount, listTlonAccountIds } from "./src/types.js";
19
- export { tlonPlugin } from "./src/channel.js";
20
- export { setTlonRuntime } from "./src/runtime.js";
1
+ import { spawn } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { defineChannelPluginEntry, } from 'openclaw/plugin-sdk/core';
6
+ import { onDiagnosticEvent, onInternalDiagnosticEvent, } from 'openclaw/plugin-sdk/diagnostic-runtime';
7
+ import { tlonPlugin } from './src/channel.js';
8
+ import { installTlonDiagnosticSubscriptions, shouldInstallTlonDiagnosticSubscriptions, } from './src/diagnostic-subscriptions.js';
9
+ import { sendGatewayStop } from './src/gateway-status.js';
10
+ import { createGatewayStatusManager, setGatewayStatusManager, } from './src/gateway-status.js';
11
+ import { resolveBridgeForCommand } from './src/monitor/command-auth.js';
12
+ import { isRouteDebugEnabled } from './src/monitor/session-routing.js';
13
+ import { handleOwnerListenCommand } from './src/owner-listen-command.js';
14
+ import { setTlonRuntime } from './src/runtime.js';
15
+ import { getSessionRole } from './src/session-roles.js';
16
+ import { parseTlonTarget } from './src/targets.js';
17
+ import { formatTlonTelemetryErrorText, recordToolCall, reportHarnessError, reportOutboundRoute, reportPluginError, reportSessionDiagnostic, reportSessionLifecycle, reportSessionTurnCreated, reportTelemetryError, } from './src/telemetry.js';
18
+ import { resolveTlonBinary } from './src/tlon-binary.js';
19
+ import { checkBlockedSendOperation } from './src/tlon-tool-guard.js';
20
+ import { formatToolTraceEvent, liveToolTraceContentsEnabled, shouldLogAfterToolTrace, } from './src/tool-trace.js';
21
+ import { listTlonAccountIds, resolveTlonAccount } from './src/types.js';
22
+ import { formatTlonVersionIdentity, resolveTlonSkillVersion, setTlonSkillVersionResolver, } from './src/version.js';
23
+ export { tlonPlugin } from './src/channel.js';
24
+ export { setTlonRuntime } from './src/runtime.js';
21
25
  const __dirname = dirname(fileURLToPath(import.meta.url));
22
26
  const require = createRequire(import.meta.url);
23
- function readPluginVersion() {
24
- try {
25
- const { version } = require("./package.json");
26
- return version;
27
- }
28
- catch {
29
- try {
30
- const raw = readFileSync(new URL("./package.json", import.meta.url), "utf-8");
31
- return JSON.parse(raw).version;
32
- }
33
- catch {
34
- return "unknown";
35
- }
36
- }
37
- }
38
27
  // Whitelist of allowed tlon subcommands
39
28
  const ALLOWED_TLON_COMMANDS = new Set([
40
- "activity",
41
- "channels",
42
- "contacts",
43
- "dms",
44
- "expose",
45
- "groups",
46
- "hooks",
47
- "messages",
48
- "notebook",
49
- "posts",
50
- "settings",
51
- "upload",
52
- "help",
53
- "version",
29
+ 'activity',
30
+ 'channels',
31
+ 'contacts',
32
+ 'dms',
33
+ 'expose',
34
+ 'groups',
35
+ 'hooks',
36
+ 'messages',
37
+ 'notebook',
38
+ 'posts',
39
+ 'settings',
40
+ 'upload',
41
+ 'help',
42
+ 'version',
54
43
  ]);
55
44
  /** Credential flags that the tlon skill binary accepts before the subcommand. */
56
- const CREDENTIAL_FLAGS_WITH_VALUE = new Set(["--config", "--url", "--ship", "--code", "--cookie"]);
45
+ const CREDENTIAL_FLAGS_WITH_VALUE = new Set([
46
+ '--config',
47
+ '--url',
48
+ '--ship',
49
+ '--code',
50
+ '--cookie',
51
+ ]);
57
52
  /**
58
53
  * Find the first positional argument (subcommand) by skipping credential flags
59
54
  * and their values. Returns the index into `args`, or -1 if none found.
@@ -63,8 +58,8 @@ function findSubcommandIndex(args) {
63
58
  while (i < args.length) {
64
59
  const arg = args[i];
65
60
  // --flag=value form: skip one token
66
- if (arg.startsWith("--") && arg.includes("=")) {
67
- const flag = arg.slice(0, arg.indexOf("="));
61
+ if (arg.startsWith('--') && arg.includes('=')) {
62
+ const flag = arg.slice(0, arg.indexOf('='));
68
63
  if (CREDENTIAL_FLAGS_WITH_VALUE.has(flag)) {
69
64
  i += 1;
70
65
  continue;
@@ -85,7 +80,7 @@ function findSubcommandIndex(args) {
85
80
  */
86
81
  function shellSplit(str) {
87
82
  const args = [];
88
- let cur = "";
83
+ let cur = '';
89
84
  let inDouble = false;
90
85
  let inSingle = false;
91
86
  let escape = false;
@@ -95,7 +90,7 @@ function shellSplit(str) {
95
90
  escape = false;
96
91
  continue;
97
92
  }
98
- if (ch === "\\" && !inSingle) {
93
+ if (ch === '\\' && !inSingle) {
99
94
  escape = true;
100
95
  continue;
101
96
  }
@@ -110,7 +105,7 @@ function shellSplit(str) {
110
105
  if (/\s/.test(ch) && !inDouble && !inSingle) {
111
106
  if (cur) {
112
107
  args.push(cur);
113
- cur = "";
108
+ cur = '';
114
109
  }
115
110
  continue;
116
111
  }
@@ -124,7 +119,7 @@ function shellSplit(str) {
124
119
  /**
125
120
  * Run the tlon command and return the result
126
121
  */
127
- function runTlonCommand(binary, args, credentials) {
122
+ function runTlonCommand(binary, args, credentials, options) {
128
123
  return new Promise((resolve, reject) => {
129
124
  const env = { ...process.env };
130
125
  if (credentials) {
@@ -133,19 +128,39 @@ function runTlonCommand(binary, args, credentials) {
133
128
  env.URBIT_CODE = credentials.code;
134
129
  }
135
130
  const child = spawn(binary, args, { env });
136
- let stdout = "";
137
- let stderr = "";
138
- child.stdout.on("data", (data) => {
131
+ let stdout = '';
132
+ let stderr = '';
133
+ let timedOut = false;
134
+ let timeout;
135
+ const timeoutMs = options?.timeoutMs;
136
+ const cleanup = () => {
137
+ if (timeout) {
138
+ clearTimeout(timeout);
139
+ timeout = undefined;
140
+ }
141
+ };
142
+ child.stdout.on('data', (data) => {
139
143
  stdout += data.toString();
140
144
  });
141
- child.stderr.on("data", (data) => {
145
+ child.stderr.on('data', (data) => {
142
146
  stderr += data.toString();
143
147
  });
144
- child.on("error", (err) => {
148
+ child.on('error', (err) => {
149
+ cleanup();
145
150
  reject(new Error(`Failed to run tlon: ${err.message}`));
146
151
  });
147
- child.on("close", (code) => {
148
- if (code !== 0) {
152
+ if (timeoutMs) {
153
+ timeout = setTimeout(() => {
154
+ timedOut = true;
155
+ child.kill('SIGTERM');
156
+ }, timeoutMs);
157
+ }
158
+ child.on('close', (code) => {
159
+ cleanup();
160
+ if (timedOut) {
161
+ reject(new Error(`tlon timed out after ${timeoutMs}ms`));
162
+ }
163
+ else if (code !== 0) {
149
164
  reject(new Error(stderr || `tlon exited with code ${code}`));
150
165
  }
151
166
  else {
@@ -154,29 +169,388 @@ function runTlonCommand(binary, args, credentials) {
154
169
  });
155
170
  });
156
171
  }
172
+ function firstLine(value) {
173
+ return value.trim().split(/\r?\n/)[0]?.trim() || 'unknown';
174
+ }
175
+ function summarizeError(error) {
176
+ const message = error instanceof Error ? error.message : String(error);
177
+ return firstLine(message).slice(0, 180);
178
+ }
179
+ async function readTlonSkillVersion(binary) {
180
+ try {
181
+ return firstLine(await runTlonCommand(binary, ['--version'], undefined, {
182
+ timeoutMs: 5_000,
183
+ }));
184
+ }
185
+ catch (error) {
186
+ return `unavailable (${summarizeError(error)})`;
187
+ }
188
+ }
189
+ function isTlonSessionDiagnosticEvent(event) {
190
+ return (event.type === 'session.stalled' ||
191
+ event.type === 'session.stuck' ||
192
+ event.type === 'session.recovery.requested' ||
193
+ event.type === 'session.recovery.completed');
194
+ }
195
+ function stringField(event, key) {
196
+ const value = event[key];
197
+ return typeof value === 'string' && value.trim() ? value : null;
198
+ }
199
+ function numberField(event, key) {
200
+ const value = event[key];
201
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
202
+ }
203
+ function diagnosticErrorText(event) {
204
+ return stringField(event, 'error') ?? stringField(event, 'message');
205
+ }
206
+ function stringListField(event, key) {
207
+ const value = event[key];
208
+ if (!Array.isArray(value)) {
209
+ return [];
210
+ }
211
+ return value
212
+ .map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
213
+ .filter(Boolean);
214
+ }
215
+ function diagnosticSummary(parts) {
216
+ return parts
217
+ .filter(([, value]) => value !== null && value !== undefined && value !== '')
218
+ .map(([key, value]) => `${key}=${String(value)}`)
219
+ .join(' ');
220
+ }
221
+ function reportHarnessDiagnostic(event) {
222
+ const type = stringField(event, 'type');
223
+ if (!type) {
224
+ return;
225
+ }
226
+ if (type === 'session.turn.created') {
227
+ reportSessionTurnCreated({
228
+ type,
229
+ sessionKey: stringField(event, 'sessionKey'),
230
+ sessionId: stringField(event, 'sessionId'),
231
+ runId: stringField(event, 'runId'),
232
+ agentId: stringField(event, 'agentId'),
233
+ });
234
+ return;
235
+ }
236
+ const common = {
237
+ harnessEventType: type,
238
+ sessionKey: stringField(event, 'sessionKey'),
239
+ sessionId: stringField(event, 'sessionId'),
240
+ runId: stringField(event, 'runId'),
241
+ agentId: stringField(event, 'agentId'),
242
+ provider: stringField(event, 'provider'),
243
+ model: stringField(event, 'model'),
244
+ phase: stringField(event, 'phase'),
245
+ outcome: stringField(event, 'outcome'),
246
+ errorCategory: stringField(event, 'errorCategory'),
247
+ failureKind: stringField(event, 'failureKind'),
248
+ durationMs: numberField(event, 'durationMs'),
249
+ errorText: diagnosticErrorText(event),
250
+ };
251
+ switch (type) {
252
+ case 'harness.run.error':
253
+ reportHarnessError({
254
+ ...common,
255
+ errorScope: 'harness',
256
+ });
257
+ return;
258
+ case 'harness.run.completed':
259
+ if (common.outcome === 'completed') {
260
+ return;
261
+ }
262
+ reportHarnessError({
263
+ ...common,
264
+ errorScope: 'harness',
265
+ });
266
+ return;
267
+ case 'model.call.error':
268
+ reportHarnessError({
269
+ ...common,
270
+ errorScope: 'model',
271
+ });
272
+ return;
273
+ case 'model.failover': {
274
+ const reason = stringField(event, 'reason');
275
+ const fromProvider = stringField(event, 'fromProvider');
276
+ const fromModel = stringField(event, 'fromModel');
277
+ const toProvider = stringField(event, 'toProvider');
278
+ const toModel = stringField(event, 'toModel');
279
+ reportHarnessError({
280
+ ...common,
281
+ errorScope: 'model',
282
+ provider: fromProvider,
283
+ model: fromModel,
284
+ phase: stringField(event, 'lane'),
285
+ outcome: 'failover',
286
+ errorCategory: 'model_failover',
287
+ failureKind: reason,
288
+ errorText: diagnosticSummary([
289
+ ['fromProvider', fromProvider],
290
+ ['fromModel', fromModel],
291
+ ['toProvider', toProvider],
292
+ ['toModel', toModel],
293
+ ['reason', reason],
294
+ ['cascadeDepth', numberField(event, 'cascadeDepth')],
295
+ ]),
296
+ });
297
+ return;
298
+ }
299
+ case 'tool.execution.error':
300
+ reportHarnessError({
301
+ ...common,
302
+ errorScope: 'tool',
303
+ toolName: stringField(event, 'toolName'),
304
+ });
305
+ return;
306
+ case 'tool.execution.blocked': {
307
+ const deniedReason = stringField(event, 'deniedReason');
308
+ const reason = stringField(event, 'reason');
309
+ reportHarnessError({
310
+ ...common,
311
+ errorScope: 'tool',
312
+ toolName: stringField(event, 'toolName'),
313
+ phase: stringField(event, 'toolSource'),
314
+ outcome: 'blocked',
315
+ errorCategory: 'tool_blocked',
316
+ failureKind: deniedReason,
317
+ errorText: reason ?? deniedReason,
318
+ });
319
+ return;
320
+ }
321
+ case 'tool.loop': {
322
+ const level = stringField(event, 'level');
323
+ const action = stringField(event, 'action');
324
+ if (level !== 'critical' && action !== 'block') {
325
+ return;
326
+ }
327
+ reportHarnessError({
328
+ ...common,
329
+ errorScope: 'tool',
330
+ toolName: stringField(event, 'toolName'),
331
+ phase: level,
332
+ outcome: action,
333
+ errorCategory: 'tool_loop',
334
+ failureKind: stringField(event, 'detector'),
335
+ errorText: stringField(event, 'message') ??
336
+ diagnosticSummary([
337
+ ['level', level],
338
+ ['action', action],
339
+ ['detector', stringField(event, 'detector')],
340
+ ['count', numberField(event, 'count')],
341
+ ]),
342
+ });
343
+ return;
344
+ }
345
+ case 'run.completed':
346
+ if (common.outcome === 'completed') {
347
+ return;
348
+ }
349
+ reportHarnessError({
350
+ ...common,
351
+ errorScope: 'run',
352
+ });
353
+ return;
354
+ case 'message.delivery.error':
355
+ reportHarnessError({
356
+ ...common,
357
+ errorScope: 'message_delivery',
358
+ phase: stringField(event, 'deliveryKind'),
359
+ });
360
+ return;
361
+ case 'message.dispatch.completed':
362
+ if (common.outcome !== 'error') {
363
+ return;
364
+ }
365
+ reportHarnessError({
366
+ ...common,
367
+ errorScope: 'message_dispatch',
368
+ phase: stringField(event, 'source'),
369
+ });
370
+ return;
371
+ case 'message.processed':
372
+ if (common.outcome !== 'error') {
373
+ return;
374
+ }
375
+ reportHarnessError({
376
+ ...common,
377
+ errorScope: 'message_processing',
378
+ phase: stringField(event, 'channel'),
379
+ });
380
+ return;
381
+ case 'diagnostic.async_queue.dropped':
382
+ reportHarnessError({
383
+ ...common,
384
+ errorScope: 'diagnostics',
385
+ outcome: 'dropped',
386
+ errorCategory: 'diagnostic_async_queue_dropped',
387
+ failureKind: 'queue_full',
388
+ errorText: diagnosticSummary([
389
+ ['droppedEvents', numberField(event, 'droppedEvents')],
390
+ ['droppedTrustedEvents', numberField(event, 'droppedTrustedEvents')],
391
+ [
392
+ 'droppedUntrustedEvents',
393
+ numberField(event, 'droppedUntrustedEvents'),
394
+ ],
395
+ ['queueLength', numberField(event, 'queueLength')],
396
+ ['maxQueueLength', numberField(event, 'maxQueueLength')],
397
+ ]),
398
+ });
399
+ return;
400
+ case 'diagnostic.liveness.warning': {
401
+ const reasons = stringListField(event, 'reasons');
402
+ reportHarnessError({
403
+ ...common,
404
+ errorScope: 'runtime',
405
+ phase: stringField(event, 'phase'),
406
+ outcome: 'warning',
407
+ errorCategory: 'liveness_warning',
408
+ failureKind: reasons.join(',') || null,
409
+ durationMs: numberField(event, 'intervalMs'),
410
+ errorText: diagnosticSummary([
411
+ ['reasons', reasons.join(',')],
412
+ ['eventLoopDelayP99Ms', numberField(event, 'eventLoopDelayP99Ms')],
413
+ ['eventLoopDelayMaxMs', numberField(event, 'eventLoopDelayMaxMs')],
414
+ ['cpuCoreRatio', numberField(event, 'cpuCoreRatio')],
415
+ ['active', numberField(event, 'active')],
416
+ ['waiting', numberField(event, 'waiting')],
417
+ ['queued', numberField(event, 'queued')],
418
+ ]),
419
+ });
420
+ return;
421
+ }
422
+ case 'diagnostic.memory.pressure': {
423
+ const memory = event.memory;
424
+ const memoryNumber = (key) => {
425
+ const value = memory?.[key];
426
+ return typeof value === 'number' && Number.isFinite(value)
427
+ ? value
428
+ : null;
429
+ };
430
+ reportHarnessError({
431
+ ...common,
432
+ errorScope: 'runtime',
433
+ outcome: stringField(event, 'level'),
434
+ errorCategory: 'memory_pressure',
435
+ failureKind: stringField(event, 'reason'),
436
+ durationMs: numberField(event, 'windowMs'),
437
+ errorText: diagnosticSummary([
438
+ ['level', stringField(event, 'level')],
439
+ ['reason', stringField(event, 'reason')],
440
+ ['rssBytes', memoryNumber('rssBytes')],
441
+ ['heapUsedBytes', memoryNumber('heapUsedBytes')],
442
+ ['thresholdBytes', numberField(event, 'thresholdBytes')],
443
+ ['rssGrowthBytes', numberField(event, 'rssGrowthBytes')],
444
+ ]),
445
+ });
446
+ return;
447
+ }
448
+ case 'payload.large':
449
+ if (stringField(event, 'action') !== 'rejected') {
450
+ return;
451
+ }
452
+ reportHarnessError({
453
+ ...common,
454
+ errorScope: 'payload',
455
+ phase: stringField(event, 'surface'),
456
+ outcome: 'rejected',
457
+ errorCategory: 'payload_large',
458
+ failureKind: stringField(event, 'reason'),
459
+ errorText: diagnosticSummary([
460
+ ['surface', stringField(event, 'surface')],
461
+ ['channel', stringField(event, 'channel')],
462
+ ['pluginId', stringField(event, 'pluginId')],
463
+ ['bytes', numberField(event, 'bytes')],
464
+ ['limitBytes', numberField(event, 'limitBytes')],
465
+ ['count', numberField(event, 'count')],
466
+ ['reason', stringField(event, 'reason')],
467
+ ]),
468
+ });
469
+ return;
470
+ }
471
+ }
472
+ function safeTelemetryObserver(params) {
473
+ try {
474
+ params.run();
475
+ }
476
+ catch (error) {
477
+ params.logger.warn(`[tlon] Telemetry observer failed (${params.telemetrySource}${params.sourceEventName ? `:${params.sourceEventName}` : ''}): ${String(error)}`);
478
+ try {
479
+ reportTelemetryError({
480
+ telemetrySource: params.telemetrySource,
481
+ sourceEventName: params.sourceEventName,
482
+ sessionKey: params.sessionKey,
483
+ sessionId: params.sessionId,
484
+ runId: params.runId,
485
+ agentId: params.agentId,
486
+ errorKind: error instanceof Error ? error.name : typeof error,
487
+ errorText: formatTlonTelemetryErrorText(error),
488
+ });
489
+ }
490
+ catch (reportError) {
491
+ params.logger.warn(`[tlon] Telemetry error reporting failed: ${String(reportError)}`);
492
+ }
493
+ }
494
+ }
495
+ function installTelemetryDiagnosticObservers(api) {
496
+ return installTlonDiagnosticSubscriptions(() => {
497
+ const unsubscribeDiagnosticEvents = onDiagnosticEvent((event) => {
498
+ const candidate = event;
499
+ safeTelemetryObserver({
500
+ logger: api.logger,
501
+ telemetrySource: 'diagnostic_session',
502
+ sourceEventName: candidate.type,
503
+ sessionKey: candidate.sessionKey,
504
+ sessionId: candidate.sessionId,
505
+ run: () => {
506
+ if (isTlonSessionDiagnosticEvent(candidate)) {
507
+ reportSessionDiagnostic(candidate);
508
+ }
509
+ },
510
+ });
511
+ });
512
+ const unsubscribeInternalDiagnosticEvents = onInternalDiagnosticEvent((event) => {
513
+ const candidate = event;
514
+ safeTelemetryObserver({
515
+ logger: api.logger,
516
+ telemetrySource: 'diagnostic_internal',
517
+ sourceEventName: stringField(candidate, 'type'),
518
+ sessionKey: stringField(candidate, 'sessionKey'),
519
+ sessionId: stringField(candidate, 'sessionId'),
520
+ runId: stringField(candidate, 'runId'),
521
+ agentId: stringField(candidate, 'agentId'),
522
+ run: () => reportHarnessDiagnostic(candidate),
523
+ });
524
+ });
525
+ return () => {
526
+ unsubscribeDiagnosticEvents();
527
+ unsubscribeInternalDiagnosticEvents();
528
+ };
529
+ });
530
+ }
157
531
  export default defineChannelPluginEntry({
158
- id: "tlon",
159
- name: "Tlon",
160
- description: "Tlon/Urbit channel plugin",
532
+ id: 'tlon',
533
+ name: 'Tlon',
534
+ description: 'Tlon/Urbit channel plugin',
161
535
  plugin: tlonPlugin,
162
536
  setRuntime: setTlonRuntime,
163
537
  registerFull(api) {
164
- // Import version info lazily
165
- const PLUGIN_VERSION = readPluginVersion();
166
- let PLUGIN_COMMIT = "unknown";
167
- try {
168
- PLUGIN_COMMIT = require("./src/version.generated.js")
169
- .PLUGIN_COMMIT;
170
- }
171
- catch {
172
- // version.generated.js may not exist in all environments
173
- }
174
538
  // ── Gateway-status liveness integration ───────────────────
175
539
  //
176
540
  // v1 requires exactly one Tlon account. With multiple accounts, multiple
177
- // monitors call configureTlonApiWithPoke() and the last one wins the global
178
- // @tloncorp/api singleton — making it unsafe to route heartbeats or stop
179
- // pokes to a specific ship. Disable entirely rather than route to the wrong ship.
541
+ // monitors call configureTlonApiWithPoke() and the last one wins the
542
+ // global @tloncorp/api singleton — making it unsafe to route heartbeats or
543
+ // stop pokes to a specific ship. Disable entirely rather than route to the
544
+ // wrong ship.
545
+ //
546
+ // We count ALL configured account entries (not just currently-runnable
547
+ // ones) on purpose. The manager is a process-lifetime singleton created
548
+ // here in registerFull, which does NOT re-run on config reload. If we
549
+ // counted only runnable accounts, a config of one complete account plus a
550
+ // disabled/unconfigured stub would enable the singleton, and later
551
+ // completing the stub would start a second monitor that races the shared
552
+ // API slot — without registerFull re-evaluating the gate. Counting every
553
+ // entry keeps the feature off whenever a second account exists at all.
180
554
  const gsAccountIds = listTlonAccountIds(api.config);
181
555
  setGatewayStatusManager(null);
182
556
  if (gsAccountIds.length > 1) {
@@ -187,26 +561,49 @@ export default defineChannelPluginEntry({
187
561
  const gsManager = createGatewayStatusManager({
188
562
  logger: {
189
563
  log: (m) => api.logger.info(m),
190
- error: (m) => api.logger.warn(m),
564
+ error: (m) => {
565
+ reportPluginError({
566
+ pluginErrorSource: 'gateway_status_heartbeat',
567
+ errorKind: 'heartbeat',
568
+ errorText: m,
569
+ });
570
+ api.logger.warn(m);
571
+ },
191
572
  },
192
573
  });
193
574
  setGatewayStatusManager(gsManager);
194
- api.on("gateway_start", () => {
575
+ api.on('gateway_start', () => {
195
576
  gsManager.signalGatewayStarted();
196
- api.logger.info("[gateway-status] gateway_start received");
577
+ api.logger.info('[gateway-status] gateway_start received');
197
578
  });
198
- api.on("gateway_stop", async (event) => {
199
- if (!gsManager.activated || gsManager.stopped) {
579
+ api.on('gateway_stop', async (event) => {
580
+ if (gsManager.stopped) {
200
581
  return;
201
582
  }
583
+ // Latch stopped FIRST, unconditionally. An activation task may be
584
+ // in flight (between the %gateway-start poke and markActivated());
585
+ // latching here makes its post-poke recheck bail so it can't start a
586
+ // heartbeat after we've already passed the shutdown hook.
587
+ const startPokeInFlightOrDone = gsManager.activated || gsManager.starting;
202
588
  gsManager.stopHeartbeat();
203
589
  gsManager.markStopped();
590
+ // Only send %gateway-stop if a %gateway-start has been or is being
591
+ // sent. If activation never reached the start poke, there is nothing
592
+ // for the ship to stop.
593
+ if (!startPokeInFlightOrDone) {
594
+ return;
595
+ }
204
596
  try {
205
- await gatewayStop({
597
+ const sent = await sendGatewayStop({
206
598
  bootId: gsManager.bootId,
207
- reason: event.reason ?? "shutdown",
599
+ reason: event.reason ?? 'shutdown',
208
600
  });
209
- api.logger.info(`[gateway-status] stopped (reason=${event.reason ?? "shutdown"})`);
601
+ if (sent) {
602
+ api.logger.info(`[gateway-status] stopped (reason=${event.reason ?? 'shutdown'})`);
603
+ }
604
+ else {
605
+ api.logger.warn('[gateway-status] stop skipped: api-client params not published');
606
+ }
210
607
  }
211
608
  catch (err) {
212
609
  api.logger.warn(`[gateway-status] stop poke failed: ${String(err)}`);
@@ -214,21 +611,49 @@ export default defineChannelPluginEntry({
214
611
  });
215
612
  }
216
613
  // else: zero accounts configured — nothing to do
217
- // Register /tlon-version command
218
- api.registerCommand({
219
- name: "tlon-version",
220
- description: "Show Tlon plugin version.",
221
- handler: async () => {
222
- return { text: `Tlon plugin v${PLUGIN_VERSION} (${PLUGIN_COMMIT})` };
223
- },
224
- });
225
- // Register the tlon tool
614
+ // Resolve the tlon tool binary once. The tool itself and version
615
+ // diagnostics share this path so telemetry reports what OpenClaw will
616
+ // actually execute.
226
617
  const tlonBinary = resolveTlonBinary({
227
618
  moduleDir: __dirname,
228
619
  resolveModule: require.resolve,
229
620
  log: (msg) => api.logger.debug?.(msg),
230
621
  });
231
622
  api.logger.info(`[tlon] Registering tlon tool, binary: ${tlonBinary}`);
623
+ setTlonSkillVersionResolver(() => readTlonSkillVersion(tlonBinary));
624
+ const renderTlonVersion = async () => ({
625
+ text: formatTlonVersionIdentity({
626
+ tlonSkillVersion: await resolveTlonSkillVersion(),
627
+ }),
628
+ });
629
+ void resolveTlonSkillVersion().then((version) => {
630
+ api.logger.info(`[tlon] Tlon skill version: ${version}`);
631
+ });
632
+ // Register /tlon-version command
633
+ api.registerCommand({
634
+ name: 'tlon-version',
635
+ description: 'Show Tlon plugin version.',
636
+ handler: async () => {
637
+ return renderTlonVersion();
638
+ },
639
+ });
640
+ api.registerCommand({
641
+ name: 'tlon',
642
+ description: 'Tlon plugin diagnostics. Usage: /tlon version',
643
+ acceptsArgs: true,
644
+ handler: async (ctx) => {
645
+ const args = (ctx.args ?? '').trim().toLowerCase();
646
+ if (args !== 'version') {
647
+ return { text: 'Usage: /tlon version' };
648
+ }
649
+ const result = resolveBridgeForCommand(ctx);
650
+ if ('error' in result) {
651
+ return { text: result.error };
652
+ }
653
+ return renderTlonVersion();
654
+ },
655
+ });
656
+ // Register the tlon tool
232
657
  // Capture credentials from config at registration time
233
658
  const account = resolveTlonAccount(api.config);
234
659
  const credentials = account.configured && account.url && account.ship && account.code
@@ -241,22 +666,22 @@ export default defineChannelPluginEntry({
241
666
  api.logger.warn(`[tlon] No credentials configured - tlon tool will rely on env vars`);
242
667
  }
243
668
  api.registerTool({
244
- name: "tlon",
245
- label: "Tlon CLI",
246
- description: "Tlon/Urbit API for reading data and administration: activity, channels, contacts, groups, messages, posts, settings, upload, expose, hooks. " +
247
- "DO NOT use this tool to send messages — use the `message` tool instead. " +
669
+ name: 'tlon',
670
+ label: 'Tlon CLI',
671
+ description: 'Tlon/Urbit API for reading data and administration: activity, channels, contacts, groups, messages, posts, settings, upload, expose, hooks. ' +
672
+ 'DO NOT use this tool to send messages — use the `message` tool instead. ' +
248
673
  "Examples: 'activity mentions --limit 10', 'channels groups', 'contacts self', 'groups list'",
249
674
  parameters: {
250
- type: "object",
675
+ type: 'object',
251
676
  properties: {
252
677
  command: {
253
- type: "string",
254
- description: "The tlon command and arguments (read/admin operations). " +
255
- "To send messages, use the `message` tool, not this tool. " +
678
+ type: 'string',
679
+ description: 'The tlon command and arguments (read/admin operations). ' +
680
+ 'To send messages, use the `message` tool, not this tool. ' +
256
681
  "Examples: 'activity mentions --limit 10', 'contacts get ~sampel-palnet', 'groups list', 'messages dm ~ship --limit 20'",
257
682
  },
258
683
  },
259
- required: ["command"],
684
+ required: ['command'],
260
685
  },
261
686
  async execute(_id, params) {
262
687
  try {
@@ -267,8 +692,8 @@ export default defineChannelPluginEntry({
267
692
  return {
268
693
  content: [
269
694
  {
270
- type: "text",
271
- text: `Error: Unknown tlon subcommand '${subcommand ?? "(none)"}'. Allowed: ${[...ALLOWED_TLON_COMMANDS].join(", ")}`,
695
+ type: 'text',
696
+ text: `Error: Unknown tlon subcommand '${subcommand ?? '(none)'}'. Allowed: ${[...ALLOWED_TLON_COMMANDS].join(', ')}`,
272
697
  },
273
698
  ],
274
699
  details: { error: true },
@@ -278,41 +703,43 @@ export default defineChannelPluginEntry({
278
703
  const blocked = checkBlockedSendOperation(args.slice(subIdx));
279
704
  if (blocked) {
280
705
  return {
281
- content: [{ type: "text", text: blocked }],
282
- details: { blocked: true, reason: "send_operation" },
706
+ content: [{ type: 'text', text: blocked }],
707
+ details: { blocked: true, reason: 'send_operation' },
283
708
  };
284
709
  }
285
710
  const output = await runTlonCommand(tlonBinary, args, credentials);
286
711
  return {
287
- content: [{ type: "text", text: output }],
712
+ content: [{ type: 'text', text: output }],
288
713
  details: undefined,
289
714
  };
290
715
  }
291
716
  catch (error) {
292
717
  const message = error instanceof Error ? error.message : String(error);
293
718
  return {
294
- content: [{ type: "text", text: `Error: ${message}` }],
719
+ content: [{ type: 'text', text: `Error: ${message}` }],
295
720
  details: { error: true },
296
721
  };
297
722
  }
298
723
  },
299
724
  });
300
725
  // Tool access control: block sensitive tools for non-owners
301
- const ownerOnlyTools = new Set(["tlon", "cron", "read"]);
726
+ const ownerOnlyTools = new Set(['tlon', 'cron', 'read']);
302
727
  const logToolTraceContents = liveToolTraceContentsEnabled();
303
- api.on("before_tool_call", (event, ctx) => {
304
- const role = getSessionRole(ctx.sessionKey ?? "");
728
+ api.on('before_tool_call', (event, ctx) => {
729
+ const role = getSessionRole(ctx.sessionKey ?? '');
305
730
  const isOwnerOnlyTool = ownerOnlyTools.has(event.toolName);
306
- const isBlocked = isOwnerOnlyTool && role === "user";
307
- const blockReason = isBlocked ? `The ${event.toolName} tool is not available.` : undefined;
731
+ const isBlocked = isOwnerOnlyTool && role === 'user';
732
+ const blockReason = isBlocked
733
+ ? `The ${event.toolName} tool is not available.`
734
+ : undefined;
308
735
  if (logToolTraceContents) {
309
736
  api.logger.info(formatToolTraceEvent({
310
- phase: "before",
737
+ phase: 'before',
311
738
  sessionKey: ctx.sessionKey,
312
739
  toolName: event.toolName,
313
740
  payload: {
314
741
  params: event.params,
315
- role: role ?? "internal",
742
+ role: role ?? 'internal',
316
743
  blocked: isBlocked,
317
744
  ...(blockReason ? { blockReason } : {}),
318
745
  },
@@ -331,12 +758,12 @@ export default defineChannelPluginEntry({
331
758
  blockReason,
332
759
  };
333
760
  }
334
- api.logger.info(`[tlon] Allowed ${event.toolName} tool for ${role ?? "internal"} session. Session: ${ctx.sessionKey}`);
761
+ api.logger.info(`[tlon] Allowed ${event.toolName} tool for ${role ?? 'internal'} session. Session: ${ctx.sessionKey}`);
335
762
  });
336
- api.on("after_tool_call", (event, ctx) => {
763
+ api.on('after_tool_call', (event, ctx) => {
337
764
  if (logToolTraceContents && shouldLogAfterToolTrace(event)) {
338
765
  api.logger.info(formatToolTraceEvent({
339
- phase: "after",
766
+ phase: 'after',
340
767
  sessionKey: ctx.sessionKey,
341
768
  toolName: event.toolName,
342
769
  payload: {
@@ -347,103 +774,227 @@ export default defineChannelPluginEntry({
347
774
  },
348
775
  }));
349
776
  }
350
- recordToolCall({
777
+ safeTelemetryObserver({
778
+ logger: api.logger,
779
+ telemetrySource: 'after_tool_call',
780
+ sourceEventName: event.toolName,
351
781
  sessionKey: ctx.sessionKey,
352
- toolName: event.toolName,
353
- durationMs: event.durationMs,
354
- error: event.error,
782
+ run: () => {
783
+ recordToolCall({
784
+ sessionKey: ctx.sessionKey,
785
+ toolName: event.toolName,
786
+ durationMs: event.durationMs,
787
+ error: event.error,
788
+ });
789
+ },
790
+ });
791
+ });
792
+ // ── Session lifecycle / watchdog telemetry ─────────────────────────
793
+ // These hooks are global to OpenClaw, so telemetry.ts filters them through
794
+ // session keys remembered from Tlon inbound replies before emitting.
795
+ api.on('session_start', (event, ctx) => {
796
+ safeTelemetryObserver({
797
+ logger: api.logger,
798
+ telemetrySource: 'session_start',
799
+ sourceEventName: 'session_start',
800
+ sessionKey: event.sessionKey ?? ctx.sessionKey,
801
+ sessionId: event.sessionId ?? ctx.sessionId,
802
+ agentId: ctx.agentId,
803
+ run: () => {
804
+ reportSessionLifecycle({
805
+ lifecycleEvent: 'session_start',
806
+ sessionKey: event.sessionKey ?? ctx.sessionKey,
807
+ sessionId: event.sessionId ?? ctx.sessionId,
808
+ agentId: ctx.agentId,
809
+ hasNextSession: false,
810
+ });
811
+ },
812
+ });
813
+ });
814
+ api.on('session_end', (event, ctx) => {
815
+ safeTelemetryObserver({
816
+ logger: api.logger,
817
+ telemetrySource: 'session_end',
818
+ sourceEventName: 'session_end',
819
+ sessionKey: event.sessionKey ?? ctx.sessionKey,
820
+ sessionId: event.sessionId ?? ctx.sessionId,
821
+ agentId: ctx.agentId,
822
+ run: () => {
823
+ reportSessionLifecycle({
824
+ lifecycleEvent: 'session_end',
825
+ sessionKey: event.sessionKey ?? ctx.sessionKey,
826
+ sessionId: event.sessionId ?? ctx.sessionId,
827
+ agentId: ctx.agentId,
828
+ reason: event.reason ?? null,
829
+ messageCount: event.messageCount,
830
+ durationMs: event.durationMs ?? null,
831
+ transcriptArchived: event.transcriptArchived ?? null,
832
+ hasNextSession: Boolean(event.nextSessionId ?? event.nextSessionKey),
833
+ });
834
+ },
835
+ });
836
+ });
837
+ if (shouldInstallTlonDiagnosticSubscriptions(api.registrationMode)) {
838
+ const unsubscribeDiagnosticEvents = installTelemetryDiagnosticObservers(api);
839
+ api.on('gateway_stop', unsubscribeDiagnosticEvents);
840
+ }
841
+ // ── Route diagnostics ───────────────────────────────────────────────
842
+ // Fires for every outbound send OpenClaw routes — the primary streamed
843
+ // reply (resolves to `tlon`) and route-dependent sends (the shared
844
+ // `message` tool, subagents, which can resolve elsewhere). `ctx.channelId`
845
+ // is where the send resolved; `routedToTlon: false` (e.g. `webchat`) is the
846
+ // leak this work targets. Read-only; never alters delivery.
847
+ //
848
+ // Two sinks: a PostHog event (the primary, fleet-wide signal — gated by the
849
+ // existing telemetry config, on in hosted prod) so we can count how often
850
+ // sends land off-Tlon; and a debug-gated local log for single-gateway
851
+ // triage.
852
+ api.on('message_sending', (event, ctx) => {
853
+ safeTelemetryObserver({
854
+ logger: api.logger,
855
+ telemetrySource: 'message_sending',
856
+ sourceEventName: 'message_sending',
857
+ sessionKey: ctx.sessionKey,
858
+ runId: ctx.runId,
859
+ run: () => {
860
+ const resolvedChannel = ctx.channelId;
861
+ const routedToTlon = resolvedChannel === 'tlon';
862
+ // Only infer target kind for Tlon targets; a webchat target id is not
863
+ // a Tlon target and must not be misclassified.
864
+ const parsedTarget = routedToTlon ? parseTlonTarget(event.to) : null;
865
+ const targetKind = parsedTarget?.kind === 'dm'
866
+ ? 'dm'
867
+ : parsedTarget?.kind === 'channel'
868
+ ? 'group'
869
+ : 'unknown';
870
+ reportOutboundRoute({ resolvedChannel, routedToTlon, targetKind });
871
+ if (isRouteDebugEnabled()) {
872
+ api.logger.info(`[tlon][route-debug] message_sending ${JSON.stringify({
873
+ channelId: ctx.channelId,
874
+ to: event.to,
875
+ routedToTlon,
876
+ targetKind,
877
+ sessionKey: ctx.sessionKey ?? null,
878
+ conversationId: ctx.conversationId ?? null,
879
+ messageId: ctx.messageId ?? null,
880
+ threadId: event.threadId ?? null,
881
+ })}`);
882
+ }
883
+ },
884
+ });
885
+ });
886
+ api.on('message_sent', (event, ctx) => {
887
+ safeTelemetryObserver({
888
+ logger: api.logger,
889
+ telemetrySource: 'message_sent',
890
+ sourceEventName: 'message_sent',
891
+ sessionKey: event.sessionKey ?? ctx.sessionKey,
892
+ runId: event.runId ?? ctx.runId,
893
+ run: () => {
894
+ if (event.success !== false) {
895
+ return;
896
+ }
897
+ reportHarnessError({
898
+ harnessEventType: 'message_sent',
899
+ errorScope: 'message_delivery',
900
+ sessionKey: event.sessionKey ?? ctx.sessionKey,
901
+ runId: event.runId ?? ctx.runId,
902
+ errorText: event.error ?? null,
903
+ outcome: 'error',
904
+ });
905
+ },
355
906
  });
356
907
  });
357
908
  // ── Slash commands for approval & admin ────────────────────────────
358
909
  api.registerCommand({
359
- name: "allow",
360
- description: "Allow a pending DM/channel/group request",
910
+ name: 'allow',
911
+ description: 'Allow a pending DM/channel/group request',
361
912
  acceptsArgs: true,
362
913
  handler: async (ctx) => {
363
914
  const result = resolveBridgeForCommand(ctx);
364
- if ("error" in result) {
915
+ if ('error' in result) {
365
916
  return { text: result.error };
366
917
  }
367
918
  return {
368
- text: await result.bridge.handleAction("approve", ctx.args?.trim() || undefined),
919
+ text: await result.bridge.handleAction('approve', ctx.args?.trim() || undefined),
369
920
  };
370
921
  },
371
922
  });
372
923
  api.registerCommand({
373
- name: "reject",
374
- description: "Reject a pending DM/channel/group request",
924
+ name: 'reject',
925
+ description: 'Reject a pending DM/channel/group request',
375
926
  acceptsArgs: true,
376
927
  handler: async (ctx) => {
377
928
  const result = resolveBridgeForCommand(ctx);
378
- if ("error" in result) {
929
+ if ('error' in result) {
379
930
  return { text: result.error };
380
931
  }
381
932
  return {
382
- text: await result.bridge.handleAction("deny", ctx.args?.trim() || undefined),
933
+ text: await result.bridge.handleAction('deny', ctx.args?.trim() || undefined),
383
934
  };
384
935
  },
385
936
  });
386
937
  api.registerCommand({
387
- name: "ban",
388
- description: "Ban a ship and deny its pending request",
938
+ name: 'ban',
939
+ description: 'Ban a ship and deny its pending request',
389
940
  acceptsArgs: true,
390
941
  handler: async (ctx) => {
391
942
  const result = resolveBridgeForCommand(ctx);
392
- if ("error" in result) {
943
+ if ('error' in result) {
393
944
  return { text: result.error };
394
945
  }
395
946
  return {
396
- text: await result.bridge.handleAction("block", ctx.args?.trim() || undefined),
947
+ text: await result.bridge.handleAction('block', ctx.args?.trim() || undefined),
397
948
  };
398
949
  },
399
950
  });
400
951
  api.registerCommand({
401
- name: "pending",
402
- description: "List pending approval requests",
952
+ name: 'pending',
953
+ description: 'List pending approval requests',
403
954
  handler: async (ctx) => {
404
955
  const result = resolveBridgeForCommand(ctx);
405
- if ("error" in result) {
956
+ if ('error' in result) {
406
957
  return { text: result.error };
407
958
  }
408
959
  return { text: await result.bridge.getPendingList() };
409
960
  },
410
961
  });
411
962
  api.registerCommand({
412
- name: "banned",
413
- description: "List banned ships",
963
+ name: 'banned',
964
+ description: 'List banned ships',
414
965
  handler: async (ctx) => {
415
966
  const result = resolveBridgeForCommand(ctx);
416
- if ("error" in result) {
967
+ if ('error' in result) {
417
968
  return { text: result.error };
418
969
  }
419
970
  return { text: await result.bridge.getBlockedList() };
420
971
  },
421
972
  });
422
973
  api.registerCommand({
423
- name: "unban",
424
- description: "Unban a ship (e.g. /unban ~sampel-palnet)",
974
+ name: 'unban',
975
+ description: 'Unban a ship (e.g. /unban ~sampel-palnet)',
425
976
  acceptsArgs: true,
426
977
  handler: async (ctx) => {
427
978
  const result = resolveBridgeForCommand(ctx);
428
- if ("error" in result) {
979
+ if ('error' in result) {
429
980
  return { text: result.error };
430
981
  }
431
982
  const ship = ctx.args?.trim();
432
983
  if (!ship) {
433
- return { text: "Usage: /unban ~ship-name" };
984
+ return { text: 'Usage: /unban ~ship-name' };
434
985
  }
435
986
  return { text: await result.bridge.handleUnblock(ship) };
436
987
  },
437
988
  });
438
989
  api.registerCommand({
439
- name: "owner-listen",
440
- description: "Control whether the bot listens for the owner without @-mention in owned channels. " +
441
- "Usage: /owner-listen [on|off|status|list] [<channel-nest>]; " +
442
- "/owner-listen all [on|off] for the global kill switch.",
990
+ name: 'owner-listen',
991
+ description: 'Control whether the bot listens for the owner without @-mention in owned channels. ' +
992
+ 'Usage: /owner-listen [on|off|status|list] [<channel-nest>]; ' +
993
+ '/owner-listen all [on|off] for the global kill switch.',
443
994
  acceptsArgs: true,
444
995
  handler: async (ctx) => {
445
996
  const result = resolveBridgeForCommand(ctx);
446
- if ("error" in result) {
997
+ if ('error' in result) {
447
998
  return { text: result.error };
448
999
  }
449
1000
  const text = await handleOwnerListenCommand(result.bridge, ctx.args, ctx.from);