@opengsd/gsd-pi 1.0.2-dev.50223bc → 1.0.2-dev.5961fbf
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resource-loader.d.ts +5 -0
- package/dist/resource-loader.js +24 -8
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/loop.js +19 -0
- package/dist/resources/extensions/gsd/auto/phases.js +1 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +2 -54
- package/dist/resources/extensions/gsd/worktree-post-create-hook.js +117 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
- package/dist/web/standalone/.next/server/chunks/1834.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/dist/web/standalone/package.json +0 -1
- package/dist/worktree-cli.d.ts +0 -2
- package/dist/worktree-cli.js +21 -9
- package/package.json +9 -4
- package/packages/cloud-mcp-gateway/bin/gsd-cloud-mcp-gateway.js +14 -0
- package/packages/cloud-mcp-gateway/package.json +5 -4
- package/packages/contracts/package.json +2 -2
- package/packages/daemon/bin/gsd-daemon.js +14 -0
- package/packages/daemon/bin/gsd-mcp-runtime.js +14 -0
- package/packages/daemon/bin/gsd-mcp.js +14 -0
- package/packages/daemon/dist/channel-manager.d.ts +53 -0
- package/packages/daemon/dist/channel-manager.d.ts.map +1 -0
- package/packages/daemon/dist/channel-manager.js +167 -0
- package/packages/daemon/dist/channel-manager.js.map +1 -0
- package/packages/daemon/dist/cli.d.ts +3 -0
- package/packages/daemon/dist/cli.d.ts.map +1 -0
- package/packages/daemon/dist/cli.js +94 -0
- package/packages/daemon/dist/cli.js.map +1 -0
- package/packages/daemon/dist/cloud-cli.d.ts +7 -0
- package/packages/daemon/dist/cloud-cli.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-cli.js +96 -0
- package/packages/daemon/dist/cloud-cli.js.map +1 -0
- package/packages/daemon/dist/cloud-config.d.ts +18 -0
- package/packages/daemon/dist/cloud-config.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-config.js +209 -0
- package/packages/daemon/dist/cloud-config.js.map +1 -0
- package/packages/daemon/dist/cloud-config.test.d.ts +2 -0
- package/packages/daemon/dist/cloud-config.test.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-config.test.js +132 -0
- package/packages/daemon/dist/cloud-config.test.js.map +1 -0
- package/packages/daemon/dist/cloud-runtime.d.ts +26 -0
- package/packages/daemon/dist/cloud-runtime.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-runtime.js +180 -0
- package/packages/daemon/dist/cloud-runtime.js.map +1 -0
- package/packages/daemon/dist/cloud-runtime.test.d.ts +2 -0
- package/packages/daemon/dist/cloud-runtime.test.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-runtime.test.js +28 -0
- package/packages/daemon/dist/cloud-runtime.test.js.map +1 -0
- package/packages/daemon/dist/cloud-token.d.ts +3 -0
- package/packages/daemon/dist/cloud-token.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-token.js +37 -0
- package/packages/daemon/dist/cloud-token.js.map +1 -0
- package/packages/daemon/dist/commands.d.ts +25 -0
- package/packages/daemon/dist/commands.d.ts.map +1 -0
- package/packages/daemon/dist/commands.js +81 -0
- package/packages/daemon/dist/commands.js.map +1 -0
- package/packages/daemon/dist/config.d.ts +17 -0
- package/packages/daemon/dist/config.d.ts.map +1 -0
- package/packages/daemon/dist/config.js +146 -0
- package/packages/daemon/dist/config.js.map +1 -0
- package/packages/daemon/dist/daemon.d.ts +38 -0
- package/packages/daemon/dist/daemon.d.ts.map +1 -0
- package/packages/daemon/dist/daemon.js +194 -0
- package/packages/daemon/dist/daemon.js.map +1 -0
- package/packages/daemon/dist/daemon.test.d.ts +2 -0
- package/packages/daemon/dist/daemon.test.d.ts.map +1 -0
- package/packages/daemon/dist/daemon.test.js +692 -0
- package/packages/daemon/dist/daemon.test.js.map +1 -0
- package/packages/daemon/dist/discord-bot.d.ts +70 -0
- package/packages/daemon/dist/discord-bot.d.ts.map +1 -0
- package/packages/daemon/dist/discord-bot.js +433 -0
- package/packages/daemon/dist/discord-bot.js.map +1 -0
- package/packages/daemon/dist/discord-bot.test.d.ts +2 -0
- package/packages/daemon/dist/discord-bot.test.d.ts.map +1 -0
- package/packages/daemon/dist/discord-bot.test.js +667 -0
- package/packages/daemon/dist/discord-bot.test.js.map +1 -0
- package/packages/daemon/dist/event-bridge.d.ts +72 -0
- package/packages/daemon/dist/event-bridge.d.ts.map +1 -0
- package/packages/daemon/dist/event-bridge.js +366 -0
- package/packages/daemon/dist/event-bridge.js.map +1 -0
- package/packages/daemon/dist/event-bridge.test.d.ts +9 -0
- package/packages/daemon/dist/event-bridge.test.d.ts.map +1 -0
- package/packages/daemon/dist/event-bridge.test.js +528 -0
- package/packages/daemon/dist/event-bridge.test.js.map +1 -0
- package/packages/daemon/dist/event-formatter.d.ts +34 -0
- package/packages/daemon/dist/event-formatter.d.ts.map +1 -0
- package/packages/daemon/dist/event-formatter.js +355 -0
- package/packages/daemon/dist/event-formatter.js.map +1 -0
- package/packages/daemon/dist/event-formatter.test.d.ts +2 -0
- package/packages/daemon/dist/event-formatter.test.d.ts.map +1 -0
- package/packages/daemon/dist/event-formatter.test.js +333 -0
- package/packages/daemon/dist/event-formatter.test.js.map +1 -0
- package/packages/daemon/dist/index.d.ts +25 -0
- package/packages/daemon/dist/index.d.ts.map +1 -0
- package/packages/daemon/dist/index.js +17 -0
- package/packages/daemon/dist/index.js.map +1 -0
- package/packages/daemon/dist/launchd.d.ts +49 -0
- package/packages/daemon/dist/launchd.d.ts.map +1 -0
- package/packages/daemon/dist/launchd.js +188 -0
- package/packages/daemon/dist/launchd.js.map +1 -0
- package/packages/daemon/dist/launchd.test.d.ts +2 -0
- package/packages/daemon/dist/launchd.test.d.ts.map +1 -0
- package/packages/daemon/dist/launchd.test.js +296 -0
- package/packages/daemon/dist/launchd.test.js.map +1 -0
- package/packages/daemon/dist/local-tool-executor.d.ts +22 -0
- package/packages/daemon/dist/local-tool-executor.d.ts.map +1 -0
- package/packages/daemon/dist/local-tool-executor.js +307 -0
- package/packages/daemon/dist/local-tool-executor.js.map +1 -0
- package/packages/daemon/dist/local-tool-executor.test.d.ts +2 -0
- package/packages/daemon/dist/local-tool-executor.test.d.ts.map +1 -0
- package/packages/daemon/dist/local-tool-executor.test.js +111 -0
- package/packages/daemon/dist/local-tool-executor.test.js.map +1 -0
- package/packages/daemon/dist/logger.d.ts +25 -0
- package/packages/daemon/dist/logger.d.ts.map +1 -0
- package/packages/daemon/dist/logger.js +72 -0
- package/packages/daemon/dist/logger.js.map +1 -0
- package/packages/daemon/dist/mcp-cli.d.ts +3 -0
- package/packages/daemon/dist/mcp-cli.d.ts.map +1 -0
- package/packages/daemon/dist/mcp-cli.js +8 -0
- package/packages/daemon/dist/mcp-cli.js.map +1 -0
- package/packages/daemon/dist/mcp-cli.test.d.ts +2 -0
- package/packages/daemon/dist/mcp-cli.test.d.ts.map +1 -0
- package/packages/daemon/dist/mcp-cli.test.js +13 -0
- package/packages/daemon/dist/mcp-cli.test.js.map +1 -0
- package/packages/daemon/dist/mcp-runtime-cli.d.ts +3 -0
- package/packages/daemon/dist/mcp-runtime-cli.d.ts.map +1 -0
- package/packages/daemon/dist/mcp-runtime-cli.js +8 -0
- package/packages/daemon/dist/mcp-runtime-cli.js.map +1 -0
- package/packages/daemon/dist/message-batcher.d.ts +78 -0
- package/packages/daemon/dist/message-batcher.d.ts.map +1 -0
- package/packages/daemon/dist/message-batcher.js +173 -0
- package/packages/daemon/dist/message-batcher.js.map +1 -0
- package/packages/daemon/dist/message-batcher.test.d.ts +2 -0
- package/packages/daemon/dist/message-batcher.test.d.ts.map +1 -0
- package/packages/daemon/dist/message-batcher.test.js +242 -0
- package/packages/daemon/dist/message-batcher.test.js.map +1 -0
- package/packages/daemon/dist/orchestrator.d.ts +98 -0
- package/packages/daemon/dist/orchestrator.d.ts.map +1 -0
- package/packages/daemon/dist/orchestrator.js +359 -0
- package/packages/daemon/dist/orchestrator.js.map +1 -0
- package/packages/daemon/dist/orchestrator.test.d.ts +8 -0
- package/packages/daemon/dist/orchestrator.test.d.ts.map +1 -0
- package/packages/daemon/dist/orchestrator.test.js +425 -0
- package/packages/daemon/dist/orchestrator.test.js.map +1 -0
- package/packages/daemon/dist/project-scanner.d.ts +18 -0
- package/packages/daemon/dist/project-scanner.d.ts.map +1 -0
- package/packages/daemon/dist/project-scanner.js +90 -0
- package/packages/daemon/dist/project-scanner.js.map +1 -0
- package/packages/daemon/dist/project-scanner.test.d.ts +5 -0
- package/packages/daemon/dist/project-scanner.test.d.ts.map +1 -0
- package/packages/daemon/dist/project-scanner.test.js +183 -0
- package/packages/daemon/dist/project-scanner.test.js.map +1 -0
- package/packages/daemon/dist/session-manager.d.ts +70 -0
- package/packages/daemon/dist/session-manager.d.ts.map +1 -0
- package/packages/daemon/dist/session-manager.js +358 -0
- package/packages/daemon/dist/session-manager.js.map +1 -0
- package/packages/daemon/dist/session-manager.test.d.ts +9 -0
- package/packages/daemon/dist/session-manager.test.d.ts.map +1 -0
- package/packages/daemon/dist/session-manager.test.js +616 -0
- package/packages/daemon/dist/session-manager.test.js.map +1 -0
- package/packages/daemon/dist/types.d.ts +133 -0
- package/packages/daemon/dist/types.d.ts.map +1 -0
- package/packages/daemon/dist/types.js +8 -0
- package/packages/daemon/dist/types.js.map +1 -0
- package/packages/daemon/dist/verbosity.d.ts +27 -0
- package/packages/daemon/dist/verbosity.d.ts.map +1 -0
- package/packages/daemon/dist/verbosity.js +86 -0
- package/packages/daemon/dist/verbosity.js.map +1 -0
- package/packages/daemon/dist/verbosity.test.d.ts +2 -0
- package/packages/daemon/dist/verbosity.test.d.ts.map +1 -0
- package/packages/daemon/dist/verbosity.test.js +136 -0
- package/packages/daemon/dist/verbosity.test.js.map +1 -0
- package/packages/daemon/package.json +9 -8
- package/packages/gsd-agent-core/package.json +6 -6
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +3 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +2 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +8 -8
- package/packages/mcp-server/bin/gsd-mcp-server.js +14 -0
- package/packages/mcp-server/package.json +6 -5
- package/packages/native/package.json +3 -3
- package/packages/pi-agent-core/package.json +4 -4
- package/packages/pi-ai/bin/pi-ai.js +14 -0
- package/packages/pi-ai/dist/models.generated.d.ts +0 -17
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +18 -35
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +5 -4
- package/packages/pi-coding-agent/dist/core/tools/read.d.ts +2 -2
- package/packages/pi-coding-agent/dist/core/tools/read.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/read.js +5 -3
- package/packages/pi-coding-agent/dist/core/tools/read.js.map +1 -1
- package/packages/pi-coding-agent/package.json +9 -9
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +3 -3
- package/pkg/package.json +1 -1
- package/scripts/ensure-workspace-builds.cjs +4 -4
- package/scripts/install/deps.js +10 -0
- package/src/resources/extensions/gsd/auto/loop.ts +22 -0
- package/src/resources/extensions/gsd/auto/phases.ts +1 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +2 -56
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +141 -1
- package/src/resources/extensions/gsd/worktree-post-create-hook.ts +127 -0
- package/dist/tsconfig.extensions.tsbuildinfo +0 -1
- /package/dist/web/standalone/.next/static/{JP7xjsa5zSaO76XhE-mFJ → spUYLkQXoHJyxYOMH9VQy}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{JP7xjsa5zSaO76XhE-mFJ → spUYLkQXoHJyxYOMH9VQy}/_ssgManifest.js +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-cli.d.ts","sourceRoot":"","sources":["../src/mcp-cli.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { handleCloudRuntimeCommand } from "./cloud-cli.js";
|
|
3
|
+
handleCloudRuntimeCommand(process.argv.slice(2), { binaryName: "gsd-mcp" }).catch((err) => {
|
|
4
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5
|
+
process.stderr.write(`gsd-mcp: fatal: ${msg}\n`);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
});
|
|
8
|
+
//# sourceMappingURL=mcp-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-cli.js","sourceRoot":"","sources":["../src/mcp-cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAE3D,yBAAyB,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IACjG,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;IACjD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { handleCloudRuntimeCommand } from \"./cloud-cli.js\";\n\nhandleCloudRuntimeCommand(process.argv.slice(2), { binaryName: \"gsd-mcp\" }).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : String(err);\n process.stderr.write(`gsd-mcp: fatal: ${msg}\\n`);\n process.exit(1);\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-cli.test.d.ts","sourceRoot":"","sources":["../src/mcp-cli.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
test("gsd-mcp help prints execution-focused usage", () => {
|
|
8
|
+
const result = execFileSync(process.execPath, [join(__dirname, "mcp-cli.js"), "--help"], { encoding: "utf-8", timeout: 5000 });
|
|
9
|
+
assert.ok(result.includes("Usage: gsd-mcp"));
|
|
10
|
+
assert.ok(result.includes("pair --gateway"));
|
|
11
|
+
assert.ok(result.includes("connect"));
|
|
12
|
+
});
|
|
13
|
+
//# sourceMappingURL=mcp-cli.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-cli.test.js","sourceRoot":"","sources":["../src/mcp-cli.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,IAAI,CAAC,6CAA6C,EAAE,GAAG,EAAE;IACvD,MAAM,MAAM,GAAG,YAAY,CACzB,OAAO,CAAC,QAAQ,EAChB,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,QAAQ,CAAC,EACzC,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CACrC,CAAC;IACF,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAC7C,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAC7C,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;AACxC,CAAC,CAAC,CAAC","sourcesContent":["import assert from \"node:assert/strict\";\nimport { execFileSync } from \"node:child_process\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { test } from \"node:test\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\ntest(\"gsd-mcp help prints execution-focused usage\", () => {\n const result = execFileSync(\n process.execPath,\n [join(__dirname, \"mcp-cli.js\"), \"--help\"],\n { encoding: \"utf-8\", timeout: 5000 },\n );\n assert.ok(result.includes(\"Usage: gsd-mcp\"));\n assert.ok(result.includes(\"pair --gateway\"));\n assert.ok(result.includes(\"connect\"));\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-runtime-cli.d.ts","sourceRoot":"","sources":["../src/mcp-runtime-cli.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { handleCloudRuntimeCommand } from "./cloud-cli.js";
|
|
3
|
+
handleCloudRuntimeCommand(process.argv.slice(2), { binaryName: "gsd-mcp-runtime" }).catch((err) => {
|
|
4
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5
|
+
process.stderr.write(`gsd-mcp-runtime: fatal: ${msg}\n`);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
});
|
|
8
|
+
//# sourceMappingURL=mcp-runtime-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-runtime-cli.js","sourceRoot":"","sources":["../src/mcp-runtime-cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAE3D,yBAAyB,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,UAAU,EAAE,iBAAiB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IACzG,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,2BAA2B,GAAG,IAAI,CAAC,CAAC;IACzD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { handleCloudRuntimeCommand } from \"./cloud-cli.js\";\n\nhandleCloudRuntimeCommand(process.argv.slice(2), { binaryName: \"gsd-mcp-runtime\" }).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : String(err);\n process.stderr.write(`gsd-mcp-runtime: fatal: ${msg}\\n`);\n process.exit(1);\n});\n"]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* message-batcher.ts — Rate-limit-aware message batcher for Discord.
|
|
3
|
+
*
|
|
4
|
+
* Accumulates FormattedEvent payloads and flushes them to a Discord channel
|
|
5
|
+
* respecting the 5 msg/5s rate limit. Supports:
|
|
6
|
+
* - Timer-based periodic flush (default 1.5s)
|
|
7
|
+
* - Capacity-based flush when buffer hits maxBatchSize
|
|
8
|
+
* - Immediate priority flush for blockers (bypasses batching)
|
|
9
|
+
* - Combining multiple embeds into a single send() call
|
|
10
|
+
* - Error isolation: send() failures are logged, never crash the batcher
|
|
11
|
+
*/
|
|
12
|
+
import type { FormattedEvent } from './types.js';
|
|
13
|
+
/** Payload passed to the send callback — matches Discord TextChannel.send() shape. */
|
|
14
|
+
export interface SendPayload {
|
|
15
|
+
content: string;
|
|
16
|
+
embeds: unknown[];
|
|
17
|
+
components: unknown[];
|
|
18
|
+
}
|
|
19
|
+
/** Send callback abstraction. Returns void or a promise. */
|
|
20
|
+
export type SendFn = (payload: SendPayload) => Promise<void> | void;
|
|
21
|
+
/** Logger interface — just needs error/warn/debug. */
|
|
22
|
+
export interface BatcherLogger {
|
|
23
|
+
error(msg: string, data?: Record<string, unknown>): void;
|
|
24
|
+
warn(msg: string, data?: Record<string, unknown>): void;
|
|
25
|
+
debug(msg: string, data?: Record<string, unknown>): void;
|
|
26
|
+
}
|
|
27
|
+
/** MessageBatcher configuration options. */
|
|
28
|
+
export interface BatcherOptions {
|
|
29
|
+
/** Interval between timed flushes in ms. Default: 1500 */
|
|
30
|
+
flushIntervalMs?: number;
|
|
31
|
+
/** Max events before triggering an immediate capacity flush. Default: 4 */
|
|
32
|
+
maxBatchSize?: number;
|
|
33
|
+
}
|
|
34
|
+
export declare class MessageBatcher {
|
|
35
|
+
private readonly send;
|
|
36
|
+
private readonly logger;
|
|
37
|
+
private readonly flushIntervalMs;
|
|
38
|
+
private readonly maxBatchSize;
|
|
39
|
+
private buffer;
|
|
40
|
+
private timer;
|
|
41
|
+
private flushing;
|
|
42
|
+
private destroyed;
|
|
43
|
+
constructor(send: SendFn, logger?: BatcherLogger, options?: BatcherOptions);
|
|
44
|
+
/** Start the periodic flush timer. */
|
|
45
|
+
start(): void;
|
|
46
|
+
/** Stop the periodic flush timer without flushing. */
|
|
47
|
+
stop(): void;
|
|
48
|
+
/** Flush remaining buffer and stop. Safe to call multiple times. */
|
|
49
|
+
destroy(): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Enqueue a formatted event for batched sending.
|
|
52
|
+
* Triggers an immediate capacity flush if buffer reaches maxBatchSize.
|
|
53
|
+
*/
|
|
54
|
+
enqueue(formatted: FormattedEvent): void;
|
|
55
|
+
/**
|
|
56
|
+
* Immediately send a high-priority event (e.g. blocker).
|
|
57
|
+
* Flushes any pending buffer first, then sends the priority event alone.
|
|
58
|
+
*/
|
|
59
|
+
enqueueImmediate(formatted: FormattedEvent): Promise<void>;
|
|
60
|
+
/** Current number of events in the buffer (for testing/diagnostics). */
|
|
61
|
+
get pending(): number;
|
|
62
|
+
/**
|
|
63
|
+
* Flush the current buffer as a single Discord message.
|
|
64
|
+
* Multiple embeds are combined into one send() call (Discord supports up to 10).
|
|
65
|
+
* No-op if buffer is empty.
|
|
66
|
+
*/
|
|
67
|
+
private flush;
|
|
68
|
+
/**
|
|
69
|
+
* Build a SendPayload from a batch of FormattedEvents and invoke the send callback.
|
|
70
|
+
* Catches and logs errors — never throws.
|
|
71
|
+
*
|
|
72
|
+
* For batched messages (2+ events), we send content-only to avoid duplication
|
|
73
|
+
* between content text and embed descriptions, and to stay under Discord's
|
|
74
|
+
* 10-embed limit. Single-event sends include the embed for rich formatting.
|
|
75
|
+
*/
|
|
76
|
+
private doSend;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=message-batcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-batcher.d.ts","sourceRoot":"","sources":["../src/message-batcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAMjD,sFAAsF;AACtF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,EAAE,CAAC;IAClB,UAAU,EAAE,OAAO,EAAE,CAAC;CACvB;AAED,4DAA4D;AAC5D,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEpE,sDAAsD;AACtD,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC1D;AAED,4CAA4C;AAC5C,MAAM,WAAW,cAAc;IAC7B,0DAA0D;IAC1D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAgBD,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IAEtC,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,SAAS,CAAS;gBAEd,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,cAAc;IAW1E,sCAAsC;IACtC,KAAK,IAAI,IAAI;IAYb,sDAAsD;IACtD,IAAI,IAAI,IAAI;IAQZ,oEAAoE;IAC9D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ9B;;;OAGG;IACH,OAAO,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI;IAQxC;;;OAGG;IACG,gBAAgB,CAAC,SAAS,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAQhE,wEAAwE;IACxE,IAAI,OAAO,IAAI,MAAM,CAEpB;IAMD;;;;OAIG;YACW,KAAK;IAanB;;;;;;;OAOG;YACW,MAAM;CA8CrB"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* message-batcher.ts — Rate-limit-aware message batcher for Discord.
|
|
3
|
+
*
|
|
4
|
+
* Accumulates FormattedEvent payloads and flushes them to a Discord channel
|
|
5
|
+
* respecting the 5 msg/5s rate limit. Supports:
|
|
6
|
+
* - Timer-based periodic flush (default 1.5s)
|
|
7
|
+
* - Capacity-based flush when buffer hits maxBatchSize
|
|
8
|
+
* - Immediate priority flush for blockers (bypasses batching)
|
|
9
|
+
* - Combining multiple embeds into a single send() call
|
|
10
|
+
* - Error isolation: send() failures are logged, never crash the batcher
|
|
11
|
+
*/
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Default no-op logger
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const noopLogger = {
|
|
16
|
+
error() { },
|
|
17
|
+
warn() { },
|
|
18
|
+
debug() { },
|
|
19
|
+
};
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// MessageBatcher
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
export class MessageBatcher {
|
|
24
|
+
send;
|
|
25
|
+
logger;
|
|
26
|
+
flushIntervalMs;
|
|
27
|
+
maxBatchSize;
|
|
28
|
+
buffer = [];
|
|
29
|
+
timer = null;
|
|
30
|
+
flushing = false;
|
|
31
|
+
destroyed = false;
|
|
32
|
+
constructor(send, logger, options) {
|
|
33
|
+
this.send = send;
|
|
34
|
+
this.logger = logger ?? noopLogger;
|
|
35
|
+
this.flushIntervalMs = options?.flushIntervalMs ?? 1500;
|
|
36
|
+
this.maxBatchSize = options?.maxBatchSize ?? 4;
|
|
37
|
+
}
|
|
38
|
+
// -----------------------------------------------------------------------
|
|
39
|
+
// Public API
|
|
40
|
+
// -----------------------------------------------------------------------
|
|
41
|
+
/** Start the periodic flush timer. */
|
|
42
|
+
start() {
|
|
43
|
+
if (this.timer)
|
|
44
|
+
return; // already running
|
|
45
|
+
this.timer = setInterval(() => {
|
|
46
|
+
void this.flush();
|
|
47
|
+
}, this.flushIntervalMs);
|
|
48
|
+
// Don't hold the process open for the timer
|
|
49
|
+
if (this.timer && typeof this.timer === 'object' && 'unref' in this.timer) {
|
|
50
|
+
this.timer.unref();
|
|
51
|
+
}
|
|
52
|
+
this.logger.debug('Batcher started', { flushIntervalMs: this.flushIntervalMs });
|
|
53
|
+
}
|
|
54
|
+
/** Stop the periodic flush timer without flushing. */
|
|
55
|
+
stop() {
|
|
56
|
+
if (this.timer) {
|
|
57
|
+
clearInterval(this.timer);
|
|
58
|
+
this.timer = null;
|
|
59
|
+
}
|
|
60
|
+
this.logger.debug('Batcher stopped');
|
|
61
|
+
}
|
|
62
|
+
/** Flush remaining buffer and stop. Safe to call multiple times. */
|
|
63
|
+
async destroy() {
|
|
64
|
+
if (this.destroyed)
|
|
65
|
+
return;
|
|
66
|
+
this.destroyed = true;
|
|
67
|
+
this.stop();
|
|
68
|
+
await this.flush();
|
|
69
|
+
this.logger.debug('Batcher destroyed');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Enqueue a formatted event for batched sending.
|
|
73
|
+
* Triggers an immediate capacity flush if buffer reaches maxBatchSize.
|
|
74
|
+
*/
|
|
75
|
+
enqueue(formatted) {
|
|
76
|
+
if (this.destroyed)
|
|
77
|
+
return;
|
|
78
|
+
this.buffer.push(formatted);
|
|
79
|
+
if (this.buffer.length >= this.maxBatchSize) {
|
|
80
|
+
void this.flush();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Immediately send a high-priority event (e.g. blocker).
|
|
85
|
+
* Flushes any pending buffer first, then sends the priority event alone.
|
|
86
|
+
*/
|
|
87
|
+
async enqueueImmediate(formatted) {
|
|
88
|
+
if (this.destroyed)
|
|
89
|
+
return;
|
|
90
|
+
// Flush pending buffer first so ordering is preserved
|
|
91
|
+
await this.flush();
|
|
92
|
+
// Send the priority event immediately, alone
|
|
93
|
+
await this.doSend([formatted]);
|
|
94
|
+
}
|
|
95
|
+
/** Current number of events in the buffer (for testing/diagnostics). */
|
|
96
|
+
get pending() {
|
|
97
|
+
return this.buffer.length;
|
|
98
|
+
}
|
|
99
|
+
// -----------------------------------------------------------------------
|
|
100
|
+
// Internal
|
|
101
|
+
// -----------------------------------------------------------------------
|
|
102
|
+
/**
|
|
103
|
+
* Flush the current buffer as a single Discord message.
|
|
104
|
+
* Multiple embeds are combined into one send() call (Discord supports up to 10).
|
|
105
|
+
* No-op if buffer is empty.
|
|
106
|
+
*/
|
|
107
|
+
async flush() {
|
|
108
|
+
if (this.buffer.length === 0)
|
|
109
|
+
return;
|
|
110
|
+
if (this.flushing)
|
|
111
|
+
return; // prevent re-entrant flush
|
|
112
|
+
this.flushing = true;
|
|
113
|
+
const batch = this.buffer.splice(0); // take all
|
|
114
|
+
try {
|
|
115
|
+
await this.doSend(batch);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
this.flushing = false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Build a SendPayload from a batch of FormattedEvents and invoke the send callback.
|
|
123
|
+
* Catches and logs errors — never throws.
|
|
124
|
+
*
|
|
125
|
+
* For batched messages (2+ events), we send content-only to avoid duplication
|
|
126
|
+
* between content text and embed descriptions, and to stay under Discord's
|
|
127
|
+
* 10-embed limit. Single-event sends include the embed for rich formatting.
|
|
128
|
+
*/
|
|
129
|
+
async doSend(batch) {
|
|
130
|
+
if (batch.length === 0)
|
|
131
|
+
return;
|
|
132
|
+
// Combine content lines
|
|
133
|
+
const content = batch.map((e) => e.content).join('\n');
|
|
134
|
+
// For single events, include the embed for rich formatting.
|
|
135
|
+
// For batches, skip embeds — the content lines are self-descriptive and
|
|
136
|
+
// embeds would duplicate the information + risk hitting Discord's 10-embed cap.
|
|
137
|
+
const embeds = [];
|
|
138
|
+
if (batch.length === 1 && batch[0].embed) {
|
|
139
|
+
embeds.push(batch[0].embed);
|
|
140
|
+
}
|
|
141
|
+
// Collect all component rows (only from the last event with components —
|
|
142
|
+
// Discord only supports one set of components per message)
|
|
143
|
+
let components = [];
|
|
144
|
+
for (const e of batch) {
|
|
145
|
+
if (e.components && e.components.length > 0) {
|
|
146
|
+
components = e.components;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const payload = { content, embeds, components };
|
|
150
|
+
try {
|
|
151
|
+
await this.send(payload);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
155
|
+
this.logger.error('Batcher send failed', { error: message, batchSize: batch.length });
|
|
156
|
+
// Retry once after a short delay
|
|
157
|
+
try {
|
|
158
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
159
|
+
await this.send(payload);
|
|
160
|
+
this.logger.debug('Batcher retry succeeded');
|
|
161
|
+
}
|
|
162
|
+
catch (retryErr) {
|
|
163
|
+
const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
164
|
+
this.logger.warn('Batcher retry also failed, dropping batch', {
|
|
165
|
+
error: retryMessage,
|
|
166
|
+
batchSize: batch.length,
|
|
167
|
+
});
|
|
168
|
+
// Drop the batch — don't re-enqueue to prevent infinite loops
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=message-batcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-batcher.js","sourceRoot":"","sources":["../src/message-batcher.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * message-batcher.ts — Rate-limit-aware message batcher for Discord.\n *\n * Accumulates FormattedEvent payloads and flushes them to a Discord channel\n * respecting the 5 msg/5s rate limit. Supports:\n * - Timer-based periodic flush (default 1.5s)\n * - Capacity-based flush when buffer hits maxBatchSize\n * - Immediate priority flush for blockers (bypasses batching)\n * - Combining multiple embeds into a single send() call\n * - Error isolation: send() failures are logged, never crash the batcher\n */\n\nimport type { FormattedEvent } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Payload passed to the send callback — matches Discord TextChannel.send() shape. */\nexport interface SendPayload {\n content: string;\n embeds: unknown[];\n components: unknown[];\n}\n\n/** Send callback abstraction. Returns void or a promise. */\nexport type SendFn = (payload: SendPayload) => Promise<void> | void;\n\n/** Logger interface — just needs error/warn/debug. */\nexport interface BatcherLogger {\n error(msg: string, data?: Record<string, unknown>): void;\n warn(msg: string, data?: Record<string, unknown>): void;\n debug(msg: string, data?: Record<string, unknown>): void;\n}\n\n/** MessageBatcher configuration options. */\nexport interface BatcherOptions {\n /** Interval between timed flushes in ms. Default: 1500 */\n flushIntervalMs?: number;\n /** Max events before triggering an immediate capacity flush. Default: 4 */\n maxBatchSize?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Default no-op logger\n// ---------------------------------------------------------------------------\n\nconst noopLogger: BatcherLogger = {\n error() {},\n warn() {},\n debug() {},\n};\n\n// ---------------------------------------------------------------------------\n// MessageBatcher\n// ---------------------------------------------------------------------------\n\nexport class MessageBatcher {\n private readonly send: SendFn;\n private readonly logger: BatcherLogger;\n private readonly flushIntervalMs: number;\n private readonly maxBatchSize: number;\n\n private buffer: FormattedEvent[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private flushing = false;\n private destroyed = false;\n\n constructor(send: SendFn, logger?: BatcherLogger, options?: BatcherOptions) {\n this.send = send;\n this.logger = logger ?? noopLogger;\n this.flushIntervalMs = options?.flushIntervalMs ?? 1500;\n this.maxBatchSize = options?.maxBatchSize ?? 4;\n }\n\n // -----------------------------------------------------------------------\n // Public API\n // -----------------------------------------------------------------------\n\n /** Start the periodic flush timer. */\n start(): void {\n if (this.timer) return; // already running\n this.timer = setInterval(() => {\n void this.flush();\n }, this.flushIntervalMs);\n // Don't hold the process open for the timer\n if (this.timer && typeof this.timer === 'object' && 'unref' in this.timer) {\n this.timer.unref();\n }\n this.logger.debug('Batcher started', { flushIntervalMs: this.flushIntervalMs });\n }\n\n /** Stop the periodic flush timer without flushing. */\n stop(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n this.logger.debug('Batcher stopped');\n }\n\n /** Flush remaining buffer and stop. Safe to call multiple times. */\n async destroy(): Promise<void> {\n if (this.destroyed) return;\n this.destroyed = true;\n this.stop();\n await this.flush();\n this.logger.debug('Batcher destroyed');\n }\n\n /**\n * Enqueue a formatted event for batched sending.\n * Triggers an immediate capacity flush if buffer reaches maxBatchSize.\n */\n enqueue(formatted: FormattedEvent): void {\n if (this.destroyed) return;\n this.buffer.push(formatted);\n if (this.buffer.length >= this.maxBatchSize) {\n void this.flush();\n }\n }\n\n /**\n * Immediately send a high-priority event (e.g. blocker).\n * Flushes any pending buffer first, then sends the priority event alone.\n */\n async enqueueImmediate(formatted: FormattedEvent): Promise<void> {\n if (this.destroyed) return;\n // Flush pending buffer first so ordering is preserved\n await this.flush();\n // Send the priority event immediately, alone\n await this.doSend([formatted]);\n }\n\n /** Current number of events in the buffer (for testing/diagnostics). */\n get pending(): number {\n return this.buffer.length;\n }\n\n // -----------------------------------------------------------------------\n // Internal\n // -----------------------------------------------------------------------\n\n /**\n * Flush the current buffer as a single Discord message.\n * Multiple embeds are combined into one send() call (Discord supports up to 10).\n * No-op if buffer is empty.\n */\n private async flush(): Promise<void> {\n if (this.buffer.length === 0) return;\n if (this.flushing) return; // prevent re-entrant flush\n\n this.flushing = true;\n const batch = this.buffer.splice(0); // take all\n try {\n await this.doSend(batch);\n } finally {\n this.flushing = false;\n }\n }\n\n /**\n * Build a SendPayload from a batch of FormattedEvents and invoke the send callback.\n * Catches and logs errors — never throws.\n *\n * For batched messages (2+ events), we send content-only to avoid duplication\n * between content text and embed descriptions, and to stay under Discord's\n * 10-embed limit. Single-event sends include the embed for rich formatting.\n */\n private async doSend(batch: FormattedEvent[]): Promise<void> {\n if (batch.length === 0) return;\n\n // Combine content lines\n const content = batch.map((e) => e.content).join('\\n');\n\n // For single events, include the embed for rich formatting.\n // For batches, skip embeds — the content lines are self-descriptive and\n // embeds would duplicate the information + risk hitting Discord's 10-embed cap.\n const embeds: unknown[] = [];\n if (batch.length === 1 && batch[0].embed) {\n embeds.push(batch[0].embed);\n }\n\n // Collect all component rows (only from the last event with components —\n // Discord only supports one set of components per message)\n let components: unknown[] = [];\n for (const e of batch) {\n if (e.components && e.components.length > 0) {\n components = e.components;\n }\n }\n\n const payload: SendPayload = { content, embeds, components };\n\n try {\n await this.send(payload);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n this.logger.error('Batcher send failed', { error: message, batchSize: batch.length });\n\n // Retry once after a short delay\n try {\n await new Promise((r) => setTimeout(r, 1000));\n await this.send(payload);\n this.logger.debug('Batcher retry succeeded');\n } catch (retryErr) {\n const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);\n this.logger.warn('Batcher retry also failed, dropping batch', {\n error: retryMessage,\n batchSize: batch.length,\n });\n // Drop the batch — don't re-enqueue to prevent infinite loops\n }\n }\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-batcher.test.d.ts","sourceRoot":"","sources":["../src/message-batcher.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { describe, it, mock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { MessageBatcher } from './message-batcher.js';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/** Create a minimal FormattedEvent for testing. */
|
|
8
|
+
function fakeEvent(content, hasEmbed = false) {
|
|
9
|
+
const fe = { content };
|
|
10
|
+
if (hasEmbed) {
|
|
11
|
+
// Minimal mock embed — just needs to be truthy and pass through
|
|
12
|
+
fe.embed = { data: { title: content } };
|
|
13
|
+
}
|
|
14
|
+
return fe;
|
|
15
|
+
}
|
|
16
|
+
/** Create a tracking send function. */
|
|
17
|
+
function createSend() {
|
|
18
|
+
const calls = [];
|
|
19
|
+
const fn = mock.fn(async (payload) => {
|
|
20
|
+
calls.push(payload);
|
|
21
|
+
});
|
|
22
|
+
return { fn, calls };
|
|
23
|
+
}
|
|
24
|
+
/** Create a logger that captures error/warn calls. */
|
|
25
|
+
function createLogger() {
|
|
26
|
+
const errors = [];
|
|
27
|
+
const warns = [];
|
|
28
|
+
const debugs = [];
|
|
29
|
+
const logger = {
|
|
30
|
+
error(msg) { errors.push(msg); },
|
|
31
|
+
warn(msg) { warns.push(msg); },
|
|
32
|
+
debug(msg) { debugs.push(msg); },
|
|
33
|
+
};
|
|
34
|
+
return { logger, errors, warns, debugs };
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Tests
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
describe('MessageBatcher', () => {
|
|
40
|
+
describe('enqueue + capacity flush', () => {
|
|
41
|
+
it('flushes when buffer reaches maxBatchSize', async () => {
|
|
42
|
+
const { fn, calls } = createSend();
|
|
43
|
+
const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 3, flushIntervalMs: 60_000 });
|
|
44
|
+
batcher.enqueue(fakeEvent('a'));
|
|
45
|
+
batcher.enqueue(fakeEvent('b'));
|
|
46
|
+
assert.equal(calls.length, 0, 'should not flush yet');
|
|
47
|
+
batcher.enqueue(fakeEvent('c')); // hits capacity
|
|
48
|
+
// flush is async — give it a tick
|
|
49
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
50
|
+
assert.equal(calls.length, 1, 'should have flushed once');
|
|
51
|
+
assert.equal(calls[0].content, 'a\nb\nc');
|
|
52
|
+
assert.equal(batcher.pending, 0);
|
|
53
|
+
await batcher.destroy();
|
|
54
|
+
});
|
|
55
|
+
it('skips embeds for batched messages (only content)', async () => {
|
|
56
|
+
const { fn, calls } = createSend();
|
|
57
|
+
const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 2, flushIntervalMs: 60_000 });
|
|
58
|
+
batcher.enqueue(fakeEvent('a', true));
|
|
59
|
+
batcher.enqueue(fakeEvent('b', true)); // triggers flush
|
|
60
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
61
|
+
assert.equal(calls.length, 1);
|
|
62
|
+
assert.equal(calls[0].embeds.length, 0, 'batched sends skip embeds to avoid duplication');
|
|
63
|
+
assert.equal(calls[0].content, 'a\nb');
|
|
64
|
+
await batcher.destroy();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe('enqueueImmediate', () => {
|
|
68
|
+
it('flushes pending buffer then sends immediately', async () => {
|
|
69
|
+
const { fn, calls } = createSend();
|
|
70
|
+
const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 10, flushIntervalMs: 60_000 });
|
|
71
|
+
batcher.enqueue(fakeEvent('buffered-1'));
|
|
72
|
+
batcher.enqueue(fakeEvent('buffered-2'));
|
|
73
|
+
await batcher.enqueueImmediate(fakeEvent('blocker!'));
|
|
74
|
+
// First call: the pending buffer flush
|
|
75
|
+
// Second call: the immediate event
|
|
76
|
+
assert.equal(calls.length, 2, 'should have two send calls');
|
|
77
|
+
assert.equal(calls[0].content, 'buffered-1\nbuffered-2');
|
|
78
|
+
assert.equal(calls[1].content, 'blocker!');
|
|
79
|
+
assert.equal(batcher.pending, 0);
|
|
80
|
+
await batcher.destroy();
|
|
81
|
+
});
|
|
82
|
+
it('sends immediately when buffer is empty', async () => {
|
|
83
|
+
const { fn, calls } = createSend();
|
|
84
|
+
const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 10, flushIntervalMs: 60_000 });
|
|
85
|
+
await batcher.enqueueImmediate(fakeEvent('urgent'));
|
|
86
|
+
assert.equal(calls.length, 1);
|
|
87
|
+
assert.equal(calls[0].content, 'urgent');
|
|
88
|
+
await batcher.destroy();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('timer-based flush', () => {
|
|
92
|
+
it('flushes on interval', async () => {
|
|
93
|
+
const { fn, calls } = createSend();
|
|
94
|
+
const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 50 });
|
|
95
|
+
batcher.start();
|
|
96
|
+
batcher.enqueue(fakeEvent('timed-1'));
|
|
97
|
+
batcher.enqueue(fakeEvent('timed-2'));
|
|
98
|
+
// Wait longer than flushIntervalMs
|
|
99
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
100
|
+
assert.ok(calls.length >= 1, 'timer should have triggered at least one flush');
|
|
101
|
+
assert.equal(calls[0].content, 'timed-1\ntimed-2');
|
|
102
|
+
assert.equal(batcher.pending, 0);
|
|
103
|
+
await batcher.destroy();
|
|
104
|
+
});
|
|
105
|
+
it('stop prevents further timer flushes', async () => {
|
|
106
|
+
const { fn, calls } = createSend();
|
|
107
|
+
const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 30 });
|
|
108
|
+
batcher.start();
|
|
109
|
+
batcher.stop();
|
|
110
|
+
batcher.enqueue(fakeEvent('orphan'));
|
|
111
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
112
|
+
assert.equal(calls.length, 0, 'no flush after stop');
|
|
113
|
+
// Cleanup without triggering flush timer
|
|
114
|
+
batcher.stop(); // idempotent
|
|
115
|
+
// Manually drain for cleanup
|
|
116
|
+
await batcher.destroy();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('destroy', () => {
|
|
120
|
+
it('flushes remaining buffer on destroy', async () => {
|
|
121
|
+
const { fn, calls } = createSend();
|
|
122
|
+
const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 60_000 });
|
|
123
|
+
batcher.enqueue(fakeEvent('leftover-1'));
|
|
124
|
+
batcher.enqueue(fakeEvent('leftover-2'));
|
|
125
|
+
await batcher.destroy();
|
|
126
|
+
assert.equal(calls.length, 1);
|
|
127
|
+
assert.equal(calls[0].content, 'leftover-1\nleftover-2');
|
|
128
|
+
});
|
|
129
|
+
it('is idempotent — second destroy is no-op', async () => {
|
|
130
|
+
const { fn, calls } = createSend();
|
|
131
|
+
const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 60_000 });
|
|
132
|
+
batcher.enqueue(fakeEvent('once'));
|
|
133
|
+
await batcher.destroy();
|
|
134
|
+
await batcher.destroy(); // second call
|
|
135
|
+
assert.equal(calls.length, 1, 'only flushed once');
|
|
136
|
+
});
|
|
137
|
+
it('enqueue after destroy is silently ignored', async () => {
|
|
138
|
+
const { fn, calls } = createSend();
|
|
139
|
+
const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 2, flushIntervalMs: 60_000 });
|
|
140
|
+
await batcher.destroy();
|
|
141
|
+
batcher.enqueue(fakeEvent('post-destroy'));
|
|
142
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
143
|
+
assert.equal(calls.length, 0, 'no sends after destroy');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe('empty buffer', () => {
|
|
147
|
+
it('flush of empty buffer is no-op', async () => {
|
|
148
|
+
const { fn, calls } = createSend();
|
|
149
|
+
const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 60_000 });
|
|
150
|
+
batcher.start();
|
|
151
|
+
// Force a timer tick with an empty buffer
|
|
152
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
153
|
+
await batcher.destroy();
|
|
154
|
+
// Only the destroy-triggered flush, which should also be a no-op
|
|
155
|
+
assert.equal(calls.length, 0, 'no sends for empty buffer');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe('single-item flush', () => {
|
|
159
|
+
it('handles a single item in buffer at destroy', async () => {
|
|
160
|
+
const { fn, calls } = createSend();
|
|
161
|
+
const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 60_000 });
|
|
162
|
+
batcher.enqueue(fakeEvent('solo'));
|
|
163
|
+
await batcher.destroy();
|
|
164
|
+
assert.equal(calls.length, 1);
|
|
165
|
+
assert.equal(calls[0].content, 'solo');
|
|
166
|
+
assert.equal(calls[0].embeds.length, 0);
|
|
167
|
+
assert.equal(calls[0].components.length, 0);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
describe('error handling', () => {
|
|
171
|
+
it('logs error and continues when send throws', async () => {
|
|
172
|
+
let attempt = 0;
|
|
173
|
+
const sendFn = async () => {
|
|
174
|
+
attempt++;
|
|
175
|
+
throw new Error('Discord rate limit');
|
|
176
|
+
};
|
|
177
|
+
const { logger, errors, warns } = createLogger();
|
|
178
|
+
const batcher = new MessageBatcher(sendFn, logger, { maxBatchSize: 2, flushIntervalMs: 60_000 });
|
|
179
|
+
batcher.enqueue(fakeEvent('x'));
|
|
180
|
+
batcher.enqueue(fakeEvent('y')); // triggers flush
|
|
181
|
+
// Wait for flush + retry
|
|
182
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
183
|
+
assert.ok(errors.length >= 1, 'should have logged an error');
|
|
184
|
+
assert.ok(warns.length >= 1, 'should have logged a warning on retry failure');
|
|
185
|
+
assert.equal(batcher.pending, 0, 'buffer cleared even on error');
|
|
186
|
+
// Batcher should still be alive — enqueue more
|
|
187
|
+
batcher.enqueue(fakeEvent('after-error'));
|
|
188
|
+
assert.equal(batcher.pending, 1, 'can still enqueue after error');
|
|
189
|
+
await batcher.destroy();
|
|
190
|
+
});
|
|
191
|
+
it('succeeds on retry if first attempt fails', async () => {
|
|
192
|
+
let attempt = 0;
|
|
193
|
+
const calls = [];
|
|
194
|
+
const sendFn = async (payload) => {
|
|
195
|
+
attempt++;
|
|
196
|
+
if (attempt === 1)
|
|
197
|
+
throw new Error('transient');
|
|
198
|
+
calls.push(payload);
|
|
199
|
+
};
|
|
200
|
+
const { logger, errors } = createLogger();
|
|
201
|
+
const batcher = new MessageBatcher(sendFn, logger, { maxBatchSize: 2, flushIntervalMs: 60_000 });
|
|
202
|
+
batcher.enqueue(fakeEvent('retry-me'));
|
|
203
|
+
batcher.enqueue(fakeEvent('retry-too'));
|
|
204
|
+
// Wait for flush + retry delay
|
|
205
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
206
|
+
assert.equal(errors.length, 1, 'logged one error on first attempt');
|
|
207
|
+
assert.equal(calls.length, 1, 'retry succeeded');
|
|
208
|
+
assert.equal(calls[0].content, 'retry-me\nretry-too');
|
|
209
|
+
await batcher.destroy();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
describe('buffer at exactly capacity', () => {
|
|
213
|
+
it('flushes at exactly maxBatchSize', async () => {
|
|
214
|
+
const { fn, calls } = createSend();
|
|
215
|
+
const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 4, flushIntervalMs: 60_000 });
|
|
216
|
+
batcher.enqueue(fakeEvent('1'));
|
|
217
|
+
batcher.enqueue(fakeEvent('2'));
|
|
218
|
+
batcher.enqueue(fakeEvent('3'));
|
|
219
|
+
assert.equal(calls.length, 0, 'not flushed at 3/4');
|
|
220
|
+
batcher.enqueue(fakeEvent('4')); // exactly at capacity
|
|
221
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
222
|
+
assert.equal(calls.length, 1);
|
|
223
|
+
assert.equal(calls[0].content, '1\n2\n3\n4');
|
|
224
|
+
await batcher.destroy();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
describe('components handling', () => {
|
|
228
|
+
it('uses components from the last event that has them', async () => {
|
|
229
|
+
const { fn, calls } = createSend();
|
|
230
|
+
const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 3, flushIntervalMs: 60_000 });
|
|
231
|
+
const fakeRow = { type: 'ActionRow', components: [] };
|
|
232
|
+
batcher.enqueue(fakeEvent('no-components'));
|
|
233
|
+
batcher.enqueue({ content: 'with-components', components: [fakeRow] });
|
|
234
|
+
batcher.enqueue(fakeEvent('also-no-components')); // triggers flush
|
|
235
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
236
|
+
assert.equal(calls.length, 1);
|
|
237
|
+
assert.deepEqual(calls[0].components, [fakeRow]);
|
|
238
|
+
await batcher.destroy();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
//# sourceMappingURL=message-batcher.test.js.map
|