thepopebot 1.2.75 → 1.2.76-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/sync.js +5 -1
- package/lib/ai/index.js +128 -3
- package/lib/ai/model.js +6 -0
- package/lib/ai/sdk-adapters/claude-code.js +176 -0
- package/lib/ai/sdk-adapters/index.js +13 -0
- package/lib/ai/session-manager.js +33 -0
- package/lib/ai/tools.js +2 -2
- package/lib/ai/workspace-setup.js +130 -0
- package/lib/chat/actions.js +18 -0
- package/lib/chat/api.js +3 -0
- package/lib/chat/components/chats-page.js +3 -12
- package/lib/chat/components/chats-page.jsx +4 -7
- package/lib/chat/components/index.js +1 -1
- package/lib/chat/components/settings-chat-page.js +7 -117
- package/lib/chat/components/settings-chat-page.jsx +13 -132
- package/lib/chat/components/settings-secrets-layout.js +1 -1
- package/lib/chat/components/settings-secrets-layout.jsx +1 -1
- package/lib/config.js +1 -0
- package/lib/llm-providers.js +8 -0
- package/lib/tools/docker.js +3 -1
- package/package.json +2 -1
package/bin/sync.js
CHANGED
|
@@ -271,10 +271,13 @@ function buildDockerImage(projectPath) {
|
|
|
271
271
|
const version = pkg.version;
|
|
272
272
|
const imageTag = `stephengpope/thepopebot:event-handler-${version}`;
|
|
273
273
|
|
|
274
|
-
// Copy web/ to project for Docker build context
|
|
274
|
+
// Copy web/ and docker/ to project for Docker build context
|
|
275
275
|
const webSrc = path.join(PACKAGE_DIR, 'web');
|
|
276
276
|
const webDest = path.join(projectPath, 'web');
|
|
277
|
+
const dockerSrc = path.join(PACKAGE_DIR, 'docker');
|
|
278
|
+
const dockerDest = path.join(projectPath, 'docker');
|
|
277
279
|
fs.cpSync(webSrc, webDest, { recursive: true });
|
|
280
|
+
fs.cpSync(dockerSrc, dockerDest, { recursive: true });
|
|
278
281
|
|
|
279
282
|
try {
|
|
280
283
|
execSync(`docker build -f - -t ${imageTag} .`, {
|
|
@@ -284,6 +287,7 @@ function buildDockerImage(projectPath) {
|
|
|
284
287
|
});
|
|
285
288
|
} finally {
|
|
286
289
|
fs.rmSync(webDest, { recursive: true, force: true });
|
|
290
|
+
fs.rmSync(dockerDest, { recursive: true, force: true });
|
|
287
291
|
}
|
|
288
292
|
|
|
289
293
|
// Clean up dangling images from previous builds
|
package/lib/ai/index.js
CHANGED
|
@@ -7,6 +7,11 @@ import path from 'path';
|
|
|
7
7
|
import { PROJECT_ROOT } from '../paths.js';
|
|
8
8
|
import { render_md } from '../utils/render-md.js';
|
|
9
9
|
import { getChatById, createChat, saveMessage, updateChatTitle, linkChatToWorkspace } from '../db/chats.js';
|
|
10
|
+
import { getConfig } from '../config.js';
|
|
11
|
+
import { getSdkAdapter } from './sdk-adapters/index.js';
|
|
12
|
+
import { ensureWorkspaceRepo, ensureSkills } from './workspace-setup.js';
|
|
13
|
+
import { readSessionId, writeSessionId } from './session-manager.js';
|
|
14
|
+
import { workspaceDir as getWorkspaceDir } from '../tools/docker.js';
|
|
10
15
|
|
|
11
16
|
/**
|
|
12
17
|
* Ensure a chat exists in the DB and save a message.
|
|
@@ -39,6 +44,18 @@ function persistMessage(threadId, role, text, options = {}) {
|
|
|
39
44
|
* @returns {Promise<string>} AI response text
|
|
40
45
|
*/
|
|
41
46
|
async function chat(threadId, message, attachments = [], options = {}) {
|
|
47
|
+
// SDK path: delegate to chatStream and collect text
|
|
48
|
+
const sdkAdapter = getSdkAdapter(getConfig('CODING_AGENT'));
|
|
49
|
+
|
|
50
|
+
if (sdkAdapter) {
|
|
51
|
+
let fullText = '';
|
|
52
|
+
for await (const chunk of chatStream(threadId, message, attachments, options)) {
|
|
53
|
+
if (chunk.type === 'text') fullText += chunk.text;
|
|
54
|
+
}
|
|
55
|
+
return fullText;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Legacy LangGraph path
|
|
42
59
|
const agent = await getAgentChat();
|
|
43
60
|
|
|
44
61
|
// Save user message to DB
|
|
@@ -136,15 +153,123 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
136
153
|
workspaceId = workspaceId || existingChat.codeWorkspaceId;
|
|
137
154
|
}
|
|
138
155
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
156
|
+
// ── SDK path: direct in-process SDK call (no LangGraph, no Docker) ──
|
|
157
|
+
const sdkAdapter = getSdkAdapter(getConfig('CODING_AGENT'));
|
|
158
|
+
const useSDK = !!sdkAdapter;
|
|
142
159
|
|
|
143
160
|
// Save user message to DB (skip on regeneration — message already exists)
|
|
144
161
|
if (!options.skipUserPersist) {
|
|
145
162
|
persistMessage(threadId, 'user', message || '[attachment]', options);
|
|
146
163
|
}
|
|
147
164
|
|
|
165
|
+
if (useSDK) {
|
|
166
|
+
// 1. Workspace setup (idempotent — skips if .git exists)
|
|
167
|
+
const wsBaseDir = getWorkspaceDir(workspaceId);
|
|
168
|
+
const repoDir = path.join(wsBaseDir, 'workspace');
|
|
169
|
+
|
|
170
|
+
const { getCodeWorkspaceById } = await import('../db/code-workspaces.js');
|
|
171
|
+
const workspace = getCodeWorkspaceById(workspaceId);
|
|
172
|
+
const featureBranch = workspace?.featureBranch;
|
|
173
|
+
|
|
174
|
+
const setupToolCallId = `setup-${workspaceId.slice(0, 8)}`;
|
|
175
|
+
const setupArgs = { repo, branch, featureBranch };
|
|
176
|
+
yield { type: 'tool-call', toolCallId: setupToolCallId, toolName: 'set_up_workspace', args: setupArgs };
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
await ensureWorkspaceRepo({ workspaceDir: repoDir, repo, branch, featureBranch });
|
|
180
|
+
ensureSkills(repoDir, isCodeMode ? 'code' : 'agent');
|
|
181
|
+
yield { type: 'tool-result', toolCallId: setupToolCallId, result: JSON.stringify({ result: `Workspace ready on ${featureBranch || branch}` }) };
|
|
182
|
+
} catch (err) {
|
|
183
|
+
yield { type: 'tool-result', toolCallId: setupToolCallId, result: JSON.stringify({ result: `Setup failed: ${err.message}` }) };
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 2. Session continuity
|
|
188
|
+
const sessionId = readSessionId(wsBaseDir);
|
|
189
|
+
|
|
190
|
+
// 3. System prompt (agent mode: SOUL.md + SYSTEM.md, code mode: none)
|
|
191
|
+
let systemPrompt = null;
|
|
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
|
+
}
|
|
203
|
+
|
|
204
|
+
// 4. Stream from SDK adapter
|
|
205
|
+
let pendingText = '';
|
|
206
|
+
const pendingToolCalls = new Map();
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
for await (const chunk of sdkAdapter({
|
|
210
|
+
prompt: message,
|
|
211
|
+
workspaceDir: repoDir,
|
|
212
|
+
systemPrompt,
|
|
213
|
+
sessionId,
|
|
214
|
+
permissionMode: codeModeType,
|
|
215
|
+
attachments,
|
|
216
|
+
})) {
|
|
217
|
+
// Write session ID on first meta chunk
|
|
218
|
+
if (chunk.type === 'meta' && chunk.sessionId) {
|
|
219
|
+
writeSessionId(wsBaseDir, chunk.sessionId);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// DB persistence
|
|
223
|
+
if (chunk.type === 'text') {
|
|
224
|
+
pendingText += chunk.text;
|
|
225
|
+
} else if (chunk.type === 'tool-call') {
|
|
226
|
+
// Flush accumulated text before tool call
|
|
227
|
+
if (pendingText) {
|
|
228
|
+
persistMessage(threadId, 'assistant', pendingText, options);
|
|
229
|
+
pendingText = '';
|
|
230
|
+
}
|
|
231
|
+
pendingToolCalls.set(chunk.toolCallId, { toolName: chunk.toolName, args: chunk.args });
|
|
232
|
+
} else if (chunk.type === 'tool-result') {
|
|
233
|
+
const tc = pendingToolCalls.get(chunk.toolCallId);
|
|
234
|
+
if (tc) {
|
|
235
|
+
persistMessage(threadId, 'assistant', JSON.stringify({
|
|
236
|
+
type: 'tool-invocation',
|
|
237
|
+
toolCallId: chunk.toolCallId,
|
|
238
|
+
toolName: tc.toolName,
|
|
239
|
+
state: 'output-available',
|
|
240
|
+
input: tc.args,
|
|
241
|
+
output: chunk.result,
|
|
242
|
+
}), options);
|
|
243
|
+
pendingToolCalls.delete(chunk.toolCallId);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
yield chunk;
|
|
248
|
+
}
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.error('[chatStream] error:', err);
|
|
251
|
+
throw err;
|
|
252
|
+
} finally {
|
|
253
|
+
// Flush remaining text
|
|
254
|
+
if (pendingText) {
|
|
255
|
+
persistMessage(threadId, 'assistant', pendingText, options);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Auto-generate title for new chats
|
|
260
|
+
if (options.userId && message) {
|
|
261
|
+
autoTitle(threadId, message).catch(() => {});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Legacy path: LangGraph + Docker (non-claude-code agents + job mode) ──
|
|
268
|
+
|
|
269
|
+
const agent = isCodeMode
|
|
270
|
+
? await getCodeChat()
|
|
271
|
+
: await getAgentChat();
|
|
272
|
+
|
|
148
273
|
// Build content blocks: text + any image/PDF attachments as vision
|
|
149
274
|
const content = [];
|
|
150
275
|
|
package/lib/ai/model.js
CHANGED
|
@@ -118,6 +118,12 @@ export async function createModel(options = {}) {
|
|
|
118
118
|
if (!apiKey) throw new Error(LLM_NOT_CONFIGURED);
|
|
119
119
|
return new ChatOpenAI({ modelName, maxTokens, apiKey, configuration: { baseURL: 'https://openrouter.ai/api/v1' } });
|
|
120
120
|
}
|
|
121
|
+
case 'nvidia': {
|
|
122
|
+
const { ChatOpenAI } = await import('@langchain/openai');
|
|
123
|
+
const apiKey = getConfig('NVIDIA_API_KEY');
|
|
124
|
+
if (!apiKey) throw new Error(LLM_NOT_CONFIGURED);
|
|
125
|
+
return new ChatOpenAI({ modelName, maxTokens, apiKey, configuration: { baseURL: 'https://integrate.api.nvidia.com/v1' } });
|
|
126
|
+
}
|
|
121
127
|
default:
|
|
122
128
|
throw new Error(`Unknown LLM provider: ${provider}`);
|
|
123
129
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { getConfig } from '../../config.js';
|
|
3
|
+
import { buildAgentAuthEnv } from '../../tools/docker.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Claude Agent SDK adapter. Wraps the SDK's query() and yields
|
|
7
|
+
* the unified chunk format consumed by chatStream/api.js.
|
|
8
|
+
*
|
|
9
|
+
* @param {object} opts
|
|
10
|
+
* @param {string} opts.prompt - User message
|
|
11
|
+
* @param {string} opts.workspaceDir - Absolute path to workspace (git repo root)
|
|
12
|
+
* @param {string} [opts.systemPrompt] - System prompt (agent mode only)
|
|
13
|
+
* @param {string} [opts.sessionId] - Session ID to resume
|
|
14
|
+
* @param {string} [opts.permissionMode] - 'plan' or 'code'
|
|
15
|
+
* @param {Array} [opts.attachments] - Image attachments
|
|
16
|
+
* @yields {{ type: 'text'|'tool-call'|'tool-result'|'meta'|'result'|'unknown', ... }}
|
|
17
|
+
*/
|
|
18
|
+
export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, sessionId, permissionMode, attachments }) {
|
|
19
|
+
// Build a local env object with auth credentials from the settings DB.
|
|
20
|
+
// Passed via the SDK's `env` option — no process.env mutation needed.
|
|
21
|
+
const env = { ...process.env };
|
|
22
|
+
try {
|
|
23
|
+
const { env: authEnvPairs } = buildAgentAuthEnv('claude-code');
|
|
24
|
+
for (const pair of authEnvPairs) {
|
|
25
|
+
const eqIdx = pair.indexOf('=');
|
|
26
|
+
if (eqIdx > 0) {
|
|
27
|
+
env[pair.slice(0, eqIdx)] = pair.slice(eqIdx + 1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Clear conflicting auth vars so the SDK picks the right credential
|
|
32
|
+
// Priority: ANTHROPIC_AUTH_TOKEN > CLAUDE_CODE_OAUTH_TOKEN > ANTHROPIC_API_KEY
|
|
33
|
+
if (env.ANTHROPIC_AUTH_TOKEN) {
|
|
34
|
+
delete env.ANTHROPIC_API_KEY;
|
|
35
|
+
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
36
|
+
} else if (env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
37
|
+
delete env.ANTHROPIC_API_KEY;
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error('[claude-code-sdk] Failed to resolve auth:', err.message);
|
|
41
|
+
// Fall through — env may already have the right vars from process.env
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const options = {
|
|
45
|
+
cwd: workspaceDir,
|
|
46
|
+
env,
|
|
47
|
+
includePartialMessages: true,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Permission mode → allowed tools
|
|
51
|
+
if (permissionMode === 'code') {
|
|
52
|
+
options.permissionMode = 'bypassPermissions';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (sessionId) options.resume = sessionId;
|
|
56
|
+
if (systemPrompt) {
|
|
57
|
+
options.systemPrompt = { type: 'preset', preset: 'claude_code', append: systemPrompt };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Build prompt — plain string when no attachments, SDKUserMessage with content blocks when there are
|
|
61
|
+
let sdkPrompt = prompt;
|
|
62
|
+
if (attachments?.length) {
|
|
63
|
+
const content = [{ type: 'text', text: prompt }];
|
|
64
|
+
for (const att of attachments) {
|
|
65
|
+
if (att.category === 'image') {
|
|
66
|
+
const data = att.dataUrl
|
|
67
|
+
? att.dataUrl.replace(/^data:[^;]+;base64,/, '')
|
|
68
|
+
: att.data.toString('base64');
|
|
69
|
+
content.push({
|
|
70
|
+
type: 'image',
|
|
71
|
+
source: { type: 'base64', media_type: att.mimeType, data },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function* makePrompt() {
|
|
76
|
+
yield { type: 'user', message: { role: 'user', content }, parent_tool_use_id: null };
|
|
77
|
+
}
|
|
78
|
+
sdkPrompt = makePrompt();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Track tool call state for mapping stream events
|
|
82
|
+
const activeToolCalls = new Map(); // index → { id, name, argsJson }
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
for await (const message of query({ prompt: sdkPrompt, options })) {
|
|
86
|
+
// ── system messages ──
|
|
87
|
+
if (message.type === 'system') {
|
|
88
|
+
if (message.subtype === 'init') {
|
|
89
|
+
yield { type: 'meta', sessionId: message.session_id };
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── rate limit events ──
|
|
95
|
+
if (message.type === 'rate_limit_event') continue;
|
|
96
|
+
|
|
97
|
+
// ── streaming events ──
|
|
98
|
+
if (message.type === 'stream_event') {
|
|
99
|
+
const event = message.event;
|
|
100
|
+
|
|
101
|
+
if (event.type === 'content_block_start') {
|
|
102
|
+
const block = event.content_block;
|
|
103
|
+
if (block.type === 'tool_use') {
|
|
104
|
+
activeToolCalls.set(event.index, { id: block.id, name: block.name, argsJson: '' });
|
|
105
|
+
yield { type: 'tool-call', toolCallId: block.id, toolName: block.name, args: {} };
|
|
106
|
+
}
|
|
107
|
+
// Skip 'thinking', 'text' start (deltas handle text)
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (event.type === 'content_block_delta') {
|
|
112
|
+
if (event.delta.type === 'text_delta') {
|
|
113
|
+
yield { type: 'text', text: event.delta.text };
|
|
114
|
+
} else if (event.delta.type === 'input_json_delta') {
|
|
115
|
+
const tc = activeToolCalls.get(event.index);
|
|
116
|
+
if (tc) tc.argsJson += event.delta.partial_json;
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (event.type === 'content_block_stop') {
|
|
122
|
+
const tc = activeToolCalls.get(event.index);
|
|
123
|
+
if (tc && tc.argsJson) {
|
|
124
|
+
try {
|
|
125
|
+
const args = JSON.parse(tc.argsJson);
|
|
126
|
+
yield { type: 'tool-call', toolCallId: tc.id, toolName: tc.name, args };
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
activeToolCalls.delete(event.index);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// message_start, message_delta, message_stop — skip
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── user messages (tool results) ──
|
|
138
|
+
if (message.type === 'user') {
|
|
139
|
+
const blocks = message.message?.content || [];
|
|
140
|
+
for (const block of blocks) {
|
|
141
|
+
if (block.type === 'tool_result') {
|
|
142
|
+
const content = typeof block.content === 'string'
|
|
143
|
+
? block.content
|
|
144
|
+
: Array.isArray(block.content)
|
|
145
|
+
? block.content.map(b => b.type === 'text' ? b.text : JSON.stringify(b)).join('\n')
|
|
146
|
+
: JSON.stringify(block.content);
|
|
147
|
+
yield { type: 'tool-result', toolCallId: block.tool_use_id, result: content };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── assistant messages — redundant with streaming, skip ──
|
|
154
|
+
if (message.type === 'assistant') continue;
|
|
155
|
+
|
|
156
|
+
// ── result ──
|
|
157
|
+
if (message.type === 'result') {
|
|
158
|
+
console.log(`[claude-code-sdk] ${message.subtype} cost=$${message.total_cost_usd?.toFixed(4)} duration=${message.duration_ms}ms`);
|
|
159
|
+
yield {
|
|
160
|
+
type: 'result',
|
|
161
|
+
text: message.result || '',
|
|
162
|
+
cost: message.total_cost_usd,
|
|
163
|
+
duration: message.duration_ms,
|
|
164
|
+
subtype: message.subtype,
|
|
165
|
+
};
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── unknown ──
|
|
170
|
+
yield { type: 'unknown', raw: message };
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error('[claude-code-sdk] Stream error:', err);
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { claudeCodeStream } from './claude-code.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the SDK stream adapter for the given coding agent type,
|
|
5
|
+
* or null if no SDK adapter exists (fall back to legacy LangGraph/Docker path).
|
|
6
|
+
*
|
|
7
|
+
* @param {string} agentType - e.g. 'claude-code', 'pi-coding-agent', etc.
|
|
8
|
+
* @returns {Function|null} Async generator function or null
|
|
9
|
+
*/
|
|
10
|
+
export function getSdkAdapter(agentType) {
|
|
11
|
+
if (agentType === 'claude-code') return claudeCodeStream;
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read the Claude Code session ID from the workspace volume.
|
|
6
|
+
* Returns null if no session file exists.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} workspaceBaseDir - The workspace base dir (parent of workspace/)
|
|
9
|
+
* @param {number} [port=7681] - ttyd port (7681 = primary tab)
|
|
10
|
+
* @returns {string|null} Session ID or null
|
|
11
|
+
*/
|
|
12
|
+
export function readSessionId(workspaceBaseDir, port = 7681) {
|
|
13
|
+
try {
|
|
14
|
+
const filePath = path.join(workspaceBaseDir, '.claude-ttyd-sessions', String(port));
|
|
15
|
+
return readFileSync(filePath, 'utf8').trim() || null;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Write a session ID to the workspace volume so the interactive
|
|
23
|
+
* container can resume it.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} workspaceBaseDir - The workspace base dir (parent of workspace/)
|
|
26
|
+
* @param {string} sessionId - Claude Code session ID
|
|
27
|
+
* @param {number} [port=7681] - ttyd port (7681 = primary tab)
|
|
28
|
+
*/
|
|
29
|
+
export function writeSessionId(workspaceBaseDir, sessionId, port = 7681) {
|
|
30
|
+
const dir = path.join(workspaceBaseDir, '.claude-ttyd-sessions');
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
writeFileSync(path.join(dir, String(port)), sessionId + '\n');
|
|
33
|
+
}
|
package/lib/ai/tools.js
CHANGED
|
@@ -51,7 +51,7 @@ const agentChatCodingTool = tool(
|
|
|
51
51
|
const featureBranch = workspace?.featureBranch;
|
|
52
52
|
const mode = codeModeType === 'code' ? 'dangerous' : 'plan';
|
|
53
53
|
|
|
54
|
-
const codingAgent = getConfig('CODING_AGENT')
|
|
54
|
+
const codingAgent = getConfig('CODING_AGENT');
|
|
55
55
|
const containerName = `${codingAgent}-headless-${randomUUID().slice(0, 8)}`;
|
|
56
56
|
|
|
57
57
|
const { runHeadlessContainer, tailContainerLogs, waitForContainer, removeContainer } = await import('../tools/docker.js');
|
|
@@ -117,7 +117,7 @@ const codeChatCodingTool = tool(
|
|
|
117
117
|
const mode = codeModeType === 'code' ? 'dangerous' : 'plan';
|
|
118
118
|
|
|
119
119
|
const { runHeadlessContainer, tailContainerLogs, waitForContainer, removeContainer } = await import('../tools/docker.js');
|
|
120
|
-
const codingAgent = getConfig('CODING_AGENT')
|
|
120
|
+
const codingAgent = getConfig('CODING_AGENT');
|
|
121
121
|
const containerName = `${codingAgent}-headless-${randomUUID().slice(0, 8)}`;
|
|
122
122
|
|
|
123
123
|
const { backendApi } = await runHeadlessContainer({
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { execFile as execFileCb } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { existsSync, mkdirSync, symlinkSync, unlinkSync, lstatSync } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { getConfig } from '../config.js';
|
|
6
|
+
|
|
7
|
+
const execFile = promisify(execFileCb);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Run a command and return stdout. Rejects on non-zero exit.
|
|
11
|
+
*/
|
|
12
|
+
async function run(cmd, args, opts) {
|
|
13
|
+
const { stdout } = await execFile(cmd, args, opts);
|
|
14
|
+
return stdout.trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Ensure workspace directory exists and contains the git repo
|
|
19
|
+
* on the correct branch. Idempotent — safe to call on every message.
|
|
20
|
+
*
|
|
21
|
+
* Replaces Docker entrypoint scripts: setup-git.sh, clone.sh, feature-branch.sh.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} opts
|
|
24
|
+
* @param {string} opts.workspaceDir - Absolute path to workspace directory (the git repo root)
|
|
25
|
+
* @param {string} opts.repo - GitHub owner/repo (e.g. "owner/repo")
|
|
26
|
+
* @param {string} opts.branch - Base branch (e.g. "main")
|
|
27
|
+
* @param {string} [opts.featureBranch] - Feature branch to create/checkout
|
|
28
|
+
*/
|
|
29
|
+
export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureBranch }) {
|
|
30
|
+
const ghToken = getConfig('GH_TOKEN');
|
|
31
|
+
const env = { ...process.env };
|
|
32
|
+
if (ghToken) env.GH_TOKEN = ghToken;
|
|
33
|
+
|
|
34
|
+
const execOpts = { cwd: workspaceDir, env };
|
|
35
|
+
|
|
36
|
+
// 1. Create workspace directory
|
|
37
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
38
|
+
|
|
39
|
+
// 2. Configure git to use GH_TOKEN for GitHub HTTPS URLs (mirrors setup-git.sh)
|
|
40
|
+
if (ghToken) {
|
|
41
|
+
await run('gh', ['auth', 'setup-git'], execOpts);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. Clone if not already a git repo
|
|
45
|
+
const hasGit = existsSync(path.join(workspaceDir, '.git'));
|
|
46
|
+
if (!hasGit) {
|
|
47
|
+
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);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. Git identity (only if not already configured)
|
|
52
|
+
try {
|
|
53
|
+
await run('git', ['config', 'user.name'], execOpts);
|
|
54
|
+
} catch {
|
|
55
|
+
// Not configured — derive from GitHub token
|
|
56
|
+
if (ghToken) {
|
|
57
|
+
try {
|
|
58
|
+
const userJson = await run('gh', ['api', 'user', '-q', '{name: .name, login: .login, email: .email, id: .id}'], execOpts);
|
|
59
|
+
const user = JSON.parse(userJson);
|
|
60
|
+
const name = user.name || user.login;
|
|
61
|
+
const email = user.email || `${user.id}+${user.login}@users.noreply.github.com`;
|
|
62
|
+
await run('git', ['config', 'user.name', name], execOpts);
|
|
63
|
+
await run('git', ['config', 'user.email', email], execOpts);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error('[workspace-setup] Failed to set git identity:', err.message);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 4. Feature branch checkout
|
|
71
|
+
if (!featureBranch) return;
|
|
72
|
+
|
|
73
|
+
// Already on the right branch locally?
|
|
74
|
+
try {
|
|
75
|
+
await run('git', ['rev-parse', '--verify', featureBranch], execOpts);
|
|
76
|
+
// Branch exists locally — make sure we're on it
|
|
77
|
+
const current = await run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], execOpts);
|
|
78
|
+
if (current !== featureBranch) {
|
|
79
|
+
await run('git', ['checkout', featureBranch], execOpts);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
} catch {
|
|
83
|
+
// Branch doesn't exist locally — check remote
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const remoteCheck = await run('git', ['ls-remote', '--heads', 'origin', featureBranch], execOpts);
|
|
88
|
+
if (remoteCheck) {
|
|
89
|
+
// Remote branch exists — checkout tracking it
|
|
90
|
+
await run('git', ['checkout', '-B', featureBranch, `origin/${featureBranch}`], execOpts);
|
|
91
|
+
} else {
|
|
92
|
+
// Create new branch and push
|
|
93
|
+
await run('git', ['checkout', '-b', featureBranch], execOpts);
|
|
94
|
+
await run('git', ['push', '-u', 'origin', featureBranch], execOpts);
|
|
95
|
+
}
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error('[workspace-setup] Feature branch error:', err.message);
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
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
|
+
|
|
129
|
+
symlinkSync('../library/agent-job-secrets', link);
|
|
130
|
+
}
|
package/lib/chat/actions.js
CHANGED
|
@@ -942,10 +942,28 @@ export async function getChatSettings() {
|
|
|
942
942
|
const maxTokens = getConfigValue('LLM_MAX_TOKENS') || '4096';
|
|
943
943
|
const agentBackend = getConfigValue('AGENT_BACKEND') || '';
|
|
944
944
|
|
|
945
|
+
// Check if the current coding agent has an in-process SDK adapter
|
|
946
|
+
const { getConfig } = await import('../config.js');
|
|
947
|
+
const { getSdkAdapter } = await import('../ai/sdk-adapters/index.js');
|
|
948
|
+
const codingAgent = getConfig('CODING_AGENT');
|
|
949
|
+
const sdkAgentActive = !!getSdkAdapter(codingAgent);
|
|
950
|
+
|
|
951
|
+
// Human-readable agent names
|
|
952
|
+
const agentNames = {
|
|
953
|
+
'claude-code': 'Claude Code',
|
|
954
|
+
'pi-coding-agent': 'Pi Coding Agent',
|
|
955
|
+
'gemini-cli': 'Gemini CLI',
|
|
956
|
+
'codex-cli': 'Codex CLI',
|
|
957
|
+
'opencode': 'OpenCode',
|
|
958
|
+
'kimi-cli': 'Kimi CLI',
|
|
959
|
+
};
|
|
960
|
+
|
|
945
961
|
return {
|
|
946
962
|
builtinProviders: BUILTIN_PROVIDERS,
|
|
947
963
|
credentialStatuses,
|
|
948
964
|
customProviders,
|
|
965
|
+
sdkAgentActive,
|
|
966
|
+
defaultAgent: agentNames[codingAgent] || codingAgent,
|
|
949
967
|
active: {
|
|
950
968
|
provider: activeProvider,
|
|
951
969
|
model: activeModel,
|
package/lib/chat/api.js
CHANGED
|
@@ -153,6 +153,9 @@ export async function POST(request) {
|
|
|
153
153
|
output: chunk.result,
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
+
} else if (chunk.type === 'meta' || chunk.type === 'result') {
|
|
157
|
+
// Internal events — no SSE output needed
|
|
158
|
+
|
|
156
159
|
} else if (chunk.type === 'unknown') {
|
|
157
160
|
// Close any open text block before unknown event
|
|
158
161
|
if (textStarted) {
|
|
@@ -244,23 +244,14 @@ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
|
|
|
244
244
|
setEditTitle(chat.title || "");
|
|
245
245
|
};
|
|
246
246
|
return /* @__PURE__ */ jsxs(
|
|
247
|
-
"
|
|
247
|
+
"div",
|
|
248
248
|
{
|
|
249
|
-
href: chat.codeWorkspaceId && chat.containerName ? `/code/${chat.codeWorkspaceId}` : `/chat/${chat.id}`,
|
|
250
249
|
className: "relative group flex items-center gap-3 px-3 py-3 cursor-pointer hover:bg-muted/50 rounded-md",
|
|
251
|
-
style: { textDecoration: "inherit", color: "inherit" },
|
|
252
250
|
onMouseEnter: () => setHovered(true),
|
|
253
251
|
onMouseLeave: () => setHovered(false),
|
|
254
252
|
onClick: (e) => {
|
|
255
|
-
if (editing)
|
|
256
|
-
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
if (menuRef.current && menuRef.current.contains(e.target)) {
|
|
260
|
-
e.preventDefault();
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
e.preventDefault();
|
|
253
|
+
if (editing) return;
|
|
254
|
+
if (menuRef.current && menuRef.current.contains(e.target)) return;
|
|
264
255
|
if (chat.codeWorkspaceId && chat.containerName) {
|
|
265
256
|
window.location.href = `/code/${chat.codeWorkspaceId}`;
|
|
266
257
|
} else {
|
|
@@ -297,16 +297,13 @@ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
|
|
|
297
297
|
};
|
|
298
298
|
|
|
299
299
|
return (
|
|
300
|
-
<
|
|
301
|
-
href={chat.codeWorkspaceId && chat.containerName ? `/code/${chat.codeWorkspaceId}` : `/chat/${chat.id}`}
|
|
300
|
+
<div
|
|
302
301
|
className="relative group flex items-center gap-3 px-3 py-3 cursor-pointer hover:bg-muted/50 rounded-md"
|
|
303
|
-
style={{ textDecoration: 'inherit', color: 'inherit' }}
|
|
304
302
|
onMouseEnter={() => setHovered(true)}
|
|
305
303
|
onMouseLeave={() => setHovered(false)}
|
|
306
304
|
onClick={(e) => {
|
|
307
|
-
if (editing)
|
|
308
|
-
if (menuRef.current && menuRef.current.contains(e.target))
|
|
309
|
-
e.preventDefault();
|
|
305
|
+
if (editing) return;
|
|
306
|
+
if (menuRef.current && menuRef.current.contains(e.target)) return;
|
|
310
307
|
if (chat.codeWorkspaceId && chat.containerName) {
|
|
311
308
|
window.location.href = `/code/${chat.codeWorkspaceId}`;
|
|
312
309
|
} else {
|
|
@@ -410,6 +407,6 @@ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
|
|
|
410
407
|
}}
|
|
411
408
|
onCancel={() => setConfirmDelete(false)}
|
|
412
409
|
/>
|
|
413
|
-
</
|
|
410
|
+
</div>
|
|
414
411
|
);
|
|
415
412
|
}
|
|
@@ -10,7 +10,7 @@ export { SettingsLayout } from './settings-layout.js';
|
|
|
10
10
|
export { SubTabLayout, ApiKeysLayout, EventHandlerLayout, ChatSettingsLayout, GitHubSettingsLayout, SecretsLayout } from './settings-secrets-layout.js';
|
|
11
11
|
export { ApiKeysListPage, ApiKeysVoicePage, ApiKeysTelegramPage, SettingsSecretsPage } from './settings-secrets-page.js';
|
|
12
12
|
export { SettingsUsersPage } from './settings-users-page.js';
|
|
13
|
-
export { ChatConfigPage, ChatProvidersPage,
|
|
13
|
+
export { ChatConfigPage, ChatProvidersPage, SettingsChatPage } from './settings-chat-page.js';
|
|
14
14
|
export { ChatProvidersPage as LlmsPage } from './settings-chat-page.js';
|
|
15
15
|
export { CodingAgentsPage } from './settings-coding-agents-page.js';
|
|
16
16
|
export { JobsPage } from './settings-jobs-page.js';
|
|
@@ -40,12 +40,18 @@ function ChatConfigPage() {
|
|
|
40
40
|
if (settings?.error) {
|
|
41
41
|
return /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: settings.error });
|
|
42
42
|
}
|
|
43
|
+
const sdkAgentActive = settings?.sdkAgentActive;
|
|
44
|
+
const defaultAgent = settings?.defaultAgent;
|
|
43
45
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
46
|
+
sdkAgentActive && /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-destructive/30 bg-destructive/5 px-4 py-3 mb-4", children: /* @__PURE__ */ jsxs("p", { className: "text-sm text-destructive", children: [
|
|
47
|
+
defaultAgent,
|
|
48
|
+
" manages its own LLM directly. These settings only apply when using a coding agent without built-in SDK support."
|
|
49
|
+
] }) }),
|
|
44
50
|
/* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
|
|
45
51
|
/* @__PURE__ */ jsx("h2", { className: "text-base font-medium", children: "Configuration" }),
|
|
46
52
|
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Select the LLM provider and model for chat. Only providers with configured API keys appear in the dropdown." })
|
|
47
53
|
] }),
|
|
48
|
-
/* @__PURE__ */ jsx(ActiveConfig, { settings, onSave: handleSaveActive })
|
|
54
|
+
/* @__PURE__ */ jsx("div", { className: sdkAgentActive ? "opacity-50 pointer-events-none" : "", children: /* @__PURE__ */ jsx(ActiveConfig, { settings, onSave: handleSaveActive }) })
|
|
49
55
|
] });
|
|
50
56
|
}
|
|
51
57
|
function ActiveConfig({ settings, onSave }) {
|
|
@@ -731,127 +737,11 @@ function CustomProviderDialog({ open, initial, onSave, onCancel }) {
|
|
|
731
737
|
] })
|
|
732
738
|
] });
|
|
733
739
|
}
|
|
734
|
-
function ChatLlmPage() {
|
|
735
|
-
const [settings, setSettings] = useState(null);
|
|
736
|
-
const [loading, setLoading] = useState(true);
|
|
737
|
-
const [showDialog, setShowDialog] = useState(false);
|
|
738
|
-
const [editingProvider, setEditingProvider] = useState(null);
|
|
739
|
-
const loadSettings = async () => {
|
|
740
|
-
try {
|
|
741
|
-
const result = await getChatSettings();
|
|
742
|
-
setSettings(result);
|
|
743
|
-
} catch {
|
|
744
|
-
} finally {
|
|
745
|
-
setLoading(false);
|
|
746
|
-
}
|
|
747
|
-
};
|
|
748
|
-
useEffect(() => {
|
|
749
|
-
loadSettings();
|
|
750
|
-
}, []);
|
|
751
|
-
const handleSaveActive = async (provider, model, maxTokens) => {
|
|
752
|
-
const result = await setActiveLlm(provider, model, maxTokens);
|
|
753
|
-
if (result?.success) await loadSettings();
|
|
754
|
-
return result;
|
|
755
|
-
};
|
|
756
|
-
const handleUpdateCredential = async (credKey, value) => {
|
|
757
|
-
await updateProviderCredential(credKey, value);
|
|
758
|
-
await loadSettings();
|
|
759
|
-
};
|
|
760
|
-
const handleAddCustom = async (config) => {
|
|
761
|
-
await addCustomProvider(config);
|
|
762
|
-
setShowDialog(false);
|
|
763
|
-
await loadSettings();
|
|
764
|
-
};
|
|
765
|
-
const handleEditCustom = async (config) => {
|
|
766
|
-
if (editingProvider) {
|
|
767
|
-
await updateCustomProvider(editingProvider.key, config);
|
|
768
|
-
setEditingProvider(null);
|
|
769
|
-
setShowDialog(false);
|
|
770
|
-
await loadSettings();
|
|
771
|
-
}
|
|
772
|
-
};
|
|
773
|
-
const handleRemoveCustom = async (key) => {
|
|
774
|
-
await removeCustomProvider(key);
|
|
775
|
-
await loadSettings();
|
|
776
|
-
};
|
|
777
|
-
const openAdd = () => {
|
|
778
|
-
setEditingProvider(null);
|
|
779
|
-
setShowDialog(true);
|
|
780
|
-
};
|
|
781
|
-
const openEdit = (provider) => {
|
|
782
|
-
setEditingProvider(provider);
|
|
783
|
-
setShowDialog(true);
|
|
784
|
-
};
|
|
785
|
-
const closeDialog = () => {
|
|
786
|
-
setShowDialog(false);
|
|
787
|
-
setEditingProvider(null);
|
|
788
|
-
};
|
|
789
|
-
if (loading) {
|
|
790
|
-
return /* @__PURE__ */ jsx("div", { className: "h-48 animate-pulse rounded-md bg-border/50" });
|
|
791
|
-
}
|
|
792
|
-
if (settings?.error) {
|
|
793
|
-
return /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: settings.error });
|
|
794
|
-
}
|
|
795
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
796
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
797
|
-
/* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
|
|
798
|
-
/* @__PURE__ */ jsx("h2", { className: "text-base font-medium", children: "Default Provider" }),
|
|
799
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Select the LLM provider and model for chat. Only providers with configured API keys appear in the dropdown." })
|
|
800
|
-
] }),
|
|
801
|
-
/* @__PURE__ */ jsx(ActiveConfig, { settings, onSave: handleSaveActive })
|
|
802
|
-
] }),
|
|
803
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
804
|
-
/* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
|
|
805
|
-
/* @__PURE__ */ jsx("h2", { className: "text-base font-medium", children: "Providers" }),
|
|
806
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Configure API keys and credentials for each LLM provider." })
|
|
807
|
-
] }),
|
|
808
|
-
settings?.builtinProviders && /* @__PURE__ */ jsxs("div", { className: "mb-6", children: [
|
|
809
|
-
/* @__PURE__ */ jsx("h4", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3", children: "Built-in" }),
|
|
810
|
-
/* @__PURE__ */ jsx("div", { className: "space-y-8", children: Object.entries(settings.builtinProviders).map(([slug, prov]) => /* @__PURE__ */ jsx(
|
|
811
|
-
ProviderCard,
|
|
812
|
-
{
|
|
813
|
-
slug,
|
|
814
|
-
name: prov.name,
|
|
815
|
-
credentials: prov.credentials,
|
|
816
|
-
credentialStatuses: settings.credentialStatuses || [],
|
|
817
|
-
onUpdateCredential: handleUpdateCredential
|
|
818
|
-
},
|
|
819
|
-
slug
|
|
820
|
-
)) })
|
|
821
|
-
] }),
|
|
822
|
-
/* @__PURE__ */ jsxs("div", { className: "space-y-8", children: [
|
|
823
|
-
/* @__PURE__ */ jsx("h4", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wider", children: "Custom (OpenAI Compatible API)" }),
|
|
824
|
-
settings?.customProviders?.map((cp) => /* @__PURE__ */ jsx(CustomProviderCard, { provider: cp, onEdit: openEdit, onRemove: handleRemoveCustom }, cp.key)),
|
|
825
|
-
/* @__PURE__ */ jsxs(
|
|
826
|
-
"button",
|
|
827
|
-
{
|
|
828
|
-
onClick: openAdd,
|
|
829
|
-
className: "w-full rounded-lg border border-dashed p-4 text-sm text-muted-foreground hover:text-foreground hover:border-foreground/30 transition-colors flex items-center justify-center gap-2",
|
|
830
|
-
children: [
|
|
831
|
-
/* @__PURE__ */ jsx(PlusIcon, { size: 14 }),
|
|
832
|
-
"Add OpenAI Compatible API"
|
|
833
|
-
]
|
|
834
|
-
}
|
|
835
|
-
)
|
|
836
|
-
] }),
|
|
837
|
-
/* @__PURE__ */ jsx(
|
|
838
|
-
CustomProviderDialog,
|
|
839
|
-
{
|
|
840
|
-
open: showDialog,
|
|
841
|
-
initial: editingProvider,
|
|
842
|
-
onSave: editingProvider ? handleEditCustom : handleAddCustom,
|
|
843
|
-
onCancel: closeDialog
|
|
844
|
-
}
|
|
845
|
-
)
|
|
846
|
-
] })
|
|
847
|
-
] });
|
|
848
|
-
}
|
|
849
740
|
function SettingsChatPage() {
|
|
850
741
|
return /* @__PURE__ */ jsx(ChatConfigPage, {});
|
|
851
742
|
}
|
|
852
743
|
export {
|
|
853
744
|
ChatConfigPage,
|
|
854
|
-
ChatLlmPage,
|
|
855
745
|
ChatProvidersPage,
|
|
856
746
|
SettingsChatPage
|
|
857
747
|
};
|
|
@@ -52,13 +52,25 @@ export function ChatConfigPage() {
|
|
|
52
52
|
return <p className="text-sm text-destructive">{settings.error}</p>;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
const sdkAgentActive = settings?.sdkAgentActive;
|
|
56
|
+
const defaultAgent = settings?.defaultAgent;
|
|
57
|
+
|
|
55
58
|
return (
|
|
56
59
|
<div>
|
|
60
|
+
{sdkAgentActive && (
|
|
61
|
+
<div className="rounded-lg border border-destructive/30 bg-destructive/5 px-4 py-3 mb-4">
|
|
62
|
+
<p className="text-sm text-destructive">
|
|
63
|
+
{defaultAgent} manages its own LLM directly. These settings only apply when using a coding agent without built-in SDK support.
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
57
67
|
<div className="mb-4">
|
|
58
68
|
<h2 className="text-base font-medium">Configuration</h2>
|
|
59
69
|
<p className="text-sm text-muted-foreground">Select the LLM provider and model for chat. Only providers with configured API keys appear in the dropdown.</p>
|
|
60
70
|
</div>
|
|
61
|
-
<
|
|
71
|
+
<div className={sdkAgentActive ? 'opacity-50 pointer-events-none' : ''}>
|
|
72
|
+
<ActiveConfig settings={settings} onSave={handleSaveActive} />
|
|
73
|
+
</div>
|
|
62
74
|
</div>
|
|
63
75
|
);
|
|
64
76
|
}
|
|
@@ -763,137 +775,6 @@ function CustomProviderDialog({ open, initial, onSave, onCancel }) {
|
|
|
763
775
|
);
|
|
764
776
|
}
|
|
765
777
|
|
|
766
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
767
|
-
// Combined LLM page — Default Provider + Providers
|
|
768
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
769
|
-
|
|
770
|
-
export function ChatLlmPage() {
|
|
771
|
-
const [settings, setSettings] = useState(null);
|
|
772
|
-
const [loading, setLoading] = useState(true);
|
|
773
|
-
const [showDialog, setShowDialog] = useState(false);
|
|
774
|
-
const [editingProvider, setEditingProvider] = useState(null);
|
|
775
|
-
|
|
776
|
-
const loadSettings = async () => {
|
|
777
|
-
try {
|
|
778
|
-
const result = await getChatSettings();
|
|
779
|
-
setSettings(result);
|
|
780
|
-
} catch {
|
|
781
|
-
// ignore
|
|
782
|
-
} finally {
|
|
783
|
-
setLoading(false);
|
|
784
|
-
}
|
|
785
|
-
};
|
|
786
|
-
|
|
787
|
-
useEffect(() => {
|
|
788
|
-
loadSettings();
|
|
789
|
-
}, []);
|
|
790
|
-
|
|
791
|
-
// Default Provider handlers
|
|
792
|
-
const handleSaveActive = async (provider, model, maxTokens) => {
|
|
793
|
-
const result = await setActiveLlm(provider, model, maxTokens);
|
|
794
|
-
if (result?.success) await loadSettings();
|
|
795
|
-
return result;
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
// Providers handlers
|
|
799
|
-
const handleUpdateCredential = async (credKey, value) => {
|
|
800
|
-
await updateProviderCredential(credKey, value);
|
|
801
|
-
await loadSettings();
|
|
802
|
-
};
|
|
803
|
-
|
|
804
|
-
const handleAddCustom = async (config) => {
|
|
805
|
-
await addCustomProvider(config);
|
|
806
|
-
setShowDialog(false);
|
|
807
|
-
await loadSettings();
|
|
808
|
-
};
|
|
809
|
-
|
|
810
|
-
const handleEditCustom = async (config) => {
|
|
811
|
-
if (editingProvider) {
|
|
812
|
-
await updateCustomProvider(editingProvider.key, config);
|
|
813
|
-
setEditingProvider(null);
|
|
814
|
-
setShowDialog(false);
|
|
815
|
-
await loadSettings();
|
|
816
|
-
}
|
|
817
|
-
};
|
|
818
|
-
|
|
819
|
-
const handleRemoveCustom = async (key) => {
|
|
820
|
-
await removeCustomProvider(key);
|
|
821
|
-
await loadSettings();
|
|
822
|
-
};
|
|
823
|
-
|
|
824
|
-
const openAdd = () => { setEditingProvider(null); setShowDialog(true); };
|
|
825
|
-
const openEdit = (provider) => { setEditingProvider(provider); setShowDialog(true); };
|
|
826
|
-
const closeDialog = () => { setShowDialog(false); setEditingProvider(null); };
|
|
827
|
-
|
|
828
|
-
if (loading) {
|
|
829
|
-
return <div className="h-48 animate-pulse rounded-md bg-border/50" />;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
if (settings?.error) {
|
|
833
|
-
return <p className="text-sm text-destructive">{settings.error}</p>;
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
return (
|
|
837
|
-
<div className="space-y-6">
|
|
838
|
-
{/* Default Provider section */}
|
|
839
|
-
<div>
|
|
840
|
-
<div className="mb-4">
|
|
841
|
-
<h2 className="text-base font-medium">Default Provider</h2>
|
|
842
|
-
<p className="text-sm text-muted-foreground">Select the LLM provider and model for chat. Only providers with configured API keys appear in the dropdown.</p>
|
|
843
|
-
</div>
|
|
844
|
-
<ActiveConfig settings={settings} onSave={handleSaveActive} />
|
|
845
|
-
</div>
|
|
846
|
-
|
|
847
|
-
{/* Providers section */}
|
|
848
|
-
<div>
|
|
849
|
-
<div className="mb-4">
|
|
850
|
-
<h2 className="text-base font-medium">Providers</h2>
|
|
851
|
-
<p className="text-sm text-muted-foreground">Configure API keys and credentials for each LLM provider.</p>
|
|
852
|
-
</div>
|
|
853
|
-
|
|
854
|
-
{settings?.builtinProviders && (
|
|
855
|
-
<div className="mb-6">
|
|
856
|
-
<h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3">Built-in</h4>
|
|
857
|
-
<div className="space-y-8">
|
|
858
|
-
{Object.entries(settings.builtinProviders).map(([slug, prov]) => (
|
|
859
|
-
<ProviderCard
|
|
860
|
-
key={slug}
|
|
861
|
-
slug={slug}
|
|
862
|
-
name={prov.name}
|
|
863
|
-
credentials={prov.credentials}
|
|
864
|
-
credentialStatuses={settings.credentialStatuses || []}
|
|
865
|
-
onUpdateCredential={handleUpdateCredential}
|
|
866
|
-
/>
|
|
867
|
-
))}
|
|
868
|
-
</div>
|
|
869
|
-
</div>
|
|
870
|
-
)}
|
|
871
|
-
|
|
872
|
-
<div className="space-y-8">
|
|
873
|
-
<h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Custom (OpenAI Compatible API)</h4>
|
|
874
|
-
{settings?.customProviders?.map((cp) => (
|
|
875
|
-
<CustomProviderCard key={cp.key} provider={cp} onEdit={openEdit} onRemove={handleRemoveCustom} />
|
|
876
|
-
))}
|
|
877
|
-
<button
|
|
878
|
-
onClick={openAdd}
|
|
879
|
-
className="w-full rounded-lg border border-dashed p-4 text-sm text-muted-foreground hover:text-foreground hover:border-foreground/30 transition-colors flex items-center justify-center gap-2"
|
|
880
|
-
>
|
|
881
|
-
<PlusIcon size={14} />
|
|
882
|
-
Add OpenAI Compatible API
|
|
883
|
-
</button>
|
|
884
|
-
</div>
|
|
885
|
-
|
|
886
|
-
<CustomProviderDialog
|
|
887
|
-
open={showDialog}
|
|
888
|
-
initial={editingProvider}
|
|
889
|
-
onSave={editingProvider ? handleEditCustom : handleAddCustom}
|
|
890
|
-
onCancel={closeDialog}
|
|
891
|
-
/>
|
|
892
|
-
</div>
|
|
893
|
-
</div>
|
|
894
|
-
);
|
|
895
|
-
}
|
|
896
|
-
|
|
897
778
|
// Backwards compat
|
|
898
779
|
export function SettingsChatPage() {
|
|
899
780
|
return <ChatConfigPage />;
|
|
@@ -28,8 +28,8 @@ const API_KEYS_TABS = [
|
|
|
28
28
|
];
|
|
29
29
|
const EVENT_HANDLER_TABS = [
|
|
30
30
|
{ id: "llms", label: "LLMs", href: "/admin/event-handler/llms" },
|
|
31
|
-
{ id: "chat", label: "Chat", href: "/admin/event-handler/chat" },
|
|
32
31
|
{ id: "coding-agents", label: "Coding Agents", href: "/admin/event-handler/coding-agents" },
|
|
32
|
+
{ id: "chat", label: "Chat", href: "/admin/event-handler/chat" },
|
|
33
33
|
{ id: "agent-secrets", label: "Agent Secrets", href: "/admin/event-handler/agent-secrets" },
|
|
34
34
|
{ id: "webhooks", label: "Webhooks", href: "/admin/event-handler/webhooks" },
|
|
35
35
|
{ id: "telegram", label: "Telegram", href: "/admin/event-handler/telegram" },
|
|
@@ -50,8 +50,8 @@ const API_KEYS_TABS = [
|
|
|
50
50
|
|
|
51
51
|
const EVENT_HANDLER_TABS = [
|
|
52
52
|
{ id: 'llms', label: 'LLMs', href: '/admin/event-handler/llms' },
|
|
53
|
-
{ id: 'chat', label: 'Chat', href: '/admin/event-handler/chat' },
|
|
54
53
|
{ id: 'coding-agents', label: 'Coding Agents', href: '/admin/event-handler/coding-agents' },
|
|
54
|
+
{ id: 'chat', label: 'Chat', href: '/admin/event-handler/chat' },
|
|
55
55
|
{ id: 'agent-secrets', label: 'Agent Secrets', href: '/admin/event-handler/agent-secrets' },
|
|
56
56
|
{ id: 'webhooks', label: 'Webhooks', href: '/admin/event-handler/webhooks' },
|
|
57
57
|
{ id: 'telegram', label: 'Telegram', href: '/admin/event-handler/telegram' },
|
package/lib/config.js
CHANGED
package/lib/llm-providers.js
CHANGED
|
@@ -127,6 +127,14 @@ export const BUILTIN_PROVIDERS = {
|
|
|
127
127
|
anthropicEndpoint: 'https://openrouter.ai/api',
|
|
128
128
|
models: [],
|
|
129
129
|
},
|
|
130
|
+
nvidia: {
|
|
131
|
+
name: 'NVIDIA',
|
|
132
|
+
litellmProxy: true, litellmPrefix: 'nvidia_nim',
|
|
133
|
+
credentials: [
|
|
134
|
+
{ type: 'api_key', key: 'NVIDIA_API_KEY', label: 'API Key' },
|
|
135
|
+
],
|
|
136
|
+
models: [],
|
|
137
|
+
},
|
|
130
138
|
};
|
|
131
139
|
|
|
132
140
|
/**
|
package/lib/tools/docker.js
CHANGED
|
@@ -347,7 +347,7 @@ function buildAgentAuthEnv(agent) {
|
|
|
347
347
|
const model = getConfig(`${configPrefix}_MODEL`);
|
|
348
348
|
if (model) env.push(`LLM_MODEL=${model}`);
|
|
349
349
|
|
|
350
|
-
const builtinKeyMap = { anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY', google: 'GOOGLE_API_KEY', deepseek: 'DEEPSEEK_API_KEY', minimax: 'MINIMAX_API_KEY', mistral: 'MISTRAL_API_KEY', xai: 'XAI_API_KEY', openrouter: 'OPENROUTER_API_KEY' };
|
|
350
|
+
const builtinKeyMap = { anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY', google: 'GOOGLE_API_KEY', deepseek: 'DEEPSEEK_API_KEY', minimax: 'MINIMAX_API_KEY', mistral: 'MISTRAL_API_KEY', xai: 'XAI_API_KEY', openrouter: 'OPENROUTER_API_KEY', nvidia: 'NVIDIA_API_KEY' };
|
|
351
351
|
if (builtinKeyMap[provider]) {
|
|
352
352
|
const key = getConfig(builtinKeyMap[provider]);
|
|
353
353
|
if (key) env.push(`${builtinKeyMap[provider]}=${key}`);
|
|
@@ -927,6 +927,7 @@ async function runAgentJobContainer({ agentJobId, repo, branch, title, descripti
|
|
|
927
927
|
}
|
|
928
928
|
|
|
929
929
|
export {
|
|
930
|
+
CODING_AGENT_UID,
|
|
930
931
|
DockerFrameParser,
|
|
931
932
|
runInteractiveContainer,
|
|
932
933
|
runHeadlessContainer,
|
|
@@ -951,4 +952,5 @@ export {
|
|
|
951
952
|
createVolume,
|
|
952
953
|
removeVolume,
|
|
953
954
|
restartLitellm,
|
|
955
|
+
buildAgentAuthEnv,
|
|
954
956
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thepopebot",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.76-beta.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Create autonomous AI agents with a two-layer architecture: Next.js Event Handler + Docker Agent.",
|
|
6
6
|
"bin": {
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"license": "MIT",
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@ai-sdk/react": "^2.0.0",
|
|
71
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.92",
|
|
71
72
|
"@clack/prompts": "^0.10.0",
|
|
72
73
|
"@dnd-kit/core": "^6.3.1",
|
|
73
74
|
"@dnd-kit/modifiers": "^9.0.0",
|