@friendlyrobot/discord-pi-agent 0.4.7 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-service.d.ts +6 -0
- package/dist/commands.d.ts +10 -3
- package/dist/config.d.ts +8 -1
- package/dist/discord-gateway-client.d.ts +11 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.js +399 -96
- package/dist/session-registry.d.ts +26 -0
- package/dist/types.d.ts +18 -0
- package/package.json +8 -5
package/dist/agent-service.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AgentSession } from "@mariozechner/pi-coding-agent";
|
|
1
2
|
import type { AgentStatus, ResolvedDiscordPiBridgeConfig, ThinkingLevel } from "./types";
|
|
2
3
|
export declare class AgentService {
|
|
3
4
|
private readonly config;
|
|
@@ -8,6 +9,9 @@ export declare class AgentService {
|
|
|
8
9
|
private session;
|
|
9
10
|
constructor(config: ResolvedDiscordPiBridgeConfig);
|
|
10
11
|
initialize(): Promise<void>;
|
|
12
|
+
getSession(): AgentSession | null;
|
|
13
|
+
getAgentDir(): string;
|
|
14
|
+
createSession(sessionDir: string): Promise<AgentSession>;
|
|
11
15
|
prompt(text: string): Promise<string>;
|
|
12
16
|
reloadResources(): Promise<string>;
|
|
13
17
|
compact(): Promise<string>;
|
|
@@ -16,8 +20,10 @@ export declare class AgentService {
|
|
|
16
20
|
shutdown(): Promise<void>;
|
|
17
21
|
private createOrResumeSession;
|
|
18
22
|
private ensureConfiguredModel;
|
|
23
|
+
private ensureModelForSession;
|
|
19
24
|
private requireSession;
|
|
20
25
|
private applyConfiguredThinkingLevel;
|
|
26
|
+
private applyConfiguredThinkingLevelForSession;
|
|
21
27
|
getThinkingLevel(): {
|
|
22
28
|
current: ThinkingLevel;
|
|
23
29
|
available: ThinkingLevel[];
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
1
2
|
import type { AgentService } from "./agent-service";
|
|
2
3
|
import type { PromptQueue } from "./prompt-queue";
|
|
3
|
-
type CommandResult = {
|
|
4
|
+
export type CommandResult = {
|
|
4
5
|
handled: boolean;
|
|
5
6
|
response?: string;
|
|
7
|
+
/** Set to true when the command wants to archive the current thread. */
|
|
8
|
+
archive?: boolean;
|
|
6
9
|
};
|
|
7
|
-
export
|
|
8
|
-
|
|
10
|
+
export type CommandContext = {
|
|
11
|
+
agentService: AgentService;
|
|
12
|
+
promptQueue: PromptQueue;
|
|
13
|
+
session?: AgentSession;
|
|
14
|
+
};
|
|
15
|
+
export declare function handleCommand(input: string, ctx: CommandContext): Promise<CommandResult>;
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
import type { DiscordPiBridgeConfig, ResolvedDiscordPiBridgeConfig } from "./types";
|
|
1
|
+
import type { DiscordGatewayConfig, DiscordPiBridgeConfig, ResolvedDiscordGatewayConfig, ResolvedDiscordPiBridgeConfig } from "./types";
|
|
2
2
|
export declare function resolveConfig(config: DiscordPiBridgeConfig): ResolvedDiscordPiBridgeConfig;
|
|
3
3
|
export declare function loadDiscordPiBridgeConfigFromEnv(overrides?: Partial<DiscordPiBridgeConfig>): ResolvedDiscordPiBridgeConfig;
|
|
4
|
+
/**
|
|
5
|
+
* Load gateway config from env vars + overrides.
|
|
6
|
+
* Preserves gateway-specific fields (forum channels, etc.) that
|
|
7
|
+
* loadDiscordPiBridgeConfigFromEnv would drop.
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadDiscordGatewayConfigFromEnv(overrides?: Partial<DiscordGatewayConfig>): ResolvedDiscordGatewayConfig;
|
|
10
|
+
export declare function resolveGatewayConfig(config: DiscordGatewayConfig): ResolvedDiscordGatewayConfig;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Client } from "discord.js";
|
|
2
|
+
import type { AgentService } from "./agent-service";
|
|
3
|
+
import type { SessionRegistry } from "./session-registry";
|
|
4
|
+
import type { ResolvedDiscordPiBridgeConfig } from "./types";
|
|
5
|
+
export type GatewayAuthConfig = {
|
|
6
|
+
discordAllowedUserId: string;
|
|
7
|
+
discordAllowedForumChannelIds: string[];
|
|
8
|
+
discordAllowedUserIds: string[];
|
|
9
|
+
startupMessage: string | false;
|
|
10
|
+
};
|
|
11
|
+
export declare function startGatewayClient(config: ResolvedDiscordPiBridgeConfig, agentService: AgentService, sessionRegistry: SessionRegistry, authConfig: GatewayAuthConfig): Promise<Client>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import type { DiscordPiBridge, DiscordPiBridgeConfig } from "./types";
|
|
1
|
+
import type { DiscordGateway, DiscordGatewayConfig, DiscordPiBridge, DiscordPiBridgeConfig } from "./types";
|
|
2
2
|
export { buildTimeContextPrompt, type TimeContextPromptOptions, } from "./prompt-context";
|
|
3
|
-
export { loadDiscordPiBridgeConfigFromEnv, resolveConfig } from "./config";
|
|
4
|
-
export type { AgentStatus, DiscordPiBridge, DiscordPiBridgeConfig, PromptTransform, ResolvedDiscordPiBridgeConfig, } from "./types";
|
|
3
|
+
export { loadDiscordPiBridgeConfigFromEnv, loadDiscordGatewayConfigFromEnv, resolveConfig, } from "./config";
|
|
4
|
+
export type { AgentStatus, DiscordGateway, DiscordGatewayConfig, DiscordPiBridge, DiscordPiBridgeConfig, PromptTransform, ResolvedDiscordPiBridgeConfig, } from "./types";
|
|
5
|
+
/**
|
|
6
|
+
* Start the unified Discord gateway. Supports DM and forum thread sessions
|
|
7
|
+
* out of the box. Set discordAllowedForumChannelIds to enable forum support.
|
|
8
|
+
*/
|
|
9
|
+
export declare function startDiscordGateway(config: DiscordGatewayConfig): Promise<DiscordGateway>;
|
|
10
|
+
/**
|
|
11
|
+
* Legacy DM-only entry point. Now a thin wrapper over startDiscordGateway.
|
|
12
|
+
*/
|
|
5
13
|
export declare function startDiscordPiBridge(config: DiscordPiBridgeConfig): Promise<DiscordPiBridge>;
|
package/dist/index.js
CHANGED
|
@@ -180,6 +180,32 @@ class AgentService {
|
|
|
180
180
|
await this.createOrResumeSession();
|
|
181
181
|
await this.ensureConfiguredModel();
|
|
182
182
|
}
|
|
183
|
+
getSession() {
|
|
184
|
+
return this.session;
|
|
185
|
+
}
|
|
186
|
+
getAgentDir() {
|
|
187
|
+
return this.config.agentDir;
|
|
188
|
+
}
|
|
189
|
+
async createSession(sessionDir) {
|
|
190
|
+
await fs.mkdir(sessionDir, { recursive: true });
|
|
191
|
+
const { session } = await createAgentSession({
|
|
192
|
+
cwd: this.config.cwd,
|
|
193
|
+
agentDir: this.config.agentDir,
|
|
194
|
+
authStorage: this.authStorage,
|
|
195
|
+
modelRegistry: this.modelRegistry,
|
|
196
|
+
resourceLoader: this.resourceLoader,
|
|
197
|
+
settingsManager: this.settingsManager,
|
|
198
|
+
sessionManager: SessionManager.continueRecent(this.config.cwd, sessionDir),
|
|
199
|
+
thinkingLevel: this.config.thinkingLevel
|
|
200
|
+
});
|
|
201
|
+
console.log("[agent] scoped session created", {
|
|
202
|
+
sessionDir,
|
|
203
|
+
sessionId: session.sessionId,
|
|
204
|
+
sessionFile: session.sessionFile
|
|
205
|
+
});
|
|
206
|
+
await this.ensureModelForSession(session);
|
|
207
|
+
return session;
|
|
208
|
+
}
|
|
183
209
|
async prompt(text) {
|
|
184
210
|
const session = this.requireSession();
|
|
185
211
|
const transformedPrompt = await this.config.promptTransform(text);
|
|
@@ -263,7 +289,9 @@ class AgentService {
|
|
|
263
289
|
});
|
|
264
290
|
}
|
|
265
291
|
async ensureConfiguredModel() {
|
|
266
|
-
|
|
292
|
+
await this.ensureModelForSession(this.requireSession());
|
|
293
|
+
}
|
|
294
|
+
async ensureModelForSession(session) {
|
|
267
295
|
const desiredModel = this.modelRegistry.find(this.config.modelProvider, this.config.modelId);
|
|
268
296
|
const availableModels = await this.modelRegistry.getAvailable();
|
|
269
297
|
console.log("[agent] available models", {
|
|
@@ -286,7 +314,7 @@ class AgentService {
|
|
|
286
314
|
to: `${desiredModel.provider}/${desiredModel.id}`
|
|
287
315
|
});
|
|
288
316
|
await session.setModel(desiredModel);
|
|
289
|
-
await this.
|
|
317
|
+
await this.applyConfiguredThinkingLevelForSession(session);
|
|
290
318
|
}
|
|
291
319
|
requireSession() {
|
|
292
320
|
if (!this.session) {
|
|
@@ -295,7 +323,9 @@ class AgentService {
|
|
|
295
323
|
return this.session;
|
|
296
324
|
}
|
|
297
325
|
async applyConfiguredThinkingLevel() {
|
|
298
|
-
|
|
326
|
+
await this.applyConfiguredThinkingLevelForSession(this.requireSession());
|
|
327
|
+
}
|
|
328
|
+
async applyConfiguredThinkingLevelForSession(session) {
|
|
299
329
|
if (session.supportsThinking()) {
|
|
300
330
|
const available = session.getAvailableThinkingLevels();
|
|
301
331
|
if (available.includes(this.config.thinkingLevel)) {
|
|
@@ -377,6 +407,17 @@ function loadDiscordPiBridgeConfigFromEnv(overrides = {}) {
|
|
|
377
407
|
shutdownOnSignals: overrides.shutdownOnSignals
|
|
378
408
|
});
|
|
379
409
|
}
|
|
410
|
+
function loadDiscordGatewayConfigFromEnv(overrides = {}) {
|
|
411
|
+
const base = loadDiscordPiBridgeConfigFromEnv(overrides);
|
|
412
|
+
return {
|
|
413
|
+
...base,
|
|
414
|
+
discordAllowedForumChannelIds: overrides.discordAllowedForumChannelIds ?? parseStringArrayFromEnv("DISCORD_FORUM_CHANNEL_IDS") ?? [],
|
|
415
|
+
discordAllowedUserIds: overrides.discordAllowedUserIds ?? parseStringArrayFromEnv("DISCORD_ALLOWED_USER_IDS") ?? [
|
|
416
|
+
base.discordAllowedUserId
|
|
417
|
+
],
|
|
418
|
+
sessionIdleTimeoutMs: overrides.sessionIdleTimeoutMs ?? parseOptionalIntFromEnv("DISCORD_SESSION_IDLE_TIMEOUT_MS") ?? null
|
|
419
|
+
};
|
|
420
|
+
}
|
|
380
421
|
function readRequiredValue(name, value) {
|
|
381
422
|
const trimmedValue = value.trim();
|
|
382
423
|
if (!trimmedValue) {
|
|
@@ -395,6 +436,17 @@ function readStartupMessageFromEnv() {
|
|
|
395
436
|
}
|
|
396
437
|
return trimmedValue;
|
|
397
438
|
}
|
|
439
|
+
function resolveGatewayConfig(config) {
|
|
440
|
+
const base = resolveConfig(config);
|
|
441
|
+
return {
|
|
442
|
+
...base,
|
|
443
|
+
discordAllowedForumChannelIds: config.discordAllowedForumChannelIds ?? [],
|
|
444
|
+
discordAllowedUserIds: config.discordAllowedUserIds ?? [
|
|
445
|
+
base.discordAllowedUserId
|
|
446
|
+
],
|
|
447
|
+
sessionIdleTimeoutMs: config.sessionIdleTimeoutMs ?? null
|
|
448
|
+
};
|
|
449
|
+
}
|
|
398
450
|
function parseThinkingLevel(value) {
|
|
399
451
|
if (!value) {
|
|
400
452
|
return;
|
|
@@ -416,8 +468,23 @@ function parseThinkingLevel(value) {
|
|
|
416
468
|
function identityPromptTransform(input) {
|
|
417
469
|
return input;
|
|
418
470
|
}
|
|
471
|
+
function parseStringArrayFromEnv(key) {
|
|
472
|
+
const value = process.env[key];
|
|
473
|
+
if (!value) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
return value.split(",").map((id) => id.trim()).filter(Boolean);
|
|
477
|
+
}
|
|
478
|
+
function parseOptionalIntFromEnv(key) {
|
|
479
|
+
const value = process.env[key];
|
|
480
|
+
if (!value) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const parsed = parseInt(value, 10);
|
|
484
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
485
|
+
}
|
|
419
486
|
|
|
420
|
-
// src/discord-client.ts
|
|
487
|
+
// src/discord-gateway-client.ts
|
|
421
488
|
import {
|
|
422
489
|
ChannelType,
|
|
423
490
|
Client,
|
|
@@ -427,12 +494,43 @@ import {
|
|
|
427
494
|
} from "discord.js";
|
|
428
495
|
|
|
429
496
|
// src/commands.ts
|
|
430
|
-
|
|
497
|
+
function getSessionStatusText(session, promptQueue) {
|
|
498
|
+
const model = session.model ? `${session.model.provider}/${session.model.id}` : "(no model selected)";
|
|
499
|
+
const contextUsage = session.getContextUsage();
|
|
500
|
+
const contextLine = contextUsage ? contextUsage.tokens === null || contextUsage.percent === null ? `context: ?/${contextUsage.contextWindow}` : `context: ${contextUsage.tokens}/${contextUsage.contextWindow} (${Math.round(contextUsage.percent)}%)` : "context: (unavailable)";
|
|
501
|
+
const thinkingInfo = session.supportsThinking() ? `thinking: ${session.thinkingLevel} (available: ${session.getAvailableThinkingLevels().join(", ")})` : "thinking: not supported";
|
|
502
|
+
const queueStatus = promptQueue.getSnapshot();
|
|
503
|
+
return [
|
|
504
|
+
`model: ${model}`,
|
|
505
|
+
`session-id: ${session.sessionId}`,
|
|
506
|
+
`session-file: ${session.sessionFile ?? "(none)"}`,
|
|
507
|
+
`streaming: ${session.isStreaming}`,
|
|
508
|
+
thinkingInfo,
|
|
509
|
+
contextLine,
|
|
510
|
+
`queue-pending: ${queueStatus.pending}`,
|
|
511
|
+
`queue-busy: ${queueStatus.busy}`
|
|
512
|
+
].join(`
|
|
513
|
+
`);
|
|
514
|
+
}
|
|
515
|
+
function getThinkingInfo(session) {
|
|
516
|
+
if (!session.supportsThinking()) {
|
|
517
|
+
return { current: "off", available: [], supported: false };
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
current: session.thinkingLevel,
|
|
521
|
+
available: session.getAvailableThinkingLevels(),
|
|
522
|
+
supported: true
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
async function handleCommand(input, ctx) {
|
|
526
|
+
const { agentService, promptQueue, session } = ctx;
|
|
431
527
|
const trimmed = input.trim();
|
|
432
528
|
if (!trimmed.startsWith("!")) {
|
|
433
529
|
return { handled: false };
|
|
434
530
|
}
|
|
435
531
|
if (trimmed === "!help") {
|
|
532
|
+
const extraCommands = session ? `
|
|
533
|
+
!archive - archive this thread and end the session` : "";
|
|
436
534
|
return {
|
|
437
535
|
handled: true,
|
|
438
536
|
response: [
|
|
@@ -443,35 +541,49 @@ async function handleCommand(input, agentService, promptQueue) {
|
|
|
443
541
|
"!compact - compact the persistent session",
|
|
444
542
|
"!reset-session - start a fresh persistent session",
|
|
445
543
|
"!reload - reload resources (AGENTS.md, extensions, skills, etc.)",
|
|
446
|
-
|
|
447
|
-
|
|
544
|
+
extraCommands,
|
|
545
|
+
"Any other text goes to the agent session."
|
|
546
|
+
].filter(Boolean).join(`
|
|
448
547
|
`)
|
|
449
548
|
};
|
|
450
549
|
}
|
|
550
|
+
if (trimmed === "!archive") {
|
|
551
|
+
if (!session) {
|
|
552
|
+
return {
|
|
553
|
+
handled: true,
|
|
554
|
+
response: "!archive is only available in forum threads."
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
handled: true,
|
|
559
|
+
archive: true,
|
|
560
|
+
response: "Archiving thread and shutting down session."
|
|
561
|
+
};
|
|
562
|
+
}
|
|
451
563
|
if (trimmed === "!status") {
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
564
|
+
const effectiveSession = session ?? agentService.getSession();
|
|
565
|
+
if (!effectiveSession) {
|
|
566
|
+
return {
|
|
567
|
+
handled: true,
|
|
568
|
+
response: "No active session."
|
|
569
|
+
};
|
|
570
|
+
}
|
|
456
571
|
return {
|
|
457
572
|
handled: true,
|
|
458
|
-
response:
|
|
459
|
-
`model: ${agentStatus.model}`,
|
|
460
|
-
`session-id: ${agentStatus.sessionId}`,
|
|
461
|
-
`session-file: ${agentStatus.sessionFile ?? "(none)"}`,
|
|
462
|
-
`streaming: ${agentStatus.streaming}`,
|
|
463
|
-
agentStatus.thinkingInfo,
|
|
464
|
-
contextLine,
|
|
465
|
-
`queue-pending: ${queueStatus.pending}`,
|
|
466
|
-
`queue-busy: ${queueStatus.busy}`
|
|
467
|
-
].join(`
|
|
468
|
-
`)
|
|
573
|
+
response: getSessionStatusText(effectiveSession, promptQueue)
|
|
469
574
|
};
|
|
470
575
|
}
|
|
471
576
|
if (trimmed === "!thinking" || trimmed.startsWith("!thinking ")) {
|
|
577
|
+
const effectiveSession = session ?? agentService.getSession();
|
|
578
|
+
if (!effectiveSession) {
|
|
579
|
+
return {
|
|
580
|
+
handled: true,
|
|
581
|
+
response: "No active session."
|
|
582
|
+
};
|
|
583
|
+
}
|
|
472
584
|
const parts = trimmed.split(" ");
|
|
473
585
|
if (parts.length === 1) {
|
|
474
|
-
const info =
|
|
586
|
+
const info = getThinkingInfo(effectiveSession);
|
|
475
587
|
if (!info.supported) {
|
|
476
588
|
return {
|
|
477
589
|
handled: true,
|
|
@@ -489,17 +601,38 @@ async function handleCommand(input, agentService, promptQueue) {
|
|
|
489
601
|
};
|
|
490
602
|
}
|
|
491
603
|
const requestedLevel = parts[1];
|
|
492
|
-
|
|
604
|
+
if (!effectiveSession.supportsThinking()) {
|
|
605
|
+
return {
|
|
606
|
+
handled: true,
|
|
607
|
+
response: "Current model does not support reasoning/thinking."
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
const available = effectiveSession.getAvailableThinkingLevels();
|
|
611
|
+
if (!available.includes(requestedLevel)) {
|
|
612
|
+
return {
|
|
613
|
+
handled: true,
|
|
614
|
+
response: `Invalid thinking level "${requestedLevel}" for current model. Available: ${available.join(", ")}`
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
effectiveSession.setThinkingLevel(requestedLevel);
|
|
493
618
|
return {
|
|
494
619
|
handled: true,
|
|
495
|
-
response:
|
|
620
|
+
response: `Thinking level set to "${requestedLevel}".`
|
|
496
621
|
};
|
|
497
622
|
}
|
|
498
623
|
if (trimmed === "!compact") {
|
|
624
|
+
const effectiveSession = session ?? agentService.getSession();
|
|
625
|
+
if (!effectiveSession) {
|
|
626
|
+
return {
|
|
627
|
+
handled: true,
|
|
628
|
+
response: "No active session."
|
|
629
|
+
};
|
|
630
|
+
}
|
|
499
631
|
return {
|
|
500
632
|
handled: true,
|
|
501
633
|
response: await promptQueue.enqueue(async () => {
|
|
502
|
-
|
|
634
|
+
await effectiveSession.compact();
|
|
635
|
+
return `Compaction finished for session ${effectiveSession.sessionId}.`;
|
|
503
636
|
})
|
|
504
637
|
};
|
|
505
638
|
}
|
|
@@ -512,6 +645,17 @@ async function handleCommand(input, agentService, promptQueue) {
|
|
|
512
645
|
};
|
|
513
646
|
}
|
|
514
647
|
if (trimmed === "!reset-session") {
|
|
648
|
+
if (session) {
|
|
649
|
+
return {
|
|
650
|
+
handled: true,
|
|
651
|
+
response: await promptQueue.enqueue(async () => {
|
|
652
|
+
const previousSession = session;
|
|
653
|
+
await previousSession.abort();
|
|
654
|
+
previousSession.dispose();
|
|
655
|
+
return `Session reset. Old session kept at ${previousSession.sessionFile ?? "(unknown path)"}. Use !archive to archive the thread and start fresh.`;
|
|
656
|
+
})
|
|
657
|
+
};
|
|
658
|
+
}
|
|
515
659
|
return {
|
|
516
660
|
handled: true,
|
|
517
661
|
response: await promptQueue.enqueue(async () => {
|
|
@@ -556,77 +700,184 @@ function chunkMessage(text, maxChunkSize = SAFE_MESSAGE_LIMIT) {
|
|
|
556
700
|
return chunks.map((chunk) => chunk.slice(0, DISCORD_MESSAGE_LIMIT));
|
|
557
701
|
}
|
|
558
702
|
|
|
559
|
-
// src/discord-client.ts
|
|
560
|
-
|
|
703
|
+
// src/discord-gateway-client.ts
|
|
704
|
+
function resolveScope(message) {
|
|
705
|
+
if (message.channel.type === ChannelType.DM) {
|
|
706
|
+
return "dm";
|
|
707
|
+
}
|
|
708
|
+
if (message.channel.isThread()) {
|
|
709
|
+
return `thread:${message.channel.id}`;
|
|
710
|
+
}
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
function isAuthorized(message, scope, authConfig) {
|
|
714
|
+
if (scope === "dm") {
|
|
715
|
+
return message.author.id === authConfig.discordAllowedUserId;
|
|
716
|
+
}
|
|
717
|
+
if (scope.startsWith("thread:")) {
|
|
718
|
+
const channel = message.channel;
|
|
719
|
+
if (!channel.isThread()) {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
const parentId = channel.parentId;
|
|
723
|
+
if (!parentId || !authConfig.discordAllowedForumChannelIds.includes(parentId)) {
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
return authConfig.discordAllowedUserIds.includes(message.author.id);
|
|
727
|
+
}
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
var TYPING_INTERVAL_MS = 8000;
|
|
731
|
+
function startTypingInterval(channel) {
|
|
732
|
+
channel.sendTyping();
|
|
733
|
+
return setInterval(() => {
|
|
734
|
+
channel.sendTyping();
|
|
735
|
+
}, TYPING_INTERVAL_MS);
|
|
736
|
+
}
|
|
737
|
+
function stopTypingInterval(interval) {
|
|
738
|
+
if (interval) {
|
|
739
|
+
clearInterval(interval);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
async function sendReply(message, text) {
|
|
743
|
+
const channel = message.channel;
|
|
744
|
+
if (!channel.isSendable()) {
|
|
745
|
+
console.log("[gateway] reply skipped, channel not sendable", {
|
|
746
|
+
messageId: message.id
|
|
747
|
+
});
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const chunks = chunkMessage(text);
|
|
751
|
+
console.log("[gateway] sending reply", {
|
|
752
|
+
messageId: message.id,
|
|
753
|
+
chunkCount: chunks.length,
|
|
754
|
+
textLength: text.length
|
|
755
|
+
});
|
|
756
|
+
const [firstChunk, ...remainingChunks] = chunks;
|
|
757
|
+
if (!firstChunk) {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
await message.reply(firstChunk);
|
|
761
|
+
for (const chunk of remainingChunks) {
|
|
762
|
+
await channel.send(chunk);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
async function startGatewayClient(config, agentService, sessionRegistry, authConfig) {
|
|
561
766
|
const client = new Client({
|
|
562
767
|
intents: [
|
|
563
768
|
GatewayIntentBits.DirectMessages,
|
|
769
|
+
GatewayIntentBits.Guilds,
|
|
770
|
+
GatewayIntentBits.GuildMessages,
|
|
564
771
|
GatewayIntentBits.MessageContent
|
|
565
772
|
],
|
|
566
773
|
partials: [Partials.Channel]
|
|
567
774
|
});
|
|
568
775
|
client.once(Events.ClientReady, async (readyClient) => {
|
|
569
|
-
console.log(`[
|
|
570
|
-
if (!
|
|
776
|
+
console.log(`[gateway] logged in as ${readyClient.user.tag}`);
|
|
777
|
+
if (!authConfig.startupMessage) {
|
|
571
778
|
return;
|
|
572
779
|
}
|
|
573
780
|
try {
|
|
574
|
-
const user = await readyClient.users.fetch(
|
|
781
|
+
const user = await readyClient.users.fetch(authConfig.discordAllowedUserId);
|
|
575
782
|
const dmChannel = await user.createDM();
|
|
576
|
-
await dmChannel.send(
|
|
577
|
-
console.log("[
|
|
578
|
-
userId:
|
|
783
|
+
await dmChannel.send(authConfig.startupMessage);
|
|
784
|
+
console.log("[gateway] sent startup dm", {
|
|
785
|
+
userId: authConfig.discordAllowedUserId
|
|
579
786
|
});
|
|
580
787
|
} catch (error) {
|
|
581
|
-
console.error("[
|
|
788
|
+
console.error("[gateway] failed to send startup dm", error);
|
|
582
789
|
}
|
|
583
790
|
});
|
|
584
791
|
client.on(Events.MessageCreate, async (message) => {
|
|
585
|
-
|
|
792
|
+
if (message.channel.isThread()) {
|
|
793
|
+
console.log("[gateway:debug] thread message raw", {
|
|
794
|
+
messageId: message.id,
|
|
795
|
+
authorId: message.author.id,
|
|
796
|
+
authorTag: message.author.tag,
|
|
797
|
+
channelId: message.channel.id,
|
|
798
|
+
channelType: message.channel.type,
|
|
799
|
+
parentId: message.channel.parentId,
|
|
800
|
+
parentType: message.channel.parent?.type,
|
|
801
|
+
guildId: message.guild?.id,
|
|
802
|
+
content: message.content.slice(0, 500)
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
console.log("[gateway] message received", {
|
|
586
806
|
messageId: message.id,
|
|
587
807
|
authorId: message.author.id,
|
|
588
808
|
channelType: message.channel.type,
|
|
589
|
-
content: message.content
|
|
809
|
+
content: message.content.slice(0, 200)
|
|
590
810
|
});
|
|
591
811
|
try {
|
|
592
|
-
await onMessage(message, config, agentService,
|
|
812
|
+
await onMessage(message, config, agentService, sessionRegistry, authConfig);
|
|
593
813
|
} catch (error) {
|
|
594
|
-
console.error("[
|
|
814
|
+
console.error("[gateway] message handling failed", error);
|
|
595
815
|
await sendReply(message, "The bot hit an error while handling that message.");
|
|
596
816
|
}
|
|
597
817
|
});
|
|
818
|
+
client.on(Events.ThreadDelete, async (thread) => {
|
|
819
|
+
const scope = `thread:${thread.id}`;
|
|
820
|
+
console.log("[gateway] thread deleted", { threadId: thread.id, scope });
|
|
821
|
+
await sessionRegistry.remove(scope);
|
|
822
|
+
});
|
|
598
823
|
await client.login(config.discordBotToken);
|
|
599
824
|
return client;
|
|
600
825
|
}
|
|
601
|
-
async function onMessage(message, config, agentService,
|
|
826
|
+
async function onMessage(message, config, agentService, sessionRegistry, authConfig) {
|
|
602
827
|
if (message.author.bot) {
|
|
603
|
-
console.log("[
|
|
828
|
+
console.log("[gateway] ignored bot message", { messageId: message.id });
|
|
604
829
|
return;
|
|
605
830
|
}
|
|
606
|
-
|
|
607
|
-
|
|
831
|
+
const scope = resolveScope(message);
|
|
832
|
+
if (scope === null) {
|
|
833
|
+
console.log("[gateway] unsupported channel type, ignoring", {
|
|
608
834
|
messageId: message.id,
|
|
609
|
-
|
|
835
|
+
channelType: message.channel.type
|
|
610
836
|
});
|
|
611
837
|
return;
|
|
612
838
|
}
|
|
613
|
-
if (message
|
|
614
|
-
console.log("[
|
|
839
|
+
if (!isAuthorized(message, scope, authConfig)) {
|
|
840
|
+
console.log("[gateway] unauthorized", {
|
|
615
841
|
messageId: message.id,
|
|
616
|
-
|
|
842
|
+
authorId: message.author.id,
|
|
843
|
+
scope
|
|
617
844
|
});
|
|
618
845
|
return;
|
|
619
846
|
}
|
|
620
847
|
const content = message.content.trim();
|
|
621
848
|
if (!content) {
|
|
622
|
-
console.log("[
|
|
849
|
+
console.log("[gateway] ignored empty message", { messageId: message.id });
|
|
623
850
|
return;
|
|
624
851
|
}
|
|
625
|
-
const
|
|
626
|
-
|
|
852
|
+
const { session, promptQueue } = await sessionRegistry.getOrCreate(scope);
|
|
853
|
+
let typingInterval = null;
|
|
854
|
+
if (message.channel.isSendable()) {
|
|
855
|
+
typingInterval = startTypingInterval(message.channel);
|
|
856
|
+
}
|
|
857
|
+
const commandResult = await handleCommand(content, {
|
|
858
|
+
agentService,
|
|
859
|
+
promptQueue,
|
|
860
|
+
session
|
|
861
|
+
});
|
|
627
862
|
if (commandResult.handled) {
|
|
628
863
|
stopTypingInterval(typingInterval);
|
|
629
|
-
|
|
864
|
+
if (commandResult.archive && scope.startsWith("thread:")) {
|
|
865
|
+
console.log("[gateway] archiving thread", { scope });
|
|
866
|
+
const archiveChannel = message.channel;
|
|
867
|
+
if (archiveChannel.isSendable()) {
|
|
868
|
+
await archiveChannel.send(commandResult.response ?? "Archiving...");
|
|
869
|
+
}
|
|
870
|
+
try {
|
|
871
|
+
if (archiveChannel.isThread()) {
|
|
872
|
+
await archiveChannel.setArchived(true);
|
|
873
|
+
}
|
|
874
|
+
} catch (error) {
|
|
875
|
+
console.error("[gateway] failed to archive thread", error);
|
|
876
|
+
}
|
|
877
|
+
await sessionRegistry.remove(scope);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
console.log("[gateway] command handled", {
|
|
630
881
|
messageId: message.id,
|
|
631
882
|
command: content,
|
|
632
883
|
hasResponse: Boolean(commandResult.response)
|
|
@@ -638,11 +889,12 @@ async function onMessage(message, config, agentService, promptQueue) {
|
|
|
638
889
|
}
|
|
639
890
|
if (!message.channel.isSendable()) {
|
|
640
891
|
stopTypingInterval(typingInterval);
|
|
641
|
-
console.log("[
|
|
892
|
+
console.log("[gateway] channel not sendable", { messageId: message.id });
|
|
642
893
|
return;
|
|
643
894
|
}
|
|
644
895
|
const queuePosition = promptQueue.getSnapshot().pending;
|
|
645
896
|
console.log("[queue] enqueue request", {
|
|
897
|
+
scope,
|
|
646
898
|
messageId: message.id,
|
|
647
899
|
queuePosition
|
|
648
900
|
});
|
|
@@ -650,49 +902,24 @@ async function onMessage(message, config, agentService, promptQueue) {
|
|
|
650
902
|
await sendReply(message, `Queued. ${queuePosition} request(s) ahead of this one.`);
|
|
651
903
|
}
|
|
652
904
|
const response = await promptQueue.enqueue(async () => {
|
|
653
|
-
console.log(`[queue] processing message ${message.id}`);
|
|
654
|
-
|
|
905
|
+
console.log(`[queue] processing message ${message.id} in scope ${scope}`);
|
|
906
|
+
const transformedPrompt = await config.promptTransform(content);
|
|
907
|
+
return collectReply(session, transformedPrompt, {
|
|
908
|
+
logPrefix: `[agent:${session.sessionId}]`
|
|
909
|
+
});
|
|
655
910
|
});
|
|
656
911
|
stopTypingInterval(typingInterval);
|
|
657
|
-
console.log("[
|
|
912
|
+
console.log("[gateway] response ready", {
|
|
913
|
+
scope,
|
|
658
914
|
messageId: message.id,
|
|
659
915
|
responseLength: response.length,
|
|
660
916
|
preview: response.slice(0, 200)
|
|
661
917
|
});
|
|
662
918
|
await sendReply(message, response);
|
|
663
919
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
messageId: message.id
|
|
668
|
-
});
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
const chunks = chunkMessage(text);
|
|
672
|
-
console.log("[discord] sending reply", {
|
|
673
|
-
messageId: message.id,
|
|
674
|
-
chunkCount: chunks.length,
|
|
675
|
-
textLength: text.length
|
|
676
|
-
});
|
|
677
|
-
const [firstChunk, ...remainingChunks] = chunks;
|
|
678
|
-
if (!firstChunk) {
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
await message.reply(firstChunk);
|
|
682
|
-
for (const chunk of remainingChunks) {
|
|
683
|
-
await message.channel.send(chunk);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
var TYPING_INTERVAL_MS = 8000;
|
|
687
|
-
function startTypingInterval(channel) {
|
|
688
|
-
channel.sendTyping();
|
|
689
|
-
return setInterval(() => {
|
|
690
|
-
channel.sendTyping();
|
|
691
|
-
}, TYPING_INTERVAL_MS);
|
|
692
|
-
}
|
|
693
|
-
function stopTypingInterval(interval) {
|
|
694
|
-
clearInterval(interval);
|
|
695
|
-
}
|
|
920
|
+
|
|
921
|
+
// src/session-registry.ts
|
|
922
|
+
import path3 from "node:path";
|
|
696
923
|
|
|
697
924
|
// src/prompt-queue.ts
|
|
698
925
|
class PromptQueue {
|
|
@@ -732,6 +959,70 @@ class PromptQueue {
|
|
|
732
959
|
}
|
|
733
960
|
}
|
|
734
961
|
|
|
962
|
+
// src/session-registry.ts
|
|
963
|
+
function sessionDirForScope(agentDir, scope) {
|
|
964
|
+
if (scope === "dm") {
|
|
965
|
+
return path3.join(agentDir, "sessions");
|
|
966
|
+
}
|
|
967
|
+
if (scope.startsWith("thread:")) {
|
|
968
|
+
const threadId = scope.slice(7);
|
|
969
|
+
return path3.join(agentDir, "sessions", `thread-${threadId}`);
|
|
970
|
+
}
|
|
971
|
+
throw new Error(`Unknown session scope: ${scope}`);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
class SessionRegistry {
|
|
975
|
+
scopes = new Map;
|
|
976
|
+
agentService;
|
|
977
|
+
constructor(agentService) {
|
|
978
|
+
this.agentService = agentService;
|
|
979
|
+
}
|
|
980
|
+
async getOrCreate(scope) {
|
|
981
|
+
const existing = this.scopes.get(scope);
|
|
982
|
+
if (existing) {
|
|
983
|
+
return existing;
|
|
984
|
+
}
|
|
985
|
+
const sessionDir = sessionDirForScope(this.agentService.getAgentDir(), scope);
|
|
986
|
+
const session = await this.agentService.createSession(sessionDir);
|
|
987
|
+
const promptQueue = new PromptQueue;
|
|
988
|
+
const entry = {
|
|
989
|
+
session,
|
|
990
|
+
promptQueue,
|
|
991
|
+
createdAt: new Date
|
|
992
|
+
};
|
|
993
|
+
this.scopes.set(scope, entry);
|
|
994
|
+
console.log("[session-registry] scope registered", {
|
|
995
|
+
scope,
|
|
996
|
+
sessionDir,
|
|
997
|
+
sessionId: session.sessionId
|
|
998
|
+
});
|
|
999
|
+
return entry;
|
|
1000
|
+
}
|
|
1001
|
+
async remove(scope) {
|
|
1002
|
+
const entry = this.scopes.get(scope);
|
|
1003
|
+
if (!entry) {
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
console.log("[session-registry] removing scope", { scope });
|
|
1007
|
+
await entry.session.abort();
|
|
1008
|
+
entry.session.dispose();
|
|
1009
|
+
this.scopes.delete(scope);
|
|
1010
|
+
}
|
|
1011
|
+
get(scope) {
|
|
1012
|
+
return this.scopes.get(scope);
|
|
1013
|
+
}
|
|
1014
|
+
getScopes() {
|
|
1015
|
+
return Array.from(this.scopes.keys());
|
|
1016
|
+
}
|
|
1017
|
+
async shutdownAll() {
|
|
1018
|
+
console.log("[session-registry] shutting down all scopes", this.scopes.size);
|
|
1019
|
+
const scopes = Array.from(this.scopes.keys());
|
|
1020
|
+
for (const scope of scopes) {
|
|
1021
|
+
await this.remove(scope);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
735
1026
|
// src/prompt-context.ts
|
|
736
1027
|
function buildTimeContextPrompt(userMessage, options = {}) {
|
|
737
1028
|
const timeZone = options.timeZone || "UTC";
|
|
@@ -753,15 +1044,21 @@ function buildTimeContextPrompt(userMessage, options = {}) {
|
|
|
753
1044
|
}
|
|
754
1045
|
|
|
755
1046
|
// src/index.ts
|
|
756
|
-
async function
|
|
757
|
-
const resolvedConfig =
|
|
1047
|
+
async function startDiscordGateway(config) {
|
|
1048
|
+
const resolvedConfig = resolveGatewayConfig(config);
|
|
758
1049
|
const agentService = new AgentService(resolvedConfig);
|
|
759
|
-
|
|
760
|
-
console.log("[boot] initializing persistent agent session");
|
|
1050
|
+
console.log("[gateway] initializing agent service");
|
|
761
1051
|
await agentService.initialize();
|
|
762
|
-
console.log("[
|
|
763
|
-
const
|
|
764
|
-
|
|
1052
|
+
console.log("[gateway] agent ready", agentService.getStatus());
|
|
1053
|
+
const authConfig = {
|
|
1054
|
+
discordAllowedUserId: resolvedConfig.discordAllowedUserId,
|
|
1055
|
+
discordAllowedForumChannelIds: resolvedConfig.discordAllowedForumChannelIds,
|
|
1056
|
+
discordAllowedUserIds: resolvedConfig.discordAllowedUserIds,
|
|
1057
|
+
startupMessage: resolvedConfig.startupMessage
|
|
1058
|
+
};
|
|
1059
|
+
const sessionRegistry = new SessionRegistry(agentService);
|
|
1060
|
+
const client = await startGatewayClient(resolvedConfig, agentService, sessionRegistry, authConfig);
|
|
1061
|
+
const stop = createGatewayStopHandler(client, agentService, sessionRegistry, resolvedConfig);
|
|
765
1062
|
if (resolvedConfig.shutdownOnSignals) {
|
|
766
1063
|
registerSignalHandlers(stop);
|
|
767
1064
|
}
|
|
@@ -773,18 +1070,22 @@ async function startDiscordPiBridge(config) {
|
|
|
773
1070
|
}
|
|
774
1071
|
};
|
|
775
1072
|
}
|
|
776
|
-
function
|
|
1073
|
+
async function startDiscordPiBridge(config) {
|
|
1074
|
+
return startDiscordGateway(config);
|
|
1075
|
+
}
|
|
1076
|
+
function createGatewayStopHandler(client, agentService, sessionRegistry, config) {
|
|
777
1077
|
let stopped = false;
|
|
778
1078
|
return async () => {
|
|
779
1079
|
if (stopped) {
|
|
780
1080
|
return;
|
|
781
1081
|
}
|
|
782
1082
|
stopped = true;
|
|
783
|
-
console.log("[shutdown] stopping discord
|
|
1083
|
+
console.log("[shutdown] stopping discord gateway", {
|
|
784
1084
|
cwd: config.cwd,
|
|
785
1085
|
agentDir: config.agentDir
|
|
786
1086
|
});
|
|
787
1087
|
client.destroy();
|
|
1088
|
+
await sessionRegistry.shutdownAll();
|
|
788
1089
|
await agentService.shutdown();
|
|
789
1090
|
};
|
|
790
1091
|
}
|
|
@@ -805,7 +1106,9 @@ function registerSignalHandlers(stop) {
|
|
|
805
1106
|
}
|
|
806
1107
|
export {
|
|
807
1108
|
startDiscordPiBridge,
|
|
1109
|
+
startDiscordGateway,
|
|
808
1110
|
resolveConfig,
|
|
809
1111
|
loadDiscordPiBridgeConfigFromEnv,
|
|
1112
|
+
loadDiscordGatewayConfigFromEnv,
|
|
810
1113
|
buildTimeContextPrompt
|
|
811
1114
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { AgentService } from "./agent-service";
|
|
3
|
+
import { PromptQueue } from "./prompt-queue";
|
|
4
|
+
export type SessionScope = string;
|
|
5
|
+
export type ScopeEntry = {
|
|
6
|
+
session: AgentSession;
|
|
7
|
+
promptQueue: PromptQueue;
|
|
8
|
+
createdAt: Date;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Derive a deterministic session directory from a scope key.
|
|
12
|
+
*
|
|
13
|
+
* "dm" → <agentDir>/sessions
|
|
14
|
+
* "thread:<id>" → <agentDir>/sessions/thread-<id>
|
|
15
|
+
*/
|
|
16
|
+
export declare function sessionDirForScope(agentDir: string, scope: SessionScope): string;
|
|
17
|
+
export declare class SessionRegistry {
|
|
18
|
+
private readonly scopes;
|
|
19
|
+
private readonly agentService;
|
|
20
|
+
constructor(agentService: AgentService);
|
|
21
|
+
getOrCreate(scope: SessionScope): Promise<ScopeEntry>;
|
|
22
|
+
remove(scope: SessionScope): Promise<void>;
|
|
23
|
+
get(scope: SessionScope): ScopeEntry | undefined;
|
|
24
|
+
getScopes(): SessionScope[];
|
|
25
|
+
shutdownAll(): Promise<void>;
|
|
26
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -43,3 +43,21 @@ export type DiscordPiBridge = {
|
|
|
43
43
|
stop: () => Promise<void>;
|
|
44
44
|
getStatus: () => AgentStatus;
|
|
45
45
|
};
|
|
46
|
+
export type DiscordGatewayConfig = DiscordPiBridgeConfig & {
|
|
47
|
+
/** Which forum channels the bot responds in (absent = forum disabled). */
|
|
48
|
+
discordAllowedForumChannelIds?: string[];
|
|
49
|
+
/** Which users can interact in forum threads (defaults to [discordAllowedUserId]). */
|
|
50
|
+
discordAllowedUserIds?: string[];
|
|
51
|
+
/** Auto-shutdown idle thread sessions after this many ms. */
|
|
52
|
+
sessionIdleTimeoutMs?: number;
|
|
53
|
+
};
|
|
54
|
+
export type ResolvedDiscordGatewayConfig = ResolvedDiscordPiBridgeConfig & {
|
|
55
|
+
discordAllowedForumChannelIds: string[];
|
|
56
|
+
discordAllowedUserIds: string[];
|
|
57
|
+
sessionIdleTimeoutMs: number | null;
|
|
58
|
+
};
|
|
59
|
+
export type DiscordGateway = {
|
|
60
|
+
client: Client;
|
|
61
|
+
stop: () => Promise<void>;
|
|
62
|
+
getStatus: () => AgentStatus;
|
|
63
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friendlyrobot/discord-pi-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Reusable Discord gateway bridge for persistent pi agent sessions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -27,10 +27,13 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"test:watch": "vitest",
|
|
29
29
|
"test": "vitest run",
|
|
30
|
-
"update-deps": "
|
|
30
|
+
"update-deps": "npx npm-check-updates",
|
|
31
31
|
"format": "prettier --write .",
|
|
32
|
-
"build": "rm -rf dist
|
|
33
|
-
"
|
|
32
|
+
"build:01-clean": "rm -rf dist",
|
|
33
|
+
"build:02-tsgo": "tsgo -p tsconfig.json --emitDeclarationOnly --declaration --declarationMap false",
|
|
34
|
+
"build:03-build": "bun build ./src/index.ts --outdir ./dist --target node --format esm --packages external",
|
|
35
|
+
"build": "bun run --sequential 'build:*'",
|
|
36
|
+
"typecheck": "tsgo --noEmit -p tsconfig.json"
|
|
34
37
|
},
|
|
35
38
|
"dependencies": {
|
|
36
39
|
"@mariozechner/pi-ai": "^0.70.2",
|
|
@@ -42,8 +45,8 @@
|
|
|
42
45
|
},
|
|
43
46
|
"devDependencies": {
|
|
44
47
|
"@types/node": "^25.6.0",
|
|
48
|
+
"@typescript/native-preview": "^7.0.0-dev.20260424.2",
|
|
45
49
|
"@vitest/ui": "^4.1.5",
|
|
46
|
-
"typescript": "^6.0.3",
|
|
47
50
|
"vitest": "^4.1.5"
|
|
48
51
|
}
|
|
49
52
|
}
|