verybot 0.1.3
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.
Potentially problematic release.
This version of verybot might be problematic. Click here for more details.
- package/dist/brain/agent-registry.d.ts +75 -0
- package/dist/brain/agent-registry.js +124 -0
- package/dist/brain/agent.d.ts +146 -0
- package/dist/brain/agent.js +680 -0
- package/dist/brain/channel-store.d.ts +27 -0
- package/dist/brain/channel-store.js +78 -0
- package/dist/brain/compaction.d.ts +37 -0
- package/dist/brain/compaction.js +214 -0
- package/dist/brain/context.d.ts +33 -0
- package/dist/brain/context.js +77 -0
- package/dist/brain/delegation-store.d.ts +33 -0
- package/dist/brain/delegation-store.js +106 -0
- package/dist/brain/loop.d.ts +21 -0
- package/dist/brain/loop.js +161 -0
- package/dist/brain/mcp-adapter.d.ts +39 -0
- package/dist/brain/mcp-adapter.js +227 -0
- package/dist/brain/memory-extractor.d.ts +26 -0
- package/dist/brain/memory-extractor.js +82 -0
- package/dist/brain/providers.d.ts +10 -0
- package/dist/brain/providers.js +69 -0
- package/dist/brain/queue.d.ts +18 -0
- package/dist/brain/queue.js +84 -0
- package/dist/brain/run-tools.d.ts +47 -0
- package/dist/brain/run-tools.js +84 -0
- package/dist/brain/session-key.d.ts +23 -0
- package/dist/brain/session-key.js +41 -0
- package/dist/brain/session-state.d.ts +36 -0
- package/dist/brain/session-state.js +51 -0
- package/dist/brain/session-store.d.ts +50 -0
- package/dist/brain/session-store.js +207 -0
- package/dist/brain/session.d.ts +32 -0
- package/dist/brain/session.js +75 -0
- package/dist/brain/utils.d.ts +4 -0
- package/dist/brain/utils.js +26 -0
- package/dist/brain/worker-coordinator.d.ts +25 -0
- package/dist/brain/worker-coordinator.js +83 -0
- package/dist/channels/commands.d.ts +35 -0
- package/dist/channels/commands.js +65 -0
- package/dist/channels/discord/channel.d.ts +18 -0
- package/dist/channels/discord/channel.js +154 -0
- package/dist/channels/discord/markdown.d.ts +19 -0
- package/dist/channels/discord/markdown.js +62 -0
- package/dist/channels/manager.d.ts +29 -0
- package/dist/channels/manager.js +100 -0
- package/dist/channels/slack/channel.d.ts +26 -0
- package/dist/channels/slack/channel.js +207 -0
- package/dist/channels/slack/markdown.d.ts +19 -0
- package/dist/channels/slack/markdown.js +62 -0
- package/dist/channels/specs.d.ts +21 -0
- package/dist/channels/specs.js +96 -0
- package/dist/channels/telegram/channel.d.ts +18 -0
- package/dist/channels/telegram/channel.js +156 -0
- package/dist/channels/telegram/markdown.d.ts +17 -0
- package/dist/channels/telegram/markdown.js +66 -0
- package/dist/channels/types.d.ts +26 -0
- package/dist/channels/types.js +1 -0
- package/dist/channels/whatsapp/channel.d.ts +23 -0
- package/dist/channels/whatsapp/channel.js +242 -0
- package/dist/channels/whatsapp/markdown.d.ts +20 -0
- package/dist/channels/whatsapp/markdown.js +51 -0
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.js +78 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +13 -0
- package/dist/computer/browser/actions.d.ts +31 -0
- package/dist/computer/browser/actions.js +148 -0
- package/dist/computer/browser/manager.d.ts +55 -0
- package/dist/computer/browser/manager.js +496 -0
- package/dist/computer/browser/profile-badge.d.ts +13 -0
- package/dist/computer/browser/profile-badge.js +67 -0
- package/dist/computer/browser/screenshot.d.ts +5 -0
- package/dist/computer/browser/screenshot.js +21 -0
- package/dist/computer/browser/snapshot.d.ts +30 -0
- package/dist/computer/browser/snapshot.js +242 -0
- package/dist/computer/browser/tools.d.ts +5 -0
- package/dist/computer/browser/tools.js +167 -0
- package/dist/computer/desktop/adapter.d.ts +25 -0
- package/dist/computer/desktop/adapter.js +11 -0
- package/dist/computer/desktop/macos.d.ts +24 -0
- package/dist/computer/desktop/macos.js +223 -0
- package/dist/computer/desktop/tools.d.ts +25 -0
- package/dist/computer/desktop/tools.js +114 -0
- package/dist/config/agent-config.d.ts +41 -0
- package/dist/config/agent-config.js +14 -0
- package/dist/config/model-catalog.d.ts +22 -0
- package/dist/config/model-catalog.js +99 -0
- package/dist/config/store.d.ts +25 -0
- package/dist/config/store.js +143 -0
- package/dist/config.d.ts +103 -0
- package/dist/config.js +224 -0
- package/dist/control-ui/assets/index-BANXNUyt.js +143 -0
- package/dist/control-ui/assets/index-BSUFrP9R.css +1 -0
- package/dist/control-ui/assets/noto-sans-cyrillic-ext-wght-normal-DSNfmdVt.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-cyrillic-wght-normal-B2hlT84T.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-devanagari-wght-normal-Cv-Vwajv.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-greek-ext-wght-normal-12T8GTDR.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-greek-wght-normal-Ymb6dZNd.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-latin-ext-wght-normal-W1qJv59z.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-latin-wght-normal-BYSzYMf3.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-vietnamese-wght-normal-DLTJy58D.woff2 +0 -0
- package/dist/control-ui/index.html +14 -0
- package/dist/control-ui/vite.svg +1 -0
- package/dist/events.d.ts +2 -0
- package/dist/events.js +11 -0
- package/dist/gateway/broadcast.d.ts +5 -0
- package/dist/gateway/broadcast.js +33 -0
- package/dist/gateway/methods/chat.d.ts +24 -0
- package/dist/gateway/methods/chat.js +19 -0
- package/dist/gateway/methods/config.d.ts +13 -0
- package/dist/gateway/methods/config.js +14 -0
- package/dist/gateway/methods/models.d.ts +10 -0
- package/dist/gateway/methods/models.js +14 -0
- package/dist/gateway/methods/prompt-templates.d.ts +23 -0
- package/dist/gateway/methods/prompt-templates.js +82 -0
- package/dist/gateway/methods/scheduler.d.ts +62 -0
- package/dist/gateway/methods/scheduler.js +129 -0
- package/dist/gateway/methods/sessions.d.ts +26 -0
- package/dist/gateway/methods/sessions.js +54 -0
- package/dist/gateway/methods/skills.d.ts +35 -0
- package/dist/gateway/methods/skills.js +202 -0
- package/dist/gateway/methods/system.d.ts +12 -0
- package/dist/gateway/methods/system.js +39 -0
- package/dist/gateway/methods/tasks.d.ts +21 -0
- package/dist/gateway/methods/tasks.js +46 -0
- package/dist/gateway/methods/teams.d.ts +70 -0
- package/dist/gateway/methods/teams.js +374 -0
- package/dist/gateway/methods/tools.d.ts +6 -0
- package/dist/gateway/methods/tools.js +7 -0
- package/dist/gateway/methods/whatsapp.d.ts +19 -0
- package/dist/gateway/methods/whatsapp.js +35 -0
- package/dist/gateway/rpc.d.ts +38 -0
- package/dist/gateway/rpc.js +75 -0
- package/dist/gateway/server.d.ts +4 -0
- package/dist/gateway/server.js +133 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +212 -0
- package/dist/integrations/github.d.ts +7 -0
- package/dist/integrations/github.js +133 -0
- package/dist/integrations/mcp.d.ts +7 -0
- package/dist/integrations/mcp.js +106 -0
- package/dist/integrations/registry.d.ts +43 -0
- package/dist/integrations/registry.js +258 -0
- package/dist/integrations/scanner.d.ts +10 -0
- package/dist/integrations/scanner.js +122 -0
- package/dist/integrations/twitter.d.ts +10 -0
- package/dist/integrations/twitter.js +120 -0
- package/dist/integrations/types.d.ts +72 -0
- package/dist/integrations/types.js +1 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.js +104 -0
- package/dist/markdown/chunk.d.ts +9 -0
- package/dist/markdown/chunk.js +52 -0
- package/dist/markdown/ir.d.ts +37 -0
- package/dist/markdown/ir.js +529 -0
- package/dist/markdown/render.d.ts +22 -0
- package/dist/markdown/render.js +148 -0
- package/dist/markdown/table-render.d.ts +43 -0
- package/dist/markdown/table-render.js +219 -0
- package/dist/markdown/tables.d.ts +17 -0
- package/dist/markdown/tables.js +27 -0
- package/dist/memory/embedding.d.ts +16 -0
- package/dist/memory/embedding.js +66 -0
- package/dist/memory/extractor.d.ts +6 -0
- package/dist/memory/extractor.js +72 -0
- package/dist/memory/search.d.ts +15 -0
- package/dist/memory/search.js +57 -0
- package/dist/memory/store.d.ts +34 -0
- package/dist/memory/store.js +328 -0
- package/dist/memory/types.d.ts +9 -0
- package/dist/memory/types.js +2 -0
- package/dist/paths.d.ts +20 -0
- package/dist/paths.js +29 -0
- package/dist/prompt-templates/builtins.d.ts +2 -0
- package/dist/prompt-templates/builtins.js +72 -0
- package/dist/prompt-templates/store.d.ts +39 -0
- package/dist/prompt-templates/store.js +174 -0
- package/dist/prompt-templates/types.d.ts +10 -0
- package/dist/prompt-templates/types.js +1 -0
- package/dist/scheduler/connected-channels.d.ts +24 -0
- package/dist/scheduler/connected-channels.js +57 -0
- package/dist/scheduler/scheduler.d.ts +22 -0
- package/dist/scheduler/scheduler.js +132 -0
- package/dist/scheduler/store.d.ts +27 -0
- package/dist/scheduler/store.js +205 -0
- package/dist/scheduler/types.d.ts +29 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/security/command-validator.d.ts +22 -0
- package/dist/security/command-validator.js +160 -0
- package/dist/security/docker-sandbox.d.ts +48 -0
- package/dist/security/docker-sandbox.js +218 -0
- package/dist/security/env-filter.d.ts +8 -0
- package/dist/security/env-filter.js +41 -0
- package/dist/skills/loader.d.ts +33 -0
- package/dist/skills/loader.js +132 -0
- package/dist/skills/prompt.d.ts +6 -0
- package/dist/skills/prompt.js +17 -0
- package/dist/skills/read-tool.d.ts +7 -0
- package/dist/skills/read-tool.js +24 -0
- package/dist/skills/scanner.d.ts +6 -0
- package/dist/skills/scanner.js +73 -0
- package/dist/skills/types.d.ts +15 -0
- package/dist/skills/types.js +1 -0
- package/dist/tasks/store.d.ts +47 -0
- package/dist/tasks/store.js +193 -0
- package/dist/tasks/types.d.ts +75 -0
- package/dist/tasks/types.js +32 -0
- package/dist/teams/store.d.ts +78 -0
- package/dist/teams/store.js +420 -0
- package/dist/teams/types.d.ts +23 -0
- package/dist/teams/types.js +1 -0
- package/dist/tools/bash.d.ts +16 -0
- package/dist/tools/bash.js +62 -0
- package/dist/tools/channel-history.d.ts +10 -0
- package/dist/tools/channel-history.js +43 -0
- package/dist/tools/delegate.d.ts +16 -0
- package/dist/tools/delegate.js +216 -0
- package/dist/tools/fs.d.ts +4 -0
- package/dist/tools/fs.js +335 -0
- package/dist/tools/integration-toggle.d.ts +14 -0
- package/dist/tools/integration-toggle.js +47 -0
- package/dist/tools/memory.d.ts +13 -0
- package/dist/tools/memory.js +65 -0
- package/dist/tools/registry.d.ts +6 -0
- package/dist/tools/registry.js +9 -0
- package/dist/tools/schedule.d.ts +8 -0
- package/dist/tools/schedule.js +219 -0
- package/dist/tools/speak.d.ts +10 -0
- package/dist/tools/speak.js +56 -0
- package/dist/tools/tasks.d.ts +29 -0
- package/dist/tools/tasks.js +92 -0
- package/dist/tools/teams.d.ts +7 -0
- package/dist/tools/teams.js +180 -0
- package/dist/tools/web-fetch.d.ts +3 -0
- package/dist/tools/web-fetch.js +22 -0
- package/dist/tts/edge.d.ts +10 -0
- package/dist/tts/edge.js +60 -0
- package/dist/tts/speak.d.ts +12 -0
- package/dist/tts/speak.js +81 -0
- package/dist/tts/transcribe.d.ts +5 -0
- package/dist/tts/transcribe.js +40 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +22 -0
- package/package.json +90 -0
- package/verybot.js +2 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { compactMessages } from "./compaction.js";
|
|
2
|
+
export class Session {
|
|
3
|
+
key;
|
|
4
|
+
messages = [];
|
|
5
|
+
persistedCount = 0; // how many messages are already on disk
|
|
6
|
+
_pendingCompaction = null;
|
|
7
|
+
_needsFullRewrite = false;
|
|
8
|
+
constructor(key, messages = []) {
|
|
9
|
+
this.key = key;
|
|
10
|
+
this.messages = messages;
|
|
11
|
+
this.persistedCount = messages.length;
|
|
12
|
+
}
|
|
13
|
+
append(msg) {
|
|
14
|
+
this.messages.push(msg);
|
|
15
|
+
}
|
|
16
|
+
getMessages() {
|
|
17
|
+
return [...this.messages];
|
|
18
|
+
}
|
|
19
|
+
/** Returns only messages not yet written to disk (normal append path). */
|
|
20
|
+
getNewMessages() {
|
|
21
|
+
return this.messages.slice(this.persistedCount);
|
|
22
|
+
}
|
|
23
|
+
/** After compaction: returns kept + new messages (everything except the summary at index 0). */
|
|
24
|
+
getCompactionMessages() {
|
|
25
|
+
return this.messages.slice(1);
|
|
26
|
+
}
|
|
27
|
+
/** Mark all current messages as persisted. */
|
|
28
|
+
markPersisted() {
|
|
29
|
+
this.persistedCount = this.messages.length;
|
|
30
|
+
this._pendingCompaction = null;
|
|
31
|
+
this._needsFullRewrite = false;
|
|
32
|
+
}
|
|
33
|
+
/** Pending compaction info for the store to persist as a marker. */
|
|
34
|
+
get pendingCompaction() {
|
|
35
|
+
return this._pendingCompaction;
|
|
36
|
+
}
|
|
37
|
+
/** True when the file must be fully overwritten (emergency truncate). */
|
|
38
|
+
get needsFullRewrite() {
|
|
39
|
+
return this._needsFullRewrite;
|
|
40
|
+
}
|
|
41
|
+
/** LLM-based compaction — summarizes old messages when tokens exceed context budget. */
|
|
42
|
+
async compact(model, contextWindow, systemPrompt) {
|
|
43
|
+
const result = await compactMessages(model, this.messages, contextWindow, systemPrompt);
|
|
44
|
+
if (result.compacted) {
|
|
45
|
+
this.messages = result.messages;
|
|
46
|
+
if (result.summary) {
|
|
47
|
+
// LLM summarization succeeded — persist as append-only compaction marker
|
|
48
|
+
this._pendingCompaction = { summary: result.summary };
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// Fallback truncation (no summary) — needs full file rewrite
|
|
52
|
+
this._needsFullRewrite = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result.compacted;
|
|
56
|
+
}
|
|
57
|
+
/** Replace the internal messages array (e.g. after programmatic compaction). Marks full rewrite. */
|
|
58
|
+
replaceMessages(messages) {
|
|
59
|
+
this.messages = messages;
|
|
60
|
+
this._needsFullRewrite = true;
|
|
61
|
+
}
|
|
62
|
+
/** Emergency truncation — drop oldest messages, keeping last N. */
|
|
63
|
+
truncate(keep) {
|
|
64
|
+
if (this.messages.length > keep) {
|
|
65
|
+
this.messages = this.messages.slice(-keep);
|
|
66
|
+
this._needsFullRewrite = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
get messageCount() {
|
|
70
|
+
return this.messages.length;
|
|
71
|
+
}
|
|
72
|
+
get updatedAt() {
|
|
73
|
+
return Date.now();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Check if two sets have the same elements. */
|
|
2
|
+
export function setsEqual(a, b) {
|
|
3
|
+
if (a.size !== b.size)
|
|
4
|
+
return false;
|
|
5
|
+
for (const item of a) {
|
|
6
|
+
if (!b.has(item))
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
/** Extract a user-facing message from API/runtime errors. */
|
|
12
|
+
export function friendlyError(err) {
|
|
13
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14
|
+
const lower = msg.toLowerCase();
|
|
15
|
+
if (lower.includes("credit") || lower.includes("billing") || lower.includes("402") || lower.includes("payment"))
|
|
16
|
+
return "API credit balance is too low. Please top up your account and try again.";
|
|
17
|
+
if (lower.includes("rate limit") || lower.includes("429"))
|
|
18
|
+
return "Rate limited by the API. Please wait a moment and try again.";
|
|
19
|
+
if (lower.includes("authentication") || lower.includes("api key") || lower.includes("401"))
|
|
20
|
+
return "API authentication failed. Please check your API key.";
|
|
21
|
+
if (lower.includes("overloaded") || lower.includes("529"))
|
|
22
|
+
return "The AI service is currently overloaded. Please try again shortly.";
|
|
23
|
+
if (lower.includes("context") && lower.includes("overflow"))
|
|
24
|
+
return "The conversation is too long. Please start a new session.";
|
|
25
|
+
return `Something went wrong: ${msg.slice(0, 200)}`;
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MessageQueue } from "./queue.js";
|
|
2
|
+
import type { SessionStateMap } from "./session-state.js";
|
|
3
|
+
/**
|
|
4
|
+
* Coordinates worker completion and orchestrator re-triggering.
|
|
5
|
+
* Debounces multiple concurrent worker completions into a single LLM call.
|
|
6
|
+
*/
|
|
7
|
+
export declare class WorkerCoordinator {
|
|
8
|
+
private sessions;
|
|
9
|
+
private queue;
|
|
10
|
+
constructor(sessions: SessionStateMap, queue: MessageQueue);
|
|
11
|
+
/**
|
|
12
|
+
* Called when a worker finishes. Debounces multiple completions into a single
|
|
13
|
+
* orchestrator re-trigger so concurrent workers batch into one LLM call.
|
|
14
|
+
*/
|
|
15
|
+
onWorkerComplete(sessionKey: string, channelId: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Track delegation depth: synthetic worker triggers increment, real messages reset.
|
|
18
|
+
* Returns true if the message is a worker result synthetic message.
|
|
19
|
+
*/
|
|
20
|
+
trackDelegationDepth(sessionKey: string, text: string): void;
|
|
21
|
+
/** Cancel all pending debounce timers (e.g. on shutdown). */
|
|
22
|
+
clearAllTimers(): void;
|
|
23
|
+
/** Enqueue a synthetic message to re-trigger the orchestrator after workers finish. */
|
|
24
|
+
private triggerOrchestratorForWorkerResults;
|
|
25
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { logger } from "../logger.js";
|
|
2
|
+
/** Debounce delay before re-triggering orchestrator after worker completion. */
|
|
3
|
+
const WORKER_TRIGGER_DEBOUNCE_MS = 2_000;
|
|
4
|
+
/** Max times the orchestrator can be auto-triggered by worker results per user message. */
|
|
5
|
+
const MAX_DELEGATION_DEPTH = 5;
|
|
6
|
+
/** Prefix for synthetic messages injected when workers complete. */
|
|
7
|
+
const WORKER_RESULTS_PREFIX = "[worker_results]";
|
|
8
|
+
/**
|
|
9
|
+
* Coordinates worker completion and orchestrator re-triggering.
|
|
10
|
+
* Debounces multiple concurrent worker completions into a single LLM call.
|
|
11
|
+
*/
|
|
12
|
+
export class WorkerCoordinator {
|
|
13
|
+
sessions;
|
|
14
|
+
queue;
|
|
15
|
+
constructor(sessions, queue) {
|
|
16
|
+
this.sessions = sessions;
|
|
17
|
+
this.queue = queue;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Called when a worker finishes. Debounces multiple completions into a single
|
|
21
|
+
* orchestrator re-trigger so concurrent workers batch into one LLM call.
|
|
22
|
+
*/
|
|
23
|
+
onWorkerComplete(sessionKey, channelId) {
|
|
24
|
+
const state = this.sessions.get(sessionKey);
|
|
25
|
+
if (!state)
|
|
26
|
+
return;
|
|
27
|
+
// Accumulate completed channel IDs during debounce window
|
|
28
|
+
if (!state.workerPendingChannels) {
|
|
29
|
+
state.workerPendingChannels = new Set();
|
|
30
|
+
}
|
|
31
|
+
state.workerPendingChannels.add(channelId);
|
|
32
|
+
// Debounce: reset timer on each completion
|
|
33
|
+
if (state.workerTriggerTimer)
|
|
34
|
+
clearTimeout(state.workerTriggerTimer);
|
|
35
|
+
state.workerTriggerTimer = setTimeout(() => {
|
|
36
|
+
state.workerTriggerTimer = undefined;
|
|
37
|
+
this.triggerOrchestratorForWorkerResults(sessionKey);
|
|
38
|
+
}, WORKER_TRIGGER_DEBOUNCE_MS);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Track delegation depth: synthetic worker triggers increment, real messages reset.
|
|
42
|
+
* Returns true if the message is a worker result synthetic message.
|
|
43
|
+
*/
|
|
44
|
+
trackDelegationDepth(sessionKey, text) {
|
|
45
|
+
const state = this.sessions.get(sessionKey);
|
|
46
|
+
if (!state)
|
|
47
|
+
return;
|
|
48
|
+
if (text.startsWith(WORKER_RESULTS_PREFIX)) {
|
|
49
|
+
state.delegationDepth++;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
state.delegationDepth = 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Cancel all pending debounce timers (e.g. on shutdown). */
|
|
56
|
+
clearAllTimers() {
|
|
57
|
+
this.sessions.clearAllTimers();
|
|
58
|
+
}
|
|
59
|
+
/** Enqueue a synthetic message to re-trigger the orchestrator after workers finish. */
|
|
60
|
+
triggerOrchestratorForWorkerResults(sessionKey) {
|
|
61
|
+
const state = this.sessions.get(sessionKey);
|
|
62
|
+
if (!state)
|
|
63
|
+
return;
|
|
64
|
+
if (state.delegationDepth >= MAX_DELEGATION_DEPTH) {
|
|
65
|
+
logger.warn(`[${sessionKey}] Delegation depth limit reached (${MAX_DELEGATION_DEPTH}), skipping re-trigger`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const channelIds = state.workerPendingChannels ? [...state.workerPendingChannels] : [];
|
|
69
|
+
state.workerPendingChannels = undefined;
|
|
70
|
+
const channelList = channelIds.map((id) => `- ${id}`).join("\n");
|
|
71
|
+
const synthetic = `${WORKER_RESULTS_PREFIX}\nWorker agents have completed. Call read_channel for each:\n${channelList}`;
|
|
72
|
+
const deliver = state.replyCallback;
|
|
73
|
+
this.queue.enqueue(sessionKey, synthetic).then((reply) => {
|
|
74
|
+
if (reply && deliver) {
|
|
75
|
+
deliver(reply).catch((err) => {
|
|
76
|
+
logger.error(`[${sessionKey}] Failed to deliver worker trigger reply: ${err instanceof Error ? err.message : err}`);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}, (err) => {
|
|
80
|
+
logger.error(`[${sessionKey}] Failed to enqueue worker trigger: ${err instanceof Error ? err.message : err}`);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** A segment of a command response that needs channel-specific formatting. */
|
|
2
|
+
export type CommandPart = string | {
|
|
3
|
+
bold: string;
|
|
4
|
+
} | {
|
|
5
|
+
code: string;
|
|
6
|
+
};
|
|
7
|
+
export interface CommandResult {
|
|
8
|
+
parts: CommandPart[];
|
|
9
|
+
}
|
|
10
|
+
export interface CommandRouterOpts {
|
|
11
|
+
onClear?: (channelType: string, channelId: string, teamId?: string) => Promise<void>;
|
|
12
|
+
/** List available teams for the /team command. */
|
|
13
|
+
listTeams?: () => {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
}[];
|
|
17
|
+
/** Fallback team when no active team is set for a channel. */
|
|
18
|
+
defaultTeamId?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class CommandRouter {
|
|
21
|
+
private readonly opts;
|
|
22
|
+
private readonly activeTeams;
|
|
23
|
+
constructor(opts: CommandRouterOpts);
|
|
24
|
+
/** Resolve the active team for a channel, falling back to defaultTeamId. */
|
|
25
|
+
resolveTeamId(channelId: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Parse raw text for slash-style commands (e.g. "/clear", "/team foo").
|
|
28
|
+
* Returns `null` if the text is not a recognised command.
|
|
29
|
+
*/
|
|
30
|
+
handle(channelType: string, channelId: string, text: string): Promise<CommandResult | null>;
|
|
31
|
+
/** Handle /clear (or /reset). */
|
|
32
|
+
handleClear(channelType: string, channelId: string): Promise<CommandResult>;
|
|
33
|
+
/** Handle /team [name]. */
|
|
34
|
+
handleTeam(channelType: string, channelId: string, arg: string): Promise<CommandResult>;
|
|
35
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { DEFAULT_TEAM_ID } from "../config/agent-config.js";
|
|
2
|
+
export class CommandRouter {
|
|
3
|
+
opts;
|
|
4
|
+
activeTeams = new Map();
|
|
5
|
+
constructor(opts) {
|
|
6
|
+
this.opts = opts;
|
|
7
|
+
}
|
|
8
|
+
/** Resolve the active team for a channel, falling back to defaultTeamId. */
|
|
9
|
+
resolveTeamId(channelId) {
|
|
10
|
+
return this.activeTeams.get(channelId) ?? this.opts.defaultTeamId ?? DEFAULT_TEAM_ID;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Parse raw text for slash-style commands (e.g. "/clear", "/team foo").
|
|
14
|
+
* Returns `null` if the text is not a recognised command.
|
|
15
|
+
*/
|
|
16
|
+
async handle(channelType, channelId, text) {
|
|
17
|
+
const trimmed = text.trim();
|
|
18
|
+
const lower = trimmed.toLowerCase();
|
|
19
|
+
if (lower === "/clear" || lower === "/reset") {
|
|
20
|
+
return this.handleClear(channelType, channelId);
|
|
21
|
+
}
|
|
22
|
+
if ((lower === "/team" || lower.startsWith("/team ")) && this.opts.listTeams) {
|
|
23
|
+
const arg = trimmed.slice("/team".length).trim();
|
|
24
|
+
return this.handleTeam(channelType, channelId, arg);
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/** Handle /clear (or /reset). */
|
|
29
|
+
async handleClear(channelType, channelId) {
|
|
30
|
+
if (this.opts.onClear) {
|
|
31
|
+
await this.opts.onClear(channelType, channelId, this.resolveTeamId(channelId));
|
|
32
|
+
}
|
|
33
|
+
return { parts: ["Session cleared."] };
|
|
34
|
+
}
|
|
35
|
+
/** Handle /team [name]. */
|
|
36
|
+
async handleTeam(channelType, channelId, arg) {
|
|
37
|
+
const teams = this.opts.listTeams?.() ?? [];
|
|
38
|
+
// No argument → list teams with the active one highlighted.
|
|
39
|
+
if (!arg) {
|
|
40
|
+
const currentTeamId = this.resolveTeamId(channelId);
|
|
41
|
+
const parts = ["Teams:\n"];
|
|
42
|
+
for (const t of teams) {
|
|
43
|
+
if (t.id === currentTeamId) {
|
|
44
|
+
parts.push("• ", { bold: t.name }, " (active)\n");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
parts.push("• ", t.name, "\n");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
parts.push("\nSwitch with ", { code: "/team <name>" });
|
|
51
|
+
return { parts };
|
|
52
|
+
}
|
|
53
|
+
// Find team by name (case-insensitive).
|
|
54
|
+
const target = teams.find((t) => t.name.toLowerCase() === arg.toLowerCase());
|
|
55
|
+
if (!target) {
|
|
56
|
+
return {
|
|
57
|
+
parts: ['Unknown team "', arg, '". Use ', { code: "/team" }, " to list available teams."],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
this.activeTeams.set(channelId, target.id);
|
|
61
|
+
return {
|
|
62
|
+
parts: ["Switched to team ", { bold: target.name }, "."],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Channel, MessageHandler } from "../types.js";
|
|
2
|
+
import type { MarkdownTableMode } from "../../markdown/ir.js";
|
|
3
|
+
/** Callback that resolves the current table mode from config at send time. */
|
|
4
|
+
export type ResolveTableMode = () => MarkdownTableMode;
|
|
5
|
+
export interface CreateDiscordChannelOpts {
|
|
6
|
+
token: string;
|
|
7
|
+
onMessage: MessageHandler;
|
|
8
|
+
onClear?: (channelType: string, channelId: string, teamId?: string) => Promise<void>;
|
|
9
|
+
/** List available teams for the /team command. */
|
|
10
|
+
listTeams?: () => {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
}[];
|
|
14
|
+
/** Fallback team when no active team is set for a channel. */
|
|
15
|
+
defaultTeamId?: string;
|
|
16
|
+
resolveTableMode?: ResolveTableMode;
|
|
17
|
+
}
|
|
18
|
+
export declare function createDiscordChannel(opts: CreateDiscordChannelOpts): Channel;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Client, GatewayIntentBits, Partials, Events } from "discord.js";
|
|
2
|
+
import { logger } from "../../logger.js";
|
|
3
|
+
import { markdownToDiscordChunks, escapeDiscord, DISCORD_TEXT_LIMIT } from "./markdown.js";
|
|
4
|
+
import { CommandRouter } from "../commands.js";
|
|
5
|
+
/** Discord typing indicator expires after ~10s; refresh every 8s. */
|
|
6
|
+
const TYPING_INTERVAL_MS = 8000;
|
|
7
|
+
export function createDiscordChannel(opts) {
|
|
8
|
+
const { token, onMessage, onClear, listTeams, defaultTeamId, resolveTableMode = () => "code", } = opts;
|
|
9
|
+
const client = new Client({
|
|
10
|
+
intents: [
|
|
11
|
+
GatewayIntentBits.Guilds,
|
|
12
|
+
GatewayIntentBits.GuildMessages,
|
|
13
|
+
GatewayIntentBits.MessageContent,
|
|
14
|
+
GatewayIntentBits.DirectMessages,
|
|
15
|
+
],
|
|
16
|
+
partials: [Partials.Channel],
|
|
17
|
+
});
|
|
18
|
+
const typingIntervals = new Map();
|
|
19
|
+
const commands = new CommandRouter({ onClear, listTeams, defaultTeamId });
|
|
20
|
+
/** Format a CommandResult using Discord markdown syntax. */
|
|
21
|
+
function formatDiscordResult(result) {
|
|
22
|
+
return result.parts
|
|
23
|
+
.map((p) => {
|
|
24
|
+
if (typeof p === "string")
|
|
25
|
+
return escapeDiscord(p);
|
|
26
|
+
if ("bold" in p)
|
|
27
|
+
return `**${escapeDiscord(p.bold)}**`;
|
|
28
|
+
return `\`${p.code}\``;
|
|
29
|
+
})
|
|
30
|
+
.join("");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Handle text-based commands (e.g. "/clear", "/team foo").
|
|
34
|
+
* Returns the formatted response if the message was consumed, or null.
|
|
35
|
+
*/
|
|
36
|
+
async function handleTextCommand(text, channelId) {
|
|
37
|
+
try {
|
|
38
|
+
const result = await commands.handle("discord", channelId, text);
|
|
39
|
+
if (!result)
|
|
40
|
+
return null;
|
|
41
|
+
return formatDiscordResult(result);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
45
|
+
logger.error(`[discord] command error: ${msg}`);
|
|
46
|
+
return "Failed to process command. Please try again.";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function startTyping(channelId) {
|
|
50
|
+
stopTyping(channelId);
|
|
51
|
+
const discordChannel = client.channels.cache.get(channelId);
|
|
52
|
+
if (!discordChannel?.isTextBased() || !("sendTyping" in discordChannel))
|
|
53
|
+
return;
|
|
54
|
+
// Send immediately, then refresh on interval
|
|
55
|
+
discordChannel.sendTyping().catch((err) => logger.error(`[discord] typing error: ${err}`));
|
|
56
|
+
const interval = setInterval(() => {
|
|
57
|
+
discordChannel.sendTyping().catch((err) => logger.error(`[discord] typing interval error: ${err}`));
|
|
58
|
+
}, TYPING_INTERVAL_MS);
|
|
59
|
+
typingIntervals.set(channelId, interval);
|
|
60
|
+
}
|
|
61
|
+
function stopTyping(channelId) {
|
|
62
|
+
const interval = typingIntervals.get(channelId);
|
|
63
|
+
if (interval) {
|
|
64
|
+
clearInterval(interval);
|
|
65
|
+
typingIntervals.delete(channelId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Send chunked Discord markdown to a channel. */
|
|
69
|
+
async function sendChunks(channelId, text) {
|
|
70
|
+
const discordChannel = client.channels.cache.get(channelId);
|
|
71
|
+
if (!discordChannel?.isTextBased() || !("send" in discordChannel)) {
|
|
72
|
+
logger.error(`[discord] cannot send to channel ${channelId}: not a text channel`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const tableMode = resolveTableMode();
|
|
76
|
+
const chunks = markdownToDiscordChunks(text, DISCORD_TEXT_LIMIT, { tableMode });
|
|
77
|
+
const noMentions = { allowedMentions: { parse: [] } };
|
|
78
|
+
if (chunks.length === 0 && text) {
|
|
79
|
+
await discordChannel.send({ content: text, ...noMentions });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const chunk of chunks) {
|
|
83
|
+
await discordChannel.send({ content: chunk, ...noMentions });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Strip the bot mention prefix (e.g. `<@123456>`) from message text. */
|
|
87
|
+
function stripBotMention(text) {
|
|
88
|
+
const botId = client.user?.id;
|
|
89
|
+
if (!botId)
|
|
90
|
+
return text.trim();
|
|
91
|
+
const mentionRegex = new RegExp(`^\\s*<@!?${botId}>\\s*`);
|
|
92
|
+
return text.replace(mentionRegex, "").trim();
|
|
93
|
+
}
|
|
94
|
+
const channel = {
|
|
95
|
+
name: "discord",
|
|
96
|
+
start: async () => {
|
|
97
|
+
client.on(Events.MessageCreate, async (message) => {
|
|
98
|
+
try {
|
|
99
|
+
const botUser = client.user;
|
|
100
|
+
if (!botUser)
|
|
101
|
+
return;
|
|
102
|
+
// Ignore bot's own messages and other bots
|
|
103
|
+
if (message.author.id === botUser.id)
|
|
104
|
+
return;
|
|
105
|
+
if (message.author.bot)
|
|
106
|
+
return;
|
|
107
|
+
const isDM = !message.guild;
|
|
108
|
+
const isMentioned = message.mentions.has(botUser);
|
|
109
|
+
// In guilds, only respond to mentions; in DMs, always respond
|
|
110
|
+
if (!isDM && !isMentioned)
|
|
111
|
+
return;
|
|
112
|
+
const channelId = message.channelId;
|
|
113
|
+
const userId = message.author.id;
|
|
114
|
+
const rawText = message.content ?? "";
|
|
115
|
+
const text = isDM ? rawText.trim() : stripBotMention(rawText);
|
|
116
|
+
if (!text)
|
|
117
|
+
return;
|
|
118
|
+
// Try command handling first
|
|
119
|
+
const cmdResponse = await handleTextCommand(text, channelId);
|
|
120
|
+
if (cmdResponse) {
|
|
121
|
+
await message.reply({ content: cmdResponse, allowedMentions: { parse: [] } });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
logger.info(`[discord] message from ${userId} in ${channelId} (${isDM ? "DM" : "guild"})`);
|
|
125
|
+
startTyping(channelId);
|
|
126
|
+
await onMessage({
|
|
127
|
+
channelType: "discord",
|
|
128
|
+
channelId,
|
|
129
|
+
userId,
|
|
130
|
+
text,
|
|
131
|
+
teamId: commands.resolveTeamId(channelId),
|
|
132
|
+
}, channel);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
136
|
+
logger.error(`[discord] error in MessageCreate handler: ${msg}`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
await client.login(token);
|
|
140
|
+
logger.info(`[discord] connected as ${client.user?.tag}`);
|
|
141
|
+
},
|
|
142
|
+
stop: async () => {
|
|
143
|
+
for (const chatId of typingIntervals.keys())
|
|
144
|
+
stopTyping(chatId);
|
|
145
|
+
await client.destroy();
|
|
146
|
+
logger.info("[discord] disconnected");
|
|
147
|
+
},
|
|
148
|
+
send: async (channelId, text) => {
|
|
149
|
+
stopTyping(channelId);
|
|
150
|
+
await sendChunks(channelId, text);
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
return channel;
|
|
154
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → Discord markdown conversion.
|
|
3
|
+
* Discord supports standard markdown natively, with minor escaping needs.
|
|
4
|
+
*/
|
|
5
|
+
import { type MarkdownTableMode } from "../../markdown/ir.js";
|
|
6
|
+
/** Discord message character limit. */
|
|
7
|
+
export declare const DISCORD_TEXT_LIMIT = 2000;
|
|
8
|
+
/** Escape Discord markdown special characters in plain text. */
|
|
9
|
+
export declare function escapeDiscord(text: string): string;
|
|
10
|
+
export declare function markdownToDiscord(markdown: string, options?: {
|
|
11
|
+
tableMode?: MarkdownTableMode;
|
|
12
|
+
}): string;
|
|
13
|
+
/**
|
|
14
|
+
* Convert markdown to chunked Discord markdown strings.
|
|
15
|
+
* Each chunk is ≤ `limit` characters of plain-text IR.
|
|
16
|
+
*/
|
|
17
|
+
export declare function markdownToDiscordChunks(markdown: string, limit?: number, options?: {
|
|
18
|
+
tableMode?: MarkdownTableMode;
|
|
19
|
+
}): string[];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → Discord markdown conversion.
|
|
3
|
+
* Discord supports standard markdown natively, with minor escaping needs.
|
|
4
|
+
*/
|
|
5
|
+
import { chunkMarkdownIR, markdownToIR, } from "../../markdown/ir.js";
|
|
6
|
+
import { renderMarkdownWithMarkers } from "../../markdown/render.js";
|
|
7
|
+
const SAFE_LINK_PROTOCOL = /^https?:\/\/|^mailto:|^tel:/i;
|
|
8
|
+
/** Discord message character limit. */
|
|
9
|
+
export const DISCORD_TEXT_LIMIT = 2000;
|
|
10
|
+
/** Escape Discord markdown special characters in plain text. */
|
|
11
|
+
export function escapeDiscord(text) {
|
|
12
|
+
return text.replace(/([*_~`|>\\])/g, "\\$1");
|
|
13
|
+
}
|
|
14
|
+
function buildDiscordLink(link, _text) {
|
|
15
|
+
const href = link.href.trim();
|
|
16
|
+
if (!href || link.start === link.end)
|
|
17
|
+
return null;
|
|
18
|
+
if (!SAFE_LINK_PROTOCOL.test(href))
|
|
19
|
+
return null;
|
|
20
|
+
return {
|
|
21
|
+
start: link.start,
|
|
22
|
+
end: link.end,
|
|
23
|
+
open: "[",
|
|
24
|
+
close: `](${href})`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function renderDiscordMarkdown(ir) {
|
|
28
|
+
return renderMarkdownWithMarkers(ir, {
|
|
29
|
+
styleMarkers: {
|
|
30
|
+
bold: { open: "**", close: "**" },
|
|
31
|
+
italic: { open: "*", close: "*" },
|
|
32
|
+
strikethrough: { open: "~~", close: "~~" },
|
|
33
|
+
code: { open: "`", close: "`" },
|
|
34
|
+
code_block: { open: "```\n", close: "\n```" },
|
|
35
|
+
},
|
|
36
|
+
escapeText: escapeDiscord,
|
|
37
|
+
buildLink: buildDiscordLink,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function markdownToDiscord(markdown, options = {}) {
|
|
41
|
+
const ir = markdownToIR(markdown ?? "", {
|
|
42
|
+
linkify: true,
|
|
43
|
+
headingStyle: "none",
|
|
44
|
+
blockquotePrefix: "> ",
|
|
45
|
+
tableMode: options.tableMode,
|
|
46
|
+
});
|
|
47
|
+
return renderDiscordMarkdown(ir);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Convert markdown to chunked Discord markdown strings.
|
|
51
|
+
* Each chunk is ≤ `limit` characters of plain-text IR.
|
|
52
|
+
*/
|
|
53
|
+
export function markdownToDiscordChunks(markdown, limit = DISCORD_TEXT_LIMIT, options = {}) {
|
|
54
|
+
const ir = markdownToIR(markdown ?? "", {
|
|
55
|
+
linkify: true,
|
|
56
|
+
headingStyle: "none",
|
|
57
|
+
blockquotePrefix: "> ",
|
|
58
|
+
tableMode: options.tableMode,
|
|
59
|
+
});
|
|
60
|
+
const chunks = chunkMarkdownIR(ir, limit);
|
|
61
|
+
return chunks.map((chunk) => renderDiscordMarkdown(chunk));
|
|
62
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Channel } from "./types.js";
|
|
2
|
+
export interface ChannelSpec {
|
|
3
|
+
name: string;
|
|
4
|
+
/** Opaque fingerprint (e.g. hash of token). Change triggers stop→start cycle. */
|
|
5
|
+
fingerprint: string;
|
|
6
|
+
/** Factory that creates a fresh Channel instance. */
|
|
7
|
+
create: () => Channel;
|
|
8
|
+
}
|
|
9
|
+
/** Hash a token to produce a safe fingerprint (never store/log raw secrets). */
|
|
10
|
+
export declare function channelFingerprint(token: string): string;
|
|
11
|
+
export declare class ChannelManager {
|
|
12
|
+
private channels;
|
|
13
|
+
private byName;
|
|
14
|
+
private fingerprints;
|
|
15
|
+
private reconciling;
|
|
16
|
+
register(channel: Channel, fingerprint?: string): void;
|
|
17
|
+
/** Look up a channel by name (e.g. "telegram"). */
|
|
18
|
+
get(name: string): Channel | undefined;
|
|
19
|
+
startAll(): Promise<void>;
|
|
20
|
+
stopAll(): Promise<void>;
|
|
21
|
+
/** Stop and unregister a single channel by name. */
|
|
22
|
+
private stopOne;
|
|
23
|
+
/**
|
|
24
|
+
* Diff current channels against desired specs. Stop removed/changed channels,
|
|
25
|
+
* start new/changed ones. Self-guarding: concurrent calls are skipped
|
|
26
|
+
* (the next reloadConfig cycle will catch the mismatch).
|
|
27
|
+
*/
|
|
28
|
+
reconcile(specs: ChannelSpec[]): Promise<void>;
|
|
29
|
+
}
|