@hybridaione/hybridclaw 0.2.2 → 0.2.6
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 +85 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +41 -16
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +71 -8
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1303 -0
- package/container/src/browser-tools.ts +431 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +34 -13
- package/container/src/index.ts +451 -109
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +20 -10
- package/container/src/tools.ts +599 -225
- package/container/src/types.ts +32 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +10 -2
- 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 +19 -3
- 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 +4 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +45 -4
- 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 +6 -3
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +621 -125
- 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 +16 -8
- 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 +47 -22
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +103 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +58 -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 +563 -73
- 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 +179 -24
- 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 +48 -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/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 +9 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +98 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +477 -23
- 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 +29 -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 +146 -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 +37 -13
- package/package.json +8 -2
- package/src/agent.ts +16 -3
- 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 +57 -13
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +66 -7
- 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 +1048 -182
- package/src/channels/discord/stream.ts +73 -27
- 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 +141 -56
- package/src/config.ts +192 -34
- package/src/container-runner.ts +132 -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 +1019 -201
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +321 -48
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +84 -22
- package/src/hybridai-bots.ts +14 -5
- 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 +40 -17
- package/src/runtime-config.ts +1114 -99
- package/src/scheduled-task-runner.ts +63 -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 +195 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +224 -0
- package/tests/discord.basic.test.ts +82 -2
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- 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,25 @@
|
|
|
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';
|
|
7
4
|
import { logger } from '../../logger.js';
|
|
5
|
+
import {
|
|
6
|
+
getHumanDelayMs,
|
|
7
|
+
type HumanDelayConfig,
|
|
8
|
+
sleep,
|
|
9
|
+
} from './human-delay.js';
|
|
8
10
|
|
|
9
11
|
interface DiscordSendChannel {
|
|
10
|
-
send: (payload: {
|
|
12
|
+
send: (payload: {
|
|
13
|
+
content: string;
|
|
14
|
+
files?: AttachmentBuilder[];
|
|
15
|
+
}) => Promise<DiscordMessage>;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
interface DiscordEditMessage {
|
|
14
|
-
edit: (payload: {
|
|
19
|
+
edit: (payload: {
|
|
20
|
+
content: string;
|
|
21
|
+
files?: AttachmentBuilder[];
|
|
22
|
+
}) => Promise<DiscordMessage>;
|
|
15
23
|
delete: () => Promise<unknown>;
|
|
16
24
|
}
|
|
17
25
|
|
|
@@ -29,6 +37,7 @@ export interface DiscordStreamOptions {
|
|
|
29
37
|
maxLines?: number;
|
|
30
38
|
editIntervalMs?: number;
|
|
31
39
|
onFirstMessage?: () => void;
|
|
40
|
+
humanDelay?: HumanDelayConfig;
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
const DEFAULT_MAX_CHARS = 1_800;
|
|
@@ -40,20 +49,30 @@ const RETRY_BASE_DELAY_MS = 500;
|
|
|
40
49
|
function isRetryableDiscordError(error: unknown): boolean {
|
|
41
50
|
const maybe = error as DiscordErrorLike;
|
|
42
51
|
const status = maybe.status ?? maybe.httpStatus;
|
|
43
|
-
return
|
|
52
|
+
return (
|
|
53
|
+
status === 429 ||
|
|
54
|
+
(typeof status === 'number' && status >= 500 && status <= 599)
|
|
55
|
+
);
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
function extractRetryDelayMs(error: unknown, fallbackMs: number): number {
|
|
47
59
|
const maybe = error as DiscordErrorLike;
|
|
48
60
|
const retryAfterSeconds = maybe.retryAfter ?? maybe.data?.retry_after;
|
|
49
|
-
if (
|
|
61
|
+
if (
|
|
62
|
+
typeof retryAfterSeconds === 'number' &&
|
|
63
|
+
Number.isFinite(retryAfterSeconds) &&
|
|
64
|
+
retryAfterSeconds > 0
|
|
65
|
+
) {
|
|
50
66
|
return Math.max(50, Math.ceil(retryAfterSeconds * 1_000));
|
|
51
67
|
}
|
|
52
68
|
const jitter = Math.floor(Math.random() * 250);
|
|
53
69
|
return fallbackMs + jitter;
|
|
54
70
|
}
|
|
55
71
|
|
|
56
|
-
async function withDiscordRetry<T>(
|
|
72
|
+
async function withDiscordRetry<T>(
|
|
73
|
+
label: string,
|
|
74
|
+
run: () => Promise<T>,
|
|
75
|
+
): Promise<T> {
|
|
57
76
|
let attempt = 0;
|
|
58
77
|
let delayMs = RETRY_BASE_DELAY_MS;
|
|
59
78
|
while (true) {
|
|
@@ -65,7 +84,10 @@ async function withDiscordRetry<T>(label: string, run: () => Promise<T>): Promis
|
|
|
65
84
|
throw error;
|
|
66
85
|
}
|
|
67
86
|
const waitMs = extractRetryDelayMs(error, delayMs);
|
|
68
|
-
logger.warn(
|
|
87
|
+
logger.warn(
|
|
88
|
+
{ label, attempt, waitMs, error },
|
|
89
|
+
'Discord request failed; retrying',
|
|
90
|
+
);
|
|
69
91
|
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
70
92
|
delayMs = Math.min(delayMs * 2, 4_000);
|
|
71
93
|
}
|
|
@@ -79,6 +101,7 @@ export class DiscordStreamManager {
|
|
|
79
101
|
private readonly maxLines: number;
|
|
80
102
|
private readonly editIntervalMs: number;
|
|
81
103
|
private readonly onFirstMessage?: () => void;
|
|
104
|
+
private readonly humanDelay?: HumanDelayConfig;
|
|
82
105
|
|
|
83
106
|
private readonly messages: DiscordEditMessage[] = [];
|
|
84
107
|
private sentChunks: string[] = [];
|
|
@@ -93,8 +116,12 @@ export class DiscordStreamManager {
|
|
|
93
116
|
this.channel = sourceMessage.channel as unknown as DiscordSendChannel;
|
|
94
117
|
this.maxChars = Math.max(200, options?.maxChars ?? DEFAULT_MAX_CHARS);
|
|
95
118
|
this.maxLines = Math.max(4, options?.maxLines ?? DEFAULT_MAX_LINES);
|
|
96
|
-
this.editIntervalMs = Math.max(
|
|
119
|
+
this.editIntervalMs = Math.max(
|
|
120
|
+
250,
|
|
121
|
+
options?.editIntervalMs ?? DEFAULT_EDIT_INTERVAL_MS,
|
|
122
|
+
);
|
|
97
123
|
this.onFirstMessage = options?.onFirstMessage;
|
|
124
|
+
this.humanDelay = options?.humanDelay;
|
|
98
125
|
}
|
|
99
126
|
|
|
100
127
|
hasSentMessages(): boolean {
|
|
@@ -125,9 +152,7 @@ export class DiscordStreamManager {
|
|
|
125
152
|
|
|
126
153
|
fail(errorText: string): Promise<void> {
|
|
127
154
|
if (this.closed) return Promise.resolve();
|
|
128
|
-
this.content = this.content
|
|
129
|
-
? `${this.content}\n\n${errorText}`
|
|
130
|
-
: errorText;
|
|
155
|
+
this.content = this.content ? `${this.content}\n\n${errorText}` : errorText;
|
|
131
156
|
if (this.flushTimer) {
|
|
132
157
|
clearTimeout(this.flushTimer);
|
|
133
158
|
this.flushTimer = null;
|
|
@@ -159,17 +184,18 @@ export class DiscordStreamManager {
|
|
|
159
184
|
}
|
|
160
185
|
|
|
161
186
|
private enqueue(task: () => Promise<void>): Promise<void> {
|
|
162
|
-
this.opQueue = this.opQueue
|
|
163
|
-
.
|
|
164
|
-
|
|
165
|
-
logger.warn({ error }, 'Discord stream operation failed');
|
|
166
|
-
});
|
|
187
|
+
this.opQueue = this.opQueue.then(task).catch((error) => {
|
|
188
|
+
logger.warn({ error }, 'Discord stream operation failed');
|
|
189
|
+
});
|
|
167
190
|
return this.opQueue;
|
|
168
191
|
}
|
|
169
192
|
|
|
170
193
|
private scheduleFlush(): void {
|
|
171
194
|
if (this.flushTimer || this.closed) return;
|
|
172
|
-
const waitMs = Math.max(
|
|
195
|
+
const waitMs = Math.max(
|
|
196
|
+
0,
|
|
197
|
+
this.editIntervalMs - (Date.now() - this.lastEditAt),
|
|
198
|
+
);
|
|
173
199
|
this.flushTimer = setTimeout(() => {
|
|
174
200
|
this.flushTimer = null;
|
|
175
201
|
void this.enqueue(async () => {
|
|
@@ -178,7 +204,10 @@ export class DiscordStreamManager {
|
|
|
178
204
|
}, waitMs);
|
|
179
205
|
}
|
|
180
206
|
|
|
181
|
-
private async sync(
|
|
207
|
+
private async sync(
|
|
208
|
+
forceLastEdit: boolean,
|
|
209
|
+
files?: AttachmentBuilder[],
|
|
210
|
+
): Promise<void> {
|
|
182
211
|
const chunks = chunkMessage(this.content, {
|
|
183
212
|
maxChars: this.maxChars,
|
|
184
213
|
maxLines: this.maxLines,
|
|
@@ -187,7 +216,9 @@ export class DiscordStreamManager {
|
|
|
187
216
|
if (chunks.length === 0) {
|
|
188
217
|
if (files && files.length > 0) {
|
|
189
218
|
const fallback = 'Attached files:';
|
|
190
|
-
const sent = await withDiscordRetry('reply', () =>
|
|
219
|
+
const sent = await withDiscordRetry('reply', () =>
|
|
220
|
+
this.sourceMessage.reply({ content: fallback, files }),
|
|
221
|
+
);
|
|
191
222
|
this.messages.push(sent as unknown as DiscordEditMessage);
|
|
192
223
|
this.sentChunks.push(fallback);
|
|
193
224
|
this.onFirstMessage?.();
|
|
@@ -200,9 +231,20 @@ export class DiscordStreamManager {
|
|
|
200
231
|
const isLast = i === chunks.length - 1;
|
|
201
232
|
|
|
202
233
|
if (i >= this.messages.length) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
234
|
+
if (i > 0) {
|
|
235
|
+
const delayMs = getHumanDelayMs(this.humanDelay);
|
|
236
|
+
if (delayMs > 0) {
|
|
237
|
+
await sleep(delayMs);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const sent =
|
|
241
|
+
i === 0
|
|
242
|
+
? await withDiscordRetry('reply', () =>
|
|
243
|
+
this.sourceMessage.reply({ content: chunk }),
|
|
244
|
+
)
|
|
245
|
+
: await withDiscordRetry('send', () =>
|
|
246
|
+
this.channel.send({ content: chunk }),
|
|
247
|
+
);
|
|
206
248
|
this.messages.push(sent as unknown as DiscordEditMessage);
|
|
207
249
|
this.sentChunks.push(chunk);
|
|
208
250
|
this.onFirstMessage?.();
|
|
@@ -217,7 +259,9 @@ export class DiscordStreamManager {
|
|
|
217
259
|
continue;
|
|
218
260
|
}
|
|
219
261
|
|
|
220
|
-
await withDiscordRetry('edit', () =>
|
|
262
|
+
await withDiscordRetry('edit', () =>
|
|
263
|
+
this.messages[i].edit({ content: chunk }),
|
|
264
|
+
);
|
|
221
265
|
this.sentChunks[i] = chunk;
|
|
222
266
|
this.lastEditAt = Date.now();
|
|
223
267
|
}
|
|
@@ -232,7 +276,9 @@ export class DiscordStreamManager {
|
|
|
232
276
|
|
|
233
277
|
if (files && files.length > 0) {
|
|
234
278
|
const lastIndex = chunks.length - 1;
|
|
235
|
-
await withDiscordRetry('edit', () =>
|
|
279
|
+
await withDiscordRetry('edit', () =>
|
|
280
|
+
this.messages[lastIndex].edit({ content: chunks[lastIndex], files }),
|
|
281
|
+
);
|
|
236
282
|
this.sentChunks[lastIndex] = chunks[lastIndex];
|
|
237
283
|
this.lastEditAt = Date.now();
|
|
238
284
|
}
|
|
@@ -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) {
|