opc-agent 4.0.43 → 4.1.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/dist/cli.js +2 -2
- package/dist/core/runtime.js +18 -0
- package/dist/providers/index.d.ts +6 -0
- package/dist/providers/index.js +100 -10
- package/dist/schema/oad.d.ts +34 -34
- package/dist/studio/server.js +181 -19
- package/dist/studio-ui/index.html +49 -14
- package/package.json +1 -1
- package/src/cli.ts +2 -2
- package/src/core/runtime.ts +18 -0
- package/src/providers/index.ts +95 -10
- package/src/studio/server.ts +178 -21
- package/src/studio-ui/index.html +49 -14
package/dist/studio/server.js
CHANGED
|
@@ -166,12 +166,96 @@ class StudioServer {
|
|
|
166
166
|
const industry = url.searchParams.get('industry') || '';
|
|
167
167
|
const search = url.searchParams.get('q') || '';
|
|
168
168
|
data = this.getTemplates(industry, search);
|
|
169
|
+
// Merge with real workstation templates
|
|
170
|
+
try {
|
|
171
|
+
const ws = require('agent-workstation');
|
|
172
|
+
const categories = ws.getCategories();
|
|
173
|
+
const wsTemplates = [];
|
|
174
|
+
for (const cat of categories) {
|
|
175
|
+
for (const roleName of cat.roles) {
|
|
176
|
+
const role = ws.getRole(cat.name, roleName);
|
|
177
|
+
if (!role)
|
|
178
|
+
continue;
|
|
179
|
+
let oad = {};
|
|
180
|
+
try {
|
|
181
|
+
if (role.files?.['oad.yaml']) {
|
|
182
|
+
const yaml = require('js-yaml');
|
|
183
|
+
oad = yaml.load(role.files['oad.yaml']) || {};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch { }
|
|
187
|
+
const tpl = {
|
|
188
|
+
id: `ws-${cat.name}-${roleName}`,
|
|
189
|
+
name: oad.name || roleName.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
190
|
+
nameZh: oad.nameZh || '',
|
|
191
|
+
icon: oad.icon || '🤖',
|
|
192
|
+
description: oad.description || '',
|
|
193
|
+
descriptionZh: oad.descriptionZh || '',
|
|
194
|
+
industry: cat.name,
|
|
195
|
+
industryZh: cat.name,
|
|
196
|
+
tags: [cat.name, 'workstation'],
|
|
197
|
+
suggestedModel: 'auto',
|
|
198
|
+
systemPrompt: oad.systemPrompt || role.files?.['brain-seed.md'] || '',
|
|
199
|
+
source: 'workstation',
|
|
200
|
+
ego: oad.ego || null,
|
|
201
|
+
mission: oad.mission || null,
|
|
202
|
+
skills: oad.skills || [],
|
|
203
|
+
};
|
|
204
|
+
if (!search || tpl.name.toLowerCase().includes(search.toLowerCase()) || tpl.nameZh.includes(search)) {
|
|
205
|
+
if (!industry || tpl.industry === industry) {
|
|
206
|
+
wsTemplates.push(tpl);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
data.templates = [...data.templates, ...wsTemplates];
|
|
212
|
+
// Add workstation industries to list
|
|
213
|
+
const existingIds = new Set(data.industries.map((i) => i.id));
|
|
214
|
+
for (const cat of categories) {
|
|
215
|
+
if (!existingIds.has(cat.name)) {
|
|
216
|
+
data.industries.push({ id: cat.name, name: cat.name, nameZh: cat.name });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (wsErr) {
|
|
221
|
+
// workstation not available, use built-in templates only
|
|
222
|
+
}
|
|
169
223
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
170
224
|
res.end(JSON.stringify(data));
|
|
171
225
|
return;
|
|
172
226
|
}
|
|
173
227
|
if (route.match(/^templates\/[^/]+$/) && req.method === 'GET') {
|
|
174
228
|
const tplId = route.split('/')[1];
|
|
229
|
+
// Check workstation first
|
|
230
|
+
if (tplId.startsWith('ws-')) {
|
|
231
|
+
const parts = tplId.replace('ws-', '').split('-');
|
|
232
|
+
const catName = parts[0];
|
|
233
|
+
const roleName = parts.slice(1).join('-');
|
|
234
|
+
try {
|
|
235
|
+
const ws = require('agent-workstation');
|
|
236
|
+
const role = ws.getRole(catName, roleName);
|
|
237
|
+
if (role) {
|
|
238
|
+
let oad = {};
|
|
239
|
+
try {
|
|
240
|
+
if (role.files?.['oad.yaml']) {
|
|
241
|
+
const yaml = require('js-yaml');
|
|
242
|
+
oad = yaml.load(role.files['oad.yaml']) || {};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch { }
|
|
246
|
+
data = {
|
|
247
|
+
id: tplId, name: oad.name || roleName, source: 'workstation',
|
|
248
|
+
category: catName, role: roleName, files: role.files,
|
|
249
|
+
ego: oad.ego, mission: oad.mission, skills: oad.skills,
|
|
250
|
+
systemPrompt: oad.systemPrompt || role.files?.['brain-seed.md'] || '',
|
|
251
|
+
};
|
|
252
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
253
|
+
res.end(JSON.stringify(data));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch { }
|
|
258
|
+
}
|
|
175
259
|
data = this.getTemplateById(tplId);
|
|
176
260
|
res.writeHead(data.error ? 404 : 200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
177
261
|
res.end(JSON.stringify(data));
|
|
@@ -473,6 +557,64 @@ class StudioServer {
|
|
|
473
557
|
res.end(JSON.stringify(data));
|
|
474
558
|
return;
|
|
475
559
|
}
|
|
560
|
+
// === Global config API (reads/writes ~/.opc/config.json) ===
|
|
561
|
+
if (route === 'config' && req.method === 'GET') {
|
|
562
|
+
data = loadSettingsConfig();
|
|
563
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
564
|
+
res.end(JSON.stringify(data));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (route === 'config' && req.method === 'PUT') {
|
|
568
|
+
const body = JSON.parse(await this.readBody(req));
|
|
569
|
+
const cfg = loadSettingsConfig();
|
|
570
|
+
Object.assign(cfg, body);
|
|
571
|
+
saveSettingsConfig(cfg);
|
|
572
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
573
|
+
res.end(JSON.stringify({ success: true, config: cfg }));
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
// === Models API (real agentkits integration) ===
|
|
577
|
+
if (route === 'models' && req.method === 'GET') {
|
|
578
|
+
try {
|
|
579
|
+
const ak = await Promise.resolve().then(() => __importStar(require('agentkits')));
|
|
580
|
+
const providers = ak.listLLMProviders();
|
|
581
|
+
data = { providers };
|
|
582
|
+
}
|
|
583
|
+
catch (e) {
|
|
584
|
+
data = { providers: [], error: 'agentkits not available: ' + e.message };
|
|
585
|
+
}
|
|
586
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
587
|
+
res.end(JSON.stringify(data));
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
// === Memory stats API (real deepbrain integration) ===
|
|
591
|
+
if (route === 'memory/stats' && req.method === 'GET') {
|
|
592
|
+
try {
|
|
593
|
+
const { Brain } = require('deepbrain');
|
|
594
|
+
const oad = this.loadOAD();
|
|
595
|
+
const dbPath = oad?.spec?.memory?.longTerm?.database || './data/brain.db';
|
|
596
|
+
const brain = new Brain({ database: dbPath, embedding_provider: 'ollama' });
|
|
597
|
+
await brain.connect();
|
|
598
|
+
const stats = await brain.stats();
|
|
599
|
+
await brain.disconnect();
|
|
600
|
+
data = { connected: true, ...stats };
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
data = { connected: false, pages: 0, chunks: 0, error: 'DeepBrain not installed or not configured. Install with: npm i deepbrain' };
|
|
604
|
+
}
|
|
605
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
606
|
+
res.end(JSON.stringify(data));
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (route.match(/^memory\/[^/]+$/) && req.method === 'GET') {
|
|
610
|
+
const agentId = route.split('/')[1];
|
|
611
|
+
if (agentId !== 'stats' && agentId !== 'list' && agentId !== 'search') {
|
|
612
|
+
data = this.getAgentMemory(agentId);
|
|
613
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
614
|
+
res.end(JSON.stringify(data));
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
476
618
|
switch (route) {
|
|
477
619
|
case 'modules':
|
|
478
620
|
data = await this.getModulesStatus();
|
|
@@ -710,8 +852,21 @@ class StudioServer {
|
|
|
710
852
|
return { entries, timeline: entries.map((e) => ({ date: e.timestamp, summary: e.summary || e.content?.slice(0, 100) })) };
|
|
711
853
|
}
|
|
712
854
|
async handleAgentChat(req, res, agentId) {
|
|
713
|
-
|
|
714
|
-
|
|
855
|
+
let body;
|
|
856
|
+
try {
|
|
857
|
+
body = JSON.parse(await this.readBody(req));
|
|
858
|
+
}
|
|
859
|
+
catch {
|
|
860
|
+
res.writeHead(400, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
861
|
+
res.end(JSON.stringify({ error: 'Invalid JSON body' }));
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
// Accept both { messages: [...] } and { message: "...", history: [...] }
|
|
865
|
+
let messages = body.messages || [];
|
|
866
|
+
if (body.message) {
|
|
867
|
+
// Frontend sends { message, history }
|
|
868
|
+
messages = [...(body.history || []), { role: 'user', content: body.message }];
|
|
869
|
+
}
|
|
715
870
|
const agent = this.getAgentById(agentId);
|
|
716
871
|
if (agent.error) {
|
|
717
872
|
res.writeHead(404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
@@ -722,8 +877,8 @@ class StudioServer {
|
|
|
722
877
|
agent.messageCount = (agent.messageCount || 0) + 1;
|
|
723
878
|
agent.lastActive = new Date().toISOString();
|
|
724
879
|
agent.updated = new Date().toISOString();
|
|
725
|
-
const
|
|
726
|
-
(0, fs_1.writeFileSync)(
|
|
880
|
+
const agentFilePath = (0, path_1.join)(this.getAgentsDir(), `${agentId}.json`);
|
|
881
|
+
(0, fs_1.writeFileSync)(agentFilePath, JSON.stringify(agent, null, 2));
|
|
727
882
|
// SSE streaming response
|
|
728
883
|
res.writeHead(200, {
|
|
729
884
|
'Content-Type': 'text/event-stream',
|
|
@@ -731,30 +886,31 @@ class StudioServer {
|
|
|
731
886
|
'Connection': 'keep-alive',
|
|
732
887
|
'Access-Control-Allow-Origin': '*',
|
|
733
888
|
});
|
|
734
|
-
const allMsgs = [{ role: 'system', content: agent.systemPrompt }, ...messages];
|
|
735
|
-
const lastMsg = allMsgs[allMsgs.length - 1]?.content || '';
|
|
736
889
|
// Use createProvider directly to call LLM
|
|
737
890
|
try {
|
|
738
891
|
const { createProvider } = require('../providers');
|
|
739
|
-
//
|
|
892
|
+
// Determine provider: agent config > OAD yaml > env > auto
|
|
740
893
|
let providerName = agent.provider || process.env.OPC_LLM_PROVIDER;
|
|
741
894
|
if (!providerName) {
|
|
742
|
-
// Try reading from oad.yaml
|
|
743
895
|
try {
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
896
|
+
for (const fname of ['oad.yaml', 'agent.yaml']) {
|
|
897
|
+
const oadPath = (0, path_1.join)(this.config.agentDir, fname);
|
|
898
|
+
if ((0, fs_1.existsSync)(oadPath)) {
|
|
899
|
+
const yaml = require('js-yaml');
|
|
900
|
+
const oad = yaml.load((0, fs_1.readFileSync)(oadPath, 'utf-8'));
|
|
901
|
+
providerName = oad?.spec?.provider?.default;
|
|
902
|
+
if (providerName)
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
749
905
|
}
|
|
750
906
|
}
|
|
751
907
|
catch { }
|
|
752
908
|
}
|
|
753
|
-
providerName = providerName || '
|
|
909
|
+
providerName = providerName || 'auto';
|
|
754
910
|
const provider = createProvider(providerName, agent.model);
|
|
755
911
|
let fullText = '';
|
|
756
912
|
try {
|
|
757
|
-
for await (const chunk of provider.chatStream(
|
|
913
|
+
for await (const chunk of provider.chatStream(messages, agent.systemPrompt)) {
|
|
758
914
|
const sseData = JSON.stringify({
|
|
759
915
|
choices: [{ delta: { content: chunk }, index: 0 }],
|
|
760
916
|
});
|
|
@@ -764,8 +920,9 @@ class StudioServer {
|
|
|
764
920
|
}
|
|
765
921
|
catch (streamErr) {
|
|
766
922
|
if (!fullText) {
|
|
767
|
-
|
|
768
|
-
|
|
923
|
+
const errData = JSON.stringify({
|
|
924
|
+
choices: [{ delta: { content: `⚠️ LLM Error: ${streamErr.message}` }, index: 0 }],
|
|
925
|
+
});
|
|
769
926
|
res.write(`data: ${errData}\n\n`);
|
|
770
927
|
}
|
|
771
928
|
}
|
|
@@ -773,8 +930,13 @@ class StudioServer {
|
|
|
773
930
|
res.end();
|
|
774
931
|
}
|
|
775
932
|
catch (err) {
|
|
776
|
-
//
|
|
777
|
-
|
|
933
|
+
// Provider creation failed — send error as SSE so frontend can display it
|
|
934
|
+
const errData = JSON.stringify({
|
|
935
|
+
choices: [{ delta: { content: `⚠️ Provider error: ${err.message}\n\nTip: Install Claude CLI (npm i -g @anthropic-ai/claude-code) or set OPENAI_API_KEY.` }, index: 0 }],
|
|
936
|
+
});
|
|
937
|
+
res.write(`data: ${errData}\n\n`);
|
|
938
|
+
res.write('data: [DONE]\n\n');
|
|
939
|
+
res.end();
|
|
778
940
|
}
|
|
779
941
|
}
|
|
780
942
|
sendSimulatedResponse(res, lastMsg, agent) {
|
|
@@ -1317,26 +1317,61 @@
|
|
|
1317
1317
|
|
|
1318
1318
|
agentChatHistory.push({ role: 'user', content: msg });
|
|
1319
1319
|
|
|
1320
|
-
//
|
|
1320
|
+
// Add assistant message placeholder
|
|
1321
|
+
const assistantDiv = document.createElement('div');
|
|
1322
|
+
assistantDiv.className = 'agent-chat-msg assistant';
|
|
1323
|
+
assistantDiv.textContent = '';
|
|
1324
|
+
messagesEl.appendChild(assistantDiv);
|
|
1325
|
+
|
|
1326
|
+
// Send to API and parse SSE stream
|
|
1321
1327
|
try {
|
|
1322
1328
|
const res = await fetch(`/api/agents/${selectedAgentId}/chat`, {
|
|
1323
1329
|
method: 'POST',
|
|
1324
1330
|
headers: { 'Content-Type': 'application/json' },
|
|
1325
|
-
body: JSON.stringify({ message: msg, history: agentChatHistory })
|
|
1331
|
+
body: JSON.stringify({ message: msg, history: agentChatHistory.slice(0, -1) })
|
|
1326
1332
|
});
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1333
|
+
|
|
1334
|
+
if (!res.ok) {
|
|
1335
|
+
let errMsg = `HTTP ${res.status}`;
|
|
1336
|
+
try { const ej = await res.json(); errMsg = ej.error || errMsg; } catch {}
|
|
1337
|
+
throw new Error(errMsg);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
const reader = res.body.getReader();
|
|
1341
|
+
const decoder = new TextDecoder();
|
|
1342
|
+
let fullReply = '';
|
|
1343
|
+
let buffer = '';
|
|
1344
|
+
|
|
1345
|
+
while (true) {
|
|
1346
|
+
const { done, value } = await reader.read();
|
|
1347
|
+
if (done) break;
|
|
1348
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1349
|
+
const lines = buffer.split('\n');
|
|
1350
|
+
buffer = lines.pop() || '';
|
|
1351
|
+
for (const line of lines) {
|
|
1352
|
+
const trimmed = line.trim();
|
|
1353
|
+
if (!trimmed || !trimmed.startsWith('data: ')) continue;
|
|
1354
|
+
const data = trimmed.slice(6);
|
|
1355
|
+
if (data === '[DONE]') continue;
|
|
1356
|
+
try {
|
|
1357
|
+
const parsed = JSON.parse(data);
|
|
1358
|
+
const content = parsed.choices?.[0]?.delta?.content;
|
|
1359
|
+
if (content) {
|
|
1360
|
+
fullReply += content;
|
|
1361
|
+
assistantDiv.textContent = fullReply;
|
|
1362
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1363
|
+
}
|
|
1364
|
+
} catch {}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (!fullReply) {
|
|
1369
|
+
assistantDiv.textContent = '(No response received)';
|
|
1370
|
+
}
|
|
1371
|
+
agentChatHistory.push({ role: 'assistant', content: fullReply });
|
|
1334
1372
|
} catch(e) {
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
errDiv.style.borderColor = 'var(--red)';
|
|
1338
|
-
errDiv.textContent = `⚠️ 发送失败: ${e.message}`;
|
|
1339
|
-
messagesEl.appendChild(errDiv);
|
|
1373
|
+
assistantDiv.style.borderColor = 'var(--red)';
|
|
1374
|
+
assistantDiv.textContent = `⚠️ 发送失败: ${e.message}`;
|
|
1340
1375
|
}
|
|
1341
1376
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1342
1377
|
}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -771,7 +771,7 @@ program
|
|
|
771
771
|
let model: string | undefined;
|
|
772
772
|
let agentName = 'Agent';
|
|
773
773
|
let agentVersion = '1.0.0';
|
|
774
|
-
let providerName = '
|
|
774
|
+
let providerName = 'auto';
|
|
775
775
|
let skillNames: string[] = [];
|
|
776
776
|
|
|
777
777
|
// Try loading SOUL.md and CONTEXT.md for enriched system prompt
|
|
@@ -796,7 +796,7 @@ program
|
|
|
796
796
|
// Prepend SOUL.md and CONTEXT.md to system prompt
|
|
797
797
|
systemPrompt = [soulContent, contextContent, systemPrompt].filter(Boolean).join('\n\n');
|
|
798
798
|
|
|
799
|
-
const provider = createProvider(
|
|
799
|
+
const provider = createProvider(providerName, model);
|
|
800
800
|
const history: { role: 'user' | 'assistant' | 'system'; content: string }[] = [];
|
|
801
801
|
|
|
802
802
|
// Print startup banner
|
package/src/core/runtime.ts
CHANGED
|
@@ -44,6 +44,24 @@ export class AgentRuntime {
|
|
|
44
44
|
private evolveScheduler: any = null;
|
|
45
45
|
|
|
46
46
|
async loadConfig(filePath: string): Promise<OADDocument> {
|
|
47
|
+
const fs = require('fs');
|
|
48
|
+
if (!fs.existsSync(filePath)) {
|
|
49
|
+
// Auto-create a minimal oad.yaml with auto-detect provider
|
|
50
|
+
const yaml = require('js-yaml');
|
|
51
|
+
const defaultOAD = {
|
|
52
|
+
apiVersion: 'opc/v1',
|
|
53
|
+
kind: 'Agent',
|
|
54
|
+
metadata: { name: 'my-agent', version: '1.0.0', description: 'OPC Agent' },
|
|
55
|
+
spec: {
|
|
56
|
+
model: 'auto',
|
|
57
|
+
provider: { default: 'auto' },
|
|
58
|
+
systemPrompt: 'You are a helpful AI assistant.',
|
|
59
|
+
channels: [{ type: 'web', config: { port: 3000 } }],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
fs.writeFileSync(filePath, yaml.dump(defaultOAD, { lineWidth: 120 }));
|
|
63
|
+
this.logger.info('Created default oad.yaml (no config file found)');
|
|
64
|
+
}
|
|
47
65
|
this.config = loadOAD(filePath);
|
|
48
66
|
this.logger.info('Config loaded', { name: this.config.metadata.name });
|
|
49
67
|
return this.config;
|
package/src/providers/index.ts
CHANGED
|
@@ -330,7 +330,17 @@ class ClaudeCLIProvider implements LLMProvider {
|
|
|
330
330
|
private model: string;
|
|
331
331
|
|
|
332
332
|
constructor(model?: string) {
|
|
333
|
-
|
|
333
|
+
// Claude CLI uses short model names; don't pass API-style model names
|
|
334
|
+
// Let Claude CLI use its default model unless explicitly set to a known CLI model
|
|
335
|
+
const cliModels = ['sonnet', 'opus', 'haiku', 'claude-sonnet-4-20250514', 'claude-opus-4-20250514'];
|
|
336
|
+
if (model && !cliModels.includes(model)) {
|
|
337
|
+
// Map common patterns
|
|
338
|
+
if (model.includes('opus')) this.model = 'opus';
|
|
339
|
+
else if (model.includes('haiku')) this.model = 'haiku';
|
|
340
|
+
else this.model = ''; // let CLI choose default
|
|
341
|
+
} else {
|
|
342
|
+
this.model = model || '';
|
|
343
|
+
}
|
|
334
344
|
}
|
|
335
345
|
|
|
336
346
|
async chat(messages: Message[], systemPrompt?: string, options?: ChatOptions): Promise<string> {
|
|
@@ -349,7 +359,7 @@ class ClaudeCLIProvider implements LLMProvider {
|
|
|
349
359
|
prompt += buildToolPrompt(options.tools);
|
|
350
360
|
}
|
|
351
361
|
|
|
352
|
-
const args = ['-p'
|
|
362
|
+
const args = ['-p'];
|
|
353
363
|
// Write system prompt to temp file to avoid shell escaping issues
|
|
354
364
|
let tmpFile: string | undefined;
|
|
355
365
|
if (systemPrompt) {
|
|
@@ -410,7 +420,7 @@ class ClaudeCLIProvider implements LLMProvider {
|
|
|
410
420
|
}
|
|
411
421
|
|
|
412
422
|
async *chatStream(messages: Message[], systemPrompt?: string): AsyncIterable<string> {
|
|
413
|
-
const args = ['-p', '--
|
|
423
|
+
const args = ['-p', '--verbose', '--output-format', 'stream-json'];
|
|
414
424
|
if (this.model) {
|
|
415
425
|
args.push('--model', this.model);
|
|
416
426
|
}
|
|
@@ -451,14 +461,24 @@ class ClaudeCLIProvider implements LLMProvider {
|
|
|
451
461
|
if (!trimmed) continue;
|
|
452
462
|
try {
|
|
453
463
|
const event = JSON.parse(trimmed);
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
464
|
+
// Claude CLI stream-json format:
|
|
465
|
+
// {"type":"assistant","message":{"content":[{"type":"text","text":"..."}]}}
|
|
466
|
+
if (event.type === 'assistant' && event.message?.content) {
|
|
467
|
+
for (const block of event.message.content) {
|
|
468
|
+
if (block.type === 'text' && block.text) {
|
|
469
|
+
const newContent = block.text;
|
|
470
|
+
if (newContent.length > lastContent.length) {
|
|
471
|
+
yield newContent.slice(lastContent.length);
|
|
472
|
+
lastContent = newContent;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
460
475
|
}
|
|
461
476
|
}
|
|
477
|
+
// Also handle result type for final content
|
|
478
|
+
if (event.type === 'result' && event.result) {
|
|
479
|
+
const remaining = event.result.slice(lastContent.length);
|
|
480
|
+
if (remaining) yield remaining;
|
|
481
|
+
}
|
|
462
482
|
// Handle assistant message with content array
|
|
463
483
|
if (event.type === 'assistant' && event.message?.content) {
|
|
464
484
|
for (const block of event.message.content) {
|
|
@@ -513,7 +533,72 @@ class ClaudeCLIProvider implements LLMProvider {
|
|
|
513
533
|
}
|
|
514
534
|
}
|
|
515
535
|
|
|
516
|
-
|
|
536
|
+
import { execSync } from 'child_process';
|
|
537
|
+
|
|
538
|
+
function detectClaudeCLI(): boolean {
|
|
539
|
+
try {
|
|
540
|
+
execSync('claude --version', { stdio: 'pipe', timeout: 3000 });
|
|
541
|
+
return true;
|
|
542
|
+
} catch { return false; }
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function detectOllama(): boolean {
|
|
546
|
+
try {
|
|
547
|
+
// Use node http instead of curl for Windows compatibility
|
|
548
|
+
const { execSync: es } = require('child_process');
|
|
549
|
+
// Quick check: try to connect to Ollama API via node
|
|
550
|
+
const result = es(
|
|
551
|
+
`node -e "const h=require('http');const r=h.get('http://localhost:11434/api/tags',{timeout:2000},s=>{let d='';s.on('data',c=>d+=c);s.on('end',()=>{process.stdout.write(d.includes('models')?'1':'0')})});r.on('error',()=>process.stdout.write('0'));r.on('timeout',()=>{r.destroy();process.stdout.write('0')})"`,
|
|
552
|
+
{ stdio: 'pipe', timeout: 5000 }
|
|
553
|
+
);
|
|
554
|
+
return result.toString().trim() === '1';
|
|
555
|
+
} catch { return false; }
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function detectApiKeys(): { provider: string; key: string; baseUrl?: string } | null {
|
|
559
|
+
if (process.env.ANTHROPIC_API_KEY) return { provider: 'anthropic', key: process.env.ANTHROPIC_API_KEY };
|
|
560
|
+
if (process.env.OPENAI_API_KEY && process.env.OPENAI_API_KEY !== 'your-api-key-here') return { provider: 'openai', key: process.env.OPENAI_API_KEY };
|
|
561
|
+
if (process.env.DEEPSEEK_API_KEY) return { provider: 'deepseek', key: process.env.DEEPSEEK_API_KEY, baseUrl: 'https://api.deepseek.com/v1' };
|
|
562
|
+
if (process.env.DASHSCOPE_API_KEY) return { provider: 'qwen', key: process.env.DASHSCOPE_API_KEY, baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1' };
|
|
563
|
+
if (process.env.GEMINI_API_KEY) return { provider: 'gemini', key: process.env.GEMINI_API_KEY };
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function autoDetectProvider(): { name: string; model?: string; baseUrl?: string; apiKey?: string } {
|
|
568
|
+
// 1. Claude CLI (zero config, Claude Max/Pro subscription)
|
|
569
|
+
if (detectClaudeCLI()) {
|
|
570
|
+
return { name: 'claude-cli' };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 2. API keys from environment
|
|
574
|
+
const apiKey = detectApiKeys();
|
|
575
|
+
if (apiKey) {
|
|
576
|
+
return { name: apiKey.provider, apiKey: apiKey.key, baseUrl: apiKey.baseUrl };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// 3. Ollama (local, free)
|
|
580
|
+
if (detectOllama()) {
|
|
581
|
+
return { name: 'ollama', model: 'qwen2.5:7b' };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// 4. Nothing found
|
|
585
|
+
return { name: 'none' };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function createProvider(name: string = 'auto', model?: string, baseUrl?: string, apiKey?: string): LLMProvider {
|
|
589
|
+
// Auto-detect if name is 'auto' or default openai with no real key
|
|
590
|
+
const needsAutoDetect = name === 'auto' || (name === 'openai' && !apiKey && (!process.env.OPENAI_API_KEY || process.env.OPENAI_API_KEY === 'your-api-key-here'));
|
|
591
|
+
if (needsAutoDetect) {
|
|
592
|
+
const detected = autoDetectProvider();
|
|
593
|
+
if (detected.name !== 'none') {
|
|
594
|
+
console.log(`[provider] Auto-detected: ${detected.name}`);
|
|
595
|
+
name = detected.name;
|
|
596
|
+
model = model || detected.model;
|
|
597
|
+
baseUrl = baseUrl || detected.baseUrl;
|
|
598
|
+
apiKey = apiKey || detected.apiKey;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
517
602
|
const finalModel = model || process.env.OPC_LLM_MODEL || 'gpt-4o-mini';
|
|
518
603
|
|
|
519
604
|
// Claude CLI mode: use local claude command (Claude Max/Pro subscription)
|