@phuetz/code-buddy 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -165
- package/dist/agent/autonomous/agentic-coding-contract.d.ts +6 -6
- package/dist/agent/codebuddy-agent.d.ts +1 -1
- package/dist/agent/codebuddy-agent.js +69 -14
- package/dist/agent/execution/agent-executor.js +122 -12
- package/dist/agent/execution/tool-dependency-graph.js +7 -0
- package/dist/agent/execution/tool-hooks.d.ts +6 -0
- package/dist/agent/execution/tool-hooks.js +13 -5
- package/dist/agent/execution/tool-selection-strategy.d.ts +11 -2
- package/dist/agent/execution/tool-selection-strategy.js +67 -10
- package/dist/agent/extended-thinking.d.ts +8 -0
- package/dist/agent/extended-thinking.js +27 -0
- package/dist/agent/hermes-browser-backends.js +151 -11
- package/dist/agent/hermes-claw-migrate.d.ts +59 -0
- package/dist/agent/hermes-claw-migrate.js +427 -26
- package/dist/agent/hermes-parity-manifest.js +78 -71
- package/dist/agent/hermes-runtime-lifecycle.d.ts +67 -0
- package/dist/agent/hermes-runtime-lifecycle.js +468 -0
- package/dist/agent/middleware/index.d.ts +1 -0
- package/dist/agent/middleware/index.js +1 -0
- package/dist/agent/middleware/visual-validation-middleware.d.ts +17 -0
- package/dist/agent/middleware/visual-validation-middleware.js +67 -0
- package/dist/agent/model-benchmark.d.ts +77 -0
- package/dist/agent/model-benchmark.js +309 -0
- package/dist/agent/model-tier.d.ts +14 -0
- package/dist/agent/model-tier.js +71 -0
- package/dist/agent/multi-agent/agents/orchestrator-agent.js +17 -6
- package/dist/agent/multi-agent/multi-agent-system.js +3 -1
- package/dist/agent/multi-agent/session-fleet-bridge.d.ts +2 -0
- package/dist/agent/multi-agent/session-fleet-bridge.js +9 -1
- package/dist/agent/multi-agent/session-tools.js +22 -5
- package/dist/agent/reasoning/mcts.js +20 -6
- package/dist/agent/session-end-flush.d.ts +6 -1
- package/dist/agent/session-end-flush.js +20 -4
- package/dist/agent/streaming/streaming-handler.d.ts +7 -0
- package/dist/agent/streaming/streaming-handler.js +8 -1
- package/dist/agent/tool-executor.js +1 -0
- package/dist/agent/tool-handler.d.ts +5 -0
- package/dist/agent/tool-handler.js +130 -4
- package/dist/browser-automation/browser-operator-executor.js +15 -0
- package/dist/browser-automation/browser-use-runner.d.ts +96 -0
- package/dist/browser-automation/browser-use-runner.js +492 -0
- package/dist/browser-automation/camofox-runner.d.ts +107 -0
- package/dist/browser-automation/camofox-runner.js +287 -0
- package/dist/channels/dingtalk/index.js +3 -0
- package/dist/channels/discord/client.d.ts +9 -0
- package/dist/channels/discord/client.js +12 -0
- package/dist/channels/feishu/index.d.ts +167 -1
- package/dist/channels/feishu/index.js +432 -7
- package/dist/channels/gateway-lifecycle.d.ts +132 -0
- package/dist/channels/gateway-lifecycle.js +219 -0
- package/dist/channels/google-chat/index.d.ts +5 -1
- package/dist/channels/google-chat/index.js +5 -1
- package/dist/channels/imessage/index.d.ts +14 -0
- package/dist/channels/imessage/index.js +64 -17
- package/dist/channels/index.d.ts +4 -0
- package/dist/channels/index.js +4 -0
- package/dist/channels/irc/index.d.ts +73 -3
- package/dist/channels/irc/index.js +446 -11
- package/dist/channels/line/index.js +3 -0
- package/dist/channels/mattermost/index.d.ts +51 -4
- package/dist/channels/mattermost/index.js +303 -20
- package/dist/channels/nextcloud-talk/index.d.ts +89 -8
- package/dist/channels/nextcloud-talk/index.js +367 -16
- package/dist/channels/nostr/index.d.ts +121 -1
- package/dist/channels/nostr/index.js +396 -8
- package/dist/channels/ntfy/index.js +3 -0
- package/dist/channels/qq/index.js +3 -0
- package/dist/channels/slash-parity.d.ts +117 -0
- package/dist/channels/slash-parity.js +185 -0
- package/dist/channels/synology-chat/index.js +3 -0
- package/dist/channels/teams/index.d.ts +3 -0
- package/dist/channels/teams/index.js +3 -0
- package/dist/channels/telegram/client.d.ts +9 -0
- package/dist/channels/telegram/client.js +12 -0
- package/dist/channels/twilio-voice/index.js +3 -0
- package/dist/channels/webchat/index.d.ts +7 -1
- package/dist/channels/webchat/index.js +7 -1
- package/dist/channels/wecom/index.js +3 -0
- package/dist/channels/weixin/index.js +3 -0
- package/dist/channels/zalo/index.js +3 -0
- package/dist/cli/config-loader.js +1 -1
- package/dist/codebuddy/client.d.ts +30 -1
- package/dist/codebuddy/client.js +167 -20
- package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +5 -0
- package/dist/codebuddy/providers/provider-chatgpt-responses.js +1 -0
- package/dist/codebuddy/providers/provider-gemini-native.js +1 -1
- package/dist/codebuddy/providers/provider-openai-compat.d.ts +5 -0
- package/dist/codebuddy/providers/provider-openai-compat.js +82 -2
- package/dist/codebuddy/tool-definitions/computer-control-tools.js +32 -2
- package/dist/codebuddy/tool-definitions/core-tools.js +3 -3
- package/dist/codebuddy/tool-definitions/index.d.ts +2 -1
- package/dist/codebuddy/tool-definitions/index.js +2 -1
- package/dist/codebuddy/tool-definitions/multimodal-tools.d.ts +1 -0
- package/dist/codebuddy/tool-definitions/multimodal-tools.js +44 -1
- package/dist/codebuddy/tool-definitions/windows-tools.d.ts +2 -0
- package/dist/codebuddy/tool-definitions/windows-tools.js +34 -0
- package/dist/codebuddy/tools.d.ts +1 -1
- package/dist/codebuddy/tools.js +27 -24
- package/dist/commands/cli/hermes-commands.js +108 -0
- package/dist/commands/cli/native-engine-commands.js +102 -3
- package/dist/commands/cli/utility-commands.js +45 -0
- package/dist/commands/enhanced-command-handler.js +1 -1
- package/dist/commands/goal-cli.d.ts +30 -0
- package/dist/commands/goal-cli.js +210 -27
- package/dist/commands/handlers/goal-handler.d.ts +9 -2
- package/dist/commands/handlers/goal-handler.js +36 -9
- package/dist/commands/handlers/infra-handlers.js +36 -0
- package/dist/commands/handlers/memory-handlers.js +96 -1
- package/dist/commands/headless-slash.d.ts +2 -0
- package/dist/commands/headless-slash.js +12 -0
- package/dist/commands/llm-provider-resolution.d.ts +5 -3
- package/dist/commands/llm-provider-resolution.js +87 -33
- package/dist/commands/ollama.d.ts +25 -0
- package/dist/commands/ollama.js +100 -0
- package/dist/commands/provider.d.ts +5 -0
- package/dist/commands/provider.js +106 -55
- package/dist/commands/spec-next.js +2 -1
- package/dist/commands/spec-plan.js +5 -16
- package/dist/commands/tunnel.d.ts +2 -0
- package/dist/commands/tunnel.js +48 -0
- package/dist/config/config-resolver.d.ts +2 -1
- package/dist/config/config-resolver.js +54 -41
- package/dist/config/constants.d.ts +28 -0
- package/dist/config/constants.js +7 -0
- package/dist/config/env-schema.js +756 -0
- package/dist/config/model-tools.js +4 -4
- package/dist/config/toml-config.d.ts +16 -0
- package/dist/config/toml-config.js +3 -0
- package/dist/context/context-manager-v2.js +1 -0
- package/dist/daemon/autonomous-daemon.d.ts +1 -1
- package/dist/daemon/autonomous-daemon.js +3 -3
- package/dist/daemon/autonomous-loop.js +11 -2
- package/dist/daemon/colab-goal.js +9 -1
- package/dist/daemon/cron-agent-bridge.d.ts +12 -3
- package/dist/daemon/cron-agent-bridge.js +25 -9
- package/dist/desktop/codebuddy-engine-adapter.d.ts +32 -0
- package/dist/desktop/codebuddy-engine-adapter.js +257 -80
- package/dist/desktop/engine-adapter.d.ts +14 -0
- package/dist/desktop-automation/automation-manager.js +16 -0
- package/dist/desktop-automation/omniparser-runner.d.ts +63 -0
- package/dist/desktop-automation/omniparser-runner.js +115 -0
- package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
- package/dist/desktop-automation/smart-snapshot.js +72 -5
- package/dist/doctor/index.js +40 -23
- package/dist/events/types.d.ts +11 -0
- package/dist/fleet/colab-store.js +14 -2
- package/dist/fleet/peer-session-bridge.js +50 -12
- package/dist/fleet/peer-tool-bridge.js +14 -0
- package/dist/goals/goal-decomposer.d.ts +27 -0
- package/dist/goals/goal-decomposer.js +293 -0
- package/dist/goals/goal-judge-client.d.ts +15 -0
- package/dist/goals/goal-judge-client.js +45 -0
- package/dist/goals/goal-judge.d.ts +2 -0
- package/dist/goals/goal-judge.js +35 -6
- package/dist/goals/goal-loop.d.ts +14 -0
- package/dist/goals/goal-loop.js +47 -2
- package/dist/goals/goal-manager.d.ts +6 -0
- package/dist/goals/goal-manager.js +67 -14
- package/dist/goals/goal-state.d.ts +13 -0
- package/dist/goals/goal-state.js +83 -13
- package/dist/goals/goal-store.d.ts +3 -0
- package/dist/goals/goal-store.js +31 -15
- package/dist/hooks/user-hooks.js +17 -3
- package/dist/index.js +464 -25
- package/dist/input/text-to-speech.d.ts +2 -6
- package/dist/input/text-to-speech.js +2 -27
- package/dist/integrations/screenpipe/screenpipe-client.d.ts +10 -0
- package/dist/integrations/screenpipe/screenpipe-client.js +12 -2
- package/dist/integrations/tailscale.d.ts +13 -0
- package/dist/integrations/tailscale.js +87 -34
- package/dist/knowledge/workspace-indexer.js +53 -9
- package/dist/mcp/client.js +1 -0
- package/dist/mcp/mcp-desktop-tools.d.ts +23 -0
- package/dist/mcp/mcp-desktop-tools.js +158 -0
- package/dist/mcp/mcp-server.js +2 -0
- package/dist/memory/enhanced-memory.d.ts +10 -0
- package/dist/memory/enhanced-memory.js +33 -2
- package/dist/memory/index.d.ts +3 -1
- package/dist/memory/index.js +4 -1
- package/dist/memory/memory-auto-proposer.d.ts +23 -0
- package/dist/memory/memory-auto-proposer.js +308 -0
- package/dist/memory/memory-candidate-queue.d.ts +92 -0
- package/dist/memory/memory-candidate-queue.js +261 -0
- package/dist/memory/persistent-memory.d.ts +45 -2
- package/dist/memory/persistent-memory.js +236 -40
- package/dist/ml/bayesian-qualifier.d.ts +1 -1
- package/dist/ml/bayesian-qualifier.js +35 -2
- package/dist/openclaw/gateway-bridge.js +5 -0
- package/dist/plugins/marketplace.d.ts +1 -0
- package/dist/plugins/marketplace.js +7 -0
- package/dist/prompts/execution-discipline.d.ts +14 -0
- package/dist/prompts/execution-discipline.js +29 -0
- package/dist/prompts/variation-injector.js +9 -2
- package/dist/protocols/acp/acp-agentic-runner.d.ts +7 -6
- package/dist/protocols/acp/acp-agentic-runner.js +115 -18
- package/dist/protocols/acp/acp-session-store.d.ts +23 -0
- package/dist/protocols/acp/acp-session-store.js +77 -0
- package/dist/protocols/acp/acp-stdio-server.d.ts +7 -0
- package/dist/protocols/acp/acp-stdio-server.js +71 -16
- package/dist/providers/active-llm-registry.d.ts +37 -0
- package/dist/providers/active-llm-registry.js +186 -0
- package/dist/providers/auxiliary-provider.d.ts +25 -0
- package/dist/providers/auxiliary-provider.js +192 -0
- package/dist/providers/codex-oauth.d.ts +1 -1
- package/dist/providers/codex-oauth.js +27 -4
- package/dist/providers/index.d.ts +3 -0
- package/dist/providers/index.js +4 -0
- package/dist/providers/provider-catalog.d.ts +70 -0
- package/dist/providers/provider-catalog.js +738 -0
- package/dist/providers/provider-fallback.d.ts +35 -0
- package/dist/providers/provider-fallback.js +171 -0
- package/dist/providers/xai-oauth.d.ts +128 -0
- package/dist/providers/xai-oauth.js +735 -0
- package/dist/scheduler/cron-scheduler.d.ts +15 -3
- package/dist/scheduler/cron-scheduler.js +53 -7
- package/dist/scheduler/script-runner.d.ts +2 -0
- package/dist/scheduler/script-runner.js +19 -4
- package/dist/scheduler/watchdog-handlers.js +7 -20
- package/dist/search/usearch-index.js +7 -2
- package/dist/security/tool-policy/tool-groups.js +2 -0
- package/dist/server/index.js +17 -2
- package/dist/server/routes/mobile.d.ts +12 -4
- package/dist/server/routes/mobile.js +116 -1
- package/dist/server/tls-config.d.ts +35 -0
- package/dist/server/tls-config.js +142 -0
- package/dist/server/tunnel-manager.d.ts +20 -0
- package/dist/server/tunnel-manager.js +58 -0
- package/dist/server/websocket/fleet-bridge.js +5 -0
- package/dist/services/prompt-builder.d.ts +1 -0
- package/dist/services/prompt-builder.js +49 -8
- package/dist/shared/engine-types.d.ts +15 -1
- package/dist/sidecar/sidecar-bridge.d.ts +1 -0
- package/dist/sidecar/sidecar-bridge.js +21 -10
- package/dist/spec/spec-store.js +8 -1
- package/dist/talk-mode/providers/audioreader-tts.d.ts +1 -1
- package/dist/talk-mode/providers/audioreader-tts.js +8 -8
- package/dist/templates/project-scaffolding.js +3 -2
- package/dist/themes/theme-schema.d.ts +10 -10
- package/dist/tools/application-profiles.js +38 -0
- package/dist/tools/bash/bash-tool.d.ts +1 -0
- package/dist/tools/bash/bash-tool.js +16 -2
- package/dist/tools/bash/command-validator.js +3 -0
- package/dist/tools/bash/rtk-rewrite.d.ts +16 -0
- package/dist/tools/bash/rtk-rewrite.js +100 -0
- package/dist/tools/bash/streaming-executor.js +14 -1
- package/dist/tools/computer-control-tool.d.ts +16 -1
- package/dist/tools/computer-control-tool.js +317 -5
- package/dist/tools/document-generator.d.ts +14 -0
- package/dist/tools/document-generator.js +79 -0
- package/dist/tools/execute-code-rpc-invoker.d.ts +26 -2
- package/dist/tools/execute-code-rpc-invoker.js +46 -5
- package/dist/tools/metadata.js +28 -0
- package/dist/tools/office-macro-tool.d.ts +10 -0
- package/dist/tools/office-macro-tool.js +93 -0
- package/dist/tools/registry/index.d.ts +2 -1
- package/dist/tools/registry/index.js +2 -1
- package/dist/tools/registry/memory-tools.d.ts +20 -0
- package/dist/tools/registry/memory-tools.js +225 -3
- package/dist/tools/registry/multimodal-tools.js +2 -2
- package/dist/tools/registry/vision-tools.d.ts +33 -0
- package/dist/tools/registry/vision-tools.js +308 -6
- package/dist/tools/registry/windows-tools.d.ts +2 -0
- package/dist/tools/registry/windows-tools.js +47 -0
- package/dist/tools/route-peer-tool.js +14 -0
- package/dist/tools/screenshot-tool.js +14 -2
- package/dist/tools/text-to-speech-tool.d.ts +1 -1
- package/dist/tools/text-to-speech-tool.js +2 -12
- package/dist/tools/tool-selector.js +3 -59
- package/dist/utils/config-validation/schema.d.ts +40 -7
- package/dist/utils/config-validation/schema.js +130 -1
- package/dist/utils/cost-tracker.js +21 -1
- package/dist/utils/disk-guard.d.ts +180 -0
- package/dist/utils/disk-guard.js +404 -0
- package/dist/utils/interactive-setup.js +35 -2
- package/dist/utils/model-utils.js +11 -2
- package/dist/utils/output-sanitizer.js +11 -0
- package/dist/utils/provider-detector.d.ts +8 -7
- package/dist/utils/provider-detector.js +19 -77
- package/dist/utils/settings-manager.d.ts +7 -0
- package/dist/utils/token-display.d.ts +3 -3
- package/dist/utils/token-display.js +22 -4
- package/dist/wizard/onboarding.d.ts +39 -0
- package/dist/wizard/onboarding.js +433 -21
- package/dist/wizard/provider-onboarding.d.ts +8 -23
- package/dist/wizard/provider-onboarding.js +39 -158
- package/package.json +4 -2
- package/dist/plugins/provider-onboarding.d.ts +0 -23
- package/dist/plugins/provider-onboarding.js +0 -116
- package/dist/utils/kokoro-tts.d.ts +0 -8
- package/dist/utils/kokoro-tts.js +0 -67
|
@@ -1,11 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mattermost Channel Adapter
|
|
3
3
|
*
|
|
4
|
-
* Connects to Mattermost via
|
|
5
|
-
*
|
|
4
|
+
* Connects to Mattermost via its real-time WebSocket gateway
|
|
5
|
+
* (`/api/v4/websocket`) for inbound events and the REST API
|
|
6
|
+
* (`/api/v4/posts`) for outbound messages.
|
|
7
|
+
*
|
|
8
|
+
* Transport details:
|
|
9
|
+
* - Auth: a personal-access / bot token (`config.token`) is sent both as a
|
|
10
|
+
* `Bearer` header on REST calls and via the WebSocket
|
|
11
|
+
* `authentication_challenge` action after the socket opens.
|
|
12
|
+
* - Inbound: the WS pushes JSON events; `posted` events carry the new post as
|
|
13
|
+
* a nested JSON string which we parse into an `InboundMessage`.
|
|
14
|
+
* - Outbound: `send()` POSTs `{channel_id, message}` to `/api/v4/posts`.
|
|
15
|
+
* - Resilience: unintended socket drops are recovered through the shared
|
|
16
|
+
* `ReconnectionManager` (exponential backoff + jitter), the same idiom used
|
|
17
|
+
* by the discord / slack / imessage adapters. An explicit `disconnect()`
|
|
18
|
+
* sets a `closing` flag and cancels any pending reconnect so a deliberate
|
|
19
|
+
* close never spins the backoff loop.
|
|
6
20
|
*/
|
|
21
|
+
import WebSocket from 'ws';
|
|
7
22
|
import { logger } from '../../utils/logger.js';
|
|
8
|
-
import { BaseChannel } from '../core.js';
|
|
23
|
+
import { BaseChannel, } from '../core.js';
|
|
24
|
+
import { ReconnectionManager } from '../reconnection-manager.js';
|
|
25
|
+
/** Bounded wait for the WS upgrade + auth handshake before failing connect(). */
|
|
26
|
+
const MM_CONNECT_TIMEOUT_MS = 30000;
|
|
27
|
+
/**
|
|
28
|
+
* Legacy in-process adapter.
|
|
29
|
+
*
|
|
30
|
+
* @deprecated The real transport now lives in {@link MattermostChannel}, which
|
|
31
|
+
* opens a genuine WebSocket and POSTs through the REST API. This class is kept
|
|
32
|
+
* only for backward-compatible imports (`channels/index.ts` re-export and the
|
|
33
|
+
* `new-channels.test.ts` lifecycle suite) and performs no network I/O.
|
|
34
|
+
*/
|
|
9
35
|
export class MattermostAdapter {
|
|
10
36
|
config;
|
|
11
37
|
running = false;
|
|
@@ -16,14 +42,14 @@ export class MattermostAdapter {
|
|
|
16
42
|
if (this.running) {
|
|
17
43
|
throw new Error('MattermostAdapter is already running');
|
|
18
44
|
}
|
|
19
|
-
logger.debug('MattermostAdapter:
|
|
45
|
+
logger.debug('MattermostAdapter: starting (in-process compatibility shim)', { url: this.config.url });
|
|
20
46
|
this.running = true;
|
|
21
47
|
}
|
|
22
48
|
async stop() {
|
|
23
49
|
if (!this.running) {
|
|
24
50
|
throw new Error('MattermostAdapter is not running');
|
|
25
51
|
}
|
|
26
|
-
logger.debug('MattermostAdapter:
|
|
52
|
+
logger.debug('MattermostAdapter: stopping');
|
|
27
53
|
this.running = false;
|
|
28
54
|
}
|
|
29
55
|
isRunning() {
|
|
@@ -53,8 +79,26 @@ export class MattermostAdapter {
|
|
|
53
79
|
return { ...this.config };
|
|
54
80
|
}
|
|
55
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Convert an `http(s)://host[:port]` base URL into the WebSocket gateway URL.
|
|
84
|
+
*
|
|
85
|
+
* The `^http` anchor maps `https → wss` and `http → ws` cleanly without
|
|
86
|
+
* accidentally rewriting an `http` that appears later in the string.
|
|
87
|
+
*/
|
|
88
|
+
function toWebSocketUrl(baseUrl) {
|
|
89
|
+
const wsBase = baseUrl.replace(/\/+$/, '').replace(/^http/, 'ws');
|
|
90
|
+
return `${wsBase}/api/v4/websocket`;
|
|
91
|
+
}
|
|
56
92
|
export class MattermostChannel extends BaseChannel {
|
|
57
|
-
|
|
93
|
+
mmConfig;
|
|
94
|
+
ws = null;
|
|
95
|
+
reconnectionManager;
|
|
96
|
+
/** Monotonic sequence number for outbound WS actions. */
|
|
97
|
+
seq = 1;
|
|
98
|
+
/** True once `disconnect()` is in progress so drops aren't treated as faults. */
|
|
99
|
+
closing = false;
|
|
100
|
+
/** Guards against resolving the connect() promise more than once. */
|
|
101
|
+
connectSettled = false;
|
|
58
102
|
constructor(config) {
|
|
59
103
|
super('mattermost', {
|
|
60
104
|
type: 'mattermost',
|
|
@@ -67,36 +111,79 @@ export class MattermostChannel extends BaseChannel {
|
|
|
67
111
|
rateLimit: config.rateLimit,
|
|
68
112
|
options: config.options,
|
|
69
113
|
});
|
|
70
|
-
this.
|
|
114
|
+
this.mmConfig = {
|
|
71
115
|
url: config.url,
|
|
72
116
|
token: config.token,
|
|
73
117
|
teamId: config.teamId,
|
|
118
|
+
maxRetries: config.maxRetries,
|
|
119
|
+
retryDelay: config.retryDelay,
|
|
120
|
+
};
|
|
121
|
+
// Persistent WS connection — drops are recovered with shared exponential
|
|
122
|
+
// backoff (same idiom as discord / slack / imessage).
|
|
123
|
+
this.reconnectionManager = new ReconnectionManager('mattermost', {
|
|
124
|
+
maxRetries: this.mmConfig.maxRetries ?? 10,
|
|
125
|
+
initialDelayMs: this.mmConfig.retryDelay ?? 1000,
|
|
126
|
+
maxDelayMs: 60000,
|
|
74
127
|
});
|
|
75
128
|
}
|
|
129
|
+
// ==========================================================================
|
|
130
|
+
// Lifecycle
|
|
131
|
+
// ==========================================================================
|
|
76
132
|
async connect() {
|
|
77
|
-
|
|
78
|
-
this.
|
|
79
|
-
this.status.authenticated = true;
|
|
80
|
-
this.status.lastActivity = new Date();
|
|
81
|
-
this.emit('connected', this.type);
|
|
133
|
+
this.closing = false;
|
|
134
|
+
await this.openSocket();
|
|
82
135
|
}
|
|
83
136
|
async disconnect() {
|
|
84
|
-
if (!this.status.connected)
|
|
137
|
+
if (!this.status.connected && !this.ws)
|
|
85
138
|
return;
|
|
86
|
-
|
|
139
|
+
// Mark intentional so the 'close'/'error' handlers don't schedule a
|
|
140
|
+
// reconnect, then cancel any reconnect already pending.
|
|
141
|
+
this.closing = true;
|
|
142
|
+
this.reconnectionManager.cancel();
|
|
143
|
+
if (this.ws) {
|
|
144
|
+
try {
|
|
145
|
+
this.ws.removeAllListeners();
|
|
146
|
+
this.ws.close();
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
logger.debug('MattermostChannel: error closing socket', {
|
|
150
|
+
error: error instanceof Error ? error.message : String(error),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
this.ws = null;
|
|
154
|
+
}
|
|
87
155
|
this.status.connected = false;
|
|
156
|
+
this.status.authenticated = false;
|
|
88
157
|
this.status.lastActivity = new Date();
|
|
89
158
|
this.emit('disconnected', this.type);
|
|
90
159
|
}
|
|
91
160
|
async send(message) {
|
|
92
161
|
try {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
:
|
|
162
|
+
const url = `${this.mmConfig.url.replace(/\/+$/, '')}/api/v4/posts`;
|
|
163
|
+
const body = {
|
|
164
|
+
channel_id: message.channelId,
|
|
165
|
+
message: message.content,
|
|
166
|
+
};
|
|
167
|
+
if (message.replyTo) {
|
|
168
|
+
body.root_id = message.replyTo;
|
|
169
|
+
}
|
|
170
|
+
const response = await fetch(url, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: {
|
|
173
|
+
'Content-Type': 'application/json',
|
|
174
|
+
Authorization: `Bearer ${this.mmConfig.token}`,
|
|
175
|
+
},
|
|
176
|
+
body: JSON.stringify(body),
|
|
177
|
+
});
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
const errorText = await response.text().catch(() => '');
|
|
180
|
+
throw new Error(`Mattermost API error: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ''}`);
|
|
181
|
+
}
|
|
182
|
+
const post = (await response.json());
|
|
96
183
|
this.status.lastActivity = new Date();
|
|
97
184
|
return {
|
|
98
|
-
success:
|
|
99
|
-
messageId:
|
|
185
|
+
success: true,
|
|
186
|
+
messageId: post.id,
|
|
100
187
|
timestamp: new Date(),
|
|
101
188
|
};
|
|
102
189
|
}
|
|
@@ -108,6 +195,202 @@ export class MattermostChannel extends BaseChannel {
|
|
|
108
195
|
};
|
|
109
196
|
}
|
|
110
197
|
}
|
|
198
|
+
// ==========================================================================
|
|
199
|
+
// WebSocket transport
|
|
200
|
+
// ==========================================================================
|
|
201
|
+
openSocket() {
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
this.connectSettled = false;
|
|
204
|
+
const wsUrl = toWebSocketUrl(this.mmConfig.url);
|
|
205
|
+
logger.debug('MattermostChannel: opening WebSocket', { wsUrl });
|
|
206
|
+
let ws;
|
|
207
|
+
try {
|
|
208
|
+
ws = new WebSocket(wsUrl);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
this.ws = ws;
|
|
215
|
+
// The WS upgrade can succeed while the server never sends a frame (bad
|
|
216
|
+
// token held open, wrong service on the port). Without a timeout the
|
|
217
|
+
// connect() Promise would never settle. Bound the handshake.
|
|
218
|
+
const connectTimer = setTimeout(() => {
|
|
219
|
+
if (this.connectSettled)
|
|
220
|
+
return;
|
|
221
|
+
this.connectSettled = true;
|
|
222
|
+
try {
|
|
223
|
+
ws.close();
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
/* ignore */
|
|
227
|
+
}
|
|
228
|
+
reject(new Error(`Mattermost WebSocket did not authenticate within ${MM_CONNECT_TIMEOUT_MS}ms`));
|
|
229
|
+
}, MM_CONNECT_TIMEOUT_MS);
|
|
230
|
+
if (typeof connectTimer.unref === 'function') {
|
|
231
|
+
connectTimer.unref();
|
|
232
|
+
}
|
|
233
|
+
const onConnected = () => {
|
|
234
|
+
clearTimeout(connectTimer);
|
|
235
|
+
resolve();
|
|
236
|
+
};
|
|
237
|
+
ws.on('open', () => {
|
|
238
|
+
// Mattermost expects the auth challenge as the first frame after open.
|
|
239
|
+
this.sendAction('authentication_challenge', { token: this.mmConfig.token });
|
|
240
|
+
});
|
|
241
|
+
ws.on('message', (data) => {
|
|
242
|
+
try {
|
|
243
|
+
const event = JSON.parse(data.toString());
|
|
244
|
+
this.handleWsEvent(event, onConnected);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
logger.warn('MattermostChannel: failed to parse WS frame', {
|
|
248
|
+
error: error instanceof Error ? error.message : String(error),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
ws.on('close', () => {
|
|
253
|
+
clearTimeout(connectTimer);
|
|
254
|
+
// A close before the handshake settles must reject connect() (don't hang).
|
|
255
|
+
if (!this.connectSettled) {
|
|
256
|
+
this.connectSettled = true;
|
|
257
|
+
reject(new Error('Mattermost WebSocket closed before authentication'));
|
|
258
|
+
}
|
|
259
|
+
this.handleDrop();
|
|
260
|
+
});
|
|
261
|
+
ws.on('error', (error) => {
|
|
262
|
+
clearTimeout(connectTimer);
|
|
263
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
264
|
+
if (this.listenerCount('error') > 0) {
|
|
265
|
+
this.emit('error', this.type, err);
|
|
266
|
+
}
|
|
267
|
+
if (!this.connectSettled) {
|
|
268
|
+
this.connectSettled = true;
|
|
269
|
+
reject(err);
|
|
270
|
+
}
|
|
271
|
+
this.handleDrop();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
handleWsEvent(event, resolveConnect) {
|
|
276
|
+
// The `hello` event (or a successful auth reply) confirms the session.
|
|
277
|
+
const authedReply = event.seq_reply !== undefined && event.status === 'OK';
|
|
278
|
+
if (event.event === 'hello' || authedReply) {
|
|
279
|
+
this.onAuthenticated();
|
|
280
|
+
if (!this.connectSettled) {
|
|
281
|
+
this.connectSettled = true;
|
|
282
|
+
resolveConnect();
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (event.event === 'posted') {
|
|
287
|
+
this.handlePostedEvent(event);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// Respond to a server-side ping with a pong when applicable.
|
|
291
|
+
if (event.event === 'ping') {
|
|
292
|
+
this.sendAction('pong', {});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
onAuthenticated() {
|
|
296
|
+
// Connectivity confirmed — reset the shared backoff state so a future drop
|
|
297
|
+
// starts from delay 0 rather than inheriting a stale retryCount.
|
|
298
|
+
this.reconnectionManager.onConnected();
|
|
299
|
+
this.status.connected = true;
|
|
300
|
+
this.status.authenticated = true;
|
|
301
|
+
this.status.lastActivity = new Date();
|
|
302
|
+
this.emit('connected', this.type);
|
|
303
|
+
logger.debug('MattermostChannel: authenticated and connected');
|
|
304
|
+
}
|
|
305
|
+
handlePostedEvent(event) {
|
|
306
|
+
const rawPost = event.data?.['post'];
|
|
307
|
+
if (typeof rawPost !== 'string')
|
|
308
|
+
return;
|
|
309
|
+
let post;
|
|
310
|
+
try {
|
|
311
|
+
post = JSON.parse(rawPost);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
logger.warn('MattermostChannel: failed to parse nested post JSON', {
|
|
315
|
+
error: error instanceof Error ? error.message : String(error),
|
|
316
|
+
});
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
// System posts (joins, header changes, …) have a non-empty `type` — skip.
|
|
320
|
+
if (post.type)
|
|
321
|
+
return;
|
|
322
|
+
const channelName = typeof event.data?.['channel_name'] === 'string'
|
|
323
|
+
? event.data['channel_name']
|
|
324
|
+
: undefined;
|
|
325
|
+
const inbound = {
|
|
326
|
+
id: post.id,
|
|
327
|
+
channel: {
|
|
328
|
+
id: post.channel_id,
|
|
329
|
+
type: 'mattermost',
|
|
330
|
+
name: channelName,
|
|
331
|
+
},
|
|
332
|
+
sender: {
|
|
333
|
+
id: post.user_id,
|
|
334
|
+
},
|
|
335
|
+
content: post.message,
|
|
336
|
+
contentType: 'text',
|
|
337
|
+
timestamp: post.create_at ? new Date(post.create_at) : new Date(),
|
|
338
|
+
threadId: post.root_id || undefined,
|
|
339
|
+
replyTo: post.root_id || undefined,
|
|
340
|
+
raw: { event, post },
|
|
341
|
+
};
|
|
342
|
+
this.status.lastActivity = new Date();
|
|
343
|
+
const parsed = this.parseCommand(inbound);
|
|
344
|
+
this.emit('message', parsed);
|
|
345
|
+
if (parsed.isCommand) {
|
|
346
|
+
this.emit('command', parsed);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Handle a socket drop. If the close was intentional (`disconnect()`), do
|
|
351
|
+
* nothing. Otherwise schedule a reconnect through the shared manager.
|
|
352
|
+
*/
|
|
353
|
+
handleDrop() {
|
|
354
|
+
if (this.closing)
|
|
355
|
+
return;
|
|
356
|
+
const wasConnected = this.status.connected;
|
|
357
|
+
this.status.connected = false;
|
|
358
|
+
this.status.authenticated = false;
|
|
359
|
+
// Tear down the dead socket so we don't double-handle further events.
|
|
360
|
+
if (this.ws) {
|
|
361
|
+
try {
|
|
362
|
+
this.ws.removeAllListeners();
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
/* ignore */
|
|
366
|
+
}
|
|
367
|
+
this.ws = null;
|
|
368
|
+
}
|
|
369
|
+
if (wasConnected) {
|
|
370
|
+
this.emit('disconnected', this.type);
|
|
371
|
+
}
|
|
372
|
+
logger.debug('MattermostChannel: connection dropped, scheduling reconnect');
|
|
373
|
+
this.reconnectionManager.scheduleReconnect(async () => {
|
|
374
|
+
await this.openSocket();
|
|
375
|
+
// onConnected() is called from onAuthenticated() once `hello` arrives.
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
sendAction(action, data) {
|
|
379
|
+
if (this.ws?.readyState !== WebSocket.OPEN)
|
|
380
|
+
return;
|
|
381
|
+
this.ws.send(JSON.stringify({
|
|
382
|
+
seq: this.seq++,
|
|
383
|
+
action,
|
|
384
|
+
data,
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
/** Expose the resolved WS gateway URL (handy for diagnostics / tests). */
|
|
388
|
+
getWebSocketUrl() {
|
|
389
|
+
return toWebSocketUrl(this.mmConfig.url);
|
|
390
|
+
}
|
|
391
|
+
getConfig() {
|
|
392
|
+
return { ...this.mmConfig };
|
|
393
|
+
}
|
|
111
394
|
}
|
|
112
|
-
export default
|
|
395
|
+
export default MattermostChannel;
|
|
113
396
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Nextcloud Talk Channel Adapter
|
|
3
3
|
*
|
|
4
|
-
* Connects to Nextcloud Talk for messaging
|
|
5
|
-
*
|
|
4
|
+
* Connects to Nextcloud Talk (Spreed) for messaging via the real Spreed chat
|
|
5
|
+
* REST API using an HTTP long-poll receive loop (`lookIntoFuture=1`), with
|
|
6
|
+
* auto-reconnect through the shared ReconnectionManager (same idiom as
|
|
7
|
+
* imessage/irc/discord/slack).
|
|
6
8
|
*/
|
|
9
|
+
import { EventEmitter } from 'events';
|
|
7
10
|
import { BaseChannel, ChannelConfig, DeliveryResult, OutboundMessage } from '../core.js';
|
|
8
11
|
export interface NextcloudTalkConfig {
|
|
9
12
|
url: string;
|
|
10
13
|
username: string;
|
|
11
14
|
password: string;
|
|
12
15
|
}
|
|
13
|
-
export interface NextcloudTalkChannelConfig extends ChannelConfig {
|
|
14
|
-
url: string;
|
|
15
|
-
username: string;
|
|
16
|
-
password: string;
|
|
17
|
-
}
|
|
18
16
|
export declare class NextcloudTalkAdapter {
|
|
19
17
|
private config;
|
|
20
18
|
private running;
|
|
@@ -40,8 +38,91 @@ export declare class NextcloudTalkAdapter {
|
|
|
40
38
|
}>;
|
|
41
39
|
getConfig(): NextcloudTalkConfig;
|
|
42
40
|
}
|
|
41
|
+
export interface NextcloudTalkChannelConfig extends ChannelConfig {
|
|
42
|
+
/** Base Nextcloud URL, e.g. https://cloud.example.com (no trailing slash needed). */
|
|
43
|
+
url: string;
|
|
44
|
+
/** Nextcloud user for HTTP Basic auth. */
|
|
45
|
+
username: string;
|
|
46
|
+
/** App password / login password for HTTP Basic auth. */
|
|
47
|
+
password: string;
|
|
48
|
+
/** Optional explicit app password (preferred over `password` if set). */
|
|
49
|
+
appPassword?: string;
|
|
50
|
+
/** Optional bearer token — used instead of Basic auth when provided. */
|
|
51
|
+
token?: string;
|
|
52
|
+
bearer?: string;
|
|
53
|
+
/** Conversation/room token to long-poll for inbound messages. */
|
|
54
|
+
roomToken?: string;
|
|
55
|
+
/** Long-poll hold timeout in seconds (server-side). Default 30. */
|
|
56
|
+
pollTimeoutSecs?: number;
|
|
57
|
+
maxRetries?: number;
|
|
58
|
+
retryDelayMs?: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Real Nextcloud Talk (Spreed) client. Holds one outstanding long-poll request
|
|
62
|
+
* against `chat/<roomToken>?lookIntoFuture=1` at a time; the server returns 200
|
|
63
|
+
* with new messages or 304 on timeout. Drops are recovered via the shared
|
|
64
|
+
* ReconnectionManager. This is a genuinely persistent connection model — the
|
|
65
|
+
* same shape as imessage/irc.
|
|
66
|
+
*/
|
|
67
|
+
export declare class NextcloudTalkClient extends EventEmitter {
|
|
68
|
+
private readonly baseUrl;
|
|
69
|
+
private readonly username;
|
|
70
|
+
private readonly password;
|
|
71
|
+
private readonly bearer;
|
|
72
|
+
private readonly roomToken;
|
|
73
|
+
private readonly pollTimeoutSecs;
|
|
74
|
+
private running;
|
|
75
|
+
/** True while an intentional disconnect is in progress — suppresses reconnect. */
|
|
76
|
+
private closing;
|
|
77
|
+
private reconnecting;
|
|
78
|
+
private lastKnownMessageId;
|
|
79
|
+
/** AbortController for the in-flight long-poll request. */
|
|
80
|
+
private pollAbort;
|
|
81
|
+
private readonly reconnectionManager;
|
|
82
|
+
constructor(config: NextcloudTalkChannelConfig);
|
|
83
|
+
isRunning(): boolean;
|
|
84
|
+
getRoomToken(): string;
|
|
85
|
+
/**
|
|
86
|
+
* Begin the long-poll receive loop. Returns immediately — `connect()` does
|
|
87
|
+
* NOT block on the first poll (a long-poll can hold open up to
|
|
88
|
+
* `pollTimeoutSecs`). The first successful poll (200 or 304) emits
|
|
89
|
+
* 'connected'.
|
|
90
|
+
*/
|
|
91
|
+
start(): void;
|
|
92
|
+
/**
|
|
93
|
+
* Stop the loop, abort the in-flight request, and cancel any pending
|
|
94
|
+
* reconnect. Idempotent.
|
|
95
|
+
*/
|
|
96
|
+
stop(): void;
|
|
97
|
+
private runPollLoop;
|
|
98
|
+
/**
|
|
99
|
+
* Issue one long-poll request. Resolves on 200 (with messages emitted) or 304
|
|
100
|
+
* (timeout, no messages). Throws on network failure / non-2xx-non-304 status
|
|
101
|
+
* (a real drop → triggers reconnect).
|
|
102
|
+
*/
|
|
103
|
+
private pollOnce;
|
|
104
|
+
private toInbound;
|
|
105
|
+
/** Emit 'connected' exactly once per session (on first successful poll). */
|
|
106
|
+
private connectedEmitted;
|
|
107
|
+
private onConnectedOnce;
|
|
108
|
+
/**
|
|
109
|
+
* Recover a dropped connection via the shared ReconnectionManager
|
|
110
|
+
* (exponential backoff + jitter + exhaustion). `scheduleReconnect` is
|
|
111
|
+
* single-shot, so a failed attempt re-drives it (deferred past the manager's
|
|
112
|
+
* internal active-guard) until recovery or exhaustion — same idiom as
|
|
113
|
+
* irc/imessage.
|
|
114
|
+
*/
|
|
115
|
+
private scheduleReconnect;
|
|
116
|
+
sendMessage(roomToken: string, text: string): Promise<{
|
|
117
|
+
success: boolean;
|
|
118
|
+
messageId?: string;
|
|
119
|
+
}>;
|
|
120
|
+
private buildHeaders;
|
|
121
|
+
private isAbortError;
|
|
122
|
+
}
|
|
43
123
|
export declare class NextcloudTalkChannel extends BaseChannel {
|
|
44
|
-
private
|
|
124
|
+
private client;
|
|
125
|
+
private readonly ncConfig;
|
|
45
126
|
constructor(config: NextcloudTalkChannelConfig);
|
|
46
127
|
connect(): Promise<void>;
|
|
47
128
|
disconnect(): Promise<void>;
|