@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.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/dist/allow-list.d.ts +20 -0
  3. package/dist/allow-list.d.ts.map +1 -0
  4. package/dist/allow-list.js +12 -0
  5. package/dist/allow-list.js.map +1 -0
  6. package/dist/approval.d.ts +29 -0
  7. package/dist/approval.d.ts.map +1 -0
  8. package/dist/approval.js +65 -0
  9. package/dist/approval.js.map +1 -0
  10. package/dist/channel/allow-list-store.d.ts +22 -0
  11. package/dist/channel/allow-list-store.d.ts.map +1 -0
  12. package/dist/channel/allow-list-store.js +45 -0
  13. package/dist/channel/allow-list-store.js.map +1 -0
  14. package/dist/channel/approval-prompt.d.ts +16 -0
  15. package/dist/channel/approval-prompt.d.ts.map +1 -0
  16. package/dist/channel/approval-prompt.js +27 -0
  17. package/dist/channel/approval-prompt.js.map +1 -0
  18. package/dist/channel/components.d.ts +27 -0
  19. package/dist/channel/components.d.ts.map +1 -0
  20. package/dist/channel/components.js +34 -0
  21. package/dist/channel/components.js.map +1 -0
  22. package/dist/channel/discord-like.d.ts +31 -0
  23. package/dist/channel/discord-like.d.ts.map +1 -0
  24. package/dist/channel/discord-like.js +9 -0
  25. package/dist/channel/discord-like.js.map +1 -0
  26. package/dist/channel/interaction-handler.d.ts +67 -0
  27. package/dist/channel/interaction-handler.d.ts.map +1 -0
  28. package/dist/channel/interaction-handler.js +155 -0
  29. package/dist/channel/interaction-handler.js.map +1 -0
  30. package/dist/channel/message-handler.d.ts +60 -0
  31. package/dist/channel/message-handler.d.ts.map +1 -0
  32. package/dist/channel/message-handler.js +135 -0
  33. package/dist/channel/message-handler.js.map +1 -0
  34. package/dist/channel/pairing-handler.d.ts +58 -0
  35. package/dist/channel/pairing-handler.d.ts.map +1 -0
  36. package/dist/channel/pairing-handler.js +109 -0
  37. package/dist/channel/pairing-handler.js.map +1 -0
  38. package/dist/channel/permission-prompt.d.ts +19 -0
  39. package/dist/channel/permission-prompt.d.ts.map +1 -0
  40. package/dist/channel/permission-prompt.js +38 -0
  41. package/dist/channel/permission-prompt.js.map +1 -0
  42. package/dist/channel/slash-handler.d.ts +45 -0
  43. package/dist/channel/slash-handler.d.ts.map +1 -0
  44. package/dist/channel/slash-handler.js +107 -0
  45. package/dist/channel/slash-handler.js.map +1 -0
  46. package/dist/channel/turn-runner.d.ts +49 -0
  47. package/dist/channel/turn-runner.d.ts.map +1 -0
  48. package/dist/channel/turn-runner.js +116 -0
  49. package/dist/channel/turn-runner.js.map +1 -0
  50. package/dist/channel/typing-indicator.d.ts +12 -0
  51. package/dist/channel/typing-indicator.d.ts.map +1 -0
  52. package/dist/channel/typing-indicator.js +28 -0
  53. package/dist/channel/typing-indicator.js.map +1 -0
  54. package/dist/channel/voice-handler.d.ts +41 -0
  55. package/dist/channel/voice-handler.d.ts.map +1 -0
  56. package/dist/channel/voice-handler.js +105 -0
  57. package/dist/channel/voice-handler.js.map +1 -0
  58. package/dist/channel.d.ts +107 -0
  59. package/dist/channel.d.ts.map +1 -0
  60. package/dist/channel.js +464 -0
  61. package/dist/channel.js.map +1 -0
  62. package/dist/index.d.ts +29 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +179 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/keys.d.ts +64 -0
  67. package/dist/keys.d.ts.map +1 -0
  68. package/dist/keys.js +85 -0
  69. package/dist/keys.js.map +1 -0
  70. package/dist/pair-flow.d.ts +16 -0
  71. package/dist/pair-flow.d.ts.map +1 -0
  72. package/dist/pair-flow.js +109 -0
  73. package/dist/pair-flow.js.map +1 -0
  74. package/dist/pairing.d.ts +83 -0
  75. package/dist/pairing.d.ts.map +1 -0
  76. package/dist/pairing.js +98 -0
  77. package/dist/pairing.js.map +1 -0
  78. package/dist/permission.d.ts +30 -0
  79. package/dist/permission.d.ts.map +1 -0
  80. package/dist/permission.js +54 -0
  81. package/dist/permission.js.map +1 -0
  82. package/dist/render.d.ts +34 -0
  83. package/dist/render.d.ts.map +1 -0
  84. package/dist/render.js +204 -0
  85. package/dist/render.js.map +1 -0
  86. package/dist/schema.d.ts +123 -0
  87. package/dist/schema.d.ts.map +1 -0
  88. package/dist/schema.js +70 -0
  89. package/dist/schema.js.map +1 -0
  90. package/dist/setup-wizard.d.ts +13 -0
  91. package/dist/setup-wizard.d.ts.map +1 -0
  92. package/dist/setup-wizard.js +133 -0
  93. package/dist/setup-wizard.js.map +1 -0
  94. package/package.json +89 -0
  95. package/src/allow-list.test.ts +64 -0
  96. package/src/allow-list.ts +30 -0
  97. package/src/approval.ts +76 -0
  98. package/src/channel/allow-list-store.ts +52 -0
  99. package/src/channel/approval-prompt.ts +43 -0
  100. package/src/channel/components.ts +49 -0
  101. package/src/channel/discord-like.ts +34 -0
  102. package/src/channel/interaction-handler.test.ts +212 -0
  103. package/src/channel/interaction-handler.ts +237 -0
  104. package/src/channel/message-handler.test.ts +295 -0
  105. package/src/channel/message-handler.ts +195 -0
  106. package/src/channel/pairing-handler.ts +147 -0
  107. package/src/channel/permission-prompt.ts +54 -0
  108. package/src/channel/slash-handler.ts +136 -0
  109. package/src/channel/turn-runner.test.ts +177 -0
  110. package/src/channel/turn-runner.ts +139 -0
  111. package/src/channel/typing-indicator.ts +31 -0
  112. package/src/channel/voice-handler.test.ts +177 -0
  113. package/src/channel/voice-handler.ts +150 -0
  114. package/src/channel/voice-reply.test.ts +142 -0
  115. package/src/channel.ts +582 -0
  116. package/src/index.ts +247 -0
  117. package/src/keys.ts +100 -0
  118. package/src/pair-flow.ts +122 -0
  119. package/src/pairing.test.ts +99 -0
  120. package/src/pairing.ts +178 -0
  121. package/src/permission.ts +70 -0
  122. package/src/render.test.ts +82 -0
  123. package/src/render.ts +228 -0
  124. package/src/schema.ts +94 -0
  125. package/src/setup-wizard.ts +162 -0
  126. 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":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAO/C,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AA4C9C;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,GAAmB,EACnB,KAA0B,EAC1B,IAAwB,EACxB,EAA2B;IAE3B,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC;IACpB,gEAAgE;IAChE,IAAI,GAAG,CAAC,WAAW,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,IAAI,IAAI,GAAG,CAAC,QAAQ,KAAK,KAAK,CAAC,SAAS,CAAC;QAAE,OAAO;IAE7F,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;IAChC,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC;IAE7F,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;QAChB,IAAI,OAAO,CAAC,MAAM,KAAK,YAAY,IAAI,OAAO,CAAC,MAAM,KAAK,cAAc,EAAE,CAAC;YACzE,mEAAmE;YACnE,wEAAwE;YACxE,qDAAqD;YACrD,IAAI,GAAG,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC;gBACxB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;gBAC1D,IAAI,KAAK;oBAAE,MAAM,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACpC,CAAC;YACD,OAAO;QACT,CAAC;QACD,0EAA0E;QAC1E,sEAAsE;QACtE,gEAAgE;QAChE,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;YACtB,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACxC,MAAM,GAAG,CAAC,KAAK,CAAC,+DAA+D,CAAC,CAAC;YACjF,OAAO;QACT,CAAC;QACD,MAAM,GAAG,CAAC,KAAK,CAAC,kEAAkE,CAAC,CAAC;QACpF,OAAO;IACT,CAAC;IAED,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAElD,wEAAwE;IACxE,uEAAuE;IACvE,wBAAwB;IACxB,IAAI,KAAK,CAAC,oBAAoB,IAAI,IAAI,EAAE,CAAC;QACvC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC,oBAAoB,CAAC;QAC5D,EAAE,CAAC,uBAAuB,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,OAAO,GAAG,IAAI,CAAC,gBAAgB,CAAC,sBAAsB,CAAC,UAAU,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;QACzF,MAAM,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,gBAAgB,QAAQ,GAAG,CAAC,CAAC,CAAC,oCAAoC,CAAC,CAAC;QAC9F,OAAO;IACT,CAAC;IAED,+DAA+D;IAC/D,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,IAAI,KAAK,CAAC,cAAc,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACjE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;YAC1C,MAAM,GAAG,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC9C,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;QACxC,CAAC;QACD,OAAO;IACT,CAAC;IAED,IAAI,IAAI,KAAK,OAAO,IAAI,GAAG,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC;QAC5C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC3D,MAAM,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,wCAAwC,CAAC,CAAC,CAAC,oCAAoC,CAAC,CAAC;QAC3G,OAAO;IACT,CAAC;IACD,IAAI,IAAI,KAAK,QAAQ,IAAI,GAAG,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC;QAC7C,MAAM,GAAG,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;QACzD,OAAO;IACT,CAAC;IAED,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,IAAI,CAAC,KAAK,CAAC,OAAO;YAAE,OAAO;QAC3B,MAAM,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC1C,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,IAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,OAAO,EAAE;YAC1E,UAAU,EAAE,EAAE,CAAC,UAAU;YACzB,KAAK,EAAE,EAAE,CAAC,KAAK;YACf,oBAAoB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CACvC,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC;SACxD,CAAC,CAAC;QACH,IAAI,KAAK;YAAE,MAAM,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACvF,OAAO;IACT,CAAC;IAED,qEAAqE;IACrE,IAAI,MAAM,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC;QAAE,OAAO;IAC1C,IAAI,CAAC,IAAI;QAAE,OAAO;IAElB,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QACf,MAAM,GAAG,CAAC,KAAK,CAAC,sEAAsE,CAAC,CAAC;QACxF,OAAO;IACT,CAAC;IAED,MAAM,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AAClC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,MAAgC,EAChC,MAA0B,EAC1B,KAAyE,EACzE,IAAyE,EACzE,EAAwE;IAExE,IAAI,CAAC,KAAK,CAAC,OAAO;QAAE,OAAO,2BAA2B,CAAC;IACvD,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACtB,qEAAqE;QACrE,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,YAAY,CAAC,EAAE,GAAG,CAAC,CAAC;QAC7D,OAAO,MAAM,IAAI,yBAAyB,CAAC;IAC7C,CAAC;IACD,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;QACvB,OAAO,KAAK,MAAM,IAAI,SAAS,EAAE,CAAC;IACpC,CAAC;IACD,mBAAmB;IACnB,IAAI,KAAK,CAAC,cAAc,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACjE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAC3C,CAAC;IACD,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAClB,EAAE,CAAC,uBAAuB,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IAChD,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IAClD,2EAA2E;IAC3E,wEAAwE;IACxE,2CAA2C;IAC3C,IAAI,CAAC;QACH,IAAI,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,KAAK,UAAU;YAAE,MAAM,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;;YACtE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;IACjC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,kBAAkB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,wBAAwB,CAAC;IACpG,CAAC;IACD,OAAO,KAAK,MAAM,IAAI,4CAA4C,EAAE,CAAC;AACvE,CAAC"}
@@ -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"}