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
package/lib/ai/line-mappers.js
CHANGED
|
@@ -29,6 +29,8 @@ export function mapLine(line, mapper = mapClaudeCodeLine) {
|
|
|
29
29
|
try {
|
|
30
30
|
parsed = JSON.parse(line);
|
|
31
31
|
} catch {
|
|
32
|
+
// Suppress entrypoint.sh step markers ("→ Setup Git", "→ Clone", ...)
|
|
33
|
+
if (line.startsWith('→ ')) return [{ type: 'skip' }];
|
|
32
34
|
console.warn('[line-mappers] JSON parse failed, length:', line.length, 'preview:', line.slice(0, 120));
|
|
33
35
|
// Non-JSON lines (NO_CHANGES, PUSH_SUCCESS, AGENT_FAILED, etc.)
|
|
34
36
|
return [{ type: 'text', text: `\n${line}\n` }];
|
|
@@ -116,15 +118,16 @@ export function mapClaudeCodeLine(parsed) {
|
|
|
116
118
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
117
119
|
// Pi Coding Agent: --mode json
|
|
118
120
|
//
|
|
119
|
-
//
|
|
120
|
-
// session, agent_start, agent_end
|
|
121
|
-
// turn_start, turn_end
|
|
122
|
-
// message_start, message_update, message_end
|
|
123
|
-
// tool_execution_start
|
|
121
|
+
// Top-level event types:
|
|
122
|
+
// session, agent_start, agent_end — session/lifecycle (skip)
|
|
123
|
+
// turn_start, turn_end — turn lifecycle (skip)
|
|
124
|
+
// message_start, message_update, message_end — message lifecycle
|
|
125
|
+
// tool_execution_start/update/end — tool execution
|
|
124
126
|
//
|
|
125
127
|
// message_update.assistantMessageEvent subtypes:
|
|
126
|
-
// text_start, text_delta, text_end
|
|
127
|
-
//
|
|
128
|
+
// text_start, text_delta, text_end — only text_delta carries new content
|
|
129
|
+
// thinking_start, thinking_delta, thinking_end — extended-thinking blocks
|
|
130
|
+
// toolcall_start, toolcall_delta, toolcall_end — only toolcall_end carries complete args
|
|
128
131
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
129
132
|
|
|
130
133
|
/**
|
|
@@ -136,22 +139,51 @@ export function mapPiLine(parsed) {
|
|
|
136
139
|
const events = [];
|
|
137
140
|
const { type } = parsed;
|
|
138
141
|
|
|
142
|
+
// Lifecycle / session noise — skip silently so they don't render as Unknown Events
|
|
143
|
+
if (
|
|
144
|
+
type === 'session' ||
|
|
145
|
+
type === 'agent_start' ||
|
|
146
|
+
type === 'agent_end' ||
|
|
147
|
+
type === 'turn_start' ||
|
|
148
|
+
type === 'turn_end' ||
|
|
149
|
+
type === 'message_start' ||
|
|
150
|
+
type === 'message_end' ||
|
|
151
|
+
type === 'tool_execution_start' ||
|
|
152
|
+
type === 'tool_execution_update'
|
|
153
|
+
) {
|
|
154
|
+
return [{ type: 'skip' }];
|
|
155
|
+
}
|
|
156
|
+
|
|
139
157
|
if (type === 'message_update' && parsed.assistantMessageEvent) {
|
|
140
158
|
const evt = parsed.assistantMessageEvent;
|
|
141
159
|
|
|
142
|
-
// Text streaming
|
|
160
|
+
// Text streaming — deltas are authoritative; start/end are lifecycle only
|
|
143
161
|
if (evt.type === 'text_delta' && evt.delta) {
|
|
144
162
|
events.push({ type: 'text', text: evt.delta });
|
|
163
|
+
} else if (evt.type === 'text_start' || evt.type === 'text_end') {
|
|
164
|
+
return [{ type: 'skip' }];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Extended-thinking streaming — map to UI thinking chunks (rendered as a
|
|
168
|
+
// collapsible ephemeral card by api.js, never persisted)
|
|
169
|
+
else if (evt.type === 'thinking_start') {
|
|
170
|
+
events.push({ type: 'thinking-start' });
|
|
171
|
+
} else if (evt.type === 'thinking_delta' && evt.delta) {
|
|
172
|
+
events.push({ type: 'thinking', delta: evt.delta });
|
|
173
|
+
} else if (evt.type === 'thinking_end') {
|
|
174
|
+
events.push({ type: 'thinking-end' });
|
|
145
175
|
}
|
|
146
176
|
|
|
147
|
-
// Tool call — emit on toolcall_end
|
|
148
|
-
if (evt.type === 'toolcall_end' && evt.toolCall) {
|
|
177
|
+
// Tool call — emit once on toolcall_end with complete args. Start/delta are skipped.
|
|
178
|
+
else if (evt.type === 'toolcall_end' && evt.toolCall) {
|
|
149
179
|
events.push({
|
|
150
180
|
type: 'tool-call',
|
|
151
181
|
toolCallId: evt.toolCall.id,
|
|
152
182
|
toolName: evt.toolCall.name,
|
|
153
183
|
args: evt.toolCall.arguments || {},
|
|
154
184
|
});
|
|
185
|
+
} else if (evt.type === 'toolcall_start' || evt.type === 'toolcall_delta') {
|
|
186
|
+
return [{ type: 'skip' }];
|
|
155
187
|
}
|
|
156
188
|
}
|
|
157
189
|
|
|
@@ -167,20 +199,6 @@ export function mapPiLine(parsed) {
|
|
|
167
199
|
});
|
|
168
200
|
}
|
|
169
201
|
|
|
170
|
-
// Final summary
|
|
171
|
-
else if (type === 'agent_end' && parsed.messages) {
|
|
172
|
-
const lastAssistant = [...parsed.messages].reverse().find(m => m.role === 'assistant');
|
|
173
|
-
if (lastAssistant) {
|
|
174
|
-
const text = (lastAssistant.content || [])
|
|
175
|
-
.filter(b => b.type === 'text')
|
|
176
|
-
.map(b => b.text)
|
|
177
|
-
.join('');
|
|
178
|
-
if (text) {
|
|
179
|
-
events.push({ type: 'text', text });
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
202
|
return events;
|
|
185
203
|
}
|
|
186
204
|
|
package/lib/ai/scope.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve working directory and skills directory for a given agent scope.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} repoRoot - Absolute path to the git repo root
|
|
8
|
+
* @param {string|null} scope - Relative subdirectory path (e.g., 'agents/gary-v') or null/empty for root
|
|
9
|
+
* @returns {{ workingDir: string, skillsDir: string|null }}
|
|
10
|
+
*/
|
|
11
|
+
export function resolveAgentScope(repoRoot, scope) {
|
|
12
|
+
const workingDir = scope ? path.join(repoRoot, scope) : repoRoot;
|
|
13
|
+
|
|
14
|
+
// Skills: check scoped dir first, fall back to repo root
|
|
15
|
+
const scopedSkills = path.join(workingDir, 'skills');
|
|
16
|
+
const rootSkills = path.join(repoRoot, 'skills');
|
|
17
|
+
|
|
18
|
+
let skillsDir = null;
|
|
19
|
+
if (existsSync(scopedSkills)) {
|
|
20
|
+
skillsDir = scopedSkills;
|
|
21
|
+
} else if (existsSync(rootSkills)) {
|
|
22
|
+
skillsDir = rootSkills;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { workingDir, skillsDir };
|
|
26
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# lib/ai/sdk-adapters/ — SDK Adapter System
|
|
2
|
+
|
|
3
|
+
In-process SDK adapters that run a coding agent's SDK directly (no container) and yield a unified chunk stream consumed by `chatStream()` in `lib/ai/index.js`. Used when the active coding agent has a registered adapter; otherwise `chatStream()` uses the direct headless-container path.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Browser → POST /stream/chat (api.js)
|
|
9
|
+
→ chatStream() (index.js)
|
|
10
|
+
→ workspace setup (ensureWorkspaceRepo)
|
|
11
|
+
→ getSdkAdapter() returns adapter function or null
|
|
12
|
+
→ if adapter: streamViaSdk — in-process SDK call, yields normalized chunks
|
|
13
|
+
→ if null: streamViaContainer — headless Docker container, parseHeadlessStream yields chunks
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The adapter is a pure stream translator — it receives a prompt and options, calls the SDK, and yields normalized chunks. Everything else (workspace setup, DB persistence, session continuity, system prompts) is handled by `chatStream()` in `index.js`.
|
|
17
|
+
|
|
18
|
+
## Existing Adapter
|
|
19
|
+
|
|
20
|
+
| File | Agent | SDK |
|
|
21
|
+
|------|-------|-----|
|
|
22
|
+
| `claude-code.js` | `claude-code` | `@anthropic-ai/claude-agent-sdk` |
|
|
23
|
+
|
|
24
|
+
## Adding a New SDK Adapter
|
|
25
|
+
|
|
26
|
+
### 1. Create the adapter file
|
|
27
|
+
|
|
28
|
+
Create `{agent-name}.js` in this directory. Export a single async generator function:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
export async function* myAgentStream({ prompt, workspaceDir, systemPrompt, sessionId, permissionMode, attachments }) {
|
|
32
|
+
// ... call the SDK, yield chunks
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Required chunk types to yield
|
|
37
|
+
|
|
38
|
+
The adapter MUST yield these chunk types for `chatStream()` and `api.js` to work correctly:
|
|
39
|
+
|
|
40
|
+
| Chunk | Shape | When | Purpose |
|
|
41
|
+
|-------|-------|------|---------|
|
|
42
|
+
| `meta` | `{ type: 'meta', sessionId: string }` | First event | Session ID for continuity across messages. `chatStream()` writes this to disk via `writeSessionId()` so subsequent messages resume the session. |
|
|
43
|
+
| `text` | `{ type: 'text', text: string }` | Text output | Streamed to UI as deltas. Accumulated by `chatStream()` and flushed to DB as assistant messages at tool boundaries and stream end. |
|
|
44
|
+
| `tool-call` | `{ type: 'tool-call', toolCallId: string, toolName: string, args: object }` | Tool invocation starts | Triggers tool UI in the browser. May be yielded twice: once at start with `args: {}`, once at `content_block_stop` with complete args. `chatStream()` tracks these in `pendingToolCalls` for pairing with results. |
|
|
45
|
+
| `tool-result` | `{ type: 'tool-result', toolCallId: string, result: string }` | Tool completes | Paired with the matching `tool-call` by `toolCallId`. `chatStream()` persists the pair as a `tool-invocation` JSON message in the DB. |
|
|
46
|
+
| `result` | `{ type: 'result', text: string, cost?: number, duration?: number, subtype?: string }` | Stream ends | Final summary. Logged by `chatStream()`, not persisted or sent to UI. |
|
|
47
|
+
|
|
48
|
+
Optional:
|
|
49
|
+
| `unknown` | `{ type: 'unknown', raw: any }` | Unrecognized events | `api.js` renders these as collapsible boxes in the UI. Use for debugging unhandled SDK events. |
|
|
50
|
+
|
|
51
|
+
### 3. Register in index.js
|
|
52
|
+
|
|
53
|
+
Add the import and mapping in `getSdkAdapter()`:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import { myAgentStream } from './my-agent.js';
|
|
57
|
+
|
|
58
|
+
export function getSdkAdapter(agentType) {
|
|
59
|
+
if (agentType === 'claude-code') return claudeCodeStream;
|
|
60
|
+
if (agentType === 'my-agent') return myAgentStream;
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The `agentType` string comes from the `CODING_AGENT` config value set in the admin UI.
|
|
66
|
+
|
|
67
|
+
### 4. Auth resolution
|
|
68
|
+
|
|
69
|
+
Use `buildAgentAuthEnv(agentType)` from `lib/tools/docker.js` to get credentials from the settings DB. This returns `{ env: string[], backendApi: string }` where `env` is an array of `KEY=value` strings. Parse them into an env object:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
import { buildAgentAuthEnv } from '../../tools/docker.js';
|
|
73
|
+
|
|
74
|
+
const env = { ...process.env };
|
|
75
|
+
const { env: authEnvPairs } = buildAgentAuthEnv('my-agent');
|
|
76
|
+
for (const pair of authEnvPairs) {
|
|
77
|
+
const eqIdx = pair.indexOf('=');
|
|
78
|
+
if (eqIdx > 0) env[pair.slice(0, eqIdx)] = pair.slice(eqIdx + 1);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The agent's auth config (API keys, OAuth tokens, provider selection) is managed in the admin UI at `/admin/event-handler/coding-agents` and stored in the settings DB. `buildAgentAuthEnv()` reads it — you don't need to access the settings DB directly.
|
|
83
|
+
|
|
84
|
+
### 5. Function parameters
|
|
85
|
+
|
|
86
|
+
| Param | Type | Description |
|
|
87
|
+
|-------|------|-------------|
|
|
88
|
+
| `prompt` | `string` | User message text |
|
|
89
|
+
| `workspaceDir` | `string` | Absolute path to git repo root (the SDK should execute here) |
|
|
90
|
+
| `systemPrompt` | `string\|null` | System prompt for agent mode (null in code mode) |
|
|
91
|
+
| `sessionId` | `string\|null` | Previous session ID to resume (null on first message) |
|
|
92
|
+
| `permissionMode` | `string` | `'plan'` (read-only) or `'code'` (read-write). Map to the SDK's equivalent permission concept. |
|
|
93
|
+
| `attachments` | `Array` | Image attachments: `{ category: 'image', mimeType, dataUrl }` |
|
|
94
|
+
|
|
95
|
+
### 6. Session continuity contract
|
|
96
|
+
|
|
97
|
+
Multi-turn conversation works via session IDs:
|
|
98
|
+
|
|
99
|
+
1. First message: `sessionId` param is `null`. Adapter yields `{ type: 'meta', sessionId: '<new-id>' }`.
|
|
100
|
+
2. `chatStream()` writes the session ID to `{workspaceBaseDir}/.claude-ttyd-sessions/7681`.
|
|
101
|
+
3. Next message: `sessionId` param contains the saved ID. Adapter passes it to the SDK's resume mechanism.
|
|
102
|
+
|
|
103
|
+
If the SDK doesn't support session resume, the adapter can ignore `sessionId` — but multi-turn context will be lost between messages.
|
|
104
|
+
|
|
105
|
+
## What the adapter does NOT handle
|
|
106
|
+
|
|
107
|
+
These are managed by `chatStream()` in `index.js` — adapters should not duplicate them:
|
|
108
|
+
|
|
109
|
+
- **Workspace git setup** — `ensureWorkspaceRepo()` clones/checkouts before the adapter is called
|
|
110
|
+
- **DB persistence** — `chatStream()` saves user messages, assistant text, and tool invocations
|
|
111
|
+
- **Chat creation** — `chatStream()` creates the chat and workspace DB records
|
|
112
|
+
- **Auto-titling** — `chatStream()` generates a title after the first message
|
|
113
|
+
- **System prompt loading** — `chatStream()` calls `buildCodingAgentSystemPrompt()` and passes the result as `systemPrompt`
|
|
114
|
+
- **Skill activation** — `ensureSkills()` runs before the adapter is called
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
1
3
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
4
|
import { getConfig } from '../../config.js';
|
|
3
5
|
import { buildAgentAuthEnv } from '../../tools/docker.js';
|
|
6
|
+
import { createAgentJobApiKey } from '../../db/api-keys.js';
|
|
7
|
+
import { getAllAgentJobSecrets } from '../../db/config.js';
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Claude Agent SDK adapter. Wraps the SDK's query() and yields
|
|
@@ -15,10 +19,72 @@ import { buildAgentAuthEnv } from '../../tools/docker.js';
|
|
|
15
19
|
* @param {Array} [opts.attachments] - Image attachments
|
|
16
20
|
* @yields {{ type: 'text'|'tool-call'|'tool-result'|'meta'|'result'|'unknown', ... }}
|
|
17
21
|
*/
|
|
18
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Encode an absolute path the same way Claude Code encodes cwd for session storage.
|
|
24
|
+
* Non-alphanumeric characters are replaced with '-'.
|
|
25
|
+
*/
|
|
26
|
+
function encodeCwd(absolutePath) {
|
|
27
|
+
return absolutePath.replace(/[^a-zA-Z0-9]/g, '-');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Ensure the interactive container can find SDK-created sessions on the shared volume.
|
|
32
|
+
*
|
|
33
|
+
* The SDK stores sessions at $HOME/.claude/projects/<encoded-cwd>/. The interactive
|
|
34
|
+
* container uses cwd=/home/coding-agent/workspace[/scope], but the SDK adapter uses
|
|
35
|
+
* cwd=/app/data/workspaces/workspace-XXX/workspace[/scope] — different encoded paths.
|
|
36
|
+
*
|
|
37
|
+
* Creates a symlink so the interactive container's encoded path resolves to the
|
|
38
|
+
* SDK adapter's encoded path, both on the same volume.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} wsBaseDir - Volume root (e.g., /app/data/workspaces/workspace-XXX)
|
|
41
|
+
* @param {string} sdkCwd - SDK's working directory (scoped, e.g., .../workspace/agents/gary-vee)
|
|
42
|
+
* @param {string} interactiveCwd - Interactive container's equivalent cwd (e.g., /home/coding-agent/workspace/agents/gary-vee)
|
|
43
|
+
*/
|
|
44
|
+
function ensureSessionSymlink(wsBaseDir, sdkCwd, interactiveCwd) {
|
|
45
|
+
const projectsDir = path.join(wsBaseDir, '.claude', 'projects');
|
|
46
|
+
const sdkEncoded = encodeCwd(sdkCwd);
|
|
47
|
+
const interactiveEncoded = encodeCwd(interactiveCwd);
|
|
48
|
+
|
|
49
|
+
// Both point to the same dir — no symlink needed
|
|
50
|
+
if (sdkEncoded === interactiveEncoded) return;
|
|
51
|
+
|
|
52
|
+
fs.mkdirSync(path.join(projectsDir, sdkEncoded), { recursive: true });
|
|
53
|
+
|
|
54
|
+
const symlinkPath = path.join(projectsDir, interactiveEncoded);
|
|
55
|
+
try {
|
|
56
|
+
const existing = fs.readlinkSync(symlinkPath);
|
|
57
|
+
if (existing === sdkEncoded) return; // already correct
|
|
58
|
+
fs.unlinkSync(symlinkPath);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (err.code !== 'ENOENT') {
|
|
61
|
+
// It's a real directory (not a symlink) — don't touch it
|
|
62
|
+
if (err.code === 'EINVAL') return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
fs.symlinkSync(sdkEncoded, symlinkPath);
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, sessionId, permissionMode, attachments, workspaceId, chatMode }) {
|
|
72
|
+
// Point HOME at the workspace volume root (always one level above 'workspace/').
|
|
73
|
+
// workspaceDir may be scoped (e.g., .../workspace/agents/gary-vee), so we can't
|
|
74
|
+
// use path.dirname — derive from workspaceId instead.
|
|
75
|
+
const { workspaceDir: getWsDir } = await import('../../tools/docker.js');
|
|
76
|
+
const wsBaseDir = getWsDir(workspaceId);
|
|
77
|
+
|
|
78
|
+
// Build the interactive container's equivalent cwd for session symlink
|
|
79
|
+
const repoRoot = path.join(wsBaseDir, 'workspace');
|
|
80
|
+
const scope = workspaceDir.startsWith(repoRoot) ? workspaceDir.slice(repoRoot.length) : '';
|
|
81
|
+
const interactiveCwd = '/home/coding-agent/workspace' + scope;
|
|
82
|
+
ensureSessionSymlink(wsBaseDir, workspaceDir, interactiveCwd);
|
|
83
|
+
|
|
19
84
|
// Build a local env object with auth credentials from the settings DB.
|
|
20
85
|
// Passed via the SDK's `env` option — no process.env mutation needed.
|
|
21
86
|
const env = { ...process.env };
|
|
87
|
+
env.HOME = wsBaseDir;
|
|
22
88
|
try {
|
|
23
89
|
const { env: authEnvPairs } = buildAgentAuthEnv('claude-code');
|
|
24
90
|
for (const pair of authEnvPairs) {
|
|
@@ -41,16 +107,41 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
|
|
|
41
107
|
// Fall through — env may already have the right vars from process.env
|
|
42
108
|
}
|
|
43
109
|
|
|
110
|
+
// Inject agent job secrets when in agent chat mode
|
|
111
|
+
if (chatMode === 'agent') {
|
|
112
|
+
const shortId = (workspaceId || '').replace(/-/g, '').slice(0, 8);
|
|
113
|
+
const { key: agentJobToken } = createAgentJobApiKey(`claude-code-sdk-${shortId}`);
|
|
114
|
+
env.AGENT_JOB_TOKEN = agentJobToken;
|
|
115
|
+
const appUrl = getConfig('APP_URL');
|
|
116
|
+
if (appUrl) env.APP_URL = appUrl;
|
|
117
|
+
|
|
118
|
+
// Inject plain secrets as env vars (oauth types are null — agent fetches via skill)
|
|
119
|
+
const jobSecrets = getAllAgentJobSecrets();
|
|
120
|
+
for (const { key, value } of jobSecrets) {
|
|
121
|
+
if (value !== null && !env[key]) {
|
|
122
|
+
env[key] = value;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Work around SDK musl/glibc detection bug — force glibc binary on Linux
|
|
128
|
+
let executablePath;
|
|
129
|
+
if (process.platform === 'linux') {
|
|
130
|
+
try {
|
|
131
|
+
executablePath = require.resolve(`@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude`);
|
|
132
|
+
} catch {}
|
|
133
|
+
}
|
|
134
|
+
|
|
44
135
|
const options = {
|
|
45
136
|
cwd: workspaceDir,
|
|
46
137
|
env,
|
|
47
138
|
includePartialMessages: true,
|
|
139
|
+
model: getConfig('CODING_AGENT_CLAUDE_CODE_MODEL') || undefined,
|
|
140
|
+
...(executablePath ? { pathToClaudeCodeExecutable: executablePath } : {}),
|
|
48
141
|
};
|
|
49
142
|
|
|
50
|
-
// Permission mode
|
|
51
|
-
|
|
52
|
-
options.permissionMode = 'bypassPermissions';
|
|
53
|
-
}
|
|
143
|
+
// Permission mode: plan = read-only, anything else = full write access
|
|
144
|
+
options.permissionMode = permissionMode === 'plan' ? 'plan' : 'bypassPermissions';
|
|
54
145
|
|
|
55
146
|
if (sessionId) options.resume = sessionId;
|
|
56
147
|
if (systemPrompt) {
|
|
@@ -80,6 +171,8 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
|
|
|
80
171
|
|
|
81
172
|
// Track tool call state for mapping stream events
|
|
82
173
|
const activeToolCalls = new Map(); // index → { id, name, argsJson }
|
|
174
|
+
const toolNamesById = new Map(); // toolCallId → toolName (persists for tool-result lookup)
|
|
175
|
+
const activeThinkingBlocks = new Set(); // indices of active thinking blocks
|
|
83
176
|
|
|
84
177
|
try {
|
|
85
178
|
for await (const message of query({ prompt: sdkPrompt, options })) {
|
|
@@ -102,9 +195,13 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
|
|
|
102
195
|
const block = event.content_block;
|
|
103
196
|
if (block.type === 'tool_use') {
|
|
104
197
|
activeToolCalls.set(event.index, { id: block.id, name: block.name, argsJson: '' });
|
|
198
|
+
toolNamesById.set(block.id, block.name);
|
|
105
199
|
yield { type: 'tool-call', toolCallId: block.id, toolName: block.name, args: {} };
|
|
200
|
+
} else if (block.type === 'thinking') {
|
|
201
|
+
activeThinkingBlocks.add(event.index);
|
|
202
|
+
yield { type: 'thinking-start' };
|
|
106
203
|
}
|
|
107
|
-
// Skip '
|
|
204
|
+
// Skip 'text' start (deltas handle text)
|
|
108
205
|
continue;
|
|
109
206
|
}
|
|
110
207
|
|
|
@@ -114,11 +211,17 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
|
|
|
114
211
|
} else if (event.delta.type === 'input_json_delta') {
|
|
115
212
|
const tc = activeToolCalls.get(event.index);
|
|
116
213
|
if (tc) tc.argsJson += event.delta.partial_json;
|
|
214
|
+
} else if (event.delta.type === 'thinking_delta') {
|
|
215
|
+
yield { type: 'thinking', delta: event.delta.thinking };
|
|
117
216
|
}
|
|
118
217
|
continue;
|
|
119
218
|
}
|
|
120
219
|
|
|
121
220
|
if (event.type === 'content_block_stop') {
|
|
221
|
+
if (activeThinkingBlocks.has(event.index)) {
|
|
222
|
+
activeThinkingBlocks.delete(event.index);
|
|
223
|
+
yield { type: 'thinking-end' };
|
|
224
|
+
}
|
|
122
225
|
const tc = activeToolCalls.get(event.index);
|
|
123
226
|
if (tc && tc.argsJson) {
|
|
124
227
|
try {
|
|
@@ -144,14 +247,23 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
|
|
|
144
247
|
: Array.isArray(block.content)
|
|
145
248
|
? block.content.map(b => b.type === 'text' ? b.text : JSON.stringify(b)).join('\n')
|
|
146
249
|
: JSON.stringify(block.content);
|
|
147
|
-
yield { type: 'tool-result', toolCallId: block.tool_use_id, result: content };
|
|
250
|
+
yield { type: 'tool-result', toolCallId: block.tool_use_id, toolName: toolNamesById.get(block.tool_use_id), result: content };
|
|
148
251
|
}
|
|
149
252
|
}
|
|
150
253
|
continue;
|
|
151
254
|
}
|
|
152
255
|
|
|
153
256
|
// ── assistant messages — redundant with streaming, skip ──
|
|
154
|
-
|
|
257
|
+
// But extract tool names so resumed tool-results can carry them.
|
|
258
|
+
if (message.type === 'assistant') {
|
|
259
|
+
const blocks = message.message?.content || [];
|
|
260
|
+
for (const block of blocks) {
|
|
261
|
+
if (block.type === 'tool_use' && block.id && block.name) {
|
|
262
|
+
toolNamesById.set(block.id, block.name);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
155
267
|
|
|
156
268
|
// ── result ──
|
|
157
269
|
if (message.type === 'result') {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { PROJECT_ROOT } from '../paths.js';
|
|
4
|
+
import { render_md } from '../utils/render-md.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build the system prompt for a coding agent.
|
|
8
|
+
* @param {'agent'|'code'} mode - Chat mode
|
|
9
|
+
* @param {string|null} [skillsDir] - Skills directory for {{skills}} resolution.
|
|
10
|
+
* @param {string|null} [scope] - Agent scope (e.g., 'agents/gary-vee'). When set,
|
|
11
|
+
* looks for SYSTEM.md in the scoped directory first, falls back to agent-job/SYSTEM.md.
|
|
12
|
+
* @returns {string|null} Rendered system prompt, or null if not configured
|
|
13
|
+
*/
|
|
14
|
+
export function buildCodingAgentSystemPrompt(mode, skillsDir, scope) {
|
|
15
|
+
let file;
|
|
16
|
+
|
|
17
|
+
if (mode === 'agent') {
|
|
18
|
+
// Check scoped SYSTEM.md first, fall back to agent-job/SYSTEM.md
|
|
19
|
+
if (scope) {
|
|
20
|
+
const scopedFile = path.join(PROJECT_ROOT, scope, 'SYSTEM.md');
|
|
21
|
+
if (existsSync(scopedFile)) {
|
|
22
|
+
file = scopedFile;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (!file) {
|
|
26
|
+
file = path.join(PROJECT_ROOT, 'agent-job/SYSTEM.md');
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
file = path.join(PROJECT_ROOT, 'coding-workspace/SYSTEM.md');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const rendered = render_md(file, { skillsDir });
|
|
33
|
+
return rendered?.trim() || null;
|
|
34
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile as execFileCb } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
|
-
import { existsSync, mkdirSync
|
|
3
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { getConfig } from '../config.js';
|
|
6
6
|
|
|
@@ -32,20 +32,25 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
|
|
|
32
32
|
if (ghToken) env.GH_TOKEN = ghToken;
|
|
33
33
|
|
|
34
34
|
const execOpts = { cwd: workspaceDir, env };
|
|
35
|
+
const log = [];
|
|
35
36
|
|
|
36
37
|
// 1. Create workspace directory
|
|
37
38
|
mkdirSync(workspaceDir, { recursive: true });
|
|
38
39
|
|
|
39
40
|
// 2. Configure git to use GH_TOKEN for GitHub HTTPS URLs (mirrors setup-git.sh)
|
|
40
41
|
if (ghToken) {
|
|
41
|
-
await run('gh', ['auth', 'setup-git'], execOpts);
|
|
42
|
+
const out = await run('gh', ['auth', 'setup-git'], execOpts);
|
|
43
|
+
if (out) log.push(out);
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
// 3. Clone if not already a git repo
|
|
45
47
|
const hasGit = existsSync(path.join(workspaceDir, '.git'));
|
|
46
48
|
if (!hasGit) {
|
|
47
49
|
if (!repo) throw new Error('ensureWorkspaceRepo: repo is required for initial clone');
|
|
48
|
-
|
|
50
|
+
if (!branch) throw new Error(`ensureWorkspaceRepo: branch is required (could not resolve default branch for ${repo})`);
|
|
51
|
+
const out = await run('git', ['clone', '--branch', branch, `https://github.com/${repo}`, '.'], execOpts);
|
|
52
|
+
log.push(`Cloned ${repo} (branch: ${branch})`);
|
|
53
|
+
if (out) log.push(out);
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
// 3. Git identity (only if not already configured)
|
|
@@ -61,6 +66,7 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
|
|
|
61
66
|
const email = user.email || `${user.id}+${user.login}@users.noreply.github.com`;
|
|
62
67
|
await run('git', ['config', 'user.name', name], execOpts);
|
|
63
68
|
await run('git', ['config', 'user.email', email], execOpts);
|
|
69
|
+
log.push(`Git identity: ${name} <${email}>`);
|
|
64
70
|
} catch (err) {
|
|
65
71
|
console.error('[workspace-setup] Failed to set git identity:', err.message);
|
|
66
72
|
}
|
|
@@ -68,7 +74,7 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
|
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
// 4. Feature branch checkout
|
|
71
|
-
if (!featureBranch) return;
|
|
77
|
+
if (!featureBranch) return log.join('\n');
|
|
72
78
|
|
|
73
79
|
// Already on the right branch locally?
|
|
74
80
|
try {
|
|
@@ -77,8 +83,11 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
|
|
|
77
83
|
const current = await run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], execOpts);
|
|
78
84
|
if (current !== featureBranch) {
|
|
79
85
|
await run('git', ['checkout', featureBranch], execOpts);
|
|
86
|
+
log.push(`Checked out ${featureBranch}`);
|
|
87
|
+
} else {
|
|
88
|
+
log.push(`Already on ${featureBranch}`);
|
|
80
89
|
}
|
|
81
|
-
return;
|
|
90
|
+
return log.join('\n');
|
|
82
91
|
} catch {
|
|
83
92
|
// Branch doesn't exist locally — check remote
|
|
84
93
|
}
|
|
@@ -88,43 +97,18 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
|
|
|
88
97
|
if (remoteCheck) {
|
|
89
98
|
// Remote branch exists — checkout tracking it
|
|
90
99
|
await run('git', ['checkout', '-B', featureBranch, `origin/${featureBranch}`], execOpts);
|
|
100
|
+
log.push(`Checked out ${featureBranch} (tracking origin)`);
|
|
91
101
|
} else {
|
|
92
102
|
// Create new branch and push
|
|
93
103
|
await run('git', ['checkout', '-b', featureBranch], execOpts);
|
|
94
|
-
await run('git', ['push', '-u', 'origin', featureBranch], execOpts);
|
|
104
|
+
const pushOut = await run('git', ['push', '-u', 'origin', featureBranch], execOpts);
|
|
105
|
+
log.push(`Created and pushed ${featureBranch}`);
|
|
106
|
+
if (pushOut) log.push(pushOut);
|
|
95
107
|
}
|
|
96
108
|
} catch (err) {
|
|
97
109
|
console.error('[workspace-setup] Feature branch error:', err.message);
|
|
98
110
|
throw err;
|
|
99
111
|
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Activate agent-job-secrets skill in the workspace when in agent chatMode.
|
|
104
|
-
* Mirrors Docker setup.sh: ln -sfn ../library/agent-job-secrets skills/active/agent-job-secrets
|
|
105
|
-
* Idempotent — skips if library skill doesn't exist.
|
|
106
|
-
*
|
|
107
|
-
* @param {string} workspaceDir - Absolute path to workspace (git repo root)
|
|
108
|
-
* @param {string} chatMode - 'agent' or 'code'
|
|
109
|
-
*/
|
|
110
|
-
export function ensureSkills(workspaceDir, chatMode) {
|
|
111
|
-
if (chatMode !== 'agent') return;
|
|
112
|
-
|
|
113
|
-
const librarySkill = path.join(workspaceDir, 'skills', 'library', 'agent-job-secrets');
|
|
114
|
-
if (!existsSync(librarySkill)) return;
|
|
115
|
-
|
|
116
|
-
const activeDir = path.join(workspaceDir, 'skills', 'active');
|
|
117
|
-
mkdirSync(activeDir, { recursive: true });
|
|
118
|
-
|
|
119
|
-
const link = path.join(activeDir, 'agent-job-secrets');
|
|
120
|
-
|
|
121
|
-
// ln -sfn: remove existing symlink/file before creating (force + no-deref)
|
|
122
|
-
try {
|
|
123
|
-
const stat = lstatSync(link);
|
|
124
|
-
if (stat) unlinkSync(link);
|
|
125
|
-
} catch {
|
|
126
|
-
// doesn't exist — fine
|
|
127
|
-
}
|
|
128
112
|
|
|
129
|
-
|
|
113
|
+
return log.join('\n');
|
|
130
114
|
}
|
package/lib/channels/CLAUDE.md
CHANGED
|
@@ -17,7 +17,7 @@ Abstract interface for platform integrations. Methods:
|
|
|
17
17
|
**Critical distinction**: Audio is preprocessed at the adapter layer. Images are passed through to the LLM.
|
|
18
18
|
|
|
19
19
|
- **Images** (`message.photo`) → Downloaded, passed as `{ category: 'image', mimeType, data: Buffer }` attachment → LLM receives as vision content
|
|
20
|
-
- **Audio** (`message.voice`/`message.audio`) → Transcribed via
|
|
20
|
+
- **Audio** (`message.voice`/`message.audio`) → Transcribed via AssemblyAI → merged into `text` field → **never passed as attachment**
|
|
21
21
|
- **Documents** (`message.document`) → Downloaded as `{ category: 'document', mimeType, data: Buffer }`
|
|
22
22
|
|
|
23
23
|
## Factory (index.js) — Lazy Singleton
|
|
@@ -26,7 +26,17 @@ Abstract interface for platform integrations. Methods:
|
|
|
26
26
|
|
|
27
27
|
## Telegram Adapter (telegram.js)
|
|
28
28
|
|
|
29
|
-
- **
|
|
30
|
-
- **Verification flow**: If `TELEGRAM_VERIFICATION` env is set and user sends that code, bot responds with the chat ID (for initial setup).
|
|
29
|
+
- **Authorization**: per-user via the `user_channels` table. Unverified chats only accept `/verify <code>`; all other messages are dropped. See `lib/db/user-channels.js` and `lib/channels/commands/verify.js`.
|
|
31
30
|
- **Webhook auth**: Validates `x-telegram-bot-api-secret-token` header against `TELEGRAM_WEBHOOK_SECRET`.
|
|
32
|
-
- **Streaming**: `supportsStreaming` returns `false` —
|
|
31
|
+
- **Streaming**: `supportsStreaming` returns `false` — text + tool calls accumulate during streaming and are sent as complete messages once the turn ends. Progressive tool-call rendering (commit 740c734 / d9bf19a) inserts intermediate "→ used X" lines as tool calls land.
|
|
32
|
+
|
|
33
|
+
## Slash Commands (`lib/channels/commands/`)
|
|
34
|
+
|
|
35
|
+
Post-auth messages starting with `/` are dispatched here before reaching the LLM. Resolution chat.id → userId → activeThreadId happens in `api/index.js` `processChannelMessage`.
|
|
36
|
+
|
|
37
|
+
| Command | Purpose | Source |
|
|
38
|
+
|---------|---------|--------|
|
|
39
|
+
| `/verify <code>` | Verify a Telegram account against a one-time code generated in the web UI (`/profile/telegram`). Code expires in 10 minutes. Sets `verifiedAt`. | `commands/verify.js` |
|
|
40
|
+
| `/session` | List the user's recent chat threads (active thread marked) | `commands/session.js` |
|
|
41
|
+
| `/session list` | Same as `/session` | `commands/session.js` |
|
|
42
|
+
| `/session <id>` | Switch the user's `activeThreadId` so subsequent messages route to that chat | `commands/session.js` |
|
package/lib/channels/base.js
CHANGED
|
@@ -8,7 +8,10 @@ class ChannelAdapter {
|
|
|
8
8
|
* Returns normalized message data or null if no action needed.
|
|
9
9
|
*
|
|
10
10
|
* @param {Request} request - Incoming HTTP request
|
|
11
|
-
* @returns {Promise<{
|
|
11
|
+
* @returns {Promise<{ channel: string, channelChatId: string, text: string, attachments: Array, metadata: object } | null>}
|
|
12
|
+
*
|
|
13
|
+
* `channelChatId` is the channel-native identifier (e.g. Telegram chat.id). It is
|
|
14
|
+
* NOT the DB chat/thread id — that is resolved downstream via user_channels.
|
|
12
15
|
*
|
|
13
16
|
* Attachments array (may be empty) — only non-text content that the LLM needs to see:
|
|
14
17
|
* { category: "image", mimeType: "image/png", data: Buffer } — send to LLM as vision
|
|
@@ -39,8 +42,9 @@ class ChannelAdapter {
|
|
|
39
42
|
|
|
40
43
|
/**
|
|
41
44
|
* Send a complete (non-streaming) response back to the channel.
|
|
45
|
+
* `channelChatId` is the channel-native chat identifier.
|
|
42
46
|
*/
|
|
43
|
-
async sendResponse(
|
|
47
|
+
async sendResponse(channelChatId, text, metadata) {
|
|
44
48
|
throw new Error('Not implemented');
|
|
45
49
|
}
|
|
46
50
|
|