@hybridaione/hybridclaw 0.2.3 → 0.2.7
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/.github/workflows/ci.yml +70 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +95 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +57 -19
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +72 -7
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1405 -0
- package/container/src/browser-tools.ts +672 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +65 -16
- package/container/src/index.ts +463 -110
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +107 -18
- package/container/src/tools.ts +600 -225
- package/container/src/types.ts +49 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +15 -4
- package/dist/agent.js.map +1 -1
- package/dist/audit-cli.d.ts.map +1 -1
- package/dist/audit-cli.js +4 -2
- package/dist/audit-cli.js.map +1 -1
- package/dist/audit-events.d.ts.map +1 -1
- package/dist/audit-events.js +53 -3
- package/dist/audit-events.js.map +1 -1
- package/dist/audit-trail.d.ts.map +1 -1
- package/dist/audit-trail.js +17 -8
- package/dist/audit-trail.js.map +1 -1
- package/dist/channels/discord/attachments.d.ts.map +1 -1
- package/dist/channels/discord/attachments.js +14 -7
- package/dist/channels/discord/attachments.js.map +1 -1
- package/dist/channels/discord/debounce.d.ts +9 -0
- package/dist/channels/discord/debounce.d.ts.map +1 -0
- package/dist/channels/discord/debounce.js +20 -0
- package/dist/channels/discord/debounce.js.map +1 -0
- package/dist/channels/discord/delivery.d.ts +4 -1
- package/dist/channels/discord/delivery.d.ts.map +1 -1
- package/dist/channels/discord/delivery.js +24 -4
- package/dist/channels/discord/delivery.js.map +1 -1
- package/dist/channels/discord/human-delay.d.ts +16 -0
- package/dist/channels/discord/human-delay.d.ts.map +1 -0
- package/dist/channels/discord/human-delay.js +29 -0
- package/dist/channels/discord/human-delay.js.map +1 -0
- package/dist/channels/discord/inbound.d.ts +31 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +156 -6
- package/dist/channels/discord/inbound.js.map +1 -1
- package/dist/channels/discord/mentions.d.ts.map +1 -1
- package/dist/channels/discord/mentions.js +16 -4
- package/dist/channels/discord/mentions.js.map +1 -1
- package/dist/channels/discord/presence.d.ts +33 -0
- package/dist/channels/discord/presence.d.ts.map +1 -0
- package/dist/channels/discord/presence.js +111 -0
- package/dist/channels/discord/presence.js.map +1 -0
- package/dist/channels/discord/rate-limiter.d.ts +14 -0
- package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
- package/dist/channels/discord/rate-limiter.js +49 -0
- package/dist/channels/discord/rate-limiter.js.map +1 -0
- package/dist/channels/discord/reactions.d.ts +38 -0
- package/dist/channels/discord/reactions.d.ts.map +1 -0
- package/dist/channels/discord/reactions.js +151 -0
- package/dist/channels/discord/reactions.js.map +1 -0
- package/dist/channels/discord/runtime.d.ts +7 -4
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +755 -150
- package/dist/channels/discord/runtime.js.map +1 -1
- package/dist/channels/discord/stream.d.ts +4 -1
- package/dist/channels/discord/stream.d.ts.map +1 -1
- package/dist/channels/discord/stream.js +19 -12
- package/dist/channels/discord/stream.js.map +1 -1
- package/dist/channels/discord/tool-actions.d.ts.map +1 -1
- package/dist/channels/discord/tool-actions.js +24 -12
- package/dist/channels/discord/tool-actions.js.map +1 -1
- package/dist/channels/discord/typing.d.ts +15 -0
- package/dist/channels/discord/typing.d.ts.map +1 -0
- package/dist/channels/discord/typing.js +106 -0
- package/dist/channels/discord/typing.js.map +1 -0
- package/dist/chunk.d.ts.map +1 -1
- package/dist/chunk.js +4 -2
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +77 -23
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +113 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +59 -26
- package/dist/container-runner.js.map +1 -1
- package/dist/container-setup.d.ts.map +1 -1
- package/dist/container-setup.js +10 -9
- package/dist/container-setup.js.map +1 -1
- package/dist/conversation.d.ts +2 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +1 -1
- package/dist/conversation.js.map +1 -1
- package/dist/db.d.ts +118 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1568 -50
- package/dist/db.js.map +1 -1
- package/dist/delegation-manager.d.ts.map +1 -1
- package/dist/delegation-manager.js +3 -2
- package/dist/delegation-manager.js.map +1 -1
- package/dist/gateway-client.d.ts +2 -2
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +10 -4
- package/dist/gateway-client.js.map +1 -1
- package/dist/gateway-service.d.ts +3 -3
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +624 -87
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-types.d.ts +24 -0
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +303 -26
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +20 -10
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +62 -20
- package/dist/heartbeat.js.map +1 -1
- package/dist/hybridai-bots.d.ts.map +1 -1
- package/dist/hybridai-bots.js +4 -2
- package/dist/hybridai-bots.js.map +1 -1
- package/dist/hybridai-models.d.ts +8 -0
- package/dist/hybridai-models.d.ts.map +1 -0
- package/dist/hybridai-models.js +94 -0
- package/dist/hybridai-models.js.map +1 -0
- package/dist/instruction-approval-audit.d.ts.map +1 -1
- package/dist/instruction-approval-audit.js.map +1 -1
- package/dist/instruction-integrity.d.ts.map +1 -1
- package/dist/instruction-integrity.js +8 -2
- package/dist/instruction-integrity.js.map +1 -1
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +6 -1
- package/dist/ipc.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/memory-consolidation.d.ts +17 -0
- package/dist/memory-consolidation.d.ts.map +1 -0
- package/dist/memory-consolidation.js +25 -0
- package/dist/memory-consolidation.js.map +1 -0
- package/dist/memory-service.d.ts +200 -0
- package/dist/memory-service.d.ts.map +1 -0
- package/dist/memory-service.js +294 -0
- package/dist/memory-service.js.map +1 -0
- package/dist/mount-security.d.ts.map +1 -1
- package/dist/mount-security.js +31 -7
- package/dist/mount-security.js.map +1 -1
- package/dist/observability-ingest.d.ts.map +1 -1
- package/dist/observability-ingest.js +32 -11
- package/dist/observability-ingest.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +32 -9
- package/dist/onboarding.js.map +1 -1
- package/dist/proactive-policy.d.ts.map +1 -1
- package/dist/proactive-policy.js +2 -1
- package/dist/proactive-policy.js.map +1 -1
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +11 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +104 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +503 -24
- package/dist/runtime-config.js.map +1 -1
- package/dist/scheduled-task-runner.d.ts +1 -0
- package/dist/scheduled-task-runner.d.ts.map +1 -1
- package/dist/scheduled-task-runner.js +43 -10
- package/dist/scheduled-task-runner.js.map +1 -1
- package/dist/scheduler.d.ts +43 -4
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +530 -56
- package/dist/scheduler.js.map +1 -1
- package/dist/session-export.d.ts +26 -0
- package/dist/session-export.d.ts.map +1 -0
- package/dist/session-export.js +149 -0
- package/dist/session-export.js.map +1 -0
- package/dist/session-maintenance.d.ts.map +1 -1
- package/dist/session-maintenance.js +75 -13
- package/dist/session-maintenance.js.map +1 -1
- package/dist/session-transcripts.d.ts.map +1 -1
- package/dist/session-transcripts.js.map +1 -1
- package/dist/side-effects.d.ts.map +1 -1
- package/dist/side-effects.js +14 -2
- package/dist/side-effects.js.map +1 -1
- package/dist/skills-guard.d.ts.map +1 -1
- package/dist/skills-guard.js +893 -130
- package/dist/skills-guard.js.map +1 -1
- package/dist/skills.d.ts +5 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +29 -15
- package/dist/skills.js.map +1 -1
- package/dist/token-efficiency.d.ts.map +1 -1
- package/dist/token-efficiency.js.map +1 -1
- package/dist/tui.js +92 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +150 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -1
- package/dist/types.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +42 -14
- package/dist/update.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +49 -9
- package/dist/workspace.js.map +1 -1
- package/docs/chat.html +9 -3
- package/docs/index.html +72 -13
- package/package.json +8 -2
- package/src/agent.ts +31 -4
- package/src/audit-cli.ts +44 -16
- package/src/audit-events.ts +69 -5
- package/src/audit-trail.ts +41 -15
- package/src/channels/discord/attachments.ts +81 -27
- package/src/channels/discord/debounce.ts +25 -0
- package/src/channels/discord/delivery.ts +65 -14
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +190 -8
- package/src/channels/discord/mentions.ts +42 -18
- package/src/channels/discord/presence.ts +148 -0
- package/src/channels/discord/rate-limiter.ts +58 -0
- package/src/channels/discord/reactions.ts +211 -0
- package/src/channels/discord/runtime.ts +1161 -205
- package/src/channels/discord/stream.ts +85 -31
- package/src/channels/discord/tool-actions.ts +78 -37
- package/src/channels/discord/typing.ts +140 -0
- package/src/chunk.ts +12 -4
- package/src/cli.ts +174 -57
- package/src/config.ts +208 -36
- package/src/container-runner.ts +134 -42
- package/src/container-setup.ts +57 -22
- package/src/conversation.ts +9 -7
- package/src/db.ts +2217 -84
- package/src/delegation-manager.ts +6 -2
- package/src/gateway-client.ts +41 -17
- package/src/gateway-service.ts +996 -220
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +487 -52
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +99 -22
- package/src/hybridai-bots.ts +14 -5
- package/src/hybridai-models.ts +158 -0
- package/src/instruction-approval-audit.ts +4 -1
- package/src/instruction-integrity.ts +30 -9
- package/src/ipc.ts +23 -5
- package/src/logger.ts +4 -1
- package/src/memory-consolidation.ts +41 -0
- package/src/memory-service.ts +606 -0
- package/src/mount-security.ts +58 -13
- package/src/observability-ingest.ts +134 -35
- package/src/onboarding.ts +126 -35
- package/src/proactive-policy.ts +3 -1
- package/src/prompt-hooks.ts +42 -17
- package/src/runtime-config.ts +1112 -122
- package/src/scheduled-task-runner.ts +77 -11
- package/src/scheduler.ts +683 -60
- package/src/session-export.ts +196 -0
- package/src/session-maintenance.ts +125 -22
- package/src/session-transcripts.ts +12 -3
- package/src/side-effects.ts +28 -5
- package/src/skills-guard.ts +1067 -219
- package/src/skills.ts +163 -65
- package/src/token-efficiency.ts +31 -9
- package/src/tui.ts +166 -25
- package/src/types.ts +199 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +335 -0
- package/tests/discord.basic.test.ts +213 -4
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/hybridai-client.test.ts +112 -0
- package/tests/hybridai-models.test.ts +46 -0
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- package/tests/token-usage.cache.test.ts +128 -0
- package/vitest.e2e.config.ts +3 -1
- package/vitest.integration.config.ts +3 -1
- package/vitest.live.config.ts +3 -1
- package/vitest.unit.config.ts +9 -0
|
@@ -1,17 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AttachmentBuilder,
|
|
3
|
-
type Message as DiscordMessage,
|
|
4
|
-
} from 'discord.js';
|
|
1
|
+
import type { AttachmentBuilder, Message as DiscordMessage } from 'discord.js';
|
|
5
2
|
|
|
6
3
|
import { chunkMessage } from '../../chunk.js';
|
|
4
|
+
import {
|
|
5
|
+
DISCORD_MAX_LINES_PER_MESSAGE,
|
|
6
|
+
DISCORD_TEXT_CHUNK_LIMIT,
|
|
7
|
+
} from '../../config.js';
|
|
7
8
|
import { logger } from '../../logger.js';
|
|
9
|
+
import {
|
|
10
|
+
getHumanDelayMs,
|
|
11
|
+
type HumanDelayConfig,
|
|
12
|
+
sleep,
|
|
13
|
+
} from './human-delay.js';
|
|
8
14
|
|
|
9
15
|
interface DiscordSendChannel {
|
|
10
|
-
send: (payload: {
|
|
16
|
+
send: (payload: {
|
|
17
|
+
content: string;
|
|
18
|
+
files?: AttachmentBuilder[];
|
|
19
|
+
}) => Promise<DiscordMessage>;
|
|
11
20
|
}
|
|
12
21
|
|
|
13
22
|
interface DiscordEditMessage {
|
|
14
|
-
edit: (payload: {
|
|
23
|
+
edit: (payload: {
|
|
24
|
+
content: string;
|
|
25
|
+
files?: AttachmentBuilder[];
|
|
26
|
+
}) => Promise<DiscordMessage>;
|
|
15
27
|
delete: () => Promise<unknown>;
|
|
16
28
|
}
|
|
17
29
|
|
|
@@ -29,10 +41,9 @@ export interface DiscordStreamOptions {
|
|
|
29
41
|
maxLines?: number;
|
|
30
42
|
editIntervalMs?: number;
|
|
31
43
|
onFirstMessage?: () => void;
|
|
44
|
+
humanDelay?: HumanDelayConfig;
|
|
32
45
|
}
|
|
33
46
|
|
|
34
|
-
const DEFAULT_MAX_CHARS = 1_800;
|
|
35
|
-
const DEFAULT_MAX_LINES = 20;
|
|
36
47
|
const DEFAULT_EDIT_INTERVAL_MS = 1_200;
|
|
37
48
|
const RETRY_MAX_ATTEMPTS = 3;
|
|
38
49
|
const RETRY_BASE_DELAY_MS = 500;
|
|
@@ -40,20 +51,30 @@ const RETRY_BASE_DELAY_MS = 500;
|
|
|
40
51
|
function isRetryableDiscordError(error: unknown): boolean {
|
|
41
52
|
const maybe = error as DiscordErrorLike;
|
|
42
53
|
const status = maybe.status ?? maybe.httpStatus;
|
|
43
|
-
return
|
|
54
|
+
return (
|
|
55
|
+
status === 429 ||
|
|
56
|
+
(typeof status === 'number' && status >= 500 && status <= 599)
|
|
57
|
+
);
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
function extractRetryDelayMs(error: unknown, fallbackMs: number): number {
|
|
47
61
|
const maybe = error as DiscordErrorLike;
|
|
48
62
|
const retryAfterSeconds = maybe.retryAfter ?? maybe.data?.retry_after;
|
|
49
|
-
if (
|
|
63
|
+
if (
|
|
64
|
+
typeof retryAfterSeconds === 'number' &&
|
|
65
|
+
Number.isFinite(retryAfterSeconds) &&
|
|
66
|
+
retryAfterSeconds > 0
|
|
67
|
+
) {
|
|
50
68
|
return Math.max(50, Math.ceil(retryAfterSeconds * 1_000));
|
|
51
69
|
}
|
|
52
70
|
const jitter = Math.floor(Math.random() * 250);
|
|
53
71
|
return fallbackMs + jitter;
|
|
54
72
|
}
|
|
55
73
|
|
|
56
|
-
async function withDiscordRetry<T>(
|
|
74
|
+
async function withDiscordRetry<T>(
|
|
75
|
+
label: string,
|
|
76
|
+
run: () => Promise<T>,
|
|
77
|
+
): Promise<T> {
|
|
57
78
|
let attempt = 0;
|
|
58
79
|
let delayMs = RETRY_BASE_DELAY_MS;
|
|
59
80
|
while (true) {
|
|
@@ -65,7 +86,10 @@ async function withDiscordRetry<T>(label: string, run: () => Promise<T>): Promis
|
|
|
65
86
|
throw error;
|
|
66
87
|
}
|
|
67
88
|
const waitMs = extractRetryDelayMs(error, delayMs);
|
|
68
|
-
logger.warn(
|
|
89
|
+
logger.warn(
|
|
90
|
+
{ label, attempt, waitMs, error },
|
|
91
|
+
'Discord request failed; retrying',
|
|
92
|
+
);
|
|
69
93
|
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
70
94
|
delayMs = Math.min(delayMs * 2, 4_000);
|
|
71
95
|
}
|
|
@@ -79,6 +103,7 @@ export class DiscordStreamManager {
|
|
|
79
103
|
private readonly maxLines: number;
|
|
80
104
|
private readonly editIntervalMs: number;
|
|
81
105
|
private readonly onFirstMessage?: () => void;
|
|
106
|
+
private readonly humanDelay?: HumanDelayConfig;
|
|
82
107
|
|
|
83
108
|
private readonly messages: DiscordEditMessage[] = [];
|
|
84
109
|
private sentChunks: string[] = [];
|
|
@@ -91,10 +116,20 @@ export class DiscordStreamManager {
|
|
|
91
116
|
constructor(sourceMessage: DiscordMessage, options?: DiscordStreamOptions) {
|
|
92
117
|
this.sourceMessage = sourceMessage;
|
|
93
118
|
this.channel = sourceMessage.channel as unknown as DiscordSendChannel;
|
|
94
|
-
this.maxChars = Math.max(
|
|
95
|
-
|
|
96
|
-
|
|
119
|
+
this.maxChars = Math.max(
|
|
120
|
+
200,
|
|
121
|
+
Math.min(2_000, options?.maxChars ?? DISCORD_TEXT_CHUNK_LIMIT),
|
|
122
|
+
);
|
|
123
|
+
this.maxLines = Math.max(
|
|
124
|
+
4,
|
|
125
|
+
Math.min(200, options?.maxLines ?? DISCORD_MAX_LINES_PER_MESSAGE),
|
|
126
|
+
);
|
|
127
|
+
this.editIntervalMs = Math.max(
|
|
128
|
+
250,
|
|
129
|
+
options?.editIntervalMs ?? DEFAULT_EDIT_INTERVAL_MS,
|
|
130
|
+
);
|
|
97
131
|
this.onFirstMessage = options?.onFirstMessage;
|
|
132
|
+
this.humanDelay = options?.humanDelay;
|
|
98
133
|
}
|
|
99
134
|
|
|
100
135
|
hasSentMessages(): boolean {
|
|
@@ -125,9 +160,7 @@ export class DiscordStreamManager {
|
|
|
125
160
|
|
|
126
161
|
fail(errorText: string): Promise<void> {
|
|
127
162
|
if (this.closed) return Promise.resolve();
|
|
128
|
-
this.content = this.content
|
|
129
|
-
? `${this.content}\n\n${errorText}`
|
|
130
|
-
: errorText;
|
|
163
|
+
this.content = this.content ? `${this.content}\n\n${errorText}` : errorText;
|
|
131
164
|
if (this.flushTimer) {
|
|
132
165
|
clearTimeout(this.flushTimer);
|
|
133
166
|
this.flushTimer = null;
|
|
@@ -159,17 +192,18 @@ export class DiscordStreamManager {
|
|
|
159
192
|
}
|
|
160
193
|
|
|
161
194
|
private enqueue(task: () => Promise<void>): Promise<void> {
|
|
162
|
-
this.opQueue = this.opQueue
|
|
163
|
-
.
|
|
164
|
-
|
|
165
|
-
logger.warn({ error }, 'Discord stream operation failed');
|
|
166
|
-
});
|
|
195
|
+
this.opQueue = this.opQueue.then(task).catch((error) => {
|
|
196
|
+
logger.warn({ error }, 'Discord stream operation failed');
|
|
197
|
+
});
|
|
167
198
|
return this.opQueue;
|
|
168
199
|
}
|
|
169
200
|
|
|
170
201
|
private scheduleFlush(): void {
|
|
171
202
|
if (this.flushTimer || this.closed) return;
|
|
172
|
-
const waitMs = Math.max(
|
|
203
|
+
const waitMs = Math.max(
|
|
204
|
+
0,
|
|
205
|
+
this.editIntervalMs - (Date.now() - this.lastEditAt),
|
|
206
|
+
);
|
|
173
207
|
this.flushTimer = setTimeout(() => {
|
|
174
208
|
this.flushTimer = null;
|
|
175
209
|
void this.enqueue(async () => {
|
|
@@ -178,7 +212,10 @@ export class DiscordStreamManager {
|
|
|
178
212
|
}, waitMs);
|
|
179
213
|
}
|
|
180
214
|
|
|
181
|
-
private async sync(
|
|
215
|
+
private async sync(
|
|
216
|
+
forceLastEdit: boolean,
|
|
217
|
+
files?: AttachmentBuilder[],
|
|
218
|
+
): Promise<void> {
|
|
182
219
|
const chunks = chunkMessage(this.content, {
|
|
183
220
|
maxChars: this.maxChars,
|
|
184
221
|
maxLines: this.maxLines,
|
|
@@ -187,7 +224,9 @@ export class DiscordStreamManager {
|
|
|
187
224
|
if (chunks.length === 0) {
|
|
188
225
|
if (files && files.length > 0) {
|
|
189
226
|
const fallback = 'Attached files:';
|
|
190
|
-
const sent = await withDiscordRetry('reply', () =>
|
|
227
|
+
const sent = await withDiscordRetry('reply', () =>
|
|
228
|
+
this.sourceMessage.reply({ content: fallback, files }),
|
|
229
|
+
);
|
|
191
230
|
this.messages.push(sent as unknown as DiscordEditMessage);
|
|
192
231
|
this.sentChunks.push(fallback);
|
|
193
232
|
this.onFirstMessage?.();
|
|
@@ -200,9 +239,20 @@ export class DiscordStreamManager {
|
|
|
200
239
|
const isLast = i === chunks.length - 1;
|
|
201
240
|
|
|
202
241
|
if (i >= this.messages.length) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
242
|
+
if (i > 0) {
|
|
243
|
+
const delayMs = getHumanDelayMs(this.humanDelay);
|
|
244
|
+
if (delayMs > 0) {
|
|
245
|
+
await sleep(delayMs);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const sent =
|
|
249
|
+
i === 0
|
|
250
|
+
? await withDiscordRetry('reply', () =>
|
|
251
|
+
this.sourceMessage.reply({ content: chunk }),
|
|
252
|
+
)
|
|
253
|
+
: await withDiscordRetry('send', () =>
|
|
254
|
+
this.channel.send({ content: chunk }),
|
|
255
|
+
);
|
|
206
256
|
this.messages.push(sent as unknown as DiscordEditMessage);
|
|
207
257
|
this.sentChunks.push(chunk);
|
|
208
258
|
this.onFirstMessage?.();
|
|
@@ -217,7 +267,9 @@ export class DiscordStreamManager {
|
|
|
217
267
|
continue;
|
|
218
268
|
}
|
|
219
269
|
|
|
220
|
-
await withDiscordRetry('edit', () =>
|
|
270
|
+
await withDiscordRetry('edit', () =>
|
|
271
|
+
this.messages[i].edit({ content: chunk }),
|
|
272
|
+
);
|
|
221
273
|
this.sentChunks[i] = chunk;
|
|
222
274
|
this.lastEditAt = Date.now();
|
|
223
275
|
}
|
|
@@ -232,7 +284,9 @@ export class DiscordStreamManager {
|
|
|
232
284
|
|
|
233
285
|
if (files && files.length > 0) {
|
|
234
286
|
const lastIndex = chunks.length - 1;
|
|
235
|
-
await withDiscordRetry('edit', () =>
|
|
287
|
+
await withDiscordRetry('edit', () =>
|
|
288
|
+
this.messages[lastIndex].edit({ content: chunks[lastIndex], files }),
|
|
289
|
+
);
|
|
236
290
|
this.sentChunks[lastIndex] = chunks[lastIndex];
|
|
237
291
|
this.lastEditAt = Date.now();
|
|
238
292
|
}
|
|
@@ -33,7 +33,10 @@ export interface DiscordToolActionDependencies {
|
|
|
33
33
|
getDiscordPresence: (userId: string) => CachedDiscordPresence | undefined;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function sanitizeDiscordId(
|
|
36
|
+
function sanitizeDiscordId(
|
|
37
|
+
rawValue: string | undefined,
|
|
38
|
+
label: string,
|
|
39
|
+
): string {
|
|
37
40
|
const value = (rawValue || '').trim();
|
|
38
41
|
if (!/^\d{16,22}$/.test(value)) {
|
|
39
42
|
throw new Error(`${label} must be a Discord snowflake id.`);
|
|
@@ -59,7 +62,9 @@ function scoreGuildMemberForLookup(member: GuildMember, query: string): number {
|
|
|
59
62
|
const globalName = member.user.globalName?.toLowerCase() || '';
|
|
60
63
|
const nickname = member.nickname?.toLowerCase() || '';
|
|
61
64
|
const displayName = member.displayName?.toLowerCase() || '';
|
|
62
|
-
const candidates = [username, globalName, nickname, displayName].filter(
|
|
65
|
+
const candidates = [username, globalName, nickname, displayName].filter(
|
|
66
|
+
Boolean,
|
|
67
|
+
);
|
|
63
68
|
|
|
64
69
|
let score = 0;
|
|
65
70
|
if (candidates.some((value) => value === q)) score += 3;
|
|
@@ -93,7 +98,10 @@ async function resolveGuildMemberIdFromLookup(params: {
|
|
|
93
98
|
try {
|
|
94
99
|
members = await guild.members.search({ query: searchQuery, limit: 25 });
|
|
95
100
|
} catch {
|
|
96
|
-
const fetched = await guild.members.fetch({
|
|
101
|
+
const fetched = await guild.members.fetch({
|
|
102
|
+
query: searchQuery,
|
|
103
|
+
limit: 25,
|
|
104
|
+
});
|
|
97
105
|
members = fetched;
|
|
98
106
|
}
|
|
99
107
|
let best: GuildMember | null = null;
|
|
@@ -146,14 +154,22 @@ async function runDiscordReadAction(
|
|
|
146
154
|
const after = request.after?.trim();
|
|
147
155
|
const around = request.around?.trim();
|
|
148
156
|
|
|
149
|
-
const query: {
|
|
157
|
+
const query: {
|
|
158
|
+
limit: number;
|
|
159
|
+
before?: string;
|
|
160
|
+
after?: string;
|
|
161
|
+
around?: string;
|
|
162
|
+
} = { limit };
|
|
150
163
|
if (before) query.before = before;
|
|
151
164
|
if (after) query.after = after;
|
|
152
165
|
if (around) query.around = around;
|
|
153
166
|
|
|
154
167
|
const fetched = await channel.messages.fetch(query);
|
|
155
168
|
const messages = Array.from(fetched.values())
|
|
156
|
-
.sort(
|
|
169
|
+
.sort(
|
|
170
|
+
(a, b) =>
|
|
171
|
+
a.createdTimestamp - b.createdTimestamp || a.id.localeCompare(b.id),
|
|
172
|
+
)
|
|
157
173
|
.map((message) => ({
|
|
158
174
|
id: message.id,
|
|
159
175
|
channelId: message.channelId,
|
|
@@ -175,13 +191,15 @@ async function runDiscordReadAction(
|
|
|
175
191
|
displayName: message.member.displayName || null,
|
|
176
192
|
}
|
|
177
193
|
: null,
|
|
178
|
-
attachments: Array.from(message.attachments.values()).map(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
194
|
+
attachments: Array.from(message.attachments.values()).map(
|
|
195
|
+
(attachment) => ({
|
|
196
|
+
id: attachment.id,
|
|
197
|
+
name: attachment.name || null,
|
|
198
|
+
url: attachment.url,
|
|
199
|
+
contentType: attachment.contentType || null,
|
|
200
|
+
size: attachment.size,
|
|
201
|
+
}),
|
|
202
|
+
),
|
|
185
203
|
mentions: {
|
|
186
204
|
users: Array.from(message.mentions.users.values()).map((user) => ({
|
|
187
205
|
id: user.id,
|
|
@@ -192,12 +210,16 @@ async function runDiscordReadAction(
|
|
|
192
210
|
id: role.id,
|
|
193
211
|
name: role.name,
|
|
194
212
|
})),
|
|
195
|
-
channels: Array.from(message.mentions.channels.values()).map(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
213
|
+
channels: Array.from(message.mentions.channels.values()).map(
|
|
214
|
+
(mentionedChannel) => ({
|
|
215
|
+
id: mentionedChannel.id,
|
|
216
|
+
name:
|
|
217
|
+
'name' in mentionedChannel &&
|
|
218
|
+
typeof mentionedChannel.name === 'string'
|
|
219
|
+
? mentionedChannel.name
|
|
220
|
+
: null,
|
|
221
|
+
}),
|
|
222
|
+
),
|
|
201
223
|
},
|
|
202
224
|
}));
|
|
203
225
|
|
|
@@ -217,10 +239,7 @@ async function runDiscordMemberInfoAction(
|
|
|
217
239
|
const activeClient = deps.requireDiscordClientReady();
|
|
218
240
|
const guildId = sanitizeDiscordId(request.guildId, 'guildId');
|
|
219
241
|
const userLookupRaw =
|
|
220
|
-
request.userId
|
|
221
|
-
|| request.memberId
|
|
222
|
-
|| request.user
|
|
223
|
-
|| request.username;
|
|
242
|
+
request.userId || request.memberId || request.user || request.username;
|
|
224
243
|
const resolvedUser = await resolveGuildMemberIdFromLookup({
|
|
225
244
|
requireDiscordClientReady: deps.requireDiscordClientReady,
|
|
226
245
|
guildId,
|
|
@@ -258,7 +277,9 @@ async function runDiscordMemberInfoAction(
|
|
|
258
277
|
nickname: member.nickname || null,
|
|
259
278
|
joinedAt: normalizeDate(member.joinedAt),
|
|
260
279
|
premiumSince: normalizeDate(member.premiumSince),
|
|
261
|
-
communicationDisabledUntil: normalizeDate(
|
|
280
|
+
communicationDisabledUntil: normalizeDate(
|
|
281
|
+
member.communicationDisabledUntil,
|
|
282
|
+
),
|
|
262
283
|
roles,
|
|
263
284
|
},
|
|
264
285
|
...(presence
|
|
@@ -285,25 +306,43 @@ async function runDiscordChannelInfoAction(
|
|
|
285
306
|
id: channel.id,
|
|
286
307
|
type: channel.type,
|
|
287
308
|
guildId: 'guildId' in channel ? channel.guildId || null : null,
|
|
288
|
-
name:
|
|
309
|
+
name:
|
|
310
|
+
'name' in channel && typeof channel.name === 'string'
|
|
311
|
+
? channel.name
|
|
312
|
+
: null,
|
|
289
313
|
parentId: 'parentId' in channel ? channel.parentId || null : null,
|
|
290
|
-
topic:
|
|
291
|
-
|
|
314
|
+
topic:
|
|
315
|
+
'topic' in channel && typeof channel.topic === 'string'
|
|
316
|
+
? channel.topic
|
|
317
|
+
: null,
|
|
318
|
+
nsfw:
|
|
319
|
+
'nsfw' in channel && typeof channel.nsfw === 'boolean'
|
|
320
|
+
? channel.nsfw
|
|
321
|
+
: null,
|
|
292
322
|
rateLimitPerUser:
|
|
293
|
-
'rateLimitPerUser' in channel &&
|
|
323
|
+
'rateLimitPerUser' in channel &&
|
|
324
|
+
typeof channel.rateLimitPerUser === 'number'
|
|
294
325
|
? channel.rateLimitPerUser
|
|
295
326
|
: null,
|
|
296
|
-
isTextBased:
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
327
|
+
isTextBased:
|
|
328
|
+
typeof channel.isTextBased === 'function' ? channel.isTextBased() : false,
|
|
329
|
+
isDMBased:
|
|
330
|
+
typeof channel.isDMBased === 'function' ? channel.isDMBased() : false,
|
|
331
|
+
isThread:
|
|
332
|
+
typeof channel.isThread === 'function' ? channel.isThread() : false,
|
|
333
|
+
lastMessageId:
|
|
334
|
+
'lastMessageId' in channel ? channel.lastMessageId || null : null,
|
|
300
335
|
};
|
|
301
336
|
|
|
302
337
|
if (typeof channel.isThread === 'function' && channel.isThread()) {
|
|
303
338
|
channelData.archived =
|
|
304
|
-
'archived' in channel && typeof channel.archived === 'boolean'
|
|
339
|
+
'archived' in channel && typeof channel.archived === 'boolean'
|
|
340
|
+
? channel.archived
|
|
341
|
+
: null;
|
|
305
342
|
channelData.locked =
|
|
306
|
-
'locked' in channel && typeof channel.locked === 'boolean'
|
|
343
|
+
'locked' in channel && typeof channel.locked === 'boolean'
|
|
344
|
+
? channel.locked
|
|
345
|
+
: null;
|
|
307
346
|
channelData.ownerId = 'ownerId' in channel ? channel.ownerId || null : null;
|
|
308
347
|
}
|
|
309
348
|
|
|
@@ -314,9 +353,9 @@ async function runDiscordChannelInfoAction(
|
|
|
314
353
|
};
|
|
315
354
|
}
|
|
316
355
|
|
|
317
|
-
export function createDiscordToolActionRunner(
|
|
318
|
-
|
|
319
|
-
) => Promise<Record<string, unknown>> {
|
|
356
|
+
export function createDiscordToolActionRunner(
|
|
357
|
+
deps: DiscordToolActionDependencies,
|
|
358
|
+
): (request: DiscordToolActionRequest) => Promise<Record<string, unknown>> {
|
|
320
359
|
return async (request: DiscordToolActionRequest) => {
|
|
321
360
|
switch (request.action) {
|
|
322
361
|
case 'read':
|
|
@@ -326,7 +365,9 @@ export function createDiscordToolActionRunner(deps: DiscordToolActionDependencie
|
|
|
326
365
|
case 'channel-info':
|
|
327
366
|
return await runDiscordChannelInfoAction(request, deps);
|
|
328
367
|
default:
|
|
329
|
-
throw new Error(
|
|
368
|
+
throw new Error(
|
|
369
|
+
`Unsupported Discord action: ${request.action as string}`,
|
|
370
|
+
);
|
|
330
371
|
}
|
|
331
372
|
};
|
|
332
373
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { Message as DiscordMessage } from 'discord.js';
|
|
2
|
+
|
|
3
|
+
import { logger } from '../../logger.js';
|
|
4
|
+
|
|
5
|
+
export type DiscordTypingPhase =
|
|
6
|
+
| 'received'
|
|
7
|
+
| 'thinking'
|
|
8
|
+
| 'toolUse'
|
|
9
|
+
| 'streaming'
|
|
10
|
+
| 'done';
|
|
11
|
+
export type DiscordTypingMode = 'instant' | 'thinking' | 'streaming' | 'never';
|
|
12
|
+
|
|
13
|
+
export interface TypingController {
|
|
14
|
+
setPhase: (phase: DiscordTypingPhase) => void;
|
|
15
|
+
stop: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface CreateTypingControllerOptions {
|
|
19
|
+
keepaliveMs?: number;
|
|
20
|
+
ttlMs?: number;
|
|
21
|
+
stopGraceMs?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULT_KEEPALIVE_MS = 8_000;
|
|
25
|
+
const DEFAULT_TTL_MS = 60_000;
|
|
26
|
+
const DEFAULT_STOP_GRACE_MS = 500;
|
|
27
|
+
|
|
28
|
+
function isTypingActiveForPhase(
|
|
29
|
+
mode: DiscordTypingMode,
|
|
30
|
+
phase: DiscordTypingPhase,
|
|
31
|
+
): boolean {
|
|
32
|
+
if (mode === 'never') return false;
|
|
33
|
+
if (mode === 'instant') return phase !== 'done';
|
|
34
|
+
if (mode === 'thinking') return phase === 'thinking' || phase === 'toolUse';
|
|
35
|
+
return phase === 'streaming';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createTypingController(
|
|
39
|
+
message: DiscordMessage,
|
|
40
|
+
mode: DiscordTypingMode,
|
|
41
|
+
options?: CreateTypingControllerOptions,
|
|
42
|
+
): TypingController {
|
|
43
|
+
if (mode === 'never') {
|
|
44
|
+
return {
|
|
45
|
+
setPhase: () => {},
|
|
46
|
+
stop: () => {},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const keepaliveMs = Math.max(
|
|
51
|
+
2_000,
|
|
52
|
+
Math.floor(options?.keepaliveMs ?? DEFAULT_KEEPALIVE_MS),
|
|
53
|
+
);
|
|
54
|
+
const ttlMs = Math.max(5_000, Math.floor(options?.ttlMs ?? DEFAULT_TTL_MS));
|
|
55
|
+
const stopGraceMs = Math.max(
|
|
56
|
+
0,
|
|
57
|
+
Math.floor(options?.stopGraceMs ?? DEFAULT_STOP_GRACE_MS),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
let active = false;
|
|
61
|
+
let stopped = false;
|
|
62
|
+
let keepaliveTimer: ReturnType<typeof setInterval> | null = null;
|
|
63
|
+
let ttlTimer: ReturnType<typeof setTimeout> | null = null;
|
|
64
|
+
let stopTimer: ReturnType<typeof setTimeout> | null = null;
|
|
65
|
+
|
|
66
|
+
const sendTyping = async (): Promise<void> => {
|
|
67
|
+
if (stopped || !active) return;
|
|
68
|
+
if (!('sendTyping' in message.channel)) return;
|
|
69
|
+
try {
|
|
70
|
+
await message.channel.sendTyping();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
logger.debug(
|
|
73
|
+
{ error, channelId: message.channelId },
|
|
74
|
+
'Failed to send typing indicator',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const clearTimers = (): void => {
|
|
80
|
+
if (keepaliveTimer) {
|
|
81
|
+
clearInterval(keepaliveTimer);
|
|
82
|
+
keepaliveTimer = null;
|
|
83
|
+
}
|
|
84
|
+
if (ttlTimer) {
|
|
85
|
+
clearTimeout(ttlTimer);
|
|
86
|
+
ttlTimer = null;
|
|
87
|
+
}
|
|
88
|
+
if (stopTimer) {
|
|
89
|
+
clearTimeout(stopTimer);
|
|
90
|
+
stopTimer = null;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const stopNow = (): void => {
|
|
95
|
+
active = false;
|
|
96
|
+
clearTimers();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const scheduleStop = (): void => {
|
|
100
|
+
if (stopped || !active) return;
|
|
101
|
+
if (stopTimer) return;
|
|
102
|
+
stopTimer = setTimeout(() => {
|
|
103
|
+
stopTimer = null;
|
|
104
|
+
stopNow();
|
|
105
|
+
}, stopGraceMs);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const ensureRunning = (): void => {
|
|
109
|
+
if (stopped) return;
|
|
110
|
+
if (stopTimer) {
|
|
111
|
+
clearTimeout(stopTimer);
|
|
112
|
+
stopTimer = null;
|
|
113
|
+
}
|
|
114
|
+
if (active) return;
|
|
115
|
+
active = true;
|
|
116
|
+
void sendTyping();
|
|
117
|
+
keepaliveTimer = setInterval(() => {
|
|
118
|
+
void sendTyping();
|
|
119
|
+
}, keepaliveMs);
|
|
120
|
+
ttlTimer = setTimeout(() => {
|
|
121
|
+
stopNow();
|
|
122
|
+
}, ttlMs);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
setPhase: (phase) => {
|
|
127
|
+
if (stopped) return;
|
|
128
|
+
if (isTypingActiveForPhase(mode, phase)) {
|
|
129
|
+
ensureRunning();
|
|
130
|
+
} else {
|
|
131
|
+
scheduleStop();
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
stop: () => {
|
|
135
|
+
if (stopped) return;
|
|
136
|
+
stopped = true;
|
|
137
|
+
stopNow();
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
package/src/chunk.ts
CHANGED
|
@@ -82,7 +82,10 @@ function splitLongLine(line: string, maxChars: number): string[] {
|
|
|
82
82
|
return pieces;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
export function chunkMessage(
|
|
85
|
+
export function chunkMessage(
|
|
86
|
+
text: string,
|
|
87
|
+
opts?: ChunkMessageOptions,
|
|
88
|
+
): string[] {
|
|
86
89
|
const maxChars = Math.max(200, opts?.maxChars ?? DEFAULT_MAX_CHARS);
|
|
87
90
|
const maxLines = Math.max(4, opts?.maxLines ?? DEFAULT_MAX_LINES);
|
|
88
91
|
const normalized = (text || '').replace(/\r\n?/g, '\n');
|
|
@@ -120,15 +123,20 @@ export function chunkMessage(text: string, opts?: ChunkMessageOptions): string[]
|
|
|
120
123
|
};
|
|
121
124
|
|
|
122
125
|
const appendLine = (line: string): void => {
|
|
123
|
-
const addedChars =
|
|
126
|
+
const addedChars =
|
|
127
|
+
currentLines.length === 0 ? line.length : line.length + 1;
|
|
124
128
|
const nextChars = currentChars + addedChars;
|
|
125
129
|
const nextLines = currentLines.length + 1;
|
|
126
|
-
if (
|
|
130
|
+
if (
|
|
131
|
+
currentLines.length > 0 &&
|
|
132
|
+
(nextChars > maxChars || nextLines > maxLines)
|
|
133
|
+
) {
|
|
127
134
|
flush(false);
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
currentLines.push(line);
|
|
131
|
-
currentChars =
|
|
138
|
+
currentChars =
|
|
139
|
+
currentLines.length === 1 ? line.length : currentChars + line.length + 1;
|
|
132
140
|
|
|
133
141
|
if (isFenceLine(line)) {
|
|
134
142
|
if (!openFence) {
|