@moxxy/plugin-channel-discord 0.27.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/LICENSE +21 -0
- package/dist/allow-list.d.ts +20 -0
- package/dist/allow-list.d.ts.map +1 -0
- package/dist/allow-list.js +12 -0
- package/dist/allow-list.js.map +1 -0
- package/dist/approval.d.ts +29 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +65 -0
- package/dist/approval.js.map +1 -0
- package/dist/channel/allow-list-store.d.ts +22 -0
- package/dist/channel/allow-list-store.d.ts.map +1 -0
- package/dist/channel/allow-list-store.js +45 -0
- package/dist/channel/allow-list-store.js.map +1 -0
- package/dist/channel/approval-prompt.d.ts +16 -0
- package/dist/channel/approval-prompt.d.ts.map +1 -0
- package/dist/channel/approval-prompt.js +27 -0
- package/dist/channel/approval-prompt.js.map +1 -0
- package/dist/channel/components.d.ts +27 -0
- package/dist/channel/components.d.ts.map +1 -0
- package/dist/channel/components.js +34 -0
- package/dist/channel/components.js.map +1 -0
- package/dist/channel/discord-like.d.ts +31 -0
- package/dist/channel/discord-like.d.ts.map +1 -0
- package/dist/channel/discord-like.js +9 -0
- package/dist/channel/discord-like.js.map +1 -0
- package/dist/channel/interaction-handler.d.ts +67 -0
- package/dist/channel/interaction-handler.d.ts.map +1 -0
- package/dist/channel/interaction-handler.js +155 -0
- package/dist/channel/interaction-handler.js.map +1 -0
- package/dist/channel/message-handler.d.ts +60 -0
- package/dist/channel/message-handler.d.ts.map +1 -0
- package/dist/channel/message-handler.js +135 -0
- package/dist/channel/message-handler.js.map +1 -0
- package/dist/channel/pairing-handler.d.ts +58 -0
- package/dist/channel/pairing-handler.d.ts.map +1 -0
- package/dist/channel/pairing-handler.js +109 -0
- package/dist/channel/pairing-handler.js.map +1 -0
- package/dist/channel/permission-prompt.d.ts +19 -0
- package/dist/channel/permission-prompt.d.ts.map +1 -0
- package/dist/channel/permission-prompt.js +38 -0
- package/dist/channel/permission-prompt.js.map +1 -0
- package/dist/channel/slash-handler.d.ts +45 -0
- package/dist/channel/slash-handler.d.ts.map +1 -0
- package/dist/channel/slash-handler.js +107 -0
- package/dist/channel/slash-handler.js.map +1 -0
- package/dist/channel/turn-runner.d.ts +49 -0
- package/dist/channel/turn-runner.d.ts.map +1 -0
- package/dist/channel/turn-runner.js +116 -0
- package/dist/channel/turn-runner.js.map +1 -0
- package/dist/channel/typing-indicator.d.ts +12 -0
- package/dist/channel/typing-indicator.d.ts.map +1 -0
- package/dist/channel/typing-indicator.js +28 -0
- package/dist/channel/typing-indicator.js.map +1 -0
- package/dist/channel/voice-handler.d.ts +41 -0
- package/dist/channel/voice-handler.d.ts.map +1 -0
- package/dist/channel/voice-handler.js +105 -0
- package/dist/channel/voice-handler.js.map +1 -0
- package/dist/channel.d.ts +107 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +464 -0
- package/dist/channel.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +179 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +64 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +85 -0
- package/dist/keys.js.map +1 -0
- package/dist/pair-flow.d.ts +16 -0
- package/dist/pair-flow.d.ts.map +1 -0
- package/dist/pair-flow.js +109 -0
- package/dist/pair-flow.js.map +1 -0
- package/dist/pairing.d.ts +83 -0
- package/dist/pairing.d.ts.map +1 -0
- package/dist/pairing.js +98 -0
- package/dist/pairing.js.map +1 -0
- package/dist/permission.d.ts +30 -0
- package/dist/permission.d.ts.map +1 -0
- package/dist/permission.js +54 -0
- package/dist/permission.js.map +1 -0
- package/dist/render.d.ts +34 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +204 -0
- package/dist/render.js.map +1 -0
- package/dist/schema.d.ts +123 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +70 -0
- package/dist/schema.js.map +1 -0
- package/dist/setup-wizard.d.ts +13 -0
- package/dist/setup-wizard.d.ts.map +1 -0
- package/dist/setup-wizard.js +133 -0
- package/dist/setup-wizard.js.map +1 -0
- package/package.json +89 -0
- package/src/allow-list.test.ts +64 -0
- package/src/allow-list.ts +30 -0
- package/src/approval.ts +76 -0
- package/src/channel/allow-list-store.ts +52 -0
- package/src/channel/approval-prompt.ts +43 -0
- package/src/channel/components.ts +49 -0
- package/src/channel/discord-like.ts +34 -0
- package/src/channel/interaction-handler.test.ts +212 -0
- package/src/channel/interaction-handler.ts +237 -0
- package/src/channel/message-handler.test.ts +295 -0
- package/src/channel/message-handler.ts +195 -0
- package/src/channel/pairing-handler.ts +147 -0
- package/src/channel/permission-prompt.ts +54 -0
- package/src/channel/slash-handler.ts +136 -0
- package/src/channel/turn-runner.test.ts +177 -0
- package/src/channel/turn-runner.ts +139 -0
- package/src/channel/typing-indicator.ts +31 -0
- package/src/channel/voice-handler.test.ts +177 -0
- package/src/channel/voice-handler.ts +150 -0
- package/src/channel/voice-reply.test.ts +142 -0
- package/src/channel.ts +582 -0
- package/src/index.ts +247 -0
- package/src/keys.ts +100 -0
- package/src/pair-flow.ts +122 -0
- package/src/pairing.test.ts +99 -0
- package/src/pairing.ts +178 -0
- package/src/permission.ts +70 -0
- package/src/render.test.ts +82 -0
- package/src/render.ts +228 -0
- package/src/schema.ts +94 -0
- package/src/setup-wizard.ts +162 -0
- package/src/subcommands.test.ts +182 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { runSlash } from './slash-handler.js';
|
|
2
|
+
/**
|
|
3
|
+
* Interaction router (button clicks + slash commands). The authorization gate
|
|
4
|
+
* runs FIRST and for EVERY path (AGENTS.md A46 — Telegram's callback handlers
|
|
5
|
+
* once skipped it): button clicks resolve permission prompts and approvals,
|
|
6
|
+
* slash commands reach the session, and a prompt message can be seen (and its
|
|
7
|
+
* buttons clicked) by any member of a guild channel — so an unpaired user's
|
|
8
|
+
* interactions must be refused exactly like their messages.
|
|
9
|
+
*/
|
|
10
|
+
export async function handleInteraction(interaction, state, deps, cb) {
|
|
11
|
+
if (!interaction.isButton() && !interaction.isChatInputCommand())
|
|
12
|
+
return;
|
|
13
|
+
if (!deps.pairing.isAuthorized(interaction.user.id)) {
|
|
14
|
+
await safeReply(interaction, 'This bot is paired with a different Discord account.', deps.logger);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (interaction.isButton()) {
|
|
18
|
+
await handleButton(interaction, deps, cb);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
await handleSlashCommand(interaction, state, deps, cb);
|
|
22
|
+
}
|
|
23
|
+
async function handleButton(interaction, deps, cb) {
|
|
24
|
+
const data = interaction.customId ?? '';
|
|
25
|
+
if (data.startsWith('perm:')) {
|
|
26
|
+
const parts = data.split(':');
|
|
27
|
+
if (parts.length !== 3 || !parts[1] || !parts[2])
|
|
28
|
+
return;
|
|
29
|
+
const handled = deps.permissionResolver.resolvePending(parts[1], mapChoice(parts[2]));
|
|
30
|
+
const acked = await clearButtons(interaction, deps.logger);
|
|
31
|
+
if (!handled)
|
|
32
|
+
await respond(interaction, 'no pending permission', acked, deps.logger);
|
|
33
|
+
// A click must always be acked or Discord shows "interaction failed".
|
|
34
|
+
else if (!acked)
|
|
35
|
+
await respond(interaction, `→ ${parts[2]}`, acked, deps.logger);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (data.startsWith('appr:')) {
|
|
39
|
+
const idx = data.indexOf(':', 5);
|
|
40
|
+
if (idx < 0)
|
|
41
|
+
return;
|
|
42
|
+
const approvalId = data.slice(5, idx);
|
|
43
|
+
const optionId = data.slice(idx + 1);
|
|
44
|
+
const pending = deps.approvalResolver.getPending(approvalId);
|
|
45
|
+
if (!pending) {
|
|
46
|
+
await safeReply(interaction, 'no pending approval', deps.logger);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const option = pending.request.options.find((o) => o.id === optionId);
|
|
50
|
+
if (!option) {
|
|
51
|
+
await safeReply(interaction, 'unknown option', deps.logger);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const acked = await clearButtons(interaction, deps.logger);
|
|
55
|
+
if (option.requestsText) {
|
|
56
|
+
// Don't resolve yet — capture the user's next message as the follow-up
|
|
57
|
+
// text. Mirrors the TUI dialog's text-entry sub-mode.
|
|
58
|
+
cb.setAwaitingApprovalText({ approvalId, optionId });
|
|
59
|
+
const prompt = option.textPrompt ??
|
|
60
|
+
`Send your message — the next text you type becomes the ${optionId} input.`;
|
|
61
|
+
await respond(interaction, `✏️ ${prompt}`, acked, deps.logger);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
deps.approvalResolver.resolvePending(approvalId, optionId);
|
|
65
|
+
await respond(interaction, `→ ${option.label}`, acked, deps.logger);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function handleSlashCommand(interaction, state, deps, cb) {
|
|
69
|
+
const name = interaction.commandName ?? '';
|
|
70
|
+
const channelId = interaction.channelId ?? null;
|
|
71
|
+
const inGuild = interaction.guildId != null;
|
|
72
|
+
// Allow-list management works from the guild channel itself; /allow is the
|
|
73
|
+
// one command that must work BEFORE the channel is allow-listed.
|
|
74
|
+
if (name === 'allow' && inGuild && channelId) {
|
|
75
|
+
await deps.allowList.add(channelId);
|
|
76
|
+
await safeReply(interaction, '✓ this channel can now drive moxxy. Use /deny here to revoke.', deps.logger);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (name === 'deny' && inGuild && channelId) {
|
|
80
|
+
const removed = await deps.allowList.remove(channelId);
|
|
81
|
+
await safeReply(interaction, removed ? '✓ channel removed from the allow-list.' : 'this channel was not allow-listed.', deps.logger);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Everything else obeys the same channel gate as plain messages: a guild
|
|
85
|
+
// channel must be allow-listed (DMs with the paired user always pass).
|
|
86
|
+
if (inGuild && channelId && !deps.allowList.has(channelId)) {
|
|
87
|
+
await safeReply(interaction, 'This channel is not allow-listed. Send /allow here to enable it.', deps.logger);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (name === 'cancel') {
|
|
91
|
+
if (state.turnController && !state.turnController.signal.aborted) {
|
|
92
|
+
state.turnController.abort('user cancel');
|
|
93
|
+
await safeReply(interaction, 'cancelling current turn…', deps.logger);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
await safeReply(interaction, 'nothing to cancel.', deps.logger);
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!state.session) {
|
|
101
|
+
await safeReply(interaction, 'Session is not ready yet.', deps.logger);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const reply = await runSlash(name, '', state.session, {
|
|
105
|
+
toggleYolo: cb.toggleYolo,
|
|
106
|
+
voice: cb.voice,
|
|
107
|
+
performSessionAction: cb.performSessionAction,
|
|
108
|
+
});
|
|
109
|
+
await safeReply(interaction, reply.length > 1_900 ? reply.slice(0, 1_899) + '…' : reply, deps.logger);
|
|
110
|
+
}
|
|
111
|
+
function mapChoice(choice) {
|
|
112
|
+
if (choice === 'allow')
|
|
113
|
+
return { mode: 'allow' };
|
|
114
|
+
if (choice === 'allow_session')
|
|
115
|
+
return { mode: 'allow_session' };
|
|
116
|
+
return { mode: 'deny', reason: 'denied by user' };
|
|
117
|
+
}
|
|
118
|
+
/** Clear the prompt's button row. Returns true when the update ACKED the
|
|
119
|
+
* interaction (later messages must then use followUp, not reply). */
|
|
120
|
+
async function clearButtons(interaction, logger) {
|
|
121
|
+
if (!interaction.update)
|
|
122
|
+
return false;
|
|
123
|
+
try {
|
|
124
|
+
await interaction.update({ components: [] });
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
logger?.warn('discord clear-buttons failed', { err: String(err) });
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Send a user-visible ack: followUp when the interaction was already acked
|
|
133
|
+
* (a successful `update`), plain ephemeral reply otherwise. */
|
|
134
|
+
async function respond(interaction, content, acked, logger) {
|
|
135
|
+
try {
|
|
136
|
+
if (acked && interaction.followUp) {
|
|
137
|
+
await interaction.followUp({ content, ephemeral: true });
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
await interaction.reply({ content, ephemeral: true });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
logger?.warn('discord interaction respond failed', { err: String(err) });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function safeReply(interaction, content, logger) {
|
|
148
|
+
try {
|
|
149
|
+
await interaction.reply({ content, ephemeral: true });
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
logger?.warn('discord interaction reply failed', { err: String(err) });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=interaction-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interaction-handler.js","sourceRoot":"","sources":["../../src/channel/interaction-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ChannelHandle, ClientSession as Session } from '@moxxy/sdk';
|
|
2
|
+
import type { InboundMessage } from '../schema.js';
|
|
3
|
+
import type { DiscordApprovalResolver } from '../approval.js';
|
|
4
|
+
import type { DiscordPermissionResolver } from '../permission.js';
|
|
5
|
+
import type { AllowListStore } from './allow-list-store.js';
|
|
6
|
+
import type { ChannelLogger, SendableChannelLike } from './discord-like.js';
|
|
7
|
+
import type { PairingHandler } from './pairing-handler.js';
|
|
8
|
+
export interface AwaitingApprovalText {
|
|
9
|
+
approvalId: string;
|
|
10
|
+
optionId: string;
|
|
11
|
+
}
|
|
12
|
+
/** A validated inbound message plus the transport bits needed to answer it. */
|
|
13
|
+
export interface InboundContext {
|
|
14
|
+
readonly msg: InboundMessage;
|
|
15
|
+
readonly channel: SendableChannelLike;
|
|
16
|
+
reply(text: string): Promise<unknown>;
|
|
17
|
+
}
|
|
18
|
+
export interface MessageHandlerState {
|
|
19
|
+
readonly session: Session | null;
|
|
20
|
+
readonly busy: boolean;
|
|
21
|
+
readonly turnController: AbortController | null;
|
|
22
|
+
readonly awaitingApprovalText: AwaitingApprovalText | null;
|
|
23
|
+
readonly handle: ChannelHandle | null;
|
|
24
|
+
/** The bot's own user id (ignore self-authored messages defensively). */
|
|
25
|
+
readonly botUserId: string | null;
|
|
26
|
+
}
|
|
27
|
+
export interface MessageHandlerDeps {
|
|
28
|
+
readonly pairing: PairingHandler;
|
|
29
|
+
readonly allowList: AllowListStore;
|
|
30
|
+
readonly approvalResolver: DiscordApprovalResolver;
|
|
31
|
+
readonly permissionResolver: DiscordPermissionResolver;
|
|
32
|
+
readonly logger?: ChannelLogger;
|
|
33
|
+
}
|
|
34
|
+
export interface MessageHandlerCallbacks {
|
|
35
|
+
readonly setAwaitingApprovalText: (state: AwaitingApprovalText | null) => void;
|
|
36
|
+
readonly toggleYolo: () => boolean;
|
|
37
|
+
readonly setYolo: (value: boolean) => void;
|
|
38
|
+
/** Handle `/voice [on|off|status]` — persist + apply, return the reply text. */
|
|
39
|
+
readonly voice: (arg: string) => Promise<string>;
|
|
40
|
+
readonly runUserTurn: (ctx: InboundContext, text: string) => Promise<void>;
|
|
41
|
+
/** Handle audio attachments (voice messages). Returns true when it consumed
|
|
42
|
+
* the message (so the text path is skipped). */
|
|
43
|
+
readonly runVoiceMessage: (ctx: InboundContext) => Promise<boolean>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Top-level dispatch for inbound Discord messages. Every session-reaching path
|
|
47
|
+
* is behind the pairing + allow-list gate (AGENTS.md A46: gate EVERY path):
|
|
48
|
+
* validation happened upstream (the caller only builds an {@link
|
|
49
|
+
* InboundContext} from a zod-validated extraction), authorization happens
|
|
50
|
+
* here, and only then do approval-capture / cancel / slash / turn paths run.
|
|
51
|
+
*/
|
|
52
|
+
export declare function handleInboundMessage(ctx: InboundContext, state: MessageHandlerState, deps: MessageHandlerDeps, cb: MessageHandlerCallbacks): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Channel-side handler for `session-action` outputs from registered commands.
|
|
55
|
+
* Mirrors the Telegram channel's semantics; returns the reply text. Shared by
|
|
56
|
+
* the plain-text slash path (above) and the interaction (slash-command) path
|
|
57
|
+
* in the channel.
|
|
58
|
+
*/
|
|
59
|
+
export declare function performSessionAction(action: 'new' | 'clear' | 'exit', notice: string | undefined, state: Pick<MessageHandlerState, 'session' | 'turnController' | 'handle'>, deps: Pick<MessageHandlerDeps, 'approvalResolver' | 'permissionResolver'>, cb: Pick<MessageHandlerCallbacks, 'setAwaitingApprovalText' | 'setYolo'>): Promise<string>;
|
|
60
|
+
//# sourceMappingURL=message-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-handler.d.ts","sourceRoot":"","sources":["../../src/channel/message-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { gateInbound } from '../allow-list.js';
|
|
2
|
+
import { runSlash } from './slash-handler.js';
|
|
3
|
+
/**
|
|
4
|
+
* Top-level dispatch for inbound Discord messages. Every session-reaching path
|
|
5
|
+
* is behind the pairing + allow-list gate (AGENTS.md A46: gate EVERY path):
|
|
6
|
+
* validation happened upstream (the caller only builds an {@link
|
|
7
|
+
* InboundContext} from a zod-validated extraction), authorization happens
|
|
8
|
+
* here, and only then do approval-capture / cancel / slash / turn paths run.
|
|
9
|
+
*/
|
|
10
|
+
export async function handleInboundMessage(ctx, state, deps, cb) {
|
|
11
|
+
const { msg } = ctx;
|
|
12
|
+
// Never react to bots (including ourselves) — bot-to-bot loops.
|
|
13
|
+
if (msg.authorIsBot || (state.botUserId != null && msg.authorId === state.botUserId))
|
|
14
|
+
return;
|
|
15
|
+
const text = msg.content.trim();
|
|
16
|
+
const verdict = gateInbound(msg, deps.pairing.authorizedUserId(), deps.allowList.snapshot());
|
|
17
|
+
if (!verdict.ok) {
|
|
18
|
+
if (verdict.reason === 'not-paired' || verdict.reason === 'foreign-user') {
|
|
19
|
+
// DMs get pairing guidance (a fresh code when a window is armed, a
|
|
20
|
+
// rejection for a foreign account); guild messages are dropped silently
|
|
21
|
+
// so an unpaired bot can't be made to spam a server.
|
|
22
|
+
if (msg.guildId == null) {
|
|
23
|
+
const reply = deps.pairing.handleUnpairedDm(msg.authorId);
|
|
24
|
+
if (reply)
|
|
25
|
+
await ctx.reply(reply);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// Paired user in a guild channel that isn't allow-listed: /allow opts the
|
|
30
|
+
// channel in (this is the ONE command that must work pre-allow-list —
|
|
31
|
+
// it's how the list gets populated); anything else gets a hint.
|
|
32
|
+
if (text === '/allow') {
|
|
33
|
+
await deps.allowList.add(msg.channelId);
|
|
34
|
+
await ctx.reply('✓ this channel can now drive moxxy. Use /deny here to revoke.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
await ctx.reply('This channel is not allow-listed. Send /allow here to enable it.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!text && msg.attachments.length === 0)
|
|
41
|
+
return;
|
|
42
|
+
// Capture awaiting-text BEFORE the busy guard so the user can answer an
|
|
43
|
+
// approval text prompt even while the strategy is technically mid-turn
|
|
44
|
+
// (it's pending on us).
|
|
45
|
+
if (state.awaitingApprovalText && text) {
|
|
46
|
+
const { approvalId, optionId } = state.awaitingApprovalText;
|
|
47
|
+
cb.setAwaitingApprovalText(null);
|
|
48
|
+
const handled = deps.approvalResolver.resolvePendingWithText(approvalId, optionId, text);
|
|
49
|
+
await ctx.reply(handled ? `✓ submitted (${optionId})` : 'that approval is no longer pending');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// /cancel works even while busy; everything else routes below.
|
|
53
|
+
if (text === '/cancel') {
|
|
54
|
+
if (state.turnController && !state.turnController.signal.aborted) {
|
|
55
|
+
state.turnController.abort('user cancel');
|
|
56
|
+
await ctx.reply('cancelling current turn…');
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
await ctx.reply('nothing to cancel.');
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (text === '/deny' && msg.guildId != null) {
|
|
64
|
+
const removed = await deps.allowList.remove(msg.channelId);
|
|
65
|
+
await ctx.reply(removed ? '✓ channel removed from the allow-list.' : 'this channel was not allow-listed.');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (text === '/allow' && msg.guildId != null) {
|
|
69
|
+
await ctx.reply('this channel is already allow-listed.');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (text.startsWith('/')) {
|
|
73
|
+
if (!state.session)
|
|
74
|
+
return;
|
|
75
|
+
const [head, ...rest] = text.split(/\s+/);
|
|
76
|
+
const reply = await runSlash(head.slice(1), rest.join(' '), state.session, {
|
|
77
|
+
toggleYolo: cb.toggleYolo,
|
|
78
|
+
voice: cb.voice,
|
|
79
|
+
performSessionAction: (action, notice) => performSessionAction(action, notice, state, deps, cb),
|
|
80
|
+
});
|
|
81
|
+
if (reply)
|
|
82
|
+
await ctx.reply(reply.length > 1_900 ? reply.slice(0, 1_899) + '…' : reply);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Voice messages / audio uploads take the transcribe-then-turn path.
|
|
86
|
+
if (await cb.runVoiceMessage(ctx))
|
|
87
|
+
return;
|
|
88
|
+
if (!text)
|
|
89
|
+
return;
|
|
90
|
+
if (state.busy) {
|
|
91
|
+
await ctx.reply('I am still working on the previous prompt. Send /cancel to abort it.');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
await cb.runUserTurn(ctx, text);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Channel-side handler for `session-action` outputs from registered commands.
|
|
98
|
+
* Mirrors the Telegram channel's semantics; returns the reply text. Shared by
|
|
99
|
+
* the plain-text slash path (above) and the interaction (slash-command) path
|
|
100
|
+
* in the channel.
|
|
101
|
+
*/
|
|
102
|
+
export async function performSessionAction(action, notice, state, deps, cb) {
|
|
103
|
+
if (!state.session)
|
|
104
|
+
return 'session is not ready yet.';
|
|
105
|
+
if (action === 'exit') {
|
|
106
|
+
// Fire the stop AFTER we return so the reply can still be delivered.
|
|
107
|
+
setTimeout(() => void state.handle?.stop('user /exit'), 250);
|
|
108
|
+
return notice ?? 'closing Discord channel';
|
|
109
|
+
}
|
|
110
|
+
if (action === 'clear') {
|
|
111
|
+
return `✓ ${notice ?? 'cleared'}`;
|
|
112
|
+
}
|
|
113
|
+
// action === 'new'
|
|
114
|
+
if (state.turnController && !state.turnController.signal.aborted) {
|
|
115
|
+
state.turnController.abort('user reset');
|
|
116
|
+
}
|
|
117
|
+
cb.setYolo(false);
|
|
118
|
+
cb.setAwaitingApprovalText(null);
|
|
119
|
+
deps.approvalResolver.abortAll('session reset');
|
|
120
|
+
deps.permissionResolver.abortAll('session reset');
|
|
121
|
+
// Wipe the history at its source (RemoteSession.reset() asks the runner; a
|
|
122
|
+
// mirror-only log.clear() would desync) and only claim success when the
|
|
123
|
+
// reset actually happened (AGENTS.md A10).
|
|
124
|
+
try {
|
|
125
|
+
if (typeof state.session.reset === 'function')
|
|
126
|
+
await state.session.reset();
|
|
127
|
+
else
|
|
128
|
+
state.session.log.clear();
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
return `⚠ /new failed: ${err instanceof Error ? err.message : String(err)} — history NOT cleared`;
|
|
132
|
+
}
|
|
133
|
+
return `✓ ${notice ?? 'new session — conversation history cleared'}`;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=message-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-handler.js","sourceRoot":"","sources":["../../src/channel/message-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { VaultStore } from '@moxxy/plugin-vault';
|
|
2
|
+
import { type DiscordPairingPhase } from '../pairing.js';
|
|
3
|
+
/** Result returned by {@link PairingHandler.confirmCode}. */
|
|
4
|
+
export type PairingConfirmResult = {
|
|
5
|
+
ok: true;
|
|
6
|
+
userId: string;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
reason: 'mismatch' | 'expired' | 'not-pending';
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
12
|
+
export interface PairingHandlerOptions {
|
|
13
|
+
readonly vault: VaultStore;
|
|
14
|
+
readonly logger?: {
|
|
15
|
+
info?(msg: string, meta?: Record<string, unknown>): void;
|
|
16
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Owns the pairing state machine + vault persistence for the DM code flow
|
|
21
|
+
* (see `../pairing.ts` for the flow + security property). The channel feeds it
|
|
22
|
+
* DMs from unauthorized users ({@link handleUnpairedDm}) and the terminal
|
|
23
|
+
* wizard feeds it pasted codes ({@link confirmCode}).
|
|
24
|
+
*/
|
|
25
|
+
export declare class PairingHandler {
|
|
26
|
+
private state;
|
|
27
|
+
private readonly opts;
|
|
28
|
+
private readonly pairedListeners;
|
|
29
|
+
constructor(opts: PairingHandlerOptions);
|
|
30
|
+
loadAuthorized(): Promise<void>;
|
|
31
|
+
phase(): DiscordPairingPhase;
|
|
32
|
+
isAuthorized(userId: string): boolean;
|
|
33
|
+
authorizedUserId(): string | null;
|
|
34
|
+
/** Arm the pairing window: unauthorized DMs will now be issued codes. */
|
|
35
|
+
arm(): void;
|
|
36
|
+
unpair(): void;
|
|
37
|
+
/**
|
|
38
|
+
* Subscribe to "a user just became authorized" — fires once each time pairing
|
|
39
|
+
* completes. The `pair` terminal flow uses it to print success; the channel
|
|
40
|
+
* uses it to flip its connect-state. Returns an unsubscribe function.
|
|
41
|
+
*/
|
|
42
|
+
onPaired(listener: (userId: string) => void): () => void;
|
|
43
|
+
/**
|
|
44
|
+
* An unauthorized user DMed the bot. Returns the reply text to DM back:
|
|
45
|
+
* a freshly minted one-time code (window armed), a nudge to open a window,
|
|
46
|
+
* or a foreign-account rejection. Null when no reply is warranted.
|
|
47
|
+
*/
|
|
48
|
+
handleUnpairedDm(userId: string): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* The operator pasted a code into the terminal. On match the pending DM user
|
|
51
|
+
* is authorized + persisted. The vault write happens BEFORE listeners fire so
|
|
52
|
+
* a `pair`-flow exit right after success never loses the pairing.
|
|
53
|
+
*/
|
|
54
|
+
confirmCode(rawCode: string): Promise<PairingConfirmResult>;
|
|
55
|
+
private applyConfirmDecision;
|
|
56
|
+
private emitPaired;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=pairing-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing-handler.d.ts","sourceRoot":"","sources":["../../src/channel/pairing-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EASL,KAAK,mBAAmB,EAEzB,MAAM,eAAe,CAAC;AAGvB,6DAA6D;AAC7D,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC5B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnF,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE;QAChB,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;QACzD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;KACzD,CAAC;CACH;AAED;;;;;GAKG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAoD;IACjE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAwB;IAC7C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAuC;gBAE3D,IAAI,EAAE,qBAAqB;IAIjC,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAYrC,KAAK,IAAI,mBAAmB;IAI5B,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAIrC,gBAAgB,IAAI,MAAM,GAAG,IAAI;IAIjC,yEAAyE;IACzE,GAAG,IAAI,IAAI;IAIX,MAAM,IAAI,IAAI;IAId;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAKxD;;;;OAIG;IACH,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAgB/C;;;;OAIG;IACG,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;YAKnD,oBAAoB;IAoBlC,OAAO,CAAC,UAAU;CASnB"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { armPairing, clearDiscordPairing, confirmPendingCode, createDiscordPairingState, isUserAuthorized, mintCodeForPeer, pairingPhase, } from '../pairing.js';
|
|
2
|
+
import { DISCORD_AUTHORIZED_USER_KEY, parseAuthorizedUser } from '../keys.js';
|
|
3
|
+
/**
|
|
4
|
+
* Owns the pairing state machine + vault persistence for the DM code flow
|
|
5
|
+
* (see `../pairing.ts` for the flow + security property). The channel feeds it
|
|
6
|
+
* DMs from unauthorized users ({@link handleUnpairedDm}) and the terminal
|
|
7
|
+
* wizard feeds it pasted codes ({@link confirmCode}).
|
|
8
|
+
*/
|
|
9
|
+
export class PairingHandler {
|
|
10
|
+
state = createDiscordPairingState();
|
|
11
|
+
opts;
|
|
12
|
+
pairedListeners = new Set();
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
this.opts = opts;
|
|
15
|
+
}
|
|
16
|
+
async loadAuthorized() {
|
|
17
|
+
const raw = await this.opts.vault.get(DISCORD_AUTHORIZED_USER_KEY);
|
|
18
|
+
const authorizedUserId = parseAuthorizedUser(raw);
|
|
19
|
+
if (authorizedUserId == null && raw) {
|
|
20
|
+
this.opts.logger?.warn('discord pairing: stored user id is not a snowflake — treating as unpaired', { raw });
|
|
21
|
+
}
|
|
22
|
+
this.state = createDiscordPairingState({ authorizedUserId });
|
|
23
|
+
}
|
|
24
|
+
phase() {
|
|
25
|
+
return pairingPhase(this.state);
|
|
26
|
+
}
|
|
27
|
+
isAuthorized(userId) {
|
|
28
|
+
return isUserAuthorized(this.state, userId);
|
|
29
|
+
}
|
|
30
|
+
authorizedUserId() {
|
|
31
|
+
return this.state.kit.phase === 'paired' ? this.state.kit.authorizedPeer : null;
|
|
32
|
+
}
|
|
33
|
+
/** Arm the pairing window: unauthorized DMs will now be issued codes. */
|
|
34
|
+
arm() {
|
|
35
|
+
this.state = armPairing(this.state);
|
|
36
|
+
}
|
|
37
|
+
unpair() {
|
|
38
|
+
this.state = clearDiscordPairing(this.state);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Subscribe to "a user just became authorized" — fires once each time pairing
|
|
42
|
+
* completes. The `pair` terminal flow uses it to print success; the channel
|
|
43
|
+
* uses it to flip its connect-state. Returns an unsubscribe function.
|
|
44
|
+
*/
|
|
45
|
+
onPaired(listener) {
|
|
46
|
+
this.pairedListeners.add(listener);
|
|
47
|
+
return () => this.pairedListeners.delete(listener);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* An unauthorized user DMed the bot. Returns the reply text to DM back:
|
|
51
|
+
* a freshly minted one-time code (window armed), a nudge to open a window,
|
|
52
|
+
* or a foreign-account rejection. Null when no reply is warranted.
|
|
53
|
+
*/
|
|
54
|
+
handleUnpairedDm(userId) {
|
|
55
|
+
const decision = mintCodeForPeer(this.state, userId);
|
|
56
|
+
this.state = decision.state;
|
|
57
|
+
const action = decision.action;
|
|
58
|
+
if (action.kind === 'code-minted') {
|
|
59
|
+
this.opts.logger?.info?.('discord pairing: code minted', { userId });
|
|
60
|
+
return (`Your one-time pairing code is: **${action.code}**\n` +
|
|
61
|
+
'Paste it into the moxxy terminal (`moxxy discord pair`) to finish pairing.');
|
|
62
|
+
}
|
|
63
|
+
if (action.kind === 'still-paired')
|
|
64
|
+
return 'Already paired — send me a prompt.';
|
|
65
|
+
if (action.kind === 'reject')
|
|
66
|
+
return action.message;
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The operator pasted a code into the terminal. On match the pending DM user
|
|
71
|
+
* is authorized + persisted. The vault write happens BEFORE listeners fire so
|
|
72
|
+
* a `pair`-flow exit right after success never loses the pairing.
|
|
73
|
+
*/
|
|
74
|
+
async confirmCode(rawCode) {
|
|
75
|
+
const decision = confirmPendingCode(this.state, rawCode);
|
|
76
|
+
return this.applyConfirmDecision(decision);
|
|
77
|
+
}
|
|
78
|
+
async applyConfirmDecision(decision) {
|
|
79
|
+
this.state = decision.state;
|
|
80
|
+
const action = decision.action;
|
|
81
|
+
if (action.kind === 'paired') {
|
|
82
|
+
await this.opts.vault.set(DISCORD_AUTHORIZED_USER_KEY, action.userId);
|
|
83
|
+
this.emitPaired(action.userId);
|
|
84
|
+
return { ok: true, userId: action.userId };
|
|
85
|
+
}
|
|
86
|
+
if (action.kind === 'still-paired')
|
|
87
|
+
return { ok: true, userId: action.userId };
|
|
88
|
+
if (action.kind === 'mismatch')
|
|
89
|
+
return { ok: false, reason: 'mismatch', message: action.message };
|
|
90
|
+
if (action.kind === 'expired')
|
|
91
|
+
return { ok: false, reason: 'expired', message: action.message };
|
|
92
|
+
if (action.kind === 'not-pending' || action.kind === 'reject') {
|
|
93
|
+
return { ok: false, reason: 'not-pending', message: action.message };
|
|
94
|
+
}
|
|
95
|
+
// 'code-minted' can't come out of a confirm; treat as a mismatch.
|
|
96
|
+
return { ok: false, reason: 'mismatch', message: 'unexpected pairing state' };
|
|
97
|
+
}
|
|
98
|
+
emitPaired(userId) {
|
|
99
|
+
for (const listener of this.pairedListeners) {
|
|
100
|
+
try {
|
|
101
|
+
listener(userId);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
this.opts.logger?.warn('discord pairing paired-listener threw', { err: String(err) });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=pairing-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing-handler.js","sourceRoot":"","sources":["../../src/channel/pairing-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PendingToolCall, PermissionContext } from '@moxxy/sdk';
|
|
2
|
+
import type { ClientSession as Session } from '@moxxy/sdk';
|
|
3
|
+
import type { DiscordPermissionResolver } from '../permission.js';
|
|
4
|
+
import type { ChannelLogger, SendableChannelLike } from './discord-like.js';
|
|
5
|
+
export interface PermissionPromptDeps {
|
|
6
|
+
readonly channel: SendableChannelLike | null;
|
|
7
|
+
readonly session: Session | null;
|
|
8
|
+
readonly resolver: DiscordPermissionResolver;
|
|
9
|
+
readonly yolo: boolean;
|
|
10
|
+
readonly logger?: ChannelLogger;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Render a button-row permission prompt for a pending tool call (the Discord
|
|
14
|
+
* mapping of Telegram's inline keyboard). The decider promise resolves when
|
|
15
|
+
* the paired user clicks a button (routed back via the interaction handler)
|
|
16
|
+
* or when the resolver aborts on stop.
|
|
17
|
+
*/
|
|
18
|
+
export declare function askForPermission(call: PendingToolCall, ctx: PermissionContext, deps: PermissionPromptDeps): Promise<void>;
|
|
19
|
+
//# sourceMappingURL=permission-prompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-prompt.d.ts","sourceRoot":"","sources":["../../src/channel/permission-prompt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,IAAI,OAAO,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAElE,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE5E,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC7C,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,yBAAyB,CAAC;IAC7C,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;CACjC;AAMD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,eAAe,EACrB,GAAG,EAAE,iBAAiB,EACtB,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,IAAI,CAAC,CAyBf"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { BUTTON_STYLE, button, packRows } from './components.js';
|
|
2
|
+
function truncate(s, max) {
|
|
3
|
+
return s.length <= max ? s : s.slice(0, max - 1) + '…';
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Render a button-row permission prompt for a pending tool call (the Discord
|
|
7
|
+
* mapping of Telegram's inline keyboard). The decider promise resolves when
|
|
8
|
+
* the paired user clicks a button (routed back via the interaction handler)
|
|
9
|
+
* or when the resolver aborts on stop.
|
|
10
|
+
*/
|
|
11
|
+
export async function askForPermission(call, ctx, deps) {
|
|
12
|
+
void ctx;
|
|
13
|
+
if (!deps.channel || !deps.session)
|
|
14
|
+
return;
|
|
15
|
+
// YOLO short-circuit: resolve immediately without rendering a prompt.
|
|
16
|
+
if (deps.yolo) {
|
|
17
|
+
deps.resolver.resolvePending(call.callId, { mode: 'allow', reason: 'yolo mode' });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const rows = packRows([
|
|
21
|
+
button(`perm:${call.callId}:allow`, 'Allow once', BUTTON_STYLE.success),
|
|
22
|
+
button(`perm:${call.callId}:allow_session`, 'Allow session', BUTTON_STYLE.primary),
|
|
23
|
+
button(`perm:${call.callId}:deny`, 'Deny', BUTTON_STYLE.danger),
|
|
24
|
+
]);
|
|
25
|
+
const description = deps.session.tools.get(call.name)?.description ?? '';
|
|
26
|
+
const summary = `🔐 **Tool permission requested**\n` +
|
|
27
|
+
`Tool: \`${call.name}\`\n` +
|
|
28
|
+
(description ? `Desc: ${truncate(description, 200)}\n` : '') +
|
|
29
|
+
`Input: \`${truncate(JSON.stringify(call.input), 300)}\``;
|
|
30
|
+
try {
|
|
31
|
+
await deps.channel.send({ content: summary, components: rows });
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
deps.logger?.warn('discord permission send failed', { err: String(err) });
|
|
35
|
+
deps.resolver.resolvePending(call.callId, { mode: 'deny', reason: 'unable to render prompt' });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=permission-prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-prompt.js","sourceRoot":"","sources":["../../src/channel/permission-prompt.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAWjE,SAAS,QAAQ,CAAC,CAAS,EAAE,GAAW;IACtC,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC;AACzD,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAqB,EACrB,GAAsB,EACtB,IAA0B;IAE1B,KAAK,GAAG,CAAC;IACT,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO;QAAE,OAAO;IAC3C,sEAAsE;IACtE,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;QAClF,OAAO;IACT,CAAC;IACD,MAAM,IAAI,GAAG,QAAQ,CAAC;QACpB,MAAM,CAAC,QAAQ,IAAI,CAAC,MAAM,QAAQ,EAAE,YAAY,EAAE,YAAY,CAAC,OAAO,CAAC;QACvE,MAAM,CAAC,QAAQ,IAAI,CAAC,MAAM,gBAAgB,EAAE,eAAe,EAAE,YAAY,CAAC,OAAO,CAAC;QAClF,MAAM,CAAC,QAAQ,IAAI,CAAC,MAAM,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC;KAChE,CAAC,CAAC;IACH,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,WAAW,IAAI,EAAE,CAAC;IACzE,MAAM,OAAO,GACX,oCAAoC;QACpC,WAAW,IAAI,CAAC,IAAI,MAAM;QAC1B,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,YAAY,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC;IAC5D,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;IAClE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,gCAAgC,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1E,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,yBAAyB,EAAE,CAAC,CAAC;IACjG,CAAC;AACH,CAAC"}
|