thepopebot 1.2.76-beta.2 → 1.2.76-beta.21
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 +3 -3
- package/api/CLAUDE.md +11 -4
- package/api/index.js +56 -18
- package/bin/CLAUDE.md +7 -4
- package/bin/cli.js +25 -45
- package/config/CLAUDE.md +23 -4
- package/drizzle/0021_coding_agent_workspace.sql +1 -0
- package/drizzle/0022_organic_apocalypse.sql +16 -0
- package/drizzle/0023_needy_ender_wiggin.sql +1 -0
- package/drizzle/meta/0021_snapshot.json +639 -0
- package/drizzle/meta/0022_snapshot.json +743 -0
- package/drizzle/meta/0023_snapshot.json +750 -0
- package/drizzle/meta/_journal.json +21 -0
- package/lib/CLAUDE.md +2 -2
- package/lib/actions.js +9 -1
- package/lib/ai/CLAUDE.md +72 -57
- package/lib/ai/helper-llm.js +108 -0
- package/lib/ai/index.js +308 -438
- package/lib/ai/line-mappers.js +42 -24
- package/lib/ai/scope.js +26 -0
- package/lib/ai/sdk-adapters/CLAUDE.md +114 -0
- package/lib/ai/sdk-adapters/claude-code.js +120 -8
- package/lib/ai/system-prompt.js +34 -0
- package/lib/ai/workspace-setup.js +19 -35
- package/lib/channels/CLAUDE.md +14 -4
- package/lib/channels/base.js +6 -2
- package/lib/channels/commands/index.js +42 -0
- package/lib/channels/commands/session.js +53 -0
- package/lib/channels/commands/verify.js +18 -0
- package/lib/channels/telegram.js +79 -28
- package/lib/chat/CLAUDE.md +4 -4
- package/lib/chat/actions.js +270 -49
- package/lib/chat/api.js +185 -31
- package/lib/chat/components/CLAUDE.md +6 -2
- package/lib/chat/components/chat-input.js +77 -47
- package/lib/chat/components/chat-input.jsx +77 -40
- package/lib/chat/components/chat-page.js +2 -0
- package/lib/chat/components/chat-page.jsx +3 -0
- package/lib/chat/components/chat.js +62 -14
- package/lib/chat/components/chat.jsx +68 -10
- package/lib/chat/components/code-mode-toggle.js +141 -22
- package/lib/chat/components/code-mode-toggle.jsx +129 -20
- package/lib/chat/components/containers-page.js +58 -40
- package/lib/chat/components/containers-page.jsx +64 -25
- package/lib/chat/components/crons-page.js +17 -3
- package/lib/chat/components/crons-page.jsx +34 -6
- package/lib/chat/components/index.js +2 -2
- package/lib/chat/components/message.js +18 -3
- package/lib/chat/components/message.jsx +18 -3
- package/lib/chat/components/profile-page.js +182 -4
- package/lib/chat/components/profile-page.jsx +196 -1
- package/lib/chat/components/scope-picker.js +21 -0
- package/lib/chat/components/scope-picker.jsx +27 -0
- package/lib/chat/components/settings-chat-page.js +11 -11
- package/lib/chat/components/settings-chat-page.jsx +14 -18
- package/lib/chat/components/settings-coding-agents-page.js +110 -16
- package/lib/chat/components/settings-coding-agents-page.jsx +87 -3
- package/lib/chat/components/settings-github-page.js +5 -0
- package/lib/chat/components/settings-github-page.jsx +5 -0
- package/lib/chat/components/settings-layout.js +3 -3
- package/lib/chat/components/settings-layout.jsx +3 -3
- package/lib/chat/components/settings-secrets-layout.js +1 -2
- package/lib/chat/components/settings-secrets-layout.jsx +1 -2
- package/lib/chat/components/settings-secrets-page.js +180 -75
- package/lib/chat/components/settings-secrets-page.jsx +212 -66
- package/lib/chat/components/triggers-page.js +17 -3
- package/lib/chat/components/triggers-page.jsx +34 -6
- package/lib/chat/components/ui/combobox.js +18 -2
- package/lib/chat/components/ui/combobox.jsx +17 -1
- package/lib/chat/components/ui/dropdown-menu.js +23 -2
- package/lib/chat/components/ui/dropdown-menu.jsx +27 -2
- package/lib/chat/telegram-profile.js +33 -0
- package/lib/cluster/CLAUDE.md +9 -3
- package/lib/code/CLAUDE.md +11 -3
- package/lib/code/actions.js +47 -8
- package/lib/code/terminal-view.js +31 -21
- package/lib/code/terminal-view.jsx +32 -23
- package/lib/config.js +15 -4
- package/lib/containers/CLAUDE.md +16 -6
- package/lib/db/CLAUDE.md +5 -2
- package/lib/db/chats.js +9 -17
- package/lib/db/code-workspaces.js +8 -3
- package/lib/db/config.js +0 -1
- package/lib/db/index.js +12 -0
- package/lib/db/schema.js +24 -1
- package/lib/db/user-channels.js +129 -0
- package/lib/llm-providers.js +8 -0
- package/lib/maintenance.js +31 -21
- package/lib/tools/CLAUDE.md +12 -3
- package/lib/tools/assemblyai.js +17 -0
- package/lib/tools/create-agent-job.js +12 -8
- package/lib/tools/docker.js +34 -10
- package/lib/tools/github.js +34 -0
- package/lib/tools/telegram.js +106 -0
- package/lib/utils/render-md.js +44 -18
- package/package.json +8 -8
- package/setup/CLAUDE.md +11 -5
- package/setup/lib/providers.mjs +2 -1
- package/setup/lib/targets.mjs +13 -16
- package/setup/lib/telegram.mjs +8 -69
- package/templates/.env.example +0 -7
- package/templates/.github/workflows/rebuild-event-handler.yml +1 -1
- package/templates/.gitignore.template +1 -3
- package/templates/CLAUDE.md +1 -1
- package/templates/CLAUDE.md.template +29 -7
- package/templates/agent-job/CLAUDE.md.template +5 -3
- package/templates/agent-job/CRONS.json +16 -0
- package/templates/agent-job/SYSTEM.md +16 -11
- package/templates/agents/CLAUDE.md.template +17 -17
- package/templates/coding-workspace/CLAUDE.md.template +7 -0
- package/templates/data/CLAUDE.md.template +1 -1
- package/templates/docker-compose.custom.yml +1 -0
- package/templates/docker-compose.yml +1 -0
- package/templates/event-handler/CLAUDE.md.template +79 -0
- package/templates/event-handler/TRIGGERS.json +18 -2
- package/templates/skills/CLAUDE.md.template +20 -22
- package/templates/skills/{library/agent-job-secrets → agent-job-secrets}/SKILL.md +2 -2
- package/lib/ai/agent.js +0 -65
- package/lib/ai/async-channel.js +0 -51
- package/lib/ai/model.js +0 -130
- package/lib/ai/tools.js +0 -164
- package/lib/tools/openai.js +0 -37
- package/setup/lib/telegram-verify.mjs +0 -63
- package/setup/setup-telegram.mjs +0 -260
- package/templates/agent-job/SOUL.md +0 -17
- /package/templates/{skills/active/.gitkeep → coding-workspace/SYSTEM.md} +0 -0
- /package/templates/skills/{library/agent-job-secrets → agent-job-secrets}/agent-job-secrets.js +0 -0
- /package/templates/skills/{library/playwright-cli → playwright-cli}/SKILL.md +0 -0
|
@@ -211,7 +211,7 @@ export default function TerminalView({ codeWorkspaceId, wsPath, isActive = true,
|
|
|
211
211
|
term.open(containerRef.current);
|
|
212
212
|
|
|
213
213
|
const style = document.createElement('style');
|
|
214
|
-
style.textContent = `.xterm { padding: 5px; background-color: ${theme.background} !important; } .xterm-viewport { background-color: ${theme.background} !important; } .xterm-rows span { pointer-events: none; }`;
|
|
214
|
+
style.textContent = `.xterm { padding: 5px; background-color: ${theme.background} !important; } .xterm-viewport { background-color: ${theme.background} !important; touch-action: none; } .xterm-rows span { pointer-events: none; }`;
|
|
215
215
|
containerRef.current.appendChild(style);
|
|
216
216
|
styleRef.current = style;
|
|
217
217
|
|
|
@@ -227,8 +227,13 @@ export default function TerminalView({ codeWorkspaceId, wsPath, isActive = true,
|
|
|
227
227
|
|
|
228
228
|
fitAddon.fit();
|
|
229
229
|
|
|
230
|
-
// Mobile touch scroll:
|
|
231
|
-
|
|
230
|
+
// Mobile touch scroll: xterm v5's .xterm-viewport is overflow-y:scroll and its
|
|
231
|
+
// scroll event listener syncs scrollTop → buffer position. touch-action:none on
|
|
232
|
+
// the viewport (injected above) prevents the compositor from natively scrolling
|
|
233
|
+
// it, so we manually translate finger swipes into scrollLines() calls. Capture
|
|
234
|
+
// phase + stopPropagation blocks xterm's own touch handlers on .xterm from
|
|
235
|
+
// also setting viewport.scrollTop.
|
|
236
|
+
const termContainer = containerRef.current;
|
|
232
237
|
let lastTouchY = null;
|
|
233
238
|
let touchScrollAccum = 0;
|
|
234
239
|
const LINE_HEIGHT = 15; // approximate px per line at fontSize 16
|
|
@@ -238,6 +243,7 @@ export default function TerminalView({ codeWorkspaceId, wsPath, isActive = true,
|
|
|
238
243
|
lastTouchY = ev.touches[0].clientY;
|
|
239
244
|
touchScrollAccum = 0;
|
|
240
245
|
}
|
|
246
|
+
ev.stopPropagation();
|
|
241
247
|
};
|
|
242
248
|
const onTouchMove = (ev) => {
|
|
243
249
|
if (lastTouchY === null || ev.touches.length !== 1) return;
|
|
@@ -251,14 +257,13 @@ export default function TerminalView({ codeWorkspaceId, wsPath, isActive = true,
|
|
|
251
257
|
touchScrollAccum -= lines * LINE_HEIGHT;
|
|
252
258
|
}
|
|
253
259
|
ev.preventDefault();
|
|
260
|
+
ev.stopPropagation();
|
|
254
261
|
};
|
|
255
262
|
const onTouchEnd = () => { lastTouchY = null; touchScrollAccum = 0; };
|
|
256
263
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
screenEl.addEventListener('touchend', onTouchEnd, { passive: true });
|
|
261
|
-
}
|
|
264
|
+
termContainer.addEventListener('touchstart', onTouchStart, { passive: false, capture: true });
|
|
265
|
+
termContainer.addEventListener('touchmove', onTouchMove, { passive: false, capture: true });
|
|
266
|
+
termContainer.addEventListener('touchend', onTouchEnd, { passive: true, capture: true });
|
|
262
267
|
|
|
263
268
|
term.onData((data) => {
|
|
264
269
|
const ws = wsRef.current;
|
|
@@ -302,11 +307,9 @@ export default function TerminalView({ codeWorkspaceId, wsPath, isActive = true,
|
|
|
302
307
|
clearTimeout(resizeTimeout);
|
|
303
308
|
clearTimeout(retryTimer.current);
|
|
304
309
|
window.removeEventListener('resize', handleResize);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
screenEl.removeEventListener('touchend', onTouchEnd);
|
|
309
|
-
}
|
|
310
|
+
termContainer.removeEventListener('touchstart', onTouchStart, { capture: true });
|
|
311
|
+
termContainer.removeEventListener('touchmove', onTouchMove, { capture: true });
|
|
312
|
+
termContainer.removeEventListener('touchend', onTouchEnd, { capture: true });
|
|
310
313
|
if (wsRef.current) wsRef.current.close();
|
|
311
314
|
term.dispose();
|
|
312
315
|
};
|
|
@@ -839,7 +842,7 @@ export default function TerminalView({ codeWorkspaceId, wsPath, isActive = true,
|
|
|
839
842
|
);
|
|
840
843
|
}
|
|
841
844
|
|
|
842
|
-
const STORAGE_KEY = 'thepopebot-workspace-command';
|
|
845
|
+
const STORAGE_KEY = 'thepopebot-workspace-command:code';
|
|
843
846
|
|
|
844
847
|
function ToolbarCommandButton({ codeWorkspaceId, diffStats, onDiffStatsRefresh, onShowDiff }) {
|
|
845
848
|
const [selectedCommand, setSelectedCommandState] = useState(() => {
|
|
@@ -849,6 +852,21 @@ function ToolbarCommandButton({ codeWorkspaceId, diffStats, onDiffStatsRefresh,
|
|
|
849
852
|
setSelectedCommandState(cmd);
|
|
850
853
|
try { localStorage.setItem(STORAGE_KEY, cmd); } catch {}
|
|
851
854
|
};
|
|
855
|
+
|
|
856
|
+
// Seed from admin default if user hasn't picked anything for code mode yet.
|
|
857
|
+
useEffect(() => {
|
|
858
|
+
let stored = null;
|
|
859
|
+
try { stored = localStorage.getItem(STORAGE_KEY); } catch {}
|
|
860
|
+
if (stored) return;
|
|
861
|
+
let cancelled = false;
|
|
862
|
+
import('../chat/actions.js').then(({ getModeGitActionDefault }) => {
|
|
863
|
+
getModeGitActionDefault('code').then((val) => {
|
|
864
|
+
if (cancelled || !val) return;
|
|
865
|
+
setSelectedCommandState(val);
|
|
866
|
+
}).catch(() => {});
|
|
867
|
+
}).catch(() => {});
|
|
868
|
+
return () => { cancelled = true; };
|
|
869
|
+
}, []);
|
|
852
870
|
const [commandRunning, setCommandRunning] = useState(false);
|
|
853
871
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
854
872
|
const [commandOutput, setCommandOutput] = useState('');
|
|
@@ -869,15 +887,6 @@ function ToolbarCommandButton({ codeWorkspaceId, diffStats, onDiffStatsRefresh,
|
|
|
869
887
|
const handleRun = useCallback(async () => {
|
|
870
888
|
if (commandRunning) return;
|
|
871
889
|
|
|
872
|
-
const fresh = await onDiffStatsRefresh?.();
|
|
873
|
-
const stats = fresh || diffStats;
|
|
874
|
-
if (!(stats?.insertions || 0) && !(stats?.deletions || 0)) {
|
|
875
|
-
setDialogOpen(true);
|
|
876
|
-
setCommandOutput('You have no changes.');
|
|
877
|
-
setCommandExitCode(1);
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
890
|
setCommandRunning(true);
|
|
882
891
|
setDialogOpen(true);
|
|
883
892
|
setCommandOutput('');
|
package/lib/config.js
CHANGED
|
@@ -37,7 +37,6 @@ const CONFIG_KEYS = new Set([
|
|
|
37
37
|
'LLM_MAX_TOKENS',
|
|
38
38
|
'AGENT_BACKEND',
|
|
39
39
|
'CUSTOM_OPENAI_BASE_URL',
|
|
40
|
-
'TELEGRAM_CHAT_ID',
|
|
41
40
|
'UPGRADE_INCLUDE_BETA',
|
|
42
41
|
'CODING_AGENT',
|
|
43
42
|
'CODING_AGENT_CLAUDE_CODE_ENABLED',
|
|
@@ -58,6 +57,10 @@ const CONFIG_KEYS = new Set([
|
|
|
58
57
|
'CODING_AGENT_KIMI_CLI_ENABLED',
|
|
59
58
|
'CODING_AGENT_KIMI_CLI_PROVIDER',
|
|
60
59
|
'CODING_AGENT_KIMI_CLI_MODEL',
|
|
60
|
+
'AGENT_MODE_BRANCH',
|
|
61
|
+
'CODE_MODE_BRANCH',
|
|
62
|
+
'AGENT_MODE_GIT_ACTION',
|
|
63
|
+
'CODE_MODE_GIT_ACTION',
|
|
61
64
|
]);
|
|
62
65
|
|
|
63
66
|
// Default values
|
|
@@ -74,6 +77,10 @@ const DEFAULTS = {
|
|
|
74
77
|
CODING_AGENT_CODEX_CLI_AUTH: 'api-key',
|
|
75
78
|
CODING_AGENT_OPENCODE_ENABLED: 'false',
|
|
76
79
|
CODING_AGENT_KIMI_CLI_ENABLED: 'false',
|
|
80
|
+
AGENT_MODE_BRANCH: 'default',
|
|
81
|
+
CODE_MODE_BRANCH: 'dynamic',
|
|
82
|
+
AGENT_MODE_GIT_ACTION: 'push',
|
|
83
|
+
CODE_MODE_GIT_ACTION: 'create-pr',
|
|
77
84
|
};
|
|
78
85
|
|
|
79
86
|
// In-memory cache on globalThis to survive Next.js webpack chunk duplication.
|
|
@@ -120,7 +127,13 @@ export function getConfig(key) {
|
|
|
120
127
|
|
|
121
128
|
// Infrastructure keys: fall back to .env (these live in .env, not exclusively in DB)
|
|
122
129
|
if (value === undefined) {
|
|
123
|
-
const ENV_KEYS = [
|
|
130
|
+
const ENV_KEYS = [
|
|
131
|
+
'GH_OWNER',
|
|
132
|
+
'GH_REPO',
|
|
133
|
+
'GH_TOKEN',
|
|
134
|
+
'APP_URL',
|
|
135
|
+
'APP_HOSTNAME',
|
|
136
|
+
];
|
|
124
137
|
if (ENV_KEYS.includes(key)) {
|
|
125
138
|
value = process.env[key];
|
|
126
139
|
}
|
|
@@ -147,6 +160,4 @@ export function getConfig(key) {
|
|
|
147
160
|
*/
|
|
148
161
|
export function invalidateConfigCache() {
|
|
149
162
|
_cache.clear();
|
|
150
|
-
// Reset agent singletons so they pick up new config
|
|
151
|
-
import('./ai/agent.js').then(({ resetAgentChats }) => resetAgentChats()).catch(() => {});
|
|
152
163
|
}
|
package/lib/containers/CLAUDE.md
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
# lib/containers/ — Container Streaming
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Two SSE endpoints — both authenticated via `auth()` session.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
**
|
|
8
|
-
**
|
|
9
|
-
**
|
|
5
|
+
## `stream.js` — Container Status
|
|
6
|
+
|
|
7
|
+
- **Endpoint**: `/stream/containers`
|
|
8
|
+
- **Events**: `containers` (every 3s with full container list + CPU/memory stats), `ping` (keepalive every 15s)
|
|
9
|
+
- **Data source**: `listNetworkContainers()` + `getContainerStats()` from `lib/tools/docker.js`. Filtered to containers on the event-handler's Docker network (auto-detected at boot).
|
|
10
|
+
- **Client**: `ContainersPage` connects via `new EventSource('/stream/containers')`
|
|
11
|
+
|
|
12
|
+
## `logs.js` — Container Log Tail
|
|
13
|
+
|
|
14
|
+
- **Endpoint**: `/stream/containers/logs?name=<containerName>`
|
|
15
|
+
- **Events**: raw log lines as SSE `data:` frames
|
|
16
|
+
- **Data source**: Docker `containers/{id}/logs?follow=true&stdout=1&stderr=1` via the multiplexed-stream parser in `lib/tools/docker.js`
|
|
17
|
+
- **Client**: `ContainerLogsView` (used from the Containers admin page when a row's logs are expanded)
|
|
18
|
+
|
|
19
|
+
Both endpoints share the same network filter and frame-decoding logic so all SSE consumers see the same view.
|
package/lib/db/CLAUDE.md
CHANGED
|
@@ -25,11 +25,12 @@ Key files: `schema.js` (source of truth), `drizzle/` (generated migrations), `dr
|
|
|
25
25
|
| `users` | Admin accounts (email, bcrypt password hash, role) |
|
|
26
26
|
| `chats` | Chat sessions (user_id, title, starred, chat_mode, code_workspace_id, timestamps) |
|
|
27
27
|
| `messages` | Chat messages (chat_id, role, content) |
|
|
28
|
-
| `code_workspaces` | Code workspace containers (user_id, container_name, repo, branch, feature_branch, title, last_interactive_commit, starred, has_changes) |
|
|
28
|
+
| `code_workspaces` | Code workspace containers (user_id, container_name, repo, branch, feature_branch, title, last_interactive_commit, coding_agent, scope, starred, has_changes) |
|
|
29
29
|
| `notifications` | Job completion notifications (notification text, payload, read status) |
|
|
30
30
|
| `subscriptions` | Channel subscriptions (platform, channel_id) |
|
|
31
|
+
| `user_channels` | Per-user channel linking (user_id, channel, channel_chat_id, code, code_expires_at, verified_at, active_thread_id) — Telegram verification + active thread |
|
|
31
32
|
| `clusters` | Worker clusters (user_id, name, system_prompt, folders, enabled, starred) |
|
|
32
|
-
| `cluster_roles` | Role definitions scoped to a cluster (cluster_id, role_name, role, trigger_config, max_concurrency, cleanup_worker_dir, folders) |
|
|
33
|
+
| `cluster_roles` | Role definitions scoped to a cluster (cluster_id, role_name, role, trigger_config, max_concurrency, plan_mode, cleanup_worker_dir, folders) |
|
|
33
34
|
| `settings` | Key-value configuration store (also stores API keys and OAuth tokens via type/key/value) |
|
|
34
35
|
|
|
35
36
|
## OAuth Token Storage
|
|
@@ -64,3 +65,5 @@ OAuth tokens for coding agent backends are stored as `config_secret` with LRU ro
|
|
|
64
65
|
- `chats.chatMode` — `'agent'` (default) or `'code'`. Determines which agent singleton and tools are used.
|
|
65
66
|
- `codeWorkspaces.featureBranch` — tracks the git feature branch for the workspace session.
|
|
66
67
|
- `codeWorkspaces.hasChanges` — flag set when workspace has uncommitted changes.
|
|
68
|
+
- `codeWorkspaces.codingAgent` — per-workspace coding-agent override. Falls back to global `CODING_AGENT` config, then `claude-code` (`lib/code/actions.js:410`).
|
|
69
|
+
- `codeWorkspaces.scope` — subdirectory scope within the repo (e.g., `agents/gary-vee`). Resolves the agent's working directory and skills (`lib/ai/scope.js`).
|
package/lib/db/chats.js
CHANGED
|
@@ -100,23 +100,6 @@ export function deleteChat(chatId) {
|
|
|
100
100
|
db.delete(chats).where(eq(chats.id, chatId)).run();
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
/**
|
|
104
|
-
* Delete all chats and messages for a user.
|
|
105
|
-
* @param {string} userId
|
|
106
|
-
*/
|
|
107
|
-
export function deleteAllChatsByUser(userId) {
|
|
108
|
-
const db = getDb();
|
|
109
|
-
const userChats = db
|
|
110
|
-
.select({ id: chats.id })
|
|
111
|
-
.from(chats)
|
|
112
|
-
.where(eq(chats.userId, userId))
|
|
113
|
-
.all();
|
|
114
|
-
|
|
115
|
-
for (const chat of userChats) {
|
|
116
|
-
db.delete(messages).where(eq(messages.chatId, chat.id)).run();
|
|
117
|
-
}
|
|
118
|
-
db.delete(chats).where(eq(chats.userId, userId)).run();
|
|
119
|
-
}
|
|
120
103
|
|
|
121
104
|
/**
|
|
122
105
|
* Get all messages for a chat, ordered by creation time.
|
|
@@ -146,6 +129,15 @@ export function linkChatToWorkspace(chatId, workspaceId) {
|
|
|
146
129
|
.run();
|
|
147
130
|
}
|
|
148
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Touch a chat's updatedAt timestamp.
|
|
134
|
+
* @param {string} chatId
|
|
135
|
+
*/
|
|
136
|
+
export function touchChatUpdatedAt(chatId) {
|
|
137
|
+
const db = getDb();
|
|
138
|
+
db.update(chats).set({ updatedAt: Date.now() }).where(eq(chats.id, chatId)).run();
|
|
139
|
+
}
|
|
140
|
+
|
|
149
141
|
/**
|
|
150
142
|
* Save a message to a chat. Also updates the chat's updatedAt timestamp.
|
|
151
143
|
* @param {string} chatId
|
|
@@ -14,7 +14,7 @@ import { codeWorkspaces } from './schema.js';
|
|
|
14
14
|
* @param {string} [options.id] - Optional ID (UUID). Generated if not provided.
|
|
15
15
|
* @returns {object} The created workspace
|
|
16
16
|
*/
|
|
17
|
-
export function createCodeWorkspace(userId, { containerName = null, repo = null, branch = null, title = 'Code Workspace', id = null } = {}) {
|
|
17
|
+
export function createCodeWorkspace(userId, { containerName = null, repo = null, branch = null, scope = null, title = 'Code Workspace', id = null } = {}) {
|
|
18
18
|
const db = getDb();
|
|
19
19
|
const now = Date.now();
|
|
20
20
|
const workspace = {
|
|
@@ -23,6 +23,7 @@ export function createCodeWorkspace(userId, { containerName = null, repo = null,
|
|
|
23
23
|
containerName,
|
|
24
24
|
repo,
|
|
25
25
|
branch,
|
|
26
|
+
scope,
|
|
26
27
|
title,
|
|
27
28
|
createdAt: now,
|
|
28
29
|
updatedAt: now,
|
|
@@ -33,13 +34,17 @@ export function createCodeWorkspace(userId, { containerName = null, repo = null,
|
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Update the container name on an existing workspace (when Docker launches).
|
|
37
|
+
* Also stores the coding agent used so recovery can recreate with the same agent.
|
|
36
38
|
* @param {string} id - Workspace ID
|
|
37
39
|
* @param {string} containerName - Docker container name
|
|
40
|
+
* @param {string} [codingAgent] - Coding agent identifier (e.g. 'claude-code', 'codex-cli')
|
|
38
41
|
*/
|
|
39
|
-
export function updateContainerName(id, containerName) {
|
|
42
|
+
export function updateContainerName(id, containerName, codingAgent) {
|
|
40
43
|
const db = getDb();
|
|
44
|
+
const set = { containerName, updatedAt: Date.now() };
|
|
45
|
+
if (codingAgent !== undefined) set.codingAgent = codingAgent;
|
|
41
46
|
db.update(codeWorkspaces)
|
|
42
|
-
.set(
|
|
47
|
+
.set(set)
|
|
43
48
|
.where(eq(codeWorkspaces.id, id))
|
|
44
49
|
.run();
|
|
45
50
|
}
|
package/lib/db/config.js
CHANGED
package/lib/db/index.js
CHANGED
|
@@ -50,6 +50,18 @@ export function initDatabase() {
|
|
|
50
50
|
|
|
51
51
|
migrate(db, { migrationsFolder });
|
|
52
52
|
|
|
53
|
+
// One-shot cleanup: drop orphan LangGraph checkpoint tables left behind by
|
|
54
|
+
// @langchain/langgraph-checkpoint-sqlite. The React agent has been removed —
|
|
55
|
+
// these tables are never read or written again. Safe to run on every boot
|
|
56
|
+
// (IF EXISTS is a no-op once dropped).
|
|
57
|
+
sqlite.exec(`
|
|
58
|
+
DROP TABLE IF EXISTS checkpoints;
|
|
59
|
+
DROP TABLE IF EXISTS checkpoint_blobs;
|
|
60
|
+
DROP TABLE IF EXISTS checkpoint_writes;
|
|
61
|
+
DROP TABLE IF EXISTS writes;
|
|
62
|
+
DROP TABLE IF EXISTS checkpoint_migrations;
|
|
63
|
+
`);
|
|
64
|
+
|
|
53
65
|
sqlite.close();
|
|
54
66
|
|
|
55
67
|
// Force re-creation of drizzle instance on next getDb() call
|
package/lib/db/schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
|
|
1
|
+
import { sqliteTable, text, integer, uniqueIndex, index } from 'drizzle-orm/sqlite-core';
|
|
2
2
|
|
|
3
3
|
export const users = sqliteTable('users', {
|
|
4
4
|
id: text('id').primaryKey(),
|
|
@@ -52,6 +52,8 @@ export const codeWorkspaces = sqliteTable('code_workspaces', {
|
|
|
52
52
|
featureBranch: text('feature_branch'),
|
|
53
53
|
title: text('title').notNull().default('Code Workspace'),
|
|
54
54
|
lastInteractiveCommit: text('last_interactive_commit'),
|
|
55
|
+
codingAgent: text('coding_agent'),
|
|
56
|
+
scope: text('scope'),
|
|
55
57
|
starred: integer('starred').notNull().default(0),
|
|
56
58
|
hasChanges: integer('has_changes').notNull().default(0),
|
|
57
59
|
createdAt: integer('created_at').notNull(),
|
|
@@ -96,3 +98,24 @@ export const settings = sqliteTable('settings', {
|
|
|
96
98
|
createdAt: integer('created_at').notNull(),
|
|
97
99
|
updatedAt: integer('updated_at').notNull(),
|
|
98
100
|
});
|
|
101
|
+
|
|
102
|
+
export const userChannels = sqliteTable(
|
|
103
|
+
'user_channels',
|
|
104
|
+
{
|
|
105
|
+
id: text('id').primaryKey(),
|
|
106
|
+
userId: text('user_id').notNull(),
|
|
107
|
+
channel: text('channel').notNull(),
|
|
108
|
+
channelChatId: text('channel_chat_id'),
|
|
109
|
+
code: text('code'),
|
|
110
|
+
codeExpiresAt: integer('code_expires_at'),
|
|
111
|
+
verifiedAt: integer('verified_at'),
|
|
112
|
+
activeThreadId: text('active_thread_id'),
|
|
113
|
+
createdAt: integer('created_at').notNull(),
|
|
114
|
+
updatedAt: integer('updated_at').notNull(),
|
|
115
|
+
},
|
|
116
|
+
(t) => ({
|
|
117
|
+
userChannelUnique: uniqueIndex('user_channels_user_channel_unique').on(t.userId, t.channel),
|
|
118
|
+
channelChatIdUnique: uniqueIndex('user_channels_channel_chat_id_unique').on(t.channel, t.channelChatId),
|
|
119
|
+
codeLookup: index('user_channels_code_lookup').on(t.code),
|
|
120
|
+
})
|
|
121
|
+
);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { randomUUID, randomBytes } from 'crypto';
|
|
2
|
+
import { and, eq, isNotNull } from 'drizzle-orm';
|
|
3
|
+
import { getDb } from './index.js';
|
|
4
|
+
import { userChannels } from './schema.js';
|
|
5
|
+
|
|
6
|
+
const CODE_TTL_MS = 10 * 60 * 1000;
|
|
7
|
+
const CODE_BYTES = 4;
|
|
8
|
+
|
|
9
|
+
function now() {
|
|
10
|
+
return Date.now();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function generateCode() {
|
|
14
|
+
return randomBytes(CODE_BYTES).toString('hex').toUpperCase();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getUserChannel(userId, channel) {
|
|
18
|
+
const db = getDb();
|
|
19
|
+
return db
|
|
20
|
+
.select()
|
|
21
|
+
.from(userChannels)
|
|
22
|
+
.where(and(eq(userChannels.userId, userId), eq(userChannels.channel, channel)))
|
|
23
|
+
.get();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getByChannelChatId(channel, channelChatId) {
|
|
27
|
+
const db = getDb();
|
|
28
|
+
return db
|
|
29
|
+
.select()
|
|
30
|
+
.from(userChannels)
|
|
31
|
+
.where(and(eq(userChannels.channel, channel), eq(userChannels.channelChatId, channelChatId)))
|
|
32
|
+
.get();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getByCode(code) {
|
|
36
|
+
const db = getDb();
|
|
37
|
+
return db.select().from(userChannels).where(eq(userChannels.code, code)).get();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Issue or re-issue a verification code for a user+channel.
|
|
42
|
+
* Creates the row if absent; overwrites the code if present and still pending.
|
|
43
|
+
* Throws if the row is already verified — caller should unlink first.
|
|
44
|
+
*/
|
|
45
|
+
export function issueCode(userId, channel) {
|
|
46
|
+
const db = getDb();
|
|
47
|
+
const existing = getUserChannel(userId, channel);
|
|
48
|
+
const code = generateCode();
|
|
49
|
+
const codeExpiresAt = now() + CODE_TTL_MS;
|
|
50
|
+
const timestamp = now();
|
|
51
|
+
|
|
52
|
+
if (existing) {
|
|
53
|
+
if (existing.verifiedAt) {
|
|
54
|
+
throw new Error('Channel already verified — unlink before re-issuing a code');
|
|
55
|
+
}
|
|
56
|
+
db.update(userChannels)
|
|
57
|
+
.set({ code, codeExpiresAt, updatedAt: timestamp })
|
|
58
|
+
.where(eq(userChannels.id, existing.id))
|
|
59
|
+
.run();
|
|
60
|
+
return { ...existing, code, codeExpiresAt, updatedAt: timestamp };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const row = {
|
|
64
|
+
id: randomUUID(),
|
|
65
|
+
userId,
|
|
66
|
+
channel,
|
|
67
|
+
channelChatId: null,
|
|
68
|
+
code,
|
|
69
|
+
codeExpiresAt,
|
|
70
|
+
verifiedAt: null,
|
|
71
|
+
activeThreadId: null,
|
|
72
|
+
createdAt: timestamp,
|
|
73
|
+
updatedAt: timestamp,
|
|
74
|
+
};
|
|
75
|
+
db.insert(userChannels).values(row).run();
|
|
76
|
+
return row;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Redeem a code from an incoming channel message.
|
|
81
|
+
* Returns the userId on success. Throws on expired, already-consumed, or chat-taken.
|
|
82
|
+
*/
|
|
83
|
+
export function redeemCode(channel, code, channelChatId) {
|
|
84
|
+
const db = getDb();
|
|
85
|
+
const row = getByCode(code);
|
|
86
|
+
if (!row || row.channel !== channel) throw new Error('Invalid code');
|
|
87
|
+
if (row.verifiedAt) throw new Error('Code already used');
|
|
88
|
+
if (row.codeExpiresAt && row.codeExpiresAt < now()) throw new Error('Code expired');
|
|
89
|
+
|
|
90
|
+
const chatTaken = getByChannelChatId(channel, channelChatId);
|
|
91
|
+
if (chatTaken && chatTaken.id !== row.id) {
|
|
92
|
+
throw new Error('This chat is already linked to another user');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const timestamp = now();
|
|
96
|
+
db.update(userChannels)
|
|
97
|
+
.set({
|
|
98
|
+
channelChatId,
|
|
99
|
+
code: null,
|
|
100
|
+
codeExpiresAt: null,
|
|
101
|
+
verifiedAt: timestamp,
|
|
102
|
+
updatedAt: timestamp,
|
|
103
|
+
})
|
|
104
|
+
.where(eq(userChannels.id, row.id))
|
|
105
|
+
.run();
|
|
106
|
+
return { userId: row.userId, rowId: row.id };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function setActiveThread(userId, channel, threadId) {
|
|
110
|
+
const db = getDb();
|
|
111
|
+
const timestamp = now();
|
|
112
|
+
db.update(userChannels)
|
|
113
|
+
.set({ activeThreadId: threadId, updatedAt: timestamp })
|
|
114
|
+
.where(
|
|
115
|
+
and(
|
|
116
|
+
eq(userChannels.userId, userId),
|
|
117
|
+
eq(userChannels.channel, channel),
|
|
118
|
+
isNotNull(userChannels.verifiedAt)
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
.run();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function unlink(userId, channel) {
|
|
125
|
+
const db = getDb();
|
|
126
|
+
db.delete(userChannels)
|
|
127
|
+
.where(and(eq(userChannels.userId, userId), eq(userChannels.channel, channel)))
|
|
128
|
+
.run();
|
|
129
|
+
}
|
package/lib/llm-providers.js
CHANGED
|
@@ -17,6 +17,7 @@ export const BUILTIN_PROVIDERS = {
|
|
|
17
17
|
],
|
|
18
18
|
models: [
|
|
19
19
|
{ id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', default: true },
|
|
20
|
+
{ id: 'claude-opus-4-7', name: 'Claude Opus 4.7' },
|
|
20
21
|
{ id: 'claude-opus-4-6', name: 'Claude Opus 4.6' },
|
|
21
22
|
{ id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5' },
|
|
22
23
|
{ id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4' },
|
|
@@ -61,6 +62,7 @@ export const BUILTIN_PROVIDERS = {
|
|
|
61
62
|
credentials: [
|
|
62
63
|
{ type: 'api_key', key: 'DEEPSEEK_API_KEY', label: 'API Key' },
|
|
63
64
|
],
|
|
65
|
+
baseUrl: 'https://api.deepseek.com/v1',
|
|
64
66
|
anthropicEndpoint: 'https://api.deepseek.com/anthropic',
|
|
65
67
|
models: [
|
|
66
68
|
{ id: 'deepseek-chat', name: 'DeepSeek V3', default: true },
|
|
@@ -72,6 +74,7 @@ export const BUILTIN_PROVIDERS = {
|
|
|
72
74
|
credentials: [
|
|
73
75
|
{ type: 'api_key', key: 'MINIMAX_API_KEY', label: 'API Key' },
|
|
74
76
|
],
|
|
77
|
+
baseUrl: 'https://api.minimax.io/v1',
|
|
75
78
|
anthropicEndpoint: 'https://api.minimax.io/anthropic',
|
|
76
79
|
models: [
|
|
77
80
|
{ id: 'MiniMax-M2.7', name: 'MiniMax M2.7', default: true },
|
|
@@ -83,6 +86,7 @@ export const BUILTIN_PROVIDERS = {
|
|
|
83
86
|
mistral: {
|
|
84
87
|
name: 'Mistral',
|
|
85
88
|
litellmProxy: true, litellmPrefix: 'mistral',
|
|
89
|
+
baseUrl: 'https://api.mistral.ai/v1',
|
|
86
90
|
credentials: [
|
|
87
91
|
{ type: 'api_key', key: 'MISTRAL_API_KEY', label: 'API Key' },
|
|
88
92
|
],
|
|
@@ -97,6 +101,7 @@ export const BUILTIN_PROVIDERS = {
|
|
|
97
101
|
xai: {
|
|
98
102
|
name: 'xAI',
|
|
99
103
|
litellmProxy: true, litellmPrefix: 'xai',
|
|
104
|
+
baseUrl: 'https://api.x.ai/v1',
|
|
100
105
|
credentials: [
|
|
101
106
|
{ type: 'api_key', key: 'XAI_API_KEY', label: 'API Key' },
|
|
102
107
|
],
|
|
@@ -112,6 +117,7 @@ export const BUILTIN_PROVIDERS = {
|
|
|
112
117
|
credentials: [
|
|
113
118
|
{ type: 'api_key', key: 'MOONSHOT_API_KEY', label: 'API Key' },
|
|
114
119
|
],
|
|
120
|
+
baseUrl: 'https://api.moonshot.cn/v1',
|
|
115
121
|
anthropicEndpoint: 'https://api.moonshot.cn/anthropic',
|
|
116
122
|
models: [
|
|
117
123
|
{ id: 'kimi-k2.5', name: 'Kimi K2.5', default: true },
|
|
@@ -124,12 +130,14 @@ export const BUILTIN_PROVIDERS = {
|
|
|
124
130
|
credentials: [
|
|
125
131
|
{ type: 'api_key', key: 'OPENROUTER_API_KEY', label: 'API Key' },
|
|
126
132
|
],
|
|
133
|
+
baseUrl: 'https://openrouter.ai/api/v1',
|
|
127
134
|
anthropicEndpoint: 'https://openrouter.ai/api',
|
|
128
135
|
models: [],
|
|
129
136
|
},
|
|
130
137
|
nvidia: {
|
|
131
138
|
name: 'NVIDIA',
|
|
132
139
|
litellmProxy: true, litellmPrefix: 'nvidia_nim',
|
|
140
|
+
baseUrl: 'https://integrate.api.nvidia.com/v1',
|
|
133
141
|
credentials: [
|
|
134
142
|
{ type: 'api_key', key: 'NVIDIA_API_KEY', label: 'API Key' },
|
|
135
143
|
],
|
package/lib/maintenance.js
CHANGED
|
@@ -4,44 +4,54 @@ import { getDb } from './db/index.js';
|
|
|
4
4
|
import { settings } from './db/schema.js';
|
|
5
5
|
|
|
6
6
|
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
|
|
7
|
+
const ONE_HOUR = 60 * 60 * 1000;
|
|
7
8
|
|
|
8
9
|
async function cleanExpiredAgentJobKeys() {
|
|
9
10
|
try {
|
|
10
11
|
const db = getDb();
|
|
11
|
-
const cutoff = Date.now() - TWENTY_FOUR_HOURS;
|
|
12
12
|
const rows = db
|
|
13
13
|
.select({ id: settings.id, key: settings.key, lastUsedAt: settings.lastUsedAt, createdAt: settings.createdAt })
|
|
14
14
|
.from(settings)
|
|
15
15
|
.where(eq(settings.type, 'agent_job_api_key'))
|
|
16
16
|
.all();
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
);
|
|
18
|
+
// Split into SDK keys (no container) vs container-backed keys
|
|
19
|
+
const sdkRows = rows.filter(r => r.key.includes('sdk'));
|
|
20
|
+
const containerRows = rows.filter(r => !r.key.includes('sdk'));
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
console.log(`[maintenance] No expired agent job keys (${rows.length} active)`);
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
22
|
+
let deleted = 0;
|
|
27
23
|
|
|
28
|
-
//
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
// SDK keys: delete any older than 1 hour (no container to inspect)
|
|
25
|
+
const sdkCutoff = Date.now() - ONE_HOUR;
|
|
26
|
+
for (const r of sdkRows) {
|
|
27
|
+
const age = r.lastUsedAt !== null ? r.lastUsedAt : r.createdAt;
|
|
28
|
+
if (age < sdkCutoff) {
|
|
29
|
+
db.delete(settings).where(eq(settings.id, r.id)).run();
|
|
30
|
+
deleted++;
|
|
35
31
|
}
|
|
36
32
|
}
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
// Container-backed keys: existing logic — 24h expiry + container inspect
|
|
35
|
+
const containerCutoff = Date.now() - TWENTY_FOUR_HOURS;
|
|
36
|
+
const candidates = containerRows.filter(r =>
|
|
37
|
+
(r.lastUsedAt !== null ? r.lastUsedAt : r.createdAt) < containerCutoff
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (candidates.length > 0) {
|
|
41
|
+
const { inspectContainer } = await import('./tools/docker.js');
|
|
42
|
+
for (const r of candidates) {
|
|
43
|
+
const info = await inspectContainer(r.key);
|
|
44
|
+
if (!info) {
|
|
45
|
+
db.delete(settings).where(eq(settings.id, r.id)).run();
|
|
46
|
+
deleted++;
|
|
47
|
+
}
|
|
41
48
|
}
|
|
42
|
-
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (deleted > 0) {
|
|
52
|
+
console.log(`[maintenance] Deleted ${deleted} expired agent job key(s)`);
|
|
43
53
|
} else {
|
|
44
|
-
console.log(`[maintenance] ${
|
|
54
|
+
console.log(`[maintenance] No expired agent job keys (${rows.length} active)`);
|
|
45
55
|
}
|
|
46
56
|
} catch (err) {
|
|
47
57
|
console.error('[maintenance] cleanExpiredAgentJobKeys failed:', err);
|
package/lib/tools/CLAUDE.md
CHANGED
|
@@ -12,7 +12,16 @@ Calls Docker Engine API directly through `/var/run/docker.sock` using Node's `ht
|
|
|
12
12
|
|
|
13
13
|
**Image pull on demand**: Checks if image exists locally before pulling. Avoids pre-pulling at startup.
|
|
14
14
|
|
|
15
|
-
**`buildAgentAuthEnv(agent)`**: Resolves coding agent type → auth environment variables from the settings DB.
|
|
15
|
+
**`buildAgentAuthEnv(agent)`**: Resolves coding agent type → auth environment variables from the settings DB. All credentials come from the DB (`getConfig`, `getCustomProvider`), never `.env` or GitHub secrets. Returns `{ env: string[], backendApi: string }`.
|
|
16
|
+
|
|
17
|
+
Per-agent resolution paths:
|
|
18
|
+
|
|
19
|
+
- **`claude-code`** — `CODING_AGENT_CLAUDE_CODE_BACKEND` selects backend. If `anthropic`, picks OAuth (`CLAUDE_CODE_OAUTH_TOKEN` via LRU rotation) or API key (`ANTHROPIC_API_KEY`). For Anthropic-compatible third parties (DeepSeek, MiniMax, Kimi, OpenRouter) sets `ANTHROPIC_AUTH_TOKEN` + `ANTHROPIC_BASE_URL` to the provider's `anthropicEndpoint`. For OpenAI-only builtins or custom providers, routes through the LiteLLM sidecar at `http://litellm:4000` and prefixes the model with the provider key.
|
|
20
|
+
- **`pi-coding-agent`, `opencode`, `kimi-cli`** — share a multi-provider pattern. `CODING_AGENT_{AGENT}_PROVIDER` picks anthropic/openai/google/deepseek/minimax/mistral/xai/openrouter/nvidia or a custom provider. Sets the matching `*_API_KEY` (or `CUSTOM_OPENAI_BASE_URL` + `CUSTOM_API_KEY` for custom).
|
|
21
|
+
- **`gemini-cli`** — `GOOGLE_API_KEY` only. Backend is always `google`.
|
|
22
|
+
- **`codex-cli`** — OAuth (`CODEX_OAUTH_TOKEN`) or API key (`OPENAI_API_KEY`). Backend always `openai`.
|
|
23
|
+
|
|
24
|
+
OAuth tokens use LRU rotation via `getNextOAuthToken()` (in `lib/db/oauth-tokens.js`) — distributes load across multiple stored tokens and updates `lastUsedAt` on each pick. Refresh-token rotation is handled at retrieval time in `/api/get-agent-job-secret` (under a per-token lock).
|
|
16
25
|
|
|
17
26
|
## create-agent-job.js — Agent Job Creation
|
|
18
27
|
|
|
@@ -36,6 +45,6 @@ Calls Docker Engine API directly through `/var/run/docker.sock` using Node's `ht
|
|
|
36
45
|
|
|
37
46
|
**Typing indicator with jitter**: Re-sends typing action at 5.5–8s random intervals (Telegram expires indicators after 5s). Returns a stop function.
|
|
38
47
|
|
|
39
|
-
##
|
|
48
|
+
## assemblyai.js — Voice Transcription
|
|
40
49
|
|
|
41
|
-
**Feature flag via API key**: `
|
|
50
|
+
**Feature flag via API key**: `isAssemblyAIEnabled()` checks if `ASSEMBLYAI_API_KEY` is set. Used by the Telegram adapter to conditionally offer voice transcription. `transcribeAudio(buffer)` uses the official `assemblyai` SDK (`client.transcripts.transcribe`), which handles upload + polling internally. Throws when the transcript status is `'error'`.
|