opc-agent 4.0.44 → 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 CHANGED
@@ -680,7 +680,7 @@ program
680
680
  let model;
681
681
  let agentName = 'Agent';
682
682
  let agentVersion = '1.0.0';
683
- let providerName = 'openai';
683
+ let providerName = 'auto';
684
684
  let skillNames = [];
685
685
  // Try loading SOUL.md and CONTEXT.md for enriched system prompt
686
686
  const soulPath = path.resolve('SOUL.md');
@@ -708,7 +708,7 @@ program
708
708
  }
709
709
  // Prepend SOUL.md and CONTEXT.md to system prompt
710
710
  systemPrompt = [soulContent, contextContent, systemPrompt].filter(Boolean).join('\n\n');
711
- const provider = (0, providers_1.createProvider)('openai', model);
711
+ const provider = (0, providers_1.createProvider)(providerName, model);
712
712
  const history = [];
713
713
  // Print startup banner
714
714
  const bannerLines = [
@@ -73,6 +73,24 @@ class AgentRuntime {
73
73
  agentBrain = null;
74
74
  evolveScheduler = null;
75
75
  async loadConfig(filePath) {
76
+ const fs = require('fs');
77
+ if (!fs.existsSync(filePath)) {
78
+ // Auto-create a minimal oad.yaml with auto-detect provider
79
+ const yaml = require('js-yaml');
80
+ const defaultOAD = {
81
+ apiVersion: 'opc/v1',
82
+ kind: 'Agent',
83
+ metadata: { name: 'my-agent', version: '1.0.0', description: 'OPC Agent' },
84
+ spec: {
85
+ model: 'auto',
86
+ provider: { default: 'auto' },
87
+ systemPrompt: 'You are a helpful AI assistant.',
88
+ channels: [{ type: 'web', config: { port: 3000 } }],
89
+ },
90
+ };
91
+ fs.writeFileSync(filePath, yaml.dump(defaultOAD, { lineWidth: 120 }));
92
+ this.logger.info('Created default oad.yaml (no config file found)');
93
+ }
76
94
  this.config = (0, config_1.loadOAD)(filePath);
77
95
  this.logger.info('Config loaded', { name: this.config.metadata.name });
78
96
  return this.config;
@@ -328,7 +328,21 @@ class ClaudeCLIProvider {
328
328
  name = 'claude-cli';
329
329
  model;
330
330
  constructor(model) {
331
- this.model = model || 'sonnet';
331
+ // Claude CLI uses short model names; don't pass API-style model names
332
+ // Let Claude CLI use its default model unless explicitly set to a known CLI model
333
+ const cliModels = ['sonnet', 'opus', 'haiku', 'claude-sonnet-4-20250514', 'claude-opus-4-20250514'];
334
+ if (model && !cliModels.includes(model)) {
335
+ // Map common patterns
336
+ if (model.includes('opus'))
337
+ this.model = 'opus';
338
+ else if (model.includes('haiku'))
339
+ this.model = 'haiku';
340
+ else
341
+ this.model = ''; // let CLI choose default
342
+ }
343
+ else {
344
+ this.model = model || '';
345
+ }
332
346
  }
333
347
  async chat(messages, systemPrompt, options) {
334
348
  const { writeFileSync, unlinkSync, mkdtempSync } = await Promise.resolve().then(() => __importStar(require('fs')));
@@ -343,7 +357,7 @@ class ClaudeCLIProvider {
343
357
  if (options?.tools && options.tools.length > 0) {
344
358
  prompt += buildToolPrompt(options.tools);
345
359
  }
346
- const args = ['-p', '--no-project-context'];
360
+ const args = ['-p'];
347
361
  // Write system prompt to temp file to avoid shell escaping issues
348
362
  let tmpFile;
349
363
  if (systemPrompt) {
@@ -407,7 +421,7 @@ class ClaudeCLIProvider {
407
421
  }
408
422
  }
409
423
  async *chatStream(messages, systemPrompt) {
410
- const args = ['-p', '--no-project-context', '--output-format', 'stream-json', '--include-partial-messages'];
424
+ const args = ['-p', '--verbose', '--output-format', 'stream-json'];
411
425
  if (this.model) {
412
426
  args.push('--model', this.model);
413
427
  }
@@ -442,14 +456,25 @@ class ClaudeCLIProvider {
442
456
  continue;
443
457
  try {
444
458
  const event = JSON.parse(trimmed);
445
- // Handle partial message chunks (content_block_delta style)
446
- if (event.type === 'content' && event.content) {
447
- const newContent = event.content;
448
- if (newContent.length > lastContent.length) {
449
- yield newContent.slice(lastContent.length);
450
- lastContent = newContent;
459
+ // Claude CLI stream-json format:
460
+ // {"type":"assistant","message":{"content":[{"type":"text","text":"..."}]}}
461
+ if (event.type === 'assistant' && event.message?.content) {
462
+ for (const block of event.message.content) {
463
+ if (block.type === 'text' && block.text) {
464
+ const newContent = block.text;
465
+ if (newContent.length > lastContent.length) {
466
+ yield newContent.slice(lastContent.length);
467
+ lastContent = newContent;
468
+ }
469
+ }
451
470
  }
452
471
  }
472
+ // Also handle result type for final content
473
+ if (event.type === 'result' && event.result) {
474
+ const remaining = event.result.slice(lastContent.length);
475
+ if (remaining)
476
+ yield remaining;
477
+ }
453
478
  // Handle assistant message with content array
454
479
  if (event.type === 'assistant' && event.message?.content) {
455
480
  for (const block of event.message.content) {
@@ -520,10 +545,11 @@ function detectClaudeCLI() {
520
545
  }
521
546
  function detectOllama() {
522
547
  try {
523
- (0, child_process_1.execSync)('ollama --version', { stdio: 'pipe', timeout: 3000 });
524
- // Also check if server is running
525
- const res = (0, child_process_1.execSync)('curl -s http://localhost:11434/api/tags', { stdio: 'pipe', timeout: 3000 });
526
- return res.toString().includes('models');
548
+ // Use node http instead of curl for Windows compatibility
549
+ const { execSync: es } = require('child_process');
550
+ // Quick check: try to connect to Ollama API via node
551
+ const result = es(`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')})"`, { stdio: 'pipe', timeout: 5000 });
552
+ return result.toString().trim() === '1';
527
553
  }
528
554
  catch {
529
555
  return false;
@@ -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
- const body = JSON.parse(await this.readBody(req));
714
- const { messages = [] } = body;
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 filePath = (0, path_1.join)(this.getAgentsDir(), `${agentId}.json`);
726
- (0, fs_1.writeFileSync)(filePath, JSON.stringify(agent, null, 2));
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
- // Read OAD config for provider info
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 oadPath = (0, path_1.join)(this.config.agentDir, 'oad.yaml');
745
- if ((0, fs_1.existsSync)(oadPath)) {
746
- const yaml = require('js-yaml');
747
- const oad = yaml.load((0, fs_1.readFileSync)(oadPath, 'utf-8'));
748
- providerName = oad?.spec?.provider?.default;
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 || 'openai';
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(allMsgs, agent.systemPrompt)) {
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
- // No content streamed yet, send error
768
- const errData = JSON.stringify({ error: streamErr.message });
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
- // Fallback: try simulated response
777
- this.sendSimulatedResponse(res, lastMsg, agent);
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
- // Send to API
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
- const data = await res.json();
1328
- const reply = data.reply || data.message || data.content || JSON.stringify(data);
1329
- const assistantDiv = document.createElement('div');
1330
- assistantDiv.className = 'agent-chat-msg assistant';
1331
- assistantDiv.textContent = reply;
1332
- messagesEl.appendChild(assistantDiv);
1333
- agentChatHistory.push({ role: 'assistant', content: reply });
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
- const errDiv = document.createElement('div');
1336
- errDiv.className = 'agent-chat-msg assistant';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opc-agent",
3
- "version": "4.0.44",
3
+ "version": "4.1.0",
4
4
  "description": "Open Agent Framework — Build, test, and run AI Agents for business workstations",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
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 = 'openai';
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('openai', model);
799
+ const provider = createProvider(providerName, model);
800
800
  const history: { role: 'user' | 'assistant' | 'system'; content: string }[] = [];
801
801
 
802
802
  // Print startup banner
@@ -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;
@@ -330,7 +330,17 @@ class ClaudeCLIProvider implements LLMProvider {
330
330
  private model: string;
331
331
 
332
332
  constructor(model?: string) {
333
- this.model = model || 'sonnet';
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', '--no-project-context'];
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', '--no-project-context', '--output-format', 'stream-json', '--include-partial-messages'];
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
- // Handle partial message chunks (content_block_delta style)
455
- if (event.type === 'content' && event.content) {
456
- const newContent = event.content;
457
- if (newContent.length > lastContent.length) {
458
- yield newContent.slice(lastContent.length);
459
- lastContent = newContent;
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) {
@@ -524,10 +544,14 @@ function detectClaudeCLI(): boolean {
524
544
 
525
545
  function detectOllama(): boolean {
526
546
  try {
527
- execSync('ollama --version', { stdio: 'pipe', timeout: 3000 });
528
- // Also check if server is running
529
- const res = execSync('curl -s http://localhost:11434/api/tags', { stdio: 'pipe', timeout: 3000 });
530
- return res.toString().includes('models');
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';
531
555
  } catch { return false; }
532
556
  }
533
557
 
@@ -181,12 +181,91 @@ class StudioServer {
181
181
  const industry = url.searchParams.get('industry') || '';
182
182
  const search = url.searchParams.get('q') || '';
183
183
  data = this.getTemplates(industry, search);
184
+ // Merge with real workstation templates
185
+ try {
186
+ const ws = require('agent-workstation');
187
+ const categories = ws.getCategories();
188
+ const wsTemplates: any[] = [];
189
+ for (const cat of categories) {
190
+ for (const roleName of cat.roles) {
191
+ const role = ws.getRole(cat.name, roleName);
192
+ if (!role) continue;
193
+ let oad: any = {};
194
+ try {
195
+ if (role.files?.['oad.yaml']) {
196
+ const yaml = require('js-yaml');
197
+ oad = yaml.load(role.files['oad.yaml']) || {};
198
+ }
199
+ } catch {}
200
+ const tpl = {
201
+ id: `ws-${cat.name}-${roleName}`,
202
+ name: oad.name || roleName.replace(/-/g, ' ').replace(/\b\w/g, (c: string) => c.toUpperCase()),
203
+ nameZh: oad.nameZh || '',
204
+ icon: oad.icon || '🤖',
205
+ description: oad.description || '',
206
+ descriptionZh: oad.descriptionZh || '',
207
+ industry: cat.name,
208
+ industryZh: cat.name,
209
+ tags: [cat.name, 'workstation'],
210
+ suggestedModel: 'auto',
211
+ systemPrompt: oad.systemPrompt || role.files?.['brain-seed.md'] || '',
212
+ source: 'workstation',
213
+ ego: oad.ego || null,
214
+ mission: oad.mission || null,
215
+ skills: oad.skills || [],
216
+ };
217
+ if (!search || tpl.name.toLowerCase().includes(search.toLowerCase()) || tpl.nameZh.includes(search)) {
218
+ if (!industry || tpl.industry === industry) {
219
+ wsTemplates.push(tpl);
220
+ }
221
+ }
222
+ }
223
+ }
224
+ data.templates = [...data.templates, ...wsTemplates];
225
+ // Add workstation industries to list
226
+ const existingIds = new Set(data.industries.map((i: any) => i.id));
227
+ for (const cat of categories) {
228
+ if (!existingIds.has(cat.name)) {
229
+ data.industries.push({ id: cat.name, name: cat.name, nameZh: cat.name });
230
+ }
231
+ }
232
+ } catch (wsErr: any) {
233
+ // workstation not available, use built-in templates only
234
+ }
184
235
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
185
236
  res.end(JSON.stringify(data));
186
237
  return;
187
238
  }
188
239
  if (route.match(/^templates\/[^/]+$/) && req.method === 'GET') {
189
240
  const tplId = route.split('/')[1];
241
+ // Check workstation first
242
+ if (tplId.startsWith('ws-')) {
243
+ const parts = tplId.replace('ws-', '').split('-');
244
+ const catName = parts[0];
245
+ const roleName = parts.slice(1).join('-');
246
+ try {
247
+ const ws = require('agent-workstation');
248
+ const role = ws.getRole(catName, roleName);
249
+ if (role) {
250
+ let oad: any = {};
251
+ try {
252
+ if (role.files?.['oad.yaml']) {
253
+ const yaml = require('js-yaml');
254
+ oad = yaml.load(role.files['oad.yaml']) || {};
255
+ }
256
+ } catch {}
257
+ data = {
258
+ id: tplId, name: oad.name || roleName, source: 'workstation',
259
+ category: catName, role: roleName, files: role.files,
260
+ ego: oad.ego, mission: oad.mission, skills: oad.skills,
261
+ systemPrompt: oad.systemPrompt || role.files?.['brain-seed.md'] || '',
262
+ };
263
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
264
+ res.end(JSON.stringify(data));
265
+ return;
266
+ }
267
+ } catch {}
268
+ }
190
269
  data = this.getTemplateById(tplId);
191
270
  res.writeHead(data.error ? 404 : 200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
192
271
  res.end(JSON.stringify(data));
@@ -494,6 +573,65 @@ class StudioServer {
494
573
  return;
495
574
  }
496
575
 
576
+ // === Global config API (reads/writes ~/.opc/config.json) ===
577
+ if (route === 'config' && req.method === 'GET') {
578
+ data = loadSettingsConfig();
579
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
580
+ res.end(JSON.stringify(data));
581
+ return;
582
+ }
583
+ if (route === 'config' && req.method === 'PUT') {
584
+ const body = JSON.parse(await this.readBody(req));
585
+ const cfg = loadSettingsConfig();
586
+ Object.assign(cfg, body);
587
+ saveSettingsConfig(cfg);
588
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
589
+ res.end(JSON.stringify({ success: true, config: cfg }));
590
+ return;
591
+ }
592
+
593
+ // === Models API (real agentkits integration) ===
594
+ if (route === 'models' && req.method === 'GET') {
595
+ try {
596
+ const ak = await import('agentkits');
597
+ const providers = ak.listLLMProviders();
598
+ data = { providers };
599
+ } catch (e: any) {
600
+ data = { providers: [], error: 'agentkits not available: ' + e.message };
601
+ }
602
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
603
+ res.end(JSON.stringify(data));
604
+ return;
605
+ }
606
+
607
+ // === Memory stats API (real deepbrain integration) ===
608
+ if (route === 'memory/stats' && req.method === 'GET') {
609
+ try {
610
+ const { Brain } = require('deepbrain');
611
+ const oad = this.loadOAD();
612
+ const dbPath = oad?.spec?.memory?.longTerm?.database || './data/brain.db';
613
+ const brain = new Brain({ database: dbPath, embedding_provider: 'ollama' });
614
+ await brain.connect();
615
+ const stats = await brain.stats();
616
+ await brain.disconnect();
617
+ data = { connected: true, ...stats };
618
+ } catch {
619
+ data = { connected: false, pages: 0, chunks: 0, error: 'DeepBrain not installed or not configured. Install with: npm i deepbrain' };
620
+ }
621
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
622
+ res.end(JSON.stringify(data));
623
+ return;
624
+ }
625
+ if (route.match(/^memory\/[^/]+$/) && req.method === 'GET') {
626
+ const agentId = route.split('/')[1];
627
+ if (agentId !== 'stats' && agentId !== 'list' && agentId !== 'search') {
628
+ data = this.getAgentMemory(agentId);
629
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
630
+ res.end(JSON.stringify(data));
631
+ return;
632
+ }
633
+ }
634
+
497
635
  switch (route) {
498
636
  case 'modules':
499
637
  data = await this.getModulesStatus();
@@ -708,8 +846,22 @@ class StudioServer {
708
846
  }
709
847
 
710
848
  private async handleAgentChat(req: IncomingMessage, res: ServerResponse, agentId: string): Promise<void> {
711
- const body = JSON.parse(await this.readBody(req));
712
- const { messages = [] } = body;
849
+ let body: any;
850
+ try {
851
+ body = JSON.parse(await this.readBody(req));
852
+ } catch {
853
+ res.writeHead(400, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
854
+ res.end(JSON.stringify({ error: 'Invalid JSON body' }));
855
+ return;
856
+ }
857
+
858
+ // Accept both { messages: [...] } and { message: "...", history: [...] }
859
+ let messages: any[] = body.messages || [];
860
+ if (body.message) {
861
+ // Frontend sends { message, history }
862
+ messages = [...(body.history || []), { role: 'user', content: body.message }];
863
+ }
864
+
713
865
  const agent = this.getAgentById(agentId);
714
866
  if (agent.error) {
715
867
  res.writeHead(404, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
@@ -721,8 +873,8 @@ class StudioServer {
721
873
  agent.messageCount = (agent.messageCount || 0) + 1;
722
874
  agent.lastActive = new Date().toISOString();
723
875
  agent.updated = new Date().toISOString();
724
- const filePath = join(this.getAgentsDir(), `${agentId}.json`);
725
- writeFileSync(filePath, JSON.stringify(agent, null, 2));
876
+ const agentFilePath = join(this.getAgentsDir(), `${agentId}.json`);
877
+ writeFileSync(agentFilePath, JSON.stringify(agent, null, 2));
726
878
 
727
879
  // SSE streaming response
728
880
  res.writeHead(200, {
@@ -732,31 +884,30 @@ class StudioServer {
732
884
  'Access-Control-Allow-Origin': '*',
733
885
  });
734
886
 
735
- const allMsgs = [{ role: 'system', content: agent.systemPrompt }, ...messages];
736
- const lastMsg = allMsgs[allMsgs.length - 1]?.content || '';
737
-
738
887
  // Use createProvider directly to call LLM
739
888
  try {
740
889
  const { createProvider } = require('../providers');
741
- // Read OAD config for provider info
890
+ // Determine provider: agent config > OAD yaml > env > auto
742
891
  let providerName = agent.provider || process.env.OPC_LLM_PROVIDER;
743
892
  if (!providerName) {
744
- // Try reading from oad.yaml
745
893
  try {
746
- const oadPath = join(this.config.agentDir, 'oad.yaml');
747
- if (existsSync(oadPath)) {
748
- const yaml = require('js-yaml');
749
- const oad = yaml.load(readFileSync(oadPath, 'utf-8'));
750
- providerName = oad?.spec?.provider?.default;
894
+ for (const fname of ['oad.yaml', 'agent.yaml']) {
895
+ const oadPath = join(this.config.agentDir, fname);
896
+ if (existsSync(oadPath)) {
897
+ const yaml = require('js-yaml');
898
+ const oad = yaml.load(readFileSync(oadPath, 'utf-8'));
899
+ providerName = oad?.spec?.provider?.default;
900
+ if (providerName) break;
901
+ }
751
902
  }
752
903
  } catch {}
753
904
  }
754
- providerName = providerName || 'openai';
905
+ providerName = providerName || 'auto';
755
906
  const provider = createProvider(providerName, agent.model);
756
-
907
+
757
908
  let fullText = '';
758
909
  try {
759
- for await (const chunk of provider.chatStream(allMsgs, agent.systemPrompt)) {
910
+ for await (const chunk of provider.chatStream(messages, agent.systemPrompt)) {
760
911
  const sseData = JSON.stringify({
761
912
  choices: [{ delta: { content: chunk }, index: 0 }],
762
913
  });
@@ -765,16 +916,22 @@ class StudioServer {
765
916
  }
766
917
  } catch (streamErr: any) {
767
918
  if (!fullText) {
768
- // No content streamed yet, send error
769
- const errData = JSON.stringify({ error: streamErr.message });
919
+ const errData = JSON.stringify({
920
+ choices: [{ delta: { content: `⚠️ LLM Error: ${streamErr.message}` }, index: 0 }],
921
+ });
770
922
  res.write(`data: ${errData}\n\n`);
771
923
  }
772
924
  }
773
925
  res.write('data: [DONE]\n\n');
774
926
  res.end();
775
927
  } catch (err: any) {
776
- // Fallback: try simulated response
777
- this.sendSimulatedResponse(res, lastMsg, agent);
928
+ // Provider creation failed — send error as SSE so frontend can display it
929
+ const errData = JSON.stringify({
930
+ 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 }],
931
+ });
932
+ res.write(`data: ${errData}\n\n`);
933
+ res.write('data: [DONE]\n\n');
934
+ res.end();
778
935
  }
779
936
  }
780
937
 
@@ -1317,26 +1317,61 @@
1317
1317
 
1318
1318
  agentChatHistory.push({ role: 'user', content: msg });
1319
1319
 
1320
- // Send to API
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
- const data = await res.json();
1328
- const reply = data.reply || data.message || data.content || JSON.stringify(data);
1329
- const assistantDiv = document.createElement('div');
1330
- assistantDiv.className = 'agent-chat-msg assistant';
1331
- assistantDiv.textContent = reply;
1332
- messagesEl.appendChild(assistantDiv);
1333
- agentChatHistory.push({ role: 'assistant', content: reply });
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
- const errDiv = document.createElement('div');
1336
- errDiv.className = 'agent-chat-msg assistant';
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
  }