thepopebot 1.2.76-beta.0 → 1.2.76-beta.10
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/api/index.js +22 -8
- package/bin/cli.js +27 -2
- package/drizzle/0021_coding_agent_workspace.sql +1 -0
- package/drizzle/meta/_journal.json +7 -0
- package/lib/ai/index.js +27 -17
- package/lib/ai/sdk-adapters/CLAUDE.md +113 -0
- package/lib/ai/sdk-adapters/claude-code.js +100 -8
- package/lib/ai/system-prompt.js +16 -0
- package/lib/ai/tools.js +3 -0
- package/lib/ai/workspace-setup.js +18 -5
- package/lib/channels/telegram.js +78 -7
- package/lib/chat/actions.js +205 -10
- package/lib/chat/api.js +114 -15
- package/lib/chat/components/chat-input.js +78 -33
- package/lib/chat/components/chat-input.jsx +74 -23
- package/lib/chat/components/chat.js +27 -5
- package/lib/chat/components/chat.jsx +27 -3
- package/lib/chat/components/code-mode-toggle.js +110 -14
- package/lib/chat/components/code-mode-toggle.jsx +104 -13
- package/lib/chat/components/message.js +3 -3
- package/lib/chat/components/message.jsx +3 -3
- package/lib/chat/components/settings-secrets-page.js +274 -75
- package/lib/chat/components/settings-secrets-page.jsx +327 -65
- package/lib/chat/components/ui/combobox.js +18 -2
- package/lib/chat/components/ui/combobox.jsx +17 -1
- package/lib/code/actions.js +26 -6
- package/lib/code/terminal-view.js +36 -9
- package/lib/code/terminal-view.jsx +42 -10
- package/lib/config.js +11 -1
- package/lib/db/chats.js +9 -17
- package/lib/db/code-workspaces.js +6 -2
- package/lib/db/schema.js +1 -0
- package/lib/maintenance.js +31 -21
- package/lib/tools/docker.js +21 -8
- package/lib/tools/github.js +16 -0
- package/lib/tools/telegram.js +115 -0
- package/lib/utils/render-md.js +1 -1
- package/package.json +2 -2
- package/setup/lib/telegram.mjs +9 -69
- package/templates/.github/workflows/rebuild-event-handler.yml +1 -1
- package/templates/.gitignore.template +1 -1
- package/templates/CLAUDE.md +1 -1
- package/templates/CLAUDE.md.template +2 -1
- package/templates/agent-job/CLAUDE.md.template +1 -2
- package/templates/agent-job/SYSTEM.md +2 -2
- package/templates/coding-workspace/CLAUDE.md.template +7 -0
- package/templates/coding-workspace/SYSTEM.md +0 -0
- package/templates/agent-job/SOUL.md +0 -17
package/api/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createAgentJob } from '../lib/tools/create-agent-job.js';
|
|
|
3
3
|
import { setWebhook } from '../lib/tools/telegram.js';
|
|
4
4
|
import { getAgentJobStatus, fetchAgentJobLog } from '../lib/tools/github.js';
|
|
5
5
|
import { getTelegramAdapter } from '../lib/channels/index.js';
|
|
6
|
-
import { chat, summarizeAgentJob } from '../lib/ai/index.js';
|
|
6
|
+
import { chat, chatStream, summarizeAgentJob } from '../lib/ai/index.js';
|
|
7
7
|
import { createNotification } from '../lib/db/notifications.js';
|
|
8
8
|
import { loadTriggers } from '../lib/triggers.js';
|
|
9
9
|
import { verifyApiKey } from '../lib/db/api-keys.js';
|
|
@@ -211,19 +211,33 @@ async function handleTelegramWebhook(request) {
|
|
|
211
211
|
/**
|
|
212
212
|
* Process a normalized message through the AI layer with channel UX.
|
|
213
213
|
* Message persistence is handled centrally by the AI layer.
|
|
214
|
+
*
|
|
215
|
+
* Uses chatStream() for progressive tool-call rendering when the adapter
|
|
216
|
+
* supports it (Telegram: sends each tool call as a message, reacts on completion).
|
|
217
|
+
* Falls back to chat() for adapters without streamChatResponse.
|
|
214
218
|
*/
|
|
215
219
|
async function processChannelMessage(adapter, normalized) {
|
|
216
220
|
await adapter.acknowledge(normalized.metadata);
|
|
217
221
|
const stopIndicator = adapter.startProcessingIndicator(normalized.metadata);
|
|
218
222
|
|
|
219
223
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
if (adapter.streamChatResponse) {
|
|
225
|
+
const chunks = chatStream(
|
|
226
|
+
normalized.threadId,
|
|
227
|
+
normalized.text,
|
|
228
|
+
normalized.attachments,
|
|
229
|
+
{ userId: 'telegram', chatTitle: 'Telegram' }
|
|
230
|
+
);
|
|
231
|
+
await adapter.streamChatResponse(normalized.metadata.chatId, chunks);
|
|
232
|
+
} else {
|
|
233
|
+
const response = await chat(
|
|
234
|
+
normalized.threadId,
|
|
235
|
+
normalized.text,
|
|
236
|
+
normalized.attachments,
|
|
237
|
+
{ userId: 'telegram', chatTitle: 'Telegram' }
|
|
238
|
+
);
|
|
239
|
+
await adapter.sendResponse(normalized.threadId, response, normalized.metadata);
|
|
240
|
+
}
|
|
227
241
|
} catch (err) {
|
|
228
242
|
console.error('Failed to process message with AI:', err);
|
|
229
243
|
await adapter
|
package/bin/cli.js
CHANGED
|
@@ -305,6 +305,31 @@ async function init() {
|
|
|
305
305
|
console.log(' Created .claude/skills → ../skills/active');
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
// Create .codex/skills → ../skills/active symlink
|
|
309
|
+
const codexSkillsLink = path.join(cwd, '.codex', 'skills');
|
|
310
|
+
if (!fs.existsSync(codexSkillsLink)) {
|
|
311
|
+
fs.mkdirSync(path.dirname(codexSkillsLink), { recursive: true });
|
|
312
|
+
createDirLink('../skills/active', codexSkillsLink);
|
|
313
|
+
console.log(' Created .codex/skills → ../skills/active');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Create .gemini/skills → ../skills/active symlink
|
|
317
|
+
const geminiSkillsLink = path.join(cwd, '.gemini', 'skills');
|
|
318
|
+
if (!fs.existsSync(geminiSkillsLink)) {
|
|
319
|
+
fs.mkdirSync(path.dirname(geminiSkillsLink), { recursive: true });
|
|
320
|
+
createDirLink('../skills/active', geminiSkillsLink);
|
|
321
|
+
console.log(' Created .gemini/skills → ../skills/active');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Create .kimi/skills → ../skills/active symlink
|
|
325
|
+
const kimiSkillsLink = path.join(cwd, '.kimi', 'skills');
|
|
326
|
+
if (!fs.existsSync(kimiSkillsLink)) {
|
|
327
|
+
fs.mkdirSync(path.dirname(kimiSkillsLink), { recursive: true });
|
|
328
|
+
createDirLink('../skills/active', kimiSkillsLink);
|
|
329
|
+
console.log(' Created .kimi/skills → ../skills/active');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
|
|
308
333
|
// Report backed-up files
|
|
309
334
|
if (backedUp.length > 0) {
|
|
310
335
|
console.log(`\n Backed up ${backedUp.length} file(s) to ${path.relative(cwd, backupDir)}/`);
|
|
@@ -420,7 +445,7 @@ function reset(filePath) {
|
|
|
420
445
|
console.log(` ${destPath(file)}`);
|
|
421
446
|
}
|
|
422
447
|
console.log('\nUsage: thepopebot reset <file>');
|
|
423
|
-
console.log('Example: thepopebot reset agent-job/
|
|
448
|
+
console.log('Example: thepopebot reset agent-job/SYSTEM.md\n');
|
|
424
449
|
return;
|
|
425
450
|
}
|
|
426
451
|
|
|
@@ -502,7 +527,7 @@ function diff(filePath) {
|
|
|
502
527
|
console.log(' All files match package templates.');
|
|
503
528
|
}
|
|
504
529
|
console.log('\nUsage: thepopebot diff <file>');
|
|
505
|
-
console.log('Example: thepopebot diff agent-job/
|
|
530
|
+
console.log('Example: thepopebot diff agent-job/SYSTEM.md\n');
|
|
506
531
|
return;
|
|
507
532
|
}
|
|
508
533
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `code_workspaces` ADD `coding_agent` text;
|
|
@@ -148,6 +148,13 @@
|
|
|
148
148
|
"when": 1774327178886,
|
|
149
149
|
"tag": "0020_natural_fabian_cortez",
|
|
150
150
|
"breakpoints": true
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"idx": 21,
|
|
154
|
+
"version": "6",
|
|
155
|
+
"when": 1775865600000,
|
|
156
|
+
"tag": "0021_coding_agent_workspace",
|
|
157
|
+
"breakpoints": true
|
|
151
158
|
}
|
|
152
159
|
]
|
|
153
160
|
}
|
package/lib/ai/index.js
CHANGED
|
@@ -4,8 +4,10 @@ import { z } from 'zod';
|
|
|
4
4
|
import { getAgentChat, getCodeChat } from './agent.js';
|
|
5
5
|
import { createModel } from './model.js';
|
|
6
6
|
import path from 'path';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
7
8
|
import { PROJECT_ROOT } from '../paths.js';
|
|
8
9
|
import { render_md } from '../utils/render-md.js';
|
|
10
|
+
import { buildCodingAgentSystemPrompt } from './system-prompt.js';
|
|
9
11
|
import { getChatById, createChat, saveMessage, updateChatTitle, linkChatToWorkspace } from '../db/chats.js';
|
|
10
12
|
import { getConfig } from '../config.js';
|
|
11
13
|
import { getSdkAdapter } from './sdk-adapters/index.js';
|
|
@@ -171,35 +173,41 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
171
173
|
const workspace = getCodeWorkspaceById(workspaceId);
|
|
172
174
|
const featureBranch = workspace?.featureBranch;
|
|
173
175
|
|
|
176
|
+
const needsSetup = !existsSync(path.join(repoDir, '.git'));
|
|
174
177
|
const setupToolCallId = `setup-${workspaceId.slice(0, 8)}`;
|
|
175
178
|
const setupArgs = { repo, branch, featureBranch };
|
|
176
|
-
|
|
179
|
+
|
|
180
|
+
if (needsSetup) {
|
|
181
|
+
yield { type: 'tool-call', toolCallId: setupToolCallId, toolName: 'workspace', args: setupArgs };
|
|
182
|
+
}
|
|
177
183
|
|
|
178
184
|
try {
|
|
179
|
-
await ensureWorkspaceRepo({ workspaceDir: repoDir, repo, branch, featureBranch });
|
|
185
|
+
const setupOutput = await ensureWorkspaceRepo({ workspaceDir: repoDir, repo, branch, featureBranch });
|
|
180
186
|
ensureSkills(repoDir, isCodeMode ? 'code' : 'agent');
|
|
181
|
-
|
|
187
|
+
if (needsSetup) {
|
|
188
|
+
const result = setupOutput || `Workspace ready on ${featureBranch || branch}`;
|
|
189
|
+
yield { type: 'tool-result', toolCallId: setupToolCallId, result };
|
|
190
|
+
persistMessage(threadId, 'assistant', JSON.stringify({
|
|
191
|
+
type: 'tool-invocation',
|
|
192
|
+
toolCallId: setupToolCallId,
|
|
193
|
+
toolName: 'workspace',
|
|
194
|
+
state: 'output-available',
|
|
195
|
+
input: setupArgs,
|
|
196
|
+
output: result,
|
|
197
|
+
}), options);
|
|
198
|
+
}
|
|
182
199
|
} catch (err) {
|
|
183
|
-
|
|
200
|
+
if (needsSetup) {
|
|
201
|
+
yield { type: 'tool-result', toolCallId: setupToolCallId, result: `Setup failed: ${err.message}` };
|
|
202
|
+
}
|
|
184
203
|
throw err;
|
|
185
204
|
}
|
|
186
205
|
|
|
187
206
|
// 2. Session continuity
|
|
188
207
|
const sessionId = readSessionId(wsBaseDir);
|
|
189
208
|
|
|
190
|
-
// 3. System prompt
|
|
191
|
-
|
|
192
|
-
if (!isCodeMode) {
|
|
193
|
-
const soulPath = path.join(PROJECT_ROOT, 'agent-job/SOUL.md');
|
|
194
|
-
const systemPath = path.join(PROJECT_ROOT, 'agent-job/SYSTEM.md');
|
|
195
|
-
try {
|
|
196
|
-
const soul = render_md(soulPath) || '';
|
|
197
|
-
const system = render_md(systemPath) || '';
|
|
198
|
-
systemPrompt = [soul, system].filter(Boolean).join('\n\n') || null;
|
|
199
|
-
} catch {
|
|
200
|
-
// Files may not exist — proceed without system prompt
|
|
201
|
-
}
|
|
202
|
-
}
|
|
209
|
+
// 3. System prompt
|
|
210
|
+
const systemPrompt = buildCodingAgentSystemPrompt(isCodeMode ? 'code' : 'agent');
|
|
203
211
|
|
|
204
212
|
// 4. Stream from SDK adapter
|
|
205
213
|
let pendingText = '';
|
|
@@ -213,6 +221,8 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
213
221
|
sessionId,
|
|
214
222
|
permissionMode: codeModeType,
|
|
215
223
|
attachments,
|
|
224
|
+
workspaceId,
|
|
225
|
+
chatMode: isCodeMode ? 'code' : 'agent',
|
|
216
226
|
})) {
|
|
217
227
|
// Write session ID on first meta chunk
|
|
218
228
|
if (chunk.type === 'meta' && chunk.sessionId) {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# lib/ai/sdk-adapters/ — SDK Adapter System
|
|
2
|
+
|
|
3
|
+
In-process SDK adapters that replace the legacy LangGraph + Docker path for chat. Each adapter wraps a coding agent's SDK and yields a unified chunk stream consumed by `chatStream()` in `lib/ai/index.js`.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Browser → POST /stream/chat (api.js)
|
|
9
|
+
→ chatStream() (index.js)
|
|
10
|
+
→ getSdkAdapter() returns adapter function or null
|
|
11
|
+
→ if adapter: workspace setup → SDK adapter streaming → DB persistence
|
|
12
|
+
→ if null: falls back to legacy LangGraph/Docker path
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
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`.
|
|
16
|
+
|
|
17
|
+
## Existing Adapter
|
|
18
|
+
|
|
19
|
+
| File | Agent | SDK |
|
|
20
|
+
|------|-------|-----|
|
|
21
|
+
| `claude-code.js` | `claude-code` | `@anthropic-ai/claude-agent-sdk` |
|
|
22
|
+
|
|
23
|
+
## Adding a New SDK Adapter
|
|
24
|
+
|
|
25
|
+
### 1. Create the adapter file
|
|
26
|
+
|
|
27
|
+
Create `{agent-name}.js` in this directory. Export a single async generator function:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
export async function* myAgentStream({ prompt, workspaceDir, systemPrompt, sessionId, permissionMode, attachments }) {
|
|
31
|
+
// ... call the SDK, yield chunks
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Required chunk types to yield
|
|
36
|
+
|
|
37
|
+
The adapter MUST yield these chunk types for `chatStream()` and `api.js` to work correctly:
|
|
38
|
+
|
|
39
|
+
| Chunk | Shape | When | Purpose |
|
|
40
|
+
|-------|-------|------|---------|
|
|
41
|
+
| `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. |
|
|
42
|
+
| `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. |
|
|
43
|
+
| `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. |
|
|
44
|
+
| `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. |
|
|
45
|
+
| `result` | `{ type: 'result', text: string, cost?: number, duration?: number, subtype?: string }` | Stream ends | Final summary. Logged by `chatStream()`, not persisted or sent to UI. |
|
|
46
|
+
|
|
47
|
+
Optional:
|
|
48
|
+
| `unknown` | `{ type: 'unknown', raw: any }` | Unrecognized events | `api.js` renders these as collapsible boxes in the UI. Use for debugging unhandled SDK events. |
|
|
49
|
+
|
|
50
|
+
### 3. Register in index.js
|
|
51
|
+
|
|
52
|
+
Add the import and mapping in `getSdkAdapter()`:
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import { myAgentStream } from './my-agent.js';
|
|
56
|
+
|
|
57
|
+
export function getSdkAdapter(agentType) {
|
|
58
|
+
if (agentType === 'claude-code') return claudeCodeStream;
|
|
59
|
+
if (agentType === 'my-agent') return myAgentStream;
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The `agentType` string comes from the `CODING_AGENT` config value set in the admin UI.
|
|
65
|
+
|
|
66
|
+
### 4. Auth resolution
|
|
67
|
+
|
|
68
|
+
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:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
import { buildAgentAuthEnv } from '../../tools/docker.js';
|
|
72
|
+
|
|
73
|
+
const env = { ...process.env };
|
|
74
|
+
const { env: authEnvPairs } = buildAgentAuthEnv('my-agent');
|
|
75
|
+
for (const pair of authEnvPairs) {
|
|
76
|
+
const eqIdx = pair.indexOf('=');
|
|
77
|
+
if (eqIdx > 0) env[pair.slice(0, eqIdx)] = pair.slice(eqIdx + 1);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
### 5. Function parameters
|
|
84
|
+
|
|
85
|
+
| Param | Type | Description |
|
|
86
|
+
|-------|------|-------------|
|
|
87
|
+
| `prompt` | `string` | User message text |
|
|
88
|
+
| `workspaceDir` | `string` | Absolute path to git repo root (the SDK should execute here) |
|
|
89
|
+
| `systemPrompt` | `string\|null` | System prompt for agent mode (null in code mode) |
|
|
90
|
+
| `sessionId` | `string\|null` | Previous session ID to resume (null on first message) |
|
|
91
|
+
| `permissionMode` | `string` | `'plan'` (read-only) or `'code'` (read-write). Map to the SDK's equivalent permission concept. |
|
|
92
|
+
| `attachments` | `Array` | Image attachments: `{ category: 'image', mimeType, dataUrl }` |
|
|
93
|
+
|
|
94
|
+
### 6. Session continuity contract
|
|
95
|
+
|
|
96
|
+
Multi-turn conversation works via session IDs:
|
|
97
|
+
|
|
98
|
+
1. First message: `sessionId` param is `null`. Adapter yields `{ type: 'meta', sessionId: '<new-id>' }`.
|
|
99
|
+
2. `chatStream()` writes the session ID to `{workspaceBaseDir}/.claude-ttyd-sessions/7681`.
|
|
100
|
+
3. Next message: `sessionId` param contains the saved ID. Adapter passes it to the SDK's resume mechanism.
|
|
101
|
+
|
|
102
|
+
If the SDK doesn't support session resume, the adapter can ignore `sessionId` — but multi-turn context will be lost between messages.
|
|
103
|
+
|
|
104
|
+
## What the adapter does NOT handle
|
|
105
|
+
|
|
106
|
+
These are managed by `chatStream()` in `index.js` — adapters should not duplicate them:
|
|
107
|
+
|
|
108
|
+
- **Workspace git setup** — `ensureWorkspaceRepo()` clones/checkouts before the adapter is called
|
|
109
|
+
- **DB persistence** — `chatStream()` saves user messages, assistant text, and tool invocations
|
|
110
|
+
- **Chat creation** — `chatStream()` creates the chat and workspace DB records
|
|
111
|
+
- **Auto-titling** — `chatStream()` generates a title after the first message
|
|
112
|
+
- **System prompt loading** — `chatStream()` calls `buildCodingAgentSystemPrompt()` and passes the result as `systemPrompt`
|
|
113
|
+
- **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,61 @@ 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, but the SDK adapter uses
|
|
35
|
+
* cwd=/app/data/workspaces/workspace-XXX/workspace — 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
|
+
function ensureSessionSymlink(wsBaseDir, workspaceDir) {
|
|
41
|
+
const projectsDir = path.join(wsBaseDir, '.claude', 'projects');
|
|
42
|
+
const sdkEncoded = encodeCwd(workspaceDir);
|
|
43
|
+
const interactiveEncoded = encodeCwd('/home/coding-agent/workspace');
|
|
44
|
+
|
|
45
|
+
// Both point to the same dir — no symlink needed
|
|
46
|
+
if (sdkEncoded === interactiveEncoded) return;
|
|
47
|
+
|
|
48
|
+
fs.mkdirSync(path.join(projectsDir, sdkEncoded), { recursive: true });
|
|
49
|
+
|
|
50
|
+
const symlinkPath = path.join(projectsDir, interactiveEncoded);
|
|
51
|
+
try {
|
|
52
|
+
const existing = fs.readlinkSync(symlinkPath);
|
|
53
|
+
if (existing === sdkEncoded) return; // already correct
|
|
54
|
+
fs.unlinkSync(symlinkPath);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err.code !== 'ENOENT') {
|
|
57
|
+
// It's a real directory (not a symlink) — don't touch it
|
|
58
|
+
if (err.code === 'EINVAL') return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
fs.symlinkSync(sdkEncoded, symlinkPath);
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, sessionId, permissionMode, attachments, workspaceId, chatMode }) {
|
|
68
|
+
// Point HOME at the workspace volume so the SDK stores session data on the
|
|
69
|
+
// shared volume (not the EH container's ephemeral filesystem).
|
|
70
|
+
const wsBaseDir = path.dirname(workspaceDir);
|
|
71
|
+
ensureSessionSymlink(wsBaseDir, workspaceDir);
|
|
72
|
+
|
|
19
73
|
// Build a local env object with auth credentials from the settings DB.
|
|
20
74
|
// Passed via the SDK's `env` option — no process.env mutation needed.
|
|
21
75
|
const env = { ...process.env };
|
|
76
|
+
env.HOME = wsBaseDir;
|
|
22
77
|
try {
|
|
23
78
|
const { env: authEnvPairs } = buildAgentAuthEnv('claude-code');
|
|
24
79
|
for (const pair of authEnvPairs) {
|
|
@@ -41,16 +96,32 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
|
|
|
41
96
|
// Fall through — env may already have the right vars from process.env
|
|
42
97
|
}
|
|
43
98
|
|
|
99
|
+
// Inject agent job secrets when in agent chat mode
|
|
100
|
+
if (chatMode === 'agent') {
|
|
101
|
+
const shortId = (workspaceId || '').replace(/-/g, '').slice(0, 8);
|
|
102
|
+
const { key: agentJobToken } = createAgentJobApiKey(`claude-code-sdk-${shortId}`);
|
|
103
|
+
env.AGENT_JOB_TOKEN = agentJobToken;
|
|
104
|
+
const appUrl = getConfig('APP_URL');
|
|
105
|
+
if (appUrl) env.APP_URL = appUrl;
|
|
106
|
+
|
|
107
|
+
// Inject plain secrets as env vars (oauth types are null — agent fetches via skill)
|
|
108
|
+
const jobSecrets = getAllAgentJobSecrets();
|
|
109
|
+
for (const { key, value } of jobSecrets) {
|
|
110
|
+
if (value !== null && !env[key]) {
|
|
111
|
+
env[key] = value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
44
116
|
const options = {
|
|
45
117
|
cwd: workspaceDir,
|
|
46
118
|
env,
|
|
47
119
|
includePartialMessages: true,
|
|
120
|
+
model: getConfig('CODING_AGENT_CLAUDE_CODE_MODEL') || undefined,
|
|
48
121
|
};
|
|
49
122
|
|
|
50
|
-
// Permission mode
|
|
51
|
-
|
|
52
|
-
options.permissionMode = 'bypassPermissions';
|
|
53
|
-
}
|
|
123
|
+
// Permission mode: plan = read-only, anything else = full write access
|
|
124
|
+
options.permissionMode = permissionMode === 'plan' ? 'plan' : 'bypassPermissions';
|
|
54
125
|
|
|
55
126
|
if (sessionId) options.resume = sessionId;
|
|
56
127
|
if (systemPrompt) {
|
|
@@ -80,6 +151,8 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
|
|
|
80
151
|
|
|
81
152
|
// Track tool call state for mapping stream events
|
|
82
153
|
const activeToolCalls = new Map(); // index → { id, name, argsJson }
|
|
154
|
+
const toolNamesById = new Map(); // toolCallId → toolName (persists for tool-result lookup)
|
|
155
|
+
const activeThinkingBlocks = new Set(); // indices of active thinking blocks
|
|
83
156
|
|
|
84
157
|
try {
|
|
85
158
|
for await (const message of query({ prompt: sdkPrompt, options })) {
|
|
@@ -102,9 +175,13 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
|
|
|
102
175
|
const block = event.content_block;
|
|
103
176
|
if (block.type === 'tool_use') {
|
|
104
177
|
activeToolCalls.set(event.index, { id: block.id, name: block.name, argsJson: '' });
|
|
178
|
+
toolNamesById.set(block.id, block.name);
|
|
105
179
|
yield { type: 'tool-call', toolCallId: block.id, toolName: block.name, args: {} };
|
|
180
|
+
} else if (block.type === 'thinking') {
|
|
181
|
+
activeThinkingBlocks.add(event.index);
|
|
182
|
+
yield { type: 'thinking-start' };
|
|
106
183
|
}
|
|
107
|
-
// Skip '
|
|
184
|
+
// Skip 'text' start (deltas handle text)
|
|
108
185
|
continue;
|
|
109
186
|
}
|
|
110
187
|
|
|
@@ -114,11 +191,17 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
|
|
|
114
191
|
} else if (event.delta.type === 'input_json_delta') {
|
|
115
192
|
const tc = activeToolCalls.get(event.index);
|
|
116
193
|
if (tc) tc.argsJson += event.delta.partial_json;
|
|
194
|
+
} else if (event.delta.type === 'thinking_delta') {
|
|
195
|
+
yield { type: 'thinking', delta: event.delta.thinking };
|
|
117
196
|
}
|
|
118
197
|
continue;
|
|
119
198
|
}
|
|
120
199
|
|
|
121
200
|
if (event.type === 'content_block_stop') {
|
|
201
|
+
if (activeThinkingBlocks.has(event.index)) {
|
|
202
|
+
activeThinkingBlocks.delete(event.index);
|
|
203
|
+
yield { type: 'thinking-end' };
|
|
204
|
+
}
|
|
122
205
|
const tc = activeToolCalls.get(event.index);
|
|
123
206
|
if (tc && tc.argsJson) {
|
|
124
207
|
try {
|
|
@@ -144,14 +227,23 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
|
|
|
144
227
|
: Array.isArray(block.content)
|
|
145
228
|
? block.content.map(b => b.type === 'text' ? b.text : JSON.stringify(b)).join('\n')
|
|
146
229
|
: JSON.stringify(block.content);
|
|
147
|
-
yield { type: 'tool-result', toolCallId: block.tool_use_id, result: content };
|
|
230
|
+
yield { type: 'tool-result', toolCallId: block.tool_use_id, toolName: toolNamesById.get(block.tool_use_id), result: content };
|
|
148
231
|
}
|
|
149
232
|
}
|
|
150
233
|
continue;
|
|
151
234
|
}
|
|
152
235
|
|
|
153
236
|
// ── assistant messages — redundant with streaming, skip ──
|
|
154
|
-
|
|
237
|
+
// But extract tool names so resumed tool-results can carry them.
|
|
238
|
+
if (message.type === 'assistant') {
|
|
239
|
+
const blocks = message.message?.content || [];
|
|
240
|
+
for (const block of blocks) {
|
|
241
|
+
if (block.type === 'tool_use' && block.id && block.name) {
|
|
242
|
+
toolNamesById.set(block.id, block.name);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
155
247
|
|
|
156
248
|
// ── result ──
|
|
157
249
|
if (message.type === 'result') {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { PROJECT_ROOT } from '../paths.js';
|
|
3
|
+
import { render_md } from '../utils/render-md.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build the system prompt for a coding agent.
|
|
7
|
+
* @param {'agent'|'code'} mode - Chat mode
|
|
8
|
+
* @returns {string|null} Rendered system prompt, or null if not configured
|
|
9
|
+
*/
|
|
10
|
+
export function buildCodingAgentSystemPrompt(mode) {
|
|
11
|
+
const file = mode === 'agent'
|
|
12
|
+
? path.join(PROJECT_ROOT, 'agent-job/SYSTEM.md')
|
|
13
|
+
: path.join(PROJECT_ROOT, 'coding-workspace/SYSTEM.md');
|
|
14
|
+
const rendered = render_md(file);
|
|
15
|
+
return rendered?.trim() || null;
|
|
16
|
+
}
|
package/lib/ai/tools.js
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { createAgentJob } from '../tools/create-agent-job.js';
|
|
4
4
|
|
|
5
5
|
import { getConfig } from '../config.js';
|
|
6
|
+
import { buildCodingAgentSystemPrompt } from './system-prompt.js';
|
|
6
7
|
|
|
7
8
|
const agentJobTool = tool(
|
|
8
9
|
async ({ prompt }) => {
|
|
@@ -63,6 +64,7 @@ const agentChatCodingTool = tool(
|
|
|
63
64
|
workspaceId,
|
|
64
65
|
taskPrompt: prompt,
|
|
65
66
|
mode,
|
|
67
|
+
systemPrompt: buildCodingAgentSystemPrompt('agent'),
|
|
66
68
|
injectSecrets: true,
|
|
67
69
|
});
|
|
68
70
|
|
|
@@ -124,6 +126,7 @@ const codeChatCodingTool = tool(
|
|
|
124
126
|
containerName, repo, branch, featureBranch, workspaceId,
|
|
125
127
|
taskPrompt: prompt,
|
|
126
128
|
mode,
|
|
129
|
+
systemPrompt: buildCodingAgentSystemPrompt('code'),
|
|
127
130
|
});
|
|
128
131
|
|
|
129
132
|
const chunks = [{ type: 'meta', codingAgent, backendApi }];
|
|
@@ -32,20 +32,24 @@ 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
|
-
await run('git', ['clone', '--branch', branch || 'main', `https://github.com/${repo}`, '.'], execOpts);
|
|
50
|
+
const out = await run('git', ['clone', '--branch', branch || 'main', `https://github.com/${repo}`, '.'], execOpts);
|
|
51
|
+
log.push(`Cloned ${repo} (branch: ${branch || 'main'})`);
|
|
52
|
+
if (out) log.push(out);
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
// 3. Git identity (only if not already configured)
|
|
@@ -61,6 +65,7 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
|
|
|
61
65
|
const email = user.email || `${user.id}+${user.login}@users.noreply.github.com`;
|
|
62
66
|
await run('git', ['config', 'user.name', name], execOpts);
|
|
63
67
|
await run('git', ['config', 'user.email', email], execOpts);
|
|
68
|
+
log.push(`Git identity: ${name} <${email}>`);
|
|
64
69
|
} catch (err) {
|
|
65
70
|
console.error('[workspace-setup] Failed to set git identity:', err.message);
|
|
66
71
|
}
|
|
@@ -68,7 +73,7 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
|
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
// 4. Feature branch checkout
|
|
71
|
-
if (!featureBranch) return;
|
|
76
|
+
if (!featureBranch) return log.join('\n');
|
|
72
77
|
|
|
73
78
|
// Already on the right branch locally?
|
|
74
79
|
try {
|
|
@@ -77,8 +82,11 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
|
|
|
77
82
|
const current = await run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], execOpts);
|
|
78
83
|
if (current !== featureBranch) {
|
|
79
84
|
await run('git', ['checkout', featureBranch], execOpts);
|
|
85
|
+
log.push(`Checked out ${featureBranch}`);
|
|
86
|
+
} else {
|
|
87
|
+
log.push(`Already on ${featureBranch}`);
|
|
80
88
|
}
|
|
81
|
-
return;
|
|
89
|
+
return log.join('\n');
|
|
82
90
|
} catch {
|
|
83
91
|
// Branch doesn't exist locally — check remote
|
|
84
92
|
}
|
|
@@ -88,15 +96,20 @@ export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureB
|
|
|
88
96
|
if (remoteCheck) {
|
|
89
97
|
// Remote branch exists — checkout tracking it
|
|
90
98
|
await run('git', ['checkout', '-B', featureBranch, `origin/${featureBranch}`], execOpts);
|
|
99
|
+
log.push(`Checked out ${featureBranch} (tracking origin)`);
|
|
91
100
|
} else {
|
|
92
101
|
// Create new branch and push
|
|
93
102
|
await run('git', ['checkout', '-b', featureBranch], execOpts);
|
|
94
|
-
await run('git', ['push', '-u', 'origin', featureBranch], execOpts);
|
|
103
|
+
const pushOut = await run('git', ['push', '-u', 'origin', featureBranch], execOpts);
|
|
104
|
+
log.push(`Created and pushed ${featureBranch}`);
|
|
105
|
+
if (pushOut) log.push(pushOut);
|
|
95
106
|
}
|
|
96
107
|
} catch (err) {
|
|
97
108
|
console.error('[workspace-setup] Feature branch error:', err.message);
|
|
98
109
|
throw err;
|
|
99
110
|
}
|
|
111
|
+
|
|
112
|
+
return log.join('\n');
|
|
100
113
|
}
|
|
101
114
|
|
|
102
115
|
/**
|