toolroll 0.9.37 → 0.9.39
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/CHANGELOG.md +37 -0
- package/dist/browser/THIRD_PARTY_NOTICES.txt +3 -3
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +26 -26
- package/dist/browser-crew.js +5 -0
- package/dist/browser-workspace.d.ts +8 -0
- package/dist/chat-channel.js +20 -9
- package/dist/chat-decide.d.ts +4 -3
- package/dist/chat-decide.js +19 -5
- package/dist/desktop-update.d.ts +1 -1
- package/dist/mate-progress.d.ts +26 -0
- package/dist/mate-progress.js +24 -0
- package/dist/mate.js +11 -2
- package/dist/serve.js +95 -13
- package/dist/store.d.ts +5 -0
- package/dist/store.js +14 -0
- package/dist/team-contract.d.ts +11 -7
- package/dist/telegram-flow.d.ts +9 -0
- package/dist/telegram-flow.js +40 -5
- package/dist/telegram-mate.js +1 -1
- package/dist/telegram.js +42 -26
- package/package.json +17 -12
package/dist/telegram-flow.js
CHANGED
|
@@ -24,7 +24,7 @@ import { LIMITS } from "./decision.js";
|
|
|
24
24
|
import { keptDraft } from "./flow-draft.js";
|
|
25
25
|
import { decideFlowCard, draftFor, flowDefinitionOf } from "./flow-engine.js";
|
|
26
26
|
import { chooseFlowCard, flowChoiceAt, flowSendPaths, readFlowSend } from "./flow-send.js";
|
|
27
|
-
import { deciderOf } from "./flows.js";
|
|
27
|
+
import { deciderOf, FLOW_END, replyTarget } from "./flows.js";
|
|
28
28
|
import { phoneLinkButton } from "./telegram-mate.js";
|
|
29
29
|
import { telegramButton } from "./contracts/telegram-callback.js";
|
|
30
30
|
export const FLOW_DECIDE_KEY = /^flow-decide:([1-9][0-9]{0,14}):([1-9][0-9]{0,9})$/;
|
|
@@ -41,6 +41,29 @@ export function flowDecisionAt(store, cardId, entry) {
|
|
|
41
41
|
return null;
|
|
42
42
|
return { card, flow, stage, draft: draftFor(definition, stage) };
|
|
43
43
|
}
|
|
44
|
+
/** Where Approve and Send back take the card, on one line. */
|
|
45
|
+
function decisionTerms(waiting) {
|
|
46
|
+
const approve = waiting.stage.next === null ? "Approve → done" : `Approve → ${titleOf(waiting, waiting.stage.next)}`;
|
|
47
|
+
return [approve, ...(waiting.stage.onFail === null ? [] : [`Send back → ${titleOf(waiting, waiting.stage.onFail)}, with your note`])].join(" · ");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A flow decision as Telegram shows it: the action and where each button takes the card first, then the draft (as
|
|
51
|
+
* the card holds it now, so what is approved is what is read) and why a teammate handed it over, then the flow and
|
|
52
|
+
* zone. `said` is the saved notice's body: the words before its instruction are the teammate's handoff.
|
|
53
|
+
*/
|
|
54
|
+
export function flowDecisionText(waiting, said, head = `Approve “${waiting.card.title}”`) {
|
|
55
|
+
const at = said === null ? -1 : said.indexOf(`${waiting.stage.title}: approve`);
|
|
56
|
+
const handoff = at > 0 ? said.slice(0, at).trim() : "";
|
|
57
|
+
const draft = waiting.draft === null ? undefined : waiting.card.outputs[waiting.draft.id]?.trim();
|
|
58
|
+
return [
|
|
59
|
+
head,
|
|
60
|
+
decisionTerms(waiting),
|
|
61
|
+
...(handoff === "" ? [] : ["", handoff]),
|
|
62
|
+
...(draft === undefined || draft === "" ? [] : ["", "Draft:", draft]),
|
|
63
|
+
"",
|
|
64
|
+
`${waiting.flow.name} · ${waiting.stage.title}`,
|
|
65
|
+
].join("\n");
|
|
66
|
+
}
|
|
44
67
|
/** Buttons for one visit, minted before the send; `place` stamps the message they landed on. */
|
|
45
68
|
export function flowButtons(store, binding, waiting, now) {
|
|
46
69
|
const mint = (action) => ({ action, token: randomBytes(16).toString("hex") });
|
|
@@ -64,6 +87,17 @@ export function flowSendKeyboardRow(origin, content) {
|
|
|
64
87
|
const pull = content.links.flatMap(one => "url" in one ? [{ text: one.label, url: one.url }] : []);
|
|
65
88
|
return [...pages.slice(0, 1), ...pull, ...pages.slice(1)].slice(0, 3);
|
|
66
89
|
}
|
|
90
|
+
/** A choice as Telegram shows it, first: what to choose, then where each option (and a reply) takes the card. The
|
|
91
|
+
* options are the ones its buttons carry: offered when sent, and still offered. */
|
|
92
|
+
export function flowChoiceHead(visit, content) {
|
|
93
|
+
const titleIn = (id) => visit.definition.stages.find(one => one.id === id)?.title ?? id;
|
|
94
|
+
const options = (content.options ?? []).flatMap(one => {
|
|
95
|
+
const option = visit.stage.options?.[one.choice];
|
|
96
|
+
return option === undefined || option.label !== one.label ? [] : [`${one.label} → ${option.to === FLOW_END ? "closes the card" : titleIn(option.to)}`];
|
|
97
|
+
});
|
|
98
|
+
const reply = content.reply === true ? replyTarget(visit.stage) : null;
|
|
99
|
+
return [`Choose what happens to “${visit.card.title}”`, [...options, ...(reply === null ? [] : [`or reply → ${titleIn(reply)}, with your note`])].join(" · ")];
|
|
100
|
+
}
|
|
67
101
|
/** A choice's buttons for one visit, one option to a row, minted before the send; `place` stamps the message they landed on. */
|
|
68
102
|
export function flowChoiceButtons(store, binding, visit, content, now) {
|
|
69
103
|
// Only the options the zone still offers as they were sent: a flow changed since sends no stale button.
|
|
@@ -88,7 +122,9 @@ export function applyFlowChoiceTap(store, binding, choice, message, repos, now)
|
|
|
88
122
|
if (!chosen.ok)
|
|
89
123
|
return [{ kind: "ack", text: chosen.message.slice(0, 190) }];
|
|
90
124
|
store.retireFlowChoices(choice.card, choice.entry, now);
|
|
91
|
-
|
|
125
|
+
// The door's words name the option already ("Ship it. Moved to Ship."); an ending says what happened instead.
|
|
126
|
+
const said = chosen.said.startsWith(`${choice.label}.`) ? chosen.said : `You chose “${choice.label}”. ${chosen.said}`;
|
|
127
|
+
return [{ kind: "ack", text: choice.label.slice(0, 190) }, { kind: "edit", text: `${message.text}\n\n✅ ${said}`.slice(0, 4000) }];
|
|
92
128
|
}
|
|
93
129
|
/** A tapped flow button, applied inside the update's transaction. */
|
|
94
130
|
export function applyFlowTap(store, binding, action, message, repos, now) {
|
|
@@ -107,7 +143,7 @@ export function applyFlowTap(store, binding, action, message, repos, now) {
|
|
|
107
143
|
if (!decided.ok)
|
|
108
144
|
return [{ kind: "ack", text: decided.message.slice(0, 190) }];
|
|
109
145
|
store.retireTelegramFlowVisit(action.card, action.entry, now);
|
|
110
|
-
return [{ kind: "ack", text: "Approved" }, { kind: "edit", text: `${message.text}\n\n✅
|
|
146
|
+
return [{ kind: "ack", text: "Approved" }, { kind: "edit", text: `${message.text}\n\n✅ ${decided.said}`.slice(0, 4000) }];
|
|
111
147
|
}
|
|
112
148
|
const prompt = { chatId: binding.chatId, binding: binding.id, card: action.card, entry: action.entry, mode: action.action };
|
|
113
149
|
if (action.action === "edit") {
|
|
@@ -157,8 +193,7 @@ export function applyFlowReply(store, binding, prompt, text, repos, now) {
|
|
|
157
193
|
store.retireTelegramFlowVisit(prompt.card, prompt.entry, now);
|
|
158
194
|
const fresh = flowDecisionAt(store, prompt.card, prompt.entry);
|
|
159
195
|
const buttons = flowButtons(store, binding, fresh, now);
|
|
160
|
-
return [{ kind: "decide", keyboard: buttons.keyboard, tokens: buttons.tokens,
|
|
161
|
-
text: `${waiting.flow.name}: your version of the draft for “${waiting.card.title}”\n\n${kept}\n\nApprove to send it as written.` }];
|
|
196
|
+
return [{ kind: "decide", keyboard: buttons.keyboard, tokens: buttons.tokens, text: flowDecisionText(fresh, null, `Approve your version of “${waiting.card.title}”`) }];
|
|
162
197
|
}
|
|
163
198
|
// A note over its limit is refused with the limit, never cut.
|
|
164
199
|
if (said.length > LIMITS.note)
|
package/dist/telegram-mate.js
CHANGED
|
@@ -256,7 +256,7 @@ export function applyProposalTap(store, binding, token, message, repos, options,
|
|
|
256
256
|
return;
|
|
257
257
|
}
|
|
258
258
|
ack("irreversible — confirm it");
|
|
259
|
-
edit(
|
|
259
|
+
edit(`${body}\n\n⚠ Last step: this answer can't be undone. Confirm?`, [
|
|
260
260
|
[telegramButton("⚠ Yes, answer it", yes)],
|
|
261
261
|
[telegramButton("Cancel", cancel)],
|
|
262
262
|
]);
|
package/dist/telegram.js
CHANGED
|
@@ -34,7 +34,7 @@ import { enqueueEveningDigests, finishedView, isTaskFact, joinsBatch, needsPerso
|
|
|
34
34
|
import { BATCH_MS, chatText, chatTitle, factLinkLabel, mentions, nameTelegramBot } from "./chat-voice.js";
|
|
35
35
|
import { LEAD_SAY_KIND, enqueueLeadLapses, leadSayEarlier, leadSayText, leadSubjectOf } from "./lead-voice.js";
|
|
36
36
|
import { applyTeamInbound, deliverTeamChats, teamCommand } from "./telegram-team.js";
|
|
37
|
-
import { applyFlowChoiceTap, applyFlowReply, applyFlowTap, FLOW_DECIDE_KEY, flowButtons, flowChoiceButtons, flowDecisionAt, flowSendKeyboardRow, flowSentContent } from "./telegram-flow.js";
|
|
37
|
+
import { applyFlowChoiceTap, applyFlowReply, applyFlowTap, FLOW_DECIDE_KEY, flowButtons, flowChoiceButtons, flowChoiceHead, flowDecisionAt, flowDecisionText, flowSendKeyboardRow, flowSentContent } from "./telegram-flow.js";
|
|
38
38
|
import { flowChoiceAt, flowSendTail, FLOW_CHOOSE_KEY, FLOW_SEND_KEY } from "./flow-send.js";
|
|
39
39
|
import { cleanFlowMessage, fitFlowMessage } from "./flow-items.js";
|
|
40
40
|
import { telegramReply } from "./reply-shape.js";
|
|
@@ -634,7 +634,7 @@ function decideOfferFor(store, binding, target, projects, now, root) {
|
|
|
634
634
|
if (target === null || target === undefined)
|
|
635
635
|
return null;
|
|
636
636
|
const principal = verifyApproverStanding(store, binding.approver, binding.approverGeneration, telegramConversationRepos(store, binding.approver, projects));
|
|
637
|
-
return principal.ok ? decideOffer(store, target, principal.who, now, root) : null;
|
|
637
|
+
return principal.ok ? decideOffer(store, target, principal.who, now, root, "telegram") : null;
|
|
638
638
|
}
|
|
639
639
|
/** Team-conversation traffic to the chats that follow one — after the outbox, under the same delivery switch, never a problem to raise when nothing follows anything. */
|
|
640
640
|
async function deliverTeam(store, botId, transport, clock, report, readProjects, canDeliver, phoneOrigin) {
|
|
@@ -1225,13 +1225,23 @@ async function deliverOne(store, botId, binding, sender, notification, clock, ph
|
|
|
1225
1225
|
} })();
|
|
1226
1226
|
if (sentRow !== null)
|
|
1227
1227
|
button = sentRow.length === 0 ? null : sentRow;
|
|
1228
|
+
// A flow decision or choice for this person leads with what to do and where each button takes the card
|
|
1229
|
+
// (telegram-flow.ts); the flow and zone close it. The saved notice keeps its own words for every other place.
|
|
1230
|
+
const pull = sent === null ? [] : sent.links.flatMap(one => "url" in one ? [`${one.label}: ${one.url}`] : []).slice(0, 1);
|
|
1231
|
+
const choiceHead = choosing === null || sent === null ? null : flowChoiceHead(choosing, sent);
|
|
1232
|
+
const choiceTail = choosing === null ? [] : [...pull, `${choosing.flow.name}${sent?.from === undefined ? "" : ` · after ${sent.from}`}`];
|
|
1233
|
+
if (offer === null && flowKeys !== null && waiting !== null)
|
|
1234
|
+
parts = split(`${notificationIdentity(notification)}${chatText(flowDecisionText(waiting, notification.body), task)}`);
|
|
1235
|
+
else if (offer === null && choiceHead !== null && sent !== null)
|
|
1236
|
+
parts = split(`${notificationIdentity(notification)}${chatText([...choiceHead, "", sent.summary, "", ...choiceTail.flatMap((line, index) => index === 0 ? [line] : ["", line])].join("\n"), task)}`);
|
|
1228
1237
|
// After research (flow-items.ts): each item numbered under the summary, its words cleaned as the rest of the message's
|
|
1229
1238
|
// are, within Telegram's limit with every link kept — one message, unless head, links and tail alone overflow it.
|
|
1230
1239
|
let entities = [];
|
|
1231
1240
|
if (sent?.items !== undefined && sent.items.length > 0 && offer === null) {
|
|
1232
1241
|
const subject = chatText(leadSubjectOf(store, notification, binding.approver), task);
|
|
1233
|
-
const head =
|
|
1234
|
-
|
|
1242
|
+
const head = choiceHead !== null ? `${notificationIdentity(notification)}${choiceHead.join("\n")}`
|
|
1243
|
+
: `${notificationIdentity(notification, title !== undefined && mentions(subject, title) ? undefined : title)}${subject}`;
|
|
1244
|
+
const message = cleanFlowMessage({ head, summary: sent.summary, items: sent.items, tail: choiceHead !== null ? choiceTail : flowSendTail(sent) }, words => chatText(words, task));
|
|
1235
1245
|
const voiced = fitFlowMessage(message, TELEGRAM_TEXT_MAX, shaped => telegramReply(shaped).text.length).map(telegramReply);
|
|
1236
1246
|
parts = voiced.map(one => one.text);
|
|
1237
1247
|
entities = voiced.map(one => one.entities);
|
|
@@ -1268,24 +1278,11 @@ async function deliverOne(store, botId, binding, sender, notification, clock, ph
|
|
|
1268
1278
|
return { ok: true, receipt: receiptFor(botId, binding.chatId, last) };
|
|
1269
1279
|
}
|
|
1270
1280
|
// A decision. Every safety-bearing word goes out before anything tappable
|
|
1271
|
-
// exists:
|
|
1272
|
-
// many plain messages as they need — a button whose
|
|
1273
|
-
// away is a trap, so the keyboard rides the LAST part
|
|
1274
|
-
// every earlier part arrived.
|
|
1275
|
-
const
|
|
1276
|
-
`${notificationIdentity(notification)}Decision needed`,
|
|
1277
|
-
"",
|
|
1278
|
-
decision.recap,
|
|
1279
|
-
"",
|
|
1280
|
-
`Q: ${decision.question}`,
|
|
1281
|
-
"",
|
|
1282
|
-
...decision.options.flatMap(option => [
|
|
1283
|
-
`[${option.id}] ${option.label}${option.id === decision.recommendation ? " (recommended)" : ""}${option.reversible ? "" : " — IRREVERSIBLE"}`,
|
|
1284
|
-
` ${option.consequence}`,
|
|
1285
|
-
]),
|
|
1286
|
-
...(decision.deadline === null ? [] : ["", `deadline: ${decision.deadline}`]),
|
|
1287
|
-
];
|
|
1288
|
-
const parts = split(lines.join("\n"));
|
|
1281
|
+
// exists: the question and every option's consequence first, then the
|
|
1282
|
+
// recap, split across as many plain messages as they need — a button whose
|
|
1283
|
+
// warning was truncated away is a trap, so the keyboard rides the LAST part
|
|
1284
|
+
// only, and only if every earlier part arrived.
|
|
1285
|
+
const parts = split([`${notificationIdentity(notification)}${decisionAsk(decision)}`, "", `Background: ${decision.recap}`].join("\n"));
|
|
1289
1286
|
for (const part of parts.slice(0, -1)) {
|
|
1290
1287
|
const sent = await sender(part);
|
|
1291
1288
|
if (!sent.ok)
|
|
@@ -2382,7 +2379,7 @@ function applyCallback(context, update, effects) {
|
|
|
2382
2379
|
store.createTelegramAction({ token: confirm, binding: binding.id, decision: decision.id, optionId: option.id, phase: "confirm", chatId: binding.chatId, messageId: placedOn, ttlMs: CONFIRM_TTL_MS, ...(digest === undefined ? {} : { noteDigest: digest }) }, clock());
|
|
2383
2380
|
store.createTelegramAction({ token: cancel, binding: binding.id, decision: decision.id, optionId: option.id, phase: "cancel", chatId: binding.chatId, messageId: placedOn, ttlMs: CONFIRM_TTL_MS }, clock());
|
|
2384
2381
|
ack("irreversible — confirm it");
|
|
2385
|
-
editText(
|
|
2382
|
+
editText(armedDecisionText(option, draft?.note ?? null), [
|
|
2386
2383
|
[telegramButton(`⚠ Yes, ${option.label}`, confirm)],
|
|
2387
2384
|
[telegramButton("Cancel", cancel)],
|
|
2388
2385
|
]);
|
|
@@ -2430,7 +2427,9 @@ function applyCallback(context, update, effects) {
|
|
|
2430
2427
|
store.createTelegramAction({ token: choose, binding: binding.id, decision: decision.id, optionId: option.id, phase: "choose", chatId: binding.chatId, messageId: String(message.message_id) }, clock());
|
|
2431
2428
|
}
|
|
2432
2429
|
ack("cancelled");
|
|
2433
|
-
|
|
2430
|
+
// The card again, within one message; a decision too long for one keeps the consequences its earlier parts show.
|
|
2431
|
+
const ask = decisionAsk(decision);
|
|
2432
|
+
editText(ask.length <= PART_CAP ? ask : `Decide: ${decision.question}`, fresh.map(({ option, token: choose }) => [
|
|
2434
2433
|
telegramButton(`${option.label}${option.id === decision.recommendation ? " ✓" : ""}${option.reversible ? "" : " ⚠"}`, choose),
|
|
2435
2434
|
]));
|
|
2436
2435
|
}
|
|
@@ -2460,10 +2459,27 @@ function answerNow(context, decision, choice, binding, ack, editText) {
|
|
|
2460
2459
|
}
|
|
2461
2460
|
ack(`could not answer: ${answered.reason}`);
|
|
2462
2461
|
}
|
|
2462
|
+
/** A decision's card, action first: the question, then each option with what it does, then its deadline. */
|
|
2463
|
+
function decisionAsk(decision) {
|
|
2464
|
+
return [
|
|
2465
|
+
`Decide: ${decision.question}`,
|
|
2466
|
+
...decision.options.map(option => `• ${option.label}${option.id === decision.recommendation ? " (recommended)" : ""}${option.reversible ? "" : " ⚠ can't be undone"}: ${option.consequence}`),
|
|
2467
|
+
...(decision.deadline === null ? [] : [`Decide by ${decision.deadline.slice(0, 16).replace("T", " ")} UTC`]),
|
|
2468
|
+
].join("\n");
|
|
2469
|
+
}
|
|
2470
|
+
/** The armed confirm of an option that can't be undone: what it is and what it does, then the note that travels. */
|
|
2471
|
+
function armedDecisionText(option, note) {
|
|
2472
|
+
return [
|
|
2473
|
+
`${option.label}? ⚠ This can't be undone.`,
|
|
2474
|
+
option.consequence,
|
|
2475
|
+
...(note === null ? [] : ["", "With your note:", ...note.split("\n").map(line => `| ${line}`)]),
|
|
2476
|
+
].join("\n");
|
|
2477
|
+
}
|
|
2463
2478
|
function answeredText(store, decision) {
|
|
2479
|
+
const chosen = decision.options.find(one => one.id === decision.choice)?.label ?? decision.choice ?? "?";
|
|
2464
2480
|
return [
|
|
2465
|
-
`✓
|
|
2466
|
-
|
|
2481
|
+
`✓ Answered: ${chosen}`,
|
|
2482
|
+
`${taskOf(store, decision)} · by ${decision.answeredBy ?? "?"} via ${decision.answeredVia ?? "?"}`,
|
|
2467
2483
|
// Line-prefixed, never inline: a multiline note must not be able to
|
|
2468
2484
|
// draw fake status lines (Codex free-text review, finding 7).
|
|
2469
2485
|
...(decision.note === null ? [] : ["with note:", ...decision.note.split("\n").map(line => `| ${line}`)]),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.39",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|
|
@@ -64,18 +64,18 @@
|
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@tailwindcss/cli": "4.3.3",
|
|
67
|
-
"@types/mailparser": "3.
|
|
68
|
-
"@types/node": "26.
|
|
67
|
+
"@types/mailparser": "3.9.0",
|
|
68
|
+
"@types/node": "26.6.4",
|
|
69
69
|
"@types/react": "19.3.0",
|
|
70
70
|
"@types/react-dom": "19.3.0",
|
|
71
71
|
"esbuild": "0.28.2",
|
|
72
|
-
"happy-dom": "20.
|
|
72
|
+
"happy-dom": "20.14.5",
|
|
73
73
|
"paper": "0.12.18",
|
|
74
74
|
"simple-icons": "16.34.0",
|
|
75
75
|
"tailwindcss": "4.3.3",
|
|
76
|
-
"tsx": "4.23.
|
|
76
|
+
"tsx": "4.23.15",
|
|
77
77
|
"typescript": "7.0.2",
|
|
78
|
-
"vitest": "4.1.
|
|
78
|
+
"vitest": "4.1.11"
|
|
79
79
|
},
|
|
80
80
|
"files": [
|
|
81
81
|
"dist",
|
|
@@ -93,6 +93,11 @@
|
|
|
93
93
|
"scripts/fixtures/restart-service.mjs",
|
|
94
94
|
"THIRD_PARTY_NOTICES.md"
|
|
95
95
|
],
|
|
96
|
+
"overrides": {
|
|
97
|
+
"@tailwindcss/cli@4.3.3": {
|
|
98
|
+
"@parcel/watcher": "2.6.0"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
96
101
|
"dependencies": {
|
|
97
102
|
"@radix-ui/react-collapsible": "1.1.20",
|
|
98
103
|
"@radix-ui/react-dialog": "1.1.23",
|
|
@@ -105,15 +110,15 @@
|
|
|
105
110
|
"@radix-ui/react-switch": "1.3.7",
|
|
106
111
|
"@radix-ui/react-tabs": "1.1.21",
|
|
107
112
|
"@radix-ui/react-tooltip": "1.2.16",
|
|
108
|
-
"@slack/socket-mode": "3.0
|
|
109
|
-
"@xyflow/react": "12.
|
|
113
|
+
"@slack/socket-mode": "3.1.0",
|
|
114
|
+
"@xyflow/react": "12.12.0",
|
|
110
115
|
"class-variance-authority": "0.7.1",
|
|
111
116
|
"clsx": "2.1.1",
|
|
112
117
|
"discord.js": "14.27.0",
|
|
113
|
-
"imapflow": "2.
|
|
114
|
-
"lucide-react": "1.
|
|
115
|
-
"mailparser": "3.9.
|
|
116
|
-
"nodemailer": "10.0.
|
|
118
|
+
"imapflow": "2.2.5",
|
|
119
|
+
"lucide-react": "1.52.0",
|
|
120
|
+
"mailparser": "3.9.36",
|
|
121
|
+
"nodemailer": "10.0.15",
|
|
117
122
|
"react": "19.3.0",
|
|
118
123
|
"react-dom": "19.3.0",
|
|
119
124
|
"sonner": "2.0.8",
|