diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 135cf80dd..adf49549a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: detect-libc: specifier: ^2.1.2 version: 2.1.2 + discord.js: + specifier: ^14.25.1 + version: 14.25.1 dotenv: specifier: ^17.2.3 version: 17.2.3 @@ -290,6 +293,34 @@ packages: '@cacheable/utils@2.3.2': resolution: {integrity: sha512-8kGE2P+HjfY8FglaOiW+y8qxcaQAfAhVML+i66XJR3YX5FtyDqn6Txctr3K2FrbxLKixRRYYBWMbuGciOhYNDg==} + '@discordjs/builders@1.13.1': + resolution: {integrity: sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==} + engines: {node: '>=16.11.0'} + + '@discordjs/collection@1.5.3': + resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/collection@2.1.1': + resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} + engines: {node: '>=18'} + + '@discordjs/formatters@0.6.2': + resolution: {integrity: sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/rest@2.6.0': + resolution: {integrity: sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==} + engines: {node: '>=18'} + + '@discordjs/util@1.2.0': + resolution: {integrity: sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==} + engines: {node: '>=18'} + + '@discordjs/ws@1.2.3': + resolution: {integrity: sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==} + engines: {node: '>=16.11.0'} + '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} @@ -1017,6 +1048,18 @@ packages: cpu: [x64] os: [win32] + '@sapphire/async-queue@1.5.5': + resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@sapphire/shapeshift@4.0.0': + resolution: {integrity: sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==} + engines: {node: '>=v16'} + + '@sapphire/snowflake@3.5.3': + resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + '@sinclair/typebox@0.34.41': resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==} @@ -1145,6 +1188,10 @@ packages: '@vitest/utils@4.0.16': resolution: {integrity: sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==} + '@vladfrangu/async_event_emitter@2.4.7': + resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + '@wasm-audio-decoders/common@9.0.7': resolution: {integrity: sha512-WRaUuWSKV7pkttBygml/a6dIEpatq2nnZGFIoPTc5yPLkxL6Wk4YaslPM98OPQvWacvNZ+Py9xROGDtrFBDzag==} @@ -1440,6 +1487,13 @@ packages: resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} engines: {node: '>=0.3.1'} + discord-api-types@0.38.37: + resolution: {integrity: sha512-Cv47jzY1jkGkh5sv0bfHYqGgKOWO1peOrGMkDFM4UmaGMOTgOW8QSexhvixa9sVOiz8MnVOBryWYyw/CEVhj7w==} + + discord.js@14.25.1: + resolution: {integrity: sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==} + engines: {node: '>=18'} + docx-preview@0.3.7: resolution: {integrity: sha512-Lav69CTA/IYZPJTsKH7oYeoZjyg96N0wEJMNslGJnZJ+dMUZK85Lt5ASC79yUlD48ecWjuv+rkcmFt6EVPV0Xg==} @@ -1896,6 +1950,9 @@ packages: lit@3.3.1: resolution: {integrity: sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==} + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -1918,6 +1975,9 @@ packages: lucide@0.562.0: resolution: {integrity: sha512-k1Fb8ZMnRQovWRlea7Jr0b9UKA29IM7/cu79+mJrhVohvA2YC/Ti3Sk+G+h/SIu3IlrKT4RAbWMHUBBQd1O6XA==} + magic-bytes.js@1.12.1: + resolution: {integrity: sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -2503,6 +2563,9 @@ packages: ts-algebra@2.0.0: resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-mixer@6.0.4: + resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2537,6 +2600,10 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici@6.21.3: + resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} + engines: {node: '>=18.17'} + undici@7.16.0: resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} engines: {node: '>=20.18.1'} @@ -2839,6 +2906,55 @@ snapshots: hashery: 1.3.0 keyv: 5.5.5 + '@discordjs/builders@1.13.1': + dependencies: + '@discordjs/formatters': 0.6.2 + '@discordjs/util': 1.2.0 + '@sapphire/shapeshift': 4.0.0 + discord-api-types: 0.38.37 + fast-deep-equal: 3.1.3 + ts-mixer: 6.0.4 + tslib: 2.8.1 + + '@discordjs/collection@1.5.3': {} + + '@discordjs/collection@2.1.1': {} + + '@discordjs/formatters@0.6.2': + dependencies: + discord-api-types: 0.38.37 + + '@discordjs/rest@2.6.0': + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/util': 1.2.0 + '@sapphire/async-queue': 1.5.5 + '@sapphire/snowflake': 3.5.3 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.37 + magic-bytes.js: 1.12.1 + tslib: 2.8.1 + undici: 6.21.3 + + '@discordjs/util@1.2.0': + dependencies: + discord-api-types: 0.38.37 + + '@discordjs/ws@1.2.3': + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/rest': 2.6.0 + '@discordjs/util': 1.2.0 + '@sapphire/async-queue': 1.5.5 + '@types/ws': 8.18.1 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.37 + tslib: 2.8.1 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -3398,6 +3514,15 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.54.0': optional: true + '@sapphire/async-queue@1.5.5': {} + + '@sapphire/shapeshift@4.0.0': + dependencies: + fast-deep-equal: 3.1.3 + lodash: 4.17.21 + + '@sapphire/snowflake@3.5.3': {} + '@sinclair/typebox@0.34.41': {} '@standard-schema/spec@1.1.0': {} @@ -3553,6 +3678,8 @@ snapshots: '@vitest/pretty-format': 4.0.16 tinyrainbow: 3.0.3 + '@vladfrangu/async_event_emitter@2.4.7': {} + '@wasm-audio-decoders/common@9.0.7': dependencies: '@eshaz/web-worker': 1.2.2 @@ -3856,6 +3983,27 @@ snapshots: diff@8.0.2: {} + discord-api-types@0.38.37: {} + + discord.js@14.25.1: + dependencies: + '@discordjs/builders': 1.13.1 + '@discordjs/collection': 1.5.3 + '@discordjs/formatters': 0.6.2 + '@discordjs/rest': 2.6.0 + '@discordjs/util': 1.2.0 + '@discordjs/ws': 1.2.3 + '@sapphire/snowflake': 3.5.3 + discord-api-types: 0.38.37 + fast-deep-equal: 3.1.3 + lodash.snakecase: 4.1.1 + magic-bytes.js: 1.12.1 + tslib: 2.8.1 + undici: 6.21.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + docx-preview@0.3.7: dependencies: jszip: 3.10.1 @@ -4365,6 +4513,8 @@ snapshots: lit-element: 4.2.1 lit-html: 3.3.1 + lodash.snakecase@4.1.1: {} + lodash@4.17.21: {} long@4.0.0: {} @@ -4379,6 +4529,8 @@ snapshots: lucide@0.562.0: {} + magic-bytes.js@1.12.1: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5053,6 +5205,8 @@ snapshots: ts-algebra@2.0.0: {} + ts-mixer@6.0.4: {} + tslib@2.8.1: {} tslog@4.10.2: {} @@ -5082,6 +5236,8 @@ snapshots: undici-types@7.16.0: {} + undici@6.21.3: {} + undici@7.16.0: {} unicode-properties@1.4.1: diff --git a/src/auto-reply/reply.triggers.test.ts b/src/auto-reply/reply.triggers.test.ts index 1c6835f33..c0fecc8b9 100644 --- a/src/auto-reply/reply.triggers.test.ts +++ b/src/auto-reply/reply.triggers.test.ts @@ -392,84 +392,102 @@ describe("trigger handling", () => { describe("group intro prompts", () => { it("labels Discord groups using the surface metadata", async () => { - const commandSpy = vi - .spyOn(commandReply, "runCommandReply") - .mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 1 } }); + await withTempHome(async (home) => { + vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + payloads: [{ text: "ok" }], + meta: { + durationMs: 1, + agentMeta: { sessionId: "s", provider: "p", model: "m" }, + }, + }); - await getReplyFromConfig( - { - Body: "status update", - From: "group:dev", - To: "+1888", - ChatType: "group", - GroupSubject: "Release Squad", - GroupMembers: "Alice, Bob", - Surface: "discord", - }, - {}, - baseCfg, - ); + await getReplyFromConfig( + { + Body: "status update", + From: "group:dev", + To: "+1888", + ChatType: "group", + GroupSubject: "Release Squad", + GroupMembers: "Alice, Bob", + Surface: "discord", + }, + {}, + makeCfg(home), + ); - expect(commandSpy).toHaveBeenCalledOnce(); - const body = - commandSpy.mock.calls.at(-1)?.[0]?.templatingCtx.Body ?? ""; - const intro = body.split("\n\n")[0]; - expect(intro).toBe( - 'You are replying inside the Discord group "Release Squad". Group members: Alice, Bob. Address the specific sender noted in the message context.', - ); + expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); + const extraSystemPrompt = + vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0] + ?.extraSystemPrompt ?? ""; + expect(extraSystemPrompt).toBe( + 'You are replying inside the Discord group "Release Squad". Group members: Alice, Bob. Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). Address the specific sender noted in the message context.', + ); + }); }); it("keeps WhatsApp labeling for WhatsApp group chats", async () => { - const commandSpy = vi - .spyOn(commandReply, "runCommandReply") - .mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 1 } }); + await withTempHome(async (home) => { + vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + payloads: [{ text: "ok" }], + meta: { + durationMs: 1, + agentMeta: { sessionId: "s", provider: "p", model: "m" }, + }, + }); - await getReplyFromConfig( - { - Body: "ping", - From: "123@g.us", - To: "+1999", - ChatType: "group", - GroupSubject: "Ops", - Surface: "whatsapp", - }, - {}, - baseCfg, - ); + await getReplyFromConfig( + { + Body: "ping", + From: "123@g.us", + To: "+1999", + ChatType: "group", + GroupSubject: "Ops", + Surface: "whatsapp", + }, + {}, + makeCfg(home), + ); - expect(commandSpy).toHaveBeenCalledOnce(); - const body = - commandSpy.mock.calls.at(-1)?.[0]?.templatingCtx.Body ?? ""; - const intro = body.split("\n\n")[0]; - expect(intro).toBe( - 'You are replying inside the WhatsApp group "Ops". Address the specific sender noted in the message context.', - ); + expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); + const extraSystemPrompt = + vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0] + ?.extraSystemPrompt ?? ""; + expect(extraSystemPrompt).toBe( + 'You are replying inside the WhatsApp group "Ops". Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). Address the specific sender noted in the message context.', + ); + }); }); it("labels Telegram groups using their own surface", async () => { - const commandSpy = vi - .spyOn(commandReply, "runCommandReply") - .mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 1 } }); + await withTempHome(async (home) => { + vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + payloads: [{ text: "ok" }], + meta: { + durationMs: 1, + agentMeta: { sessionId: "s", provider: "p", model: "m" }, + }, + }); - await getReplyFromConfig( - { - Body: "ping", - From: "group:tg", - To: "+1777", - ChatType: "group", - GroupSubject: "Dev Chat", - Surface: "telegram", - }, - {}, - baseCfg, - ); + await getReplyFromConfig( + { + Body: "ping", + From: "group:tg", + To: "+1777", + ChatType: "group", + GroupSubject: "Dev Chat", + Surface: "telegram", + }, + {}, + makeCfg(home), + ); - expect(commandSpy).toHaveBeenCalledOnce(); - const body = - commandSpy.mock.calls.at(-1)?.[0]?.templatingCtx.Body ?? ""; - const intro = body.split("\n\n")[0]; - expect(intro).toBe( - 'You are replying inside the Telegram group "Dev Chat". Address the specific sender noted in the message context.', - ); + expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); + const extraSystemPrompt = + vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0] + ?.extraSystemPrompt ?? ""; + expect(extraSystemPrompt).toBe( + 'You are replying inside the Telegram group "Dev Chat". Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). Address the specific sender noted in the message context.', + ); + }); }); }); diff --git a/src/commands/health.test.ts b/src/commands/health.test.ts index 6a3f860e2..de6b956cd 100644 --- a/src/commands/health.test.ts +++ b/src/commands/health.test.ts @@ -29,6 +29,7 @@ describe("healthCommand", () => { connect: { ok: true, elapsedMs: 10 }, }, telegram: { configured: true, probe: { ok: true, elapsedMs: 1 } }, + discord: { configured: false }, heartbeatSeconds: 60, sessions: { path: "/tmp/sessions.json", @@ -54,6 +55,7 @@ describe("healthCommand", () => { durationMs: 5, web: { linked: false, authAgeMs: null }, telegram: { configured: false }, + discord: { configured: false }, heartbeatSeconds: 60, sessions: { path: "/tmp/sessions.json", count: 0, recent: [] }, } satisfies HealthSummary); diff --git a/src/globals.ts b/src/globals.ts index a837db32e..a1e2dbf57 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -13,14 +13,13 @@ export function isVerbose() { } export function logVerbose(message: string) { - // if (globalVerbose) { + if (!globalVerbose) return; console.log(chalk.gray(message)); try { getLogger().debug({ message }, "verbose"); } catch { // ignore logger failures to avoid breaking verbose printing } - // } } export function setYes(v: boolean) {