opc-agent 2.0.0 → 2.0.1
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/channels/email.d.ts +32 -26
- package/dist/channels/email.js +239 -62
- package/dist/channels/feishu.d.ts +21 -6
- package/dist/channels/feishu.js +225 -126
- package/dist/channels/websocket.d.ts +46 -3
- package/dist/channels/websocket.js +306 -37
- package/dist/channels/wechat.d.ts +33 -13
- package/dist/channels/wechat.js +229 -42
- package/dist/cli.js +712 -11
- package/dist/core/a2a.d.ts +17 -0
- package/dist/core/a2a.js +43 -1
- package/dist/core/agent.d.ts +16 -0
- package/dist/core/agent.js +108 -0
- package/dist/core/runtime.d.ts +6 -0
- package/dist/core/runtime.js +161 -2
- package/dist/core/sandbox.d.ts +26 -0
- package/dist/core/sandbox.js +117 -0
- package/dist/core/workflow-graph.d.ts +93 -0
- package/dist/core/workflow-graph.js +247 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +183 -0
- package/dist/eval/index.d.ts +65 -0
- package/dist/eval/index.js +191 -0
- package/dist/index.d.ts +30 -6
- package/dist/index.js +60 -4
- package/dist/plugins/content-filter.d.ts +7 -0
- package/dist/plugins/content-filter.js +25 -0
- package/dist/plugins/index.d.ts +42 -0
- package/dist/plugins/index.js +108 -2
- package/dist/plugins/logger.d.ts +6 -0
- package/dist/plugins/logger.js +20 -0
- package/dist/plugins/rate-limiter.d.ts +7 -0
- package/dist/plugins/rate-limiter.js +35 -0
- package/dist/protocols/a2a/client.d.ts +25 -0
- package/dist/protocols/a2a/client.js +115 -0
- package/dist/protocols/a2a/index.d.ts +6 -0
- package/dist/protocols/a2a/index.js +12 -0
- package/dist/protocols/a2a/server.d.ts +41 -0
- package/dist/protocols/a2a/server.js +295 -0
- package/dist/protocols/a2a/types.d.ts +91 -0
- package/dist/protocols/a2a/types.js +15 -0
- package/dist/protocols/a2a/utils.d.ts +6 -0
- package/dist/protocols/a2a/utils.js +47 -0
- package/dist/protocols/agui/client.d.ts +10 -0
- package/dist/protocols/agui/client.js +75 -0
- package/dist/protocols/agui/index.d.ts +4 -0
- package/dist/protocols/agui/index.js +25 -0
- package/dist/protocols/agui/server.d.ts +37 -0
- package/dist/protocols/agui/server.js +191 -0
- package/dist/protocols/agui/types.d.ts +107 -0
- package/dist/protocols/agui/types.js +17 -0
- package/dist/protocols/index.d.ts +2 -0
- package/dist/protocols/index.js +19 -0
- package/dist/protocols/mcp/agent-tools.d.ts +11 -0
- package/dist/protocols/mcp/agent-tools.js +129 -0
- package/dist/protocols/mcp/index.d.ts +5 -0
- package/dist/protocols/mcp/index.js +11 -0
- package/dist/protocols/mcp/server.d.ts +31 -0
- package/dist/protocols/mcp/server.js +248 -0
- package/dist/protocols/mcp/types.d.ts +92 -0
- package/dist/protocols/mcp/types.js +17 -0
- package/dist/publish/index.d.ts +45 -0
- package/dist/publish/index.js +350 -0
- package/dist/schema/oad.d.ts +682 -65
- package/dist/schema/oad.js +36 -3
- package/dist/security/approval.d.ts +36 -0
- package/dist/security/approval.js +113 -0
- package/dist/security/index.d.ts +4 -0
- package/dist/security/index.js +8 -0
- package/dist/security/keys.d.ts +16 -0
- package/dist/security/keys.js +117 -0
- package/dist/studio/server.d.ts +63 -0
- package/dist/studio/server.js +625 -0
- package/dist/studio-ui/index.html +662 -0
- package/dist/telemetry/index.d.ts +93 -0
- package/dist/telemetry/index.js +285 -0
- package/package.json +5 -3
- package/scripts/install.ps1 +31 -0
- package/scripts/install.sh +40 -0
- package/src/channels/email.ts +351 -177
- package/src/channels/feishu.ts +349 -236
- package/src/channels/websocket.ts +399 -87
- package/src/channels/wechat.ts +329 -149
- package/src/cli.ts +783 -12
- package/src/core/a2a.ts +60 -0
- package/src/core/agent.ts +125 -0
- package/src/core/runtime.ts +127 -0
- package/src/core/sandbox.ts +143 -0
- package/src/core/workflow-graph.ts +365 -0
- package/src/doctor.ts +156 -0
- package/src/eval/index.ts +211 -0
- package/src/eval/suites/basic.json +16 -0
- package/src/eval/suites/memory.json +12 -0
- package/src/eval/suites/safety.json +14 -0
- package/src/index.ts +54 -6
- package/src/plugins/content-filter.ts +23 -0
- package/src/plugins/index.ts +133 -2
- package/src/plugins/logger.ts +18 -0
- package/src/plugins/rate-limiter.ts +38 -0
- package/src/protocols/a2a/client.ts +132 -0
- package/src/protocols/a2a/index.ts +8 -0
- package/src/protocols/a2a/server.ts +333 -0
- package/src/protocols/a2a/types.ts +88 -0
- package/src/protocols/a2a/utils.ts +50 -0
- package/src/protocols/agui/client.ts +83 -0
- package/src/protocols/agui/index.ts +4 -0
- package/src/protocols/agui/server.ts +218 -0
- package/src/protocols/agui/types.ts +153 -0
- package/src/protocols/index.ts +2 -0
- package/src/protocols/mcp/agent-tools.ts +134 -0
- package/src/protocols/mcp/index.ts +8 -0
- package/src/protocols/mcp/server.ts +262 -0
- package/src/protocols/mcp/types.ts +69 -0
- package/src/publish/index.ts +376 -0
- package/src/schema/oad.ts +39 -2
- package/src/security/approval.ts +131 -0
- package/src/security/index.ts +3 -0
- package/src/security/keys.ts +87 -0
- package/src/studio/server.ts +629 -0
- package/src/studio-ui/index.html +662 -0
- package/src/telemetry/index.ts +324 -0
- package/src/types/agent-workstation.d.ts +2 -0
- package/tests/a2a-protocol.test.ts +285 -0
- package/tests/agui-protocol.test.ts +246 -0
- package/tests/channels/discord.test.ts +79 -0
- package/tests/channels/email.test.ts +148 -0
- package/tests/channels/feishu.test.ts +123 -0
- package/tests/channels/telegram.test.ts +129 -0
- package/tests/channels/websocket.test.ts +53 -0
- package/tests/channels/wechat.test.ts +170 -0
- package/tests/chat-cli.test.ts +160 -0
- package/tests/daemon.test.ts +135 -0
- package/tests/deepbrain-wire.test.ts +234 -0
- package/tests/doctor.test.ts +38 -0
- package/tests/eval.test.ts +173 -0
- package/tests/init-role.test.ts +124 -0
- package/tests/mcp-client.test.ts +92 -0
- package/tests/mcp-server.test.ts +178 -0
- package/tests/plugin-a2a-enhanced.test.ts +230 -0
- package/tests/publish.test.ts +231 -0
- package/tests/scheduler.test.ts +200 -0
- package/tests/security-enhanced.test.ts +233 -0
- package/tests/skill-learner.test.ts +161 -0
- package/tests/studio.test.ts +229 -0
- package/tests/subagent.test.ts +63 -0
- package/tests/telemetry.test.ts +186 -0
- package/tests/tools/builtin-extended.test.ts +138 -0
- package/tests/workflow-graph.test.ts +279 -0
- package/tutorial/customer-service-agent/README.md +612 -0
- package/tutorial/customer-service-agent/SOUL.md +26 -0
- package/tutorial/customer-service-agent/agent.yaml +63 -0
- package/tutorial/customer-service-agent/package.json +19 -0
- package/tutorial/customer-service-agent/src/index.ts +69 -0
- package/tutorial/customer-service-agent/src/skills/faq.ts +27 -0
- package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -0
- package/tutorial/customer-service-agent/tsconfig.json +14 -0
package/src/cli.ts
CHANGED
|
@@ -29,10 +29,12 @@ import { createProvider } from './providers';
|
|
|
29
29
|
import { KnowledgeBase } from './core/knowledge';
|
|
30
30
|
|
|
31
31
|
import { PluginManager, createLoggingPlugin, createAnalyticsPlugin, createRateLimitPlugin } from './plugins';
|
|
32
|
+
import { runDoctor } from './doctor';
|
|
32
33
|
import { Scheduler } from './core/scheduler';
|
|
33
34
|
import type { CronJob } from './core/scheduler';
|
|
34
35
|
import type { Span } from './traces';
|
|
35
36
|
import { spawn } from 'child_process';
|
|
37
|
+
import { searchRoles, getPopularRoles, getRole, getCategories } from 'agent-workstation';
|
|
36
38
|
|
|
37
39
|
const program = new Command();
|
|
38
40
|
|
|
@@ -107,9 +109,226 @@ program
|
|
|
107
109
|
.argument('[name]', 'Project name')
|
|
108
110
|
.option('-t, --template <template>', 'Template to use')
|
|
109
111
|
.option('-y, --yes', 'Skip prompts, use defaults')
|
|
110
|
-
.
|
|
112
|
+
.option('-r, --role <role>', 'Use an agent-workstation role template')
|
|
113
|
+
.option('--list-roles', 'List available workstation roles')
|
|
114
|
+
.action(async (nameArg: string | undefined, opts: { template?: string; yes?: boolean; role?: string; listRoles?: boolean }) => {
|
|
111
115
|
console.log(`\n${icon.rocket} ${color.bold('OPC Agent - Create New Project')}\n`);
|
|
112
116
|
|
|
117
|
+
// Handle --list-roles
|
|
118
|
+
if (opts.listRoles) {
|
|
119
|
+
const roles = getPopularRoles();
|
|
120
|
+
console.log(`📋 ${color.bold('Available workstation roles:')}\n`);
|
|
121
|
+
for (const r of roles) {
|
|
122
|
+
const fullRole = getRole(r.category, r.role);
|
|
123
|
+
let roleName = r.role;
|
|
124
|
+
if (fullRole?.files?.['oad.yaml']) {
|
|
125
|
+
try {
|
|
126
|
+
const oadData = yaml.load(fullRole.files['oad.yaml']) as any;
|
|
127
|
+
if (oadData?.name) roleName = oadData.name;
|
|
128
|
+
} catch { /* ignore */ }
|
|
129
|
+
}
|
|
130
|
+
console.log(` ${color.cyan((r.category + '/' + r.role).padEnd(45))} ${roleName}`);
|
|
131
|
+
}
|
|
132
|
+
console.log(`\n Use: ${color.cyan('opc init my-agent --role <role-name>')}`);
|
|
133
|
+
console.log(` Example: ${color.cyan('opc init my-agent --role customer-service')}\n`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Handle --role: search and generate from workstation template
|
|
138
|
+
if (opts.role) {
|
|
139
|
+
const results = searchRoles(opts.role);
|
|
140
|
+
if (results.length === 0) {
|
|
141
|
+
console.error(`${icon.error} Role "${color.bold(opts.role)}" not found. Run '${color.cyan('opc init --list-roles')}' to see available roles.`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const matched = results[0];
|
|
146
|
+
const roleData = getRole(matched.category, matched.role);
|
|
147
|
+
if (!roleData || !roleData.files) {
|
|
148
|
+
console.error(`${icon.error} Could not load role data for ${matched.category}/${matched.role}.`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const name = nameArg ?? matched.role;
|
|
153
|
+
const dir = path.resolve(name);
|
|
154
|
+
if (fs.existsSync(dir)) {
|
|
155
|
+
console.error(`\n${icon.error} Directory ${color.bold(name)} already exists.`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Parse role metadata from oad.yaml
|
|
160
|
+
let roleMeta: any = {};
|
|
161
|
+
if (roleData.files['oad.yaml']) {
|
|
162
|
+
try { roleMeta = yaml.load(roleData.files['oad.yaml']) as any; } catch { /* ignore */ }
|
|
163
|
+
}
|
|
164
|
+
const roleDisplayName = roleMeta.name || matched.role;
|
|
165
|
+
const roleDescription = roleMeta.name_zh ? `${roleMeta.name} (${roleMeta.name_zh})` : (roleMeta.name || matched.role);
|
|
166
|
+
|
|
167
|
+
console.log(` ${icon.info} Matched role: ${color.cyan(matched.category + '/' + matched.role)} — ${roleDisplayName}`);
|
|
168
|
+
|
|
169
|
+
// Create directories
|
|
170
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
171
|
+
fs.mkdirSync(path.join(dir, 'src', 'skills'), { recursive: true });
|
|
172
|
+
fs.mkdirSync(path.join(dir, 'data'), { recursive: true });
|
|
173
|
+
|
|
174
|
+
// Get system prompt content
|
|
175
|
+
const systemPromptContent = roleData.files['system-prompt.md'] || roleData.files['prompts/system.md'] || '';
|
|
176
|
+
|
|
177
|
+
// agent.yaml with role system prompt
|
|
178
|
+
const firstLine = systemPromptContent.split('\n').find((l: string) => l.trim() && !l.startsWith('#'))?.trim() || 'You are a helpful AI assistant.';
|
|
179
|
+
fs.writeFileSync(
|
|
180
|
+
path.join(dir, 'agent.yaml'),
|
|
181
|
+
`apiVersion: opc/v1
|
|
182
|
+
kind: Agent
|
|
183
|
+
metadata:
|
|
184
|
+
name: ${name}
|
|
185
|
+
version: 1.0.0
|
|
186
|
+
description: ${roleDescription}
|
|
187
|
+
spec:
|
|
188
|
+
model: qwen2.5
|
|
189
|
+
provider:
|
|
190
|
+
default: ollama
|
|
191
|
+
systemPrompt: |
|
|
192
|
+
${systemPromptContent.split('\n').join('\n ')}
|
|
193
|
+
channels:
|
|
194
|
+
- type: web
|
|
195
|
+
port: 3000
|
|
196
|
+
memory:
|
|
197
|
+
shortTerm: true
|
|
198
|
+
longTerm:
|
|
199
|
+
provider: deepbrain
|
|
200
|
+
database: ./data/brain.db
|
|
201
|
+
skills: []
|
|
202
|
+
`,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// SOUL.md from system-prompt.md
|
|
206
|
+
fs.writeFileSync(path.join(dir, 'SOUL.md'), systemPromptContent);
|
|
207
|
+
|
|
208
|
+
// CONTEXT.md
|
|
209
|
+
const readmeContent = roleData.files['README.md'] || '';
|
|
210
|
+
fs.writeFileSync(
|
|
211
|
+
path.join(dir, 'CONTEXT.md'),
|
|
212
|
+
`# Project Context\n\n## Role: ${roleDisplayName}\n\n${readmeContent}\n`,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// data/brain-seed.md if available
|
|
216
|
+
if (roleData.files['brain-seed.md']) {
|
|
217
|
+
fs.writeFileSync(path.join(dir, 'data', 'brain-seed.md'), roleData.files['brain-seed.md']);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// oad.yaml from role
|
|
221
|
+
if (roleData.files['oad.yaml']) {
|
|
222
|
+
fs.writeFileSync(path.join(dir, 'oad.yaml'), roleData.files['oad.yaml']);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/index.ts — entry point (same as generic)
|
|
226
|
+
fs.writeFileSync(
|
|
227
|
+
path.join(dir, 'src', 'index.ts'),
|
|
228
|
+
`import { AgentRuntime } from 'opc-agent';
|
|
229
|
+
import { EchoSkill } from './skills/echo';
|
|
230
|
+
import { readFileSync, existsSync } from 'fs';
|
|
231
|
+
|
|
232
|
+
async function main() {
|
|
233
|
+
const runtime = new AgentRuntime();
|
|
234
|
+
const config = await runtime.loadConfig('./agent.yaml');
|
|
235
|
+
|
|
236
|
+
const soul = existsSync('./SOUL.md') ? readFileSync('./SOUL.md', 'utf-8') : '';
|
|
237
|
+
const context = existsSync('./CONTEXT.md') ? readFileSync('./CONTEXT.md', 'utf-8') : '';
|
|
238
|
+
if (soul || context) {
|
|
239
|
+
const fullPrompt = [soul, context, config.spec.systemPrompt].filter(Boolean).join('\\n\\n');
|
|
240
|
+
config.spec.systemPrompt = fullPrompt;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const agent = await runtime.initialize(config);
|
|
244
|
+
runtime.registerSkill(new EchoSkill());
|
|
245
|
+
await runtime.start();
|
|
246
|
+
|
|
247
|
+
console.log('🤖 Agent is running!');
|
|
248
|
+
console.log(' Web UI: http://localhost:3000');
|
|
249
|
+
console.log(' Press Ctrl+C to stop');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
main().catch(console.error);
|
|
253
|
+
`,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// src/skills/echo.ts
|
|
257
|
+
fs.writeFileSync(
|
|
258
|
+
path.join(dir, 'src', 'skills', 'echo.ts'),
|
|
259
|
+
`import { BaseSkill } from 'opc-agent';
|
|
260
|
+
import type { AgentContext, Message, SkillResult } from 'opc-agent';
|
|
261
|
+
|
|
262
|
+
export class EchoSkill extends BaseSkill {
|
|
263
|
+
name = 'echo';
|
|
264
|
+
description = 'Echo back the message (test skill)';
|
|
265
|
+
|
|
266
|
+
async execute(context: AgentContext, message: Message): Promise<SkillResult> {
|
|
267
|
+
if (message.content.toLowerCase().startsWith('/echo ')) {
|
|
268
|
+
const text = message.content.slice(6);
|
|
269
|
+
return this.match(\`🔊 Echo: \${text}\`);
|
|
270
|
+
}
|
|
271
|
+
return this.noMatch();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
`,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
// tsconfig.json
|
|
278
|
+
fs.writeFileSync(
|
|
279
|
+
path.join(dir, 'tsconfig.json'),
|
|
280
|
+
JSON.stringify(
|
|
281
|
+
{
|
|
282
|
+
compilerOptions: { target: 'ES2022', module: 'commonjs', lib: ['ES2022'], outDir: 'dist', rootDir: 'src', strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, resolveJsonModule: true, declaration: true, sourceMap: true },
|
|
283
|
+
include: ['src/**/*'],
|
|
284
|
+
exclude: ['node_modules', 'dist'],
|
|
285
|
+
},
|
|
286
|
+
null,
|
|
287
|
+
2,
|
|
288
|
+
),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// package.json
|
|
292
|
+
fs.writeFileSync(
|
|
293
|
+
path.join(dir, 'package.json'),
|
|
294
|
+
JSON.stringify(
|
|
295
|
+
{ name, version: '1.0.0', private: true, scripts: { start: 'opc run', dev: 'opc dev', chat: 'opc chat', build: 'tsc' }, dependencies: { 'opc-agent': '^1.3.0' }, devDependencies: { typescript: '^5.5.0', tsx: '^4.0.0' } },
|
|
296
|
+
null,
|
|
297
|
+
2,
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// .gitignore, .env.example, .env
|
|
302
|
+
fs.writeFileSync(path.join(dir, '.gitignore'), 'node_modules\ndist\n.env\n.opc-knowledge.json\ndata/\n');
|
|
303
|
+
fs.writeFileSync(path.join(dir, '.env.example'), `# LLM API Configuration\nOPC_LLM_API_KEY=your-api-key-here\nOPC_LLM_BASE_URL=https://api.openai.com/v1\nOPC_LLM_MODEL=gpt-4o-mini\n`);
|
|
304
|
+
fs.writeFileSync(path.join(dir, '.env'), `OPC_LLM_API_KEY=your-api-key-here\nOPC_LLM_BASE_URL=https://api.openai.com/v1\nOPC_LLM_MODEL=gpt-4o-mini\n`);
|
|
305
|
+
|
|
306
|
+
// README.md
|
|
307
|
+
fs.writeFileSync(
|
|
308
|
+
path.join(dir, 'README.md'),
|
|
309
|
+
`# ${name}\n\nCreated with [OPC Agent](https://github.com/Deepleaper/opc-agent) using the \`${matched.category}/${matched.role}\` workstation role.\n\n## Quick Start\n\n\`\`\`bash\nnpm install\nollama pull qwen2.5\nnpx tsx src/index.ts\n\`\`\`\n\nOpen [http://localhost:3000](http://localhost:3000)\n`,
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
// Dockerfile + docker-compose
|
|
313
|
+
fs.writeFileSync(path.join(dir, 'Dockerfile'), `FROM node:22-alpine\nWORKDIR /app\nCOPY package.json package-lock.json* ./\nRUN npm ci --production 2>/dev/null || npm install --production\nCOPY oad.yaml agent.yaml .env* ./\nCOPY src/ ./src/\nCOPY prompts/ ./prompts/ 2>/dev/null || true\nEXPOSE 3000\nCMD ["npx", "opc", "run"]\n`);
|
|
314
|
+
fs.writeFileSync(path.join(dir, 'docker-compose.yml'), `version: '3.8'\nservices:\n agent:\n build: .\n ports:\n - "3000:3000"\n env_file:\n - .env\n volumes:\n - ./agent.yaml:/app/agent.yaml:ro\n restart: unless-stopped\n`);
|
|
315
|
+
|
|
316
|
+
console.log(`\n${icon.success} Created agent project: ${color.bold(name + '/')} from role ${color.cyan(matched.category + '/' + matched.role)}`);
|
|
317
|
+
console.log(` ${icon.file} agent.yaml - Agent definition with role system prompt`);
|
|
318
|
+
console.log(` ${icon.file} SOUL.md - Role personality (${systemPromptContent.split('\n').length} lines)`);
|
|
319
|
+
console.log(` ${icon.file} CONTEXT.md - Role context & documentation`);
|
|
320
|
+
if (roleData.files['brain-seed.md']) {
|
|
321
|
+
console.log(` ${icon.file} data/brain-seed.md - Role brain seed knowledge`);
|
|
322
|
+
}
|
|
323
|
+
console.log(` ${icon.file} src/index.ts - Entry point`);
|
|
324
|
+
console.log(` ${icon.file} package.json - Dependencies`);
|
|
325
|
+
console.log(`\n${color.bold('Next steps:')}`);
|
|
326
|
+
console.log(` 1. cd ${name}`);
|
|
327
|
+
console.log(` 2. npm install`);
|
|
328
|
+
console.log(` 3. npx tsx src/index.ts\n`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
113
332
|
const name = opts.yes ? (nameArg ?? 'my-agent') : (nameArg ?? await promptUser('Project name', 'my-agent'));
|
|
114
333
|
const template = opts.yes
|
|
115
334
|
? (opts.template ?? 'customer-service')
|
|
@@ -461,6 +680,9 @@ on startup to understand the project context.
|
|
|
461
680
|
console.log(` 2. npm install`);
|
|
462
681
|
console.log(` 3. npx tsx src/index.ts ${color.dim('# or: npx opc run')}`);
|
|
463
682
|
console.log(` 4. Open http://localhost:3000\n`);
|
|
683
|
+
console.log(`${color.dim('💡 Tip: Use --role to start from a workstation template:')}`);
|
|
684
|
+
console.log(`${color.dim(' opc init my-agent --role customer-service')}`);
|
|
685
|
+
console.log(`${color.dim(' opc init --list-roles (see all roles)')}\n`);
|
|
464
686
|
});
|
|
465
687
|
|
|
466
688
|
// ── Chat command ─────────────────────────────────────────────
|
|
@@ -671,6 +893,33 @@ program
|
|
|
671
893
|
console.log(` Model: ${s.model}`);
|
|
672
894
|
console.log(` Channels: ${s.channels.map((c: any) => c.type).join(', ') || color.dim('(none)')}`);
|
|
673
895
|
console.log(` Skills: ${s.skills.map((sk: any) => sk.name).join(', ') || color.dim('(none)')}`);
|
|
896
|
+
|
|
897
|
+
// Memory info
|
|
898
|
+
const memCfg = s.memory;
|
|
899
|
+
const shortTermStatus = memCfg?.shortTerm !== false ? '✅' : '❌';
|
|
900
|
+
console.log(`\n ${color.bold('Memory:')}`);
|
|
901
|
+
console.log(` Short-term: ${shortTermStatus} InMemoryStore`);
|
|
902
|
+
if (memCfg && typeof memCfg.longTerm === 'object' && memCfg.longTerm.provider === 'deepbrain') {
|
|
903
|
+
const ltCfg = memCfg.longTerm.config as any ?? {};
|
|
904
|
+
const dbPath = ltCfg.database || './data/brain.db';
|
|
905
|
+
const autoLearn = ltCfg.autoLearn !== false ? '✅' : '❌';
|
|
906
|
+
const autoRecall = ltCfg.autoRecall !== false ? '✅' : '❌';
|
|
907
|
+
const evolveInterval = ltCfg.evolveInterval;
|
|
908
|
+
console.log(` Long-term: ✅ DeepBrain (${dbPath})`);
|
|
909
|
+
console.log(` Auto-learn: ${autoLearn}`);
|
|
910
|
+
console.log(` Auto-recall: ${autoRecall}`);
|
|
911
|
+
if (evolveInterval && evolveInterval > 0) {
|
|
912
|
+
const hours = Math.floor(evolveInterval / 3600000);
|
|
913
|
+
const mins = Math.floor((evolveInterval % 3600000) / 60000);
|
|
914
|
+
const label = hours > 0 ? `every ${hours}h${mins > 0 ? ` ${mins}m` : ''}` : `every ${mins}m`;
|
|
915
|
+
console.log(` Auto-evolve: ${label}`);
|
|
916
|
+
} else {
|
|
917
|
+
console.log(` Auto-evolve: ❌ disabled`);
|
|
918
|
+
}
|
|
919
|
+
} else {
|
|
920
|
+
console.log(` Long-term: ❌ disabled`);
|
|
921
|
+
}
|
|
922
|
+
|
|
674
923
|
console.log();
|
|
675
924
|
} catch (err) {
|
|
676
925
|
console.error(`${icon.error} Failed to read OAD:`, err instanceof Error ? err.message : err);
|
|
@@ -1006,23 +1255,87 @@ kbCmd.command('clear').action(() => {
|
|
|
1006
1255
|
|
|
1007
1256
|
// 📦 Package commands ───────────────────────────────────
|
|
1008
1257
|
|
|
1258
|
+
import { AgentPackager, AgentPublisher, AgentInstaller } from './publish';
|
|
1259
|
+
|
|
1009
1260
|
program
|
|
1010
1261
|
.command('publish')
|
|
1011
|
-
.description('
|
|
1012
|
-
.option('-
|
|
1013
|
-
.option('
|
|
1014
|
-
.option('--
|
|
1015
|
-
.
|
|
1016
|
-
|
|
1262
|
+
.description('Validate, pack, and publish agent package')
|
|
1263
|
+
.option('--dry-run', 'Show what would be published without actually publishing')
|
|
1264
|
+
.option('--tag <tag>', 'Publish tag (default: latest)', 'latest')
|
|
1265
|
+
.option('--access <access>', 'Package access level (public or private)', 'public')
|
|
1266
|
+
.option('--registry <url>', 'Registry URL')
|
|
1267
|
+
.action(async (opts: { dryRun?: boolean; tag: string; access: string; registry?: string }) => {
|
|
1268
|
+
const dir = process.cwd();
|
|
1269
|
+
const packager = new AgentPackager();
|
|
1270
|
+
const publisher = new AgentPublisher();
|
|
1271
|
+
|
|
1272
|
+
// Validate first
|
|
1273
|
+
console.log(`\n${icon.gear} Validating agent project...`);
|
|
1274
|
+
const validation = await packager.validate(dir);
|
|
1275
|
+
for (const w of validation.warnings) console.log(` ${icon.warn} ${color.yellow(w)}`);
|
|
1276
|
+
if (!validation.valid) {
|
|
1277
|
+
for (const e of validation.errors) console.log(` ${icon.error} ${color.red(e)}`);
|
|
1278
|
+
console.log(`\n${icon.error} Validation failed. Fix errors above.\n`);
|
|
1279
|
+
process.exit(1);
|
|
1280
|
+
}
|
|
1281
|
+
console.log(` ${icon.success} Validation passed.`);
|
|
1282
|
+
|
|
1283
|
+
// Pack
|
|
1284
|
+
console.log(`\n${icon.package} Packing agent...`);
|
|
1285
|
+
const { path: pkgPath, manifest } = await packager.pack(dir);
|
|
1286
|
+
console.log(` ${icon.success} Created ${color.bold(path.basename(pkgPath))} (${manifest.files.length} files)`);
|
|
1287
|
+
console.log(` ${color.dim('Checksum:')} ${manifest.checksum}`);
|
|
1288
|
+
|
|
1289
|
+
// Publish
|
|
1290
|
+
await publisher.publish(pkgPath, manifest, {
|
|
1291
|
+
dryRun: opts.dryRun,
|
|
1292
|
+
tag: opts.tag,
|
|
1293
|
+
access: opts.access as 'public' | 'private',
|
|
1294
|
+
registry: opts.registry,
|
|
1295
|
+
});
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
program
|
|
1299
|
+
.command('pack')
|
|
1300
|
+
.description('Create .opc.tgz package without publishing')
|
|
1301
|
+
.option('--list', 'List files that would be included (do not create archive)')
|
|
1302
|
+
.action(async (opts: { list?: boolean }) => {
|
|
1303
|
+
const dir = process.cwd();
|
|
1304
|
+
const packager = new AgentPackager();
|
|
1305
|
+
|
|
1306
|
+
if (opts.list) {
|
|
1307
|
+
const files = await packager.listFiles(dir);
|
|
1308
|
+
console.log(`\n${icon.package} ${color.bold('Files to include')} (${files.length}):\n`);
|
|
1309
|
+
for (const f of files) console.log(` ${f}`);
|
|
1310
|
+
console.log();
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Validate
|
|
1315
|
+
const validation = await packager.validate(dir);
|
|
1316
|
+
for (const w of validation.warnings) console.log(` ${icon.warn} ${color.yellow(w)}`);
|
|
1317
|
+
if (!validation.valid) {
|
|
1318
|
+
for (const e of validation.errors) console.log(` ${icon.error} ${color.red(e)}`);
|
|
1319
|
+
process.exit(1);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
console.log(`\n${icon.package} Packing agent...`);
|
|
1323
|
+
const { path: pkgPath, manifest } = await packager.pack(dir);
|
|
1324
|
+
console.log(` ${icon.success} Created ${color.bold(path.basename(pkgPath))}`);
|
|
1325
|
+
console.log(` Files: ${manifest.files.length}`);
|
|
1326
|
+
console.log(` Checksum: ${manifest.checksum}\n`);
|
|
1017
1327
|
});
|
|
1018
1328
|
|
|
1019
1329
|
program
|
|
1020
1330
|
.command('install')
|
|
1021
|
-
.description('Install agent from package')
|
|
1022
|
-
.argument('<source>', 'Package file path or
|
|
1023
|
-
.option('-d, --dir <dir>', 'Install directory')
|
|
1024
|
-
.action(async () => {
|
|
1025
|
-
|
|
1331
|
+
.description('Install agent from .opc.tgz package or npm')
|
|
1332
|
+
.argument('<source>', 'Package file path, URL, or npm package name')
|
|
1333
|
+
.option('-d, --dir <dir>', 'Install directory', '.')
|
|
1334
|
+
.action(async (source: string, opts: { dir: string }) => {
|
|
1335
|
+
const installer = new AgentInstaller();
|
|
1336
|
+
console.log(`\n${icon.package} Installing from ${color.bold(source)}...`);
|
|
1337
|
+
await installer.install(source, path.resolve(opts.dir));
|
|
1338
|
+
console.log();
|
|
1026
1339
|
});
|
|
1027
1340
|
|
|
1028
1341
|
// 🔌 Plugin commands ────────────────────────────────────────
|
|
@@ -1070,6 +1383,77 @@ pluginCmd.command('add')
|
|
|
1070
1383
|
}
|
|
1071
1384
|
});
|
|
1072
1385
|
|
|
1386
|
+
// 🔌 Protocol commands ───────────────────────────────────────
|
|
1387
|
+
|
|
1388
|
+
const protocolCmd = program.command('protocol').description('Manage agent protocols (A2A, AG-UI)');
|
|
1389
|
+
|
|
1390
|
+
protocolCmd.command('list')
|
|
1391
|
+
.description('List supported protocols and their status')
|
|
1392
|
+
.option('-f, --file <file>', 'OAD file', 'oad.yaml')
|
|
1393
|
+
.action((opts: { file: string }) => {
|
|
1394
|
+
let config: any = {};
|
|
1395
|
+
try { config = yaml.load(fs.readFileSync(opts.file, 'utf-8')) as any; } catch { /* no file */ }
|
|
1396
|
+
const protocols = config?.spec?.protocols || {};
|
|
1397
|
+
const items = [
|
|
1398
|
+
{ name: 'a2a', description: 'Agent-to-Agent protocol', enabled: !!protocols.a2a?.enabled, detail: protocols.a2a?.port ? `port ${protocols.a2a.port}` : '' },
|
|
1399
|
+
{ name: 'agui', description: 'AG-UI — Agent-User Interaction (SSE)', enabled: !!protocols.agui?.enabled, detail: protocols.agui?.path || '/agui' },
|
|
1400
|
+
];
|
|
1401
|
+
console.log(`\n${icon.gear} ${color.bold('Protocols')}\n`);
|
|
1402
|
+
for (const p of items) {
|
|
1403
|
+
const status = p.enabled ? color.green('enabled') : color.dim('disabled');
|
|
1404
|
+
console.log(` ${color.cyan(p.name.padEnd(10))} ${status.padEnd(20)} ${p.description} ${p.detail ? color.dim(`(${p.detail})`) : ''}`);
|
|
1405
|
+
}
|
|
1406
|
+
console.log();
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
protocolCmd.command('enable')
|
|
1410
|
+
.argument('<name>', 'Protocol name (a2a, agui)')
|
|
1411
|
+
.option('-f, --file <file>', 'OAD file', 'oad.yaml')
|
|
1412
|
+
.description('Enable a protocol')
|
|
1413
|
+
.action((name: string, opts: { file: string }) => {
|
|
1414
|
+
const validProtocols = ['a2a', 'agui'];
|
|
1415
|
+
if (!validProtocols.includes(name)) {
|
|
1416
|
+
console.error(`${icon.error} Unknown protocol: ${color.bold(name)}. Available: ${validProtocols.join(', ')}`);
|
|
1417
|
+
process.exit(1);
|
|
1418
|
+
}
|
|
1419
|
+
try {
|
|
1420
|
+
const raw = fs.readFileSync(opts.file, 'utf-8');
|
|
1421
|
+
const config = yaml.load(raw) as any;
|
|
1422
|
+
if (!config.spec.protocols) config.spec.protocols = {};
|
|
1423
|
+
if (!config.spec.protocols[name]) config.spec.protocols[name] = {};
|
|
1424
|
+
config.spec.protocols[name].enabled = true;
|
|
1425
|
+
if (name === 'agui' && !config.spec.protocols[name].path) {
|
|
1426
|
+
config.spec.protocols[name].path = '/agui';
|
|
1427
|
+
}
|
|
1428
|
+
fs.writeFileSync(opts.file, yaml.dump(config, { lineWidth: 120 }));
|
|
1429
|
+
console.log(`${icon.success} Enabled protocol "${color.cyan(name)}" in ${opts.file}`);
|
|
1430
|
+
} catch (err) {
|
|
1431
|
+
console.error(`${icon.error} Failed:`, err instanceof Error ? err.message : err);
|
|
1432
|
+
process.exit(1);
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
protocolCmd.command('disable')
|
|
1437
|
+
.argument('<name>', 'Protocol name (a2a, agui)')
|
|
1438
|
+
.option('-f, --file <file>', 'OAD file', 'oad.yaml')
|
|
1439
|
+
.description('Disable a protocol')
|
|
1440
|
+
.action((name: string, opts: { file: string }) => {
|
|
1441
|
+
try {
|
|
1442
|
+
const raw = fs.readFileSync(opts.file, 'utf-8');
|
|
1443
|
+
const config = yaml.load(raw) as any;
|
|
1444
|
+
if (config?.spec?.protocols?.[name]) {
|
|
1445
|
+
config.spec.protocols[name].enabled = false;
|
|
1446
|
+
fs.writeFileSync(opts.file, yaml.dump(config, { lineWidth: 120 }));
|
|
1447
|
+
console.log(`${icon.success} Disabled protocol "${color.cyan(name)}" in ${opts.file}`);
|
|
1448
|
+
} else {
|
|
1449
|
+
console.log(`${icon.info} Protocol "${name}" was not configured.`);
|
|
1450
|
+
}
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
console.error(`${icon.error} Failed:`, err instanceof Error ? err.message : err);
|
|
1453
|
+
process.exit(1);
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1073
1457
|
// 🔄 Migrate command ────────────────────────────────────────
|
|
1074
1458
|
|
|
1075
1459
|
program
|
|
@@ -1506,5 +1890,392 @@ skillsCmd
|
|
|
1506
1890
|
console.log(`${icon.success} Removed skill "${color.cyan(name)}".`);
|
|
1507
1891
|
});
|
|
1508
1892
|
|
|
1893
|
+
// ── Doctor command ───────────────────────────────────────────
|
|
1894
|
+
|
|
1895
|
+
program
|
|
1896
|
+
.command('studio')
|
|
1897
|
+
.description('Start OPC Studio web UI')
|
|
1898
|
+
.option('--port <port>', 'Port to listen on', '4000')
|
|
1899
|
+
.action(async (opts: any) => {
|
|
1900
|
+
const { StudioServer } = require('./studio/server');
|
|
1901
|
+
const server = new StudioServer({
|
|
1902
|
+
port: parseInt(opts.port, 10),
|
|
1903
|
+
agentDir: process.cwd(),
|
|
1904
|
+
});
|
|
1905
|
+
await server.start();
|
|
1906
|
+
console.log(color.dim('Press Ctrl+C to stop'));
|
|
1907
|
+
});
|
|
1908
|
+
|
|
1909
|
+
program
|
|
1910
|
+
.command('doctor')
|
|
1911
|
+
.description('Check environment and diagnose common issues')
|
|
1912
|
+
.action(async () => {
|
|
1913
|
+
await runDoctor();
|
|
1914
|
+
});
|
|
1915
|
+
|
|
1916
|
+
// ─── Eval command ───────────────────────────────────────────────────────────
|
|
1917
|
+
import { AgentEvaluator } from './eval';
|
|
1918
|
+
|
|
1919
|
+
program
|
|
1920
|
+
.command('eval')
|
|
1921
|
+
.argument('[suite]', 'Built-in suite name (basic, safety, memory) or omit for all')
|
|
1922
|
+
.option('-f, --file <path>', 'Path to custom eval suite JSON file')
|
|
1923
|
+
.option('-o, --output <path>', 'Save report to JSON file')
|
|
1924
|
+
.option('-v, --verbose', 'Show per-case details')
|
|
1925
|
+
.description('Run agent evaluation suites')
|
|
1926
|
+
.action(async (suiteName: string | undefined, opts: { file?: string; output?: string; verbose?: boolean }) => {
|
|
1927
|
+
const suites: import('./eval').EvalSuite[] = [];
|
|
1928
|
+
|
|
1929
|
+
if (opts.file) {
|
|
1930
|
+
suites.push(AgentEvaluator.loadSuite(opts.file));
|
|
1931
|
+
} else if (suiteName) {
|
|
1932
|
+
suites.push(AgentEvaluator.loadBuiltinSuite(suiteName));
|
|
1933
|
+
} else {
|
|
1934
|
+
// All built-in suites
|
|
1935
|
+
for (const s of AgentEvaluator.builtinSuites()) {
|
|
1936
|
+
suites.push(AgentEvaluator.loadBuiltinSuite(s.name));
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
if (!suites.length) {
|
|
1941
|
+
console.log(`${icon.warn} No eval suites found.`);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// Create a minimal mock agent for eval (real usage would load from OAD)
|
|
1946
|
+
const oadPath = path.resolve('agent.yaml');
|
|
1947
|
+
let agent: any;
|
|
1948
|
+
if (fs.existsSync(oadPath)) {
|
|
1949
|
+
const runtime = new AgentRuntime();
|
|
1950
|
+
await runtime.loadConfig(oadPath);
|
|
1951
|
+
await runtime.start();
|
|
1952
|
+
agent = (runtime as any).agent;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
if (!agent) {
|
|
1956
|
+
console.log(`${icon.warn} No agent.yaml found — running with dry-run mock agent.`);
|
|
1957
|
+
agent = { chat: async (input: string) => `[mock response to: ${input}]` };
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
const evaluator = new AgentEvaluator(agent);
|
|
1961
|
+
let allPassed = 0, allTotal = 0;
|
|
1962
|
+
|
|
1963
|
+
for (const suite of suites) {
|
|
1964
|
+
console.log(`\n${color.bold(`🧪 Suite: ${suite.name}`)} (${suite.cases.length} cases)`);
|
|
1965
|
+
const report = await evaluator.evalSuite(suite);
|
|
1966
|
+
allPassed += report.passed;
|
|
1967
|
+
allTotal += report.totalCases;
|
|
1968
|
+
|
|
1969
|
+
for (const r of report.results) {
|
|
1970
|
+
const status = r.passed ? color.green('PASS') : color.red('FAIL');
|
|
1971
|
+
console.log(` ${status} ${r.caseId}`);
|
|
1972
|
+
if (opts.verbose && !r.passed) {
|
|
1973
|
+
if (r.error) console.log(` ${color.dim('error: ' + r.error)}`);
|
|
1974
|
+
console.log(` ${color.dim('output: ' + r.output.slice(0, 120))}`);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
console.log(` ${color.dim(report.summary)}`);
|
|
1979
|
+
|
|
1980
|
+
if (opts.output) {
|
|
1981
|
+
const outPath = suites.length > 1
|
|
1982
|
+
? opts.output.replace('.json', `-${suite.name}.json`)
|
|
1983
|
+
: opts.output;
|
|
1984
|
+
AgentEvaluator.saveReport(report, outPath);
|
|
1985
|
+
console.log(` ${icon.success} Report saved to ${outPath}`);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
console.log(`\n${color.bold('Summary:')} ${allPassed}/${allTotal} passed (${allTotal ? Math.round(allPassed / allTotal * 100) : 0}%)`);
|
|
1990
|
+
});
|
|
1991
|
+
|
|
1509
1992
|
program.parse();
|
|
1510
1993
|
|
|
1994
|
+
// ── Keys command ──────────────────────────────────────────────
|
|
1995
|
+
|
|
1996
|
+
import { KeyManager } from './security/keys';
|
|
1997
|
+
import { ApprovalManager } from './security/approval';
|
|
1998
|
+
|
|
1999
|
+
const keysCmd = program.command('keys').description('Manage API keys');
|
|
2000
|
+
|
|
2001
|
+
keysCmd
|
|
2002
|
+
.command('set')
|
|
2003
|
+
.argument('<name>', 'Key name')
|
|
2004
|
+
.description('Store an API key (encrypted)')
|
|
2005
|
+
.action(async (name: string) => {
|
|
2006
|
+
const value = await promptUser(`Enter value for ${color.bold(name)}`);
|
|
2007
|
+
if (!value) {
|
|
2008
|
+
console.log(`${icon.error} No value provided.`);
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
const km = new KeyManager();
|
|
2012
|
+
km.set(name, value);
|
|
2013
|
+
console.log(`${icon.success} Key ${color.bold(name)} saved.`);
|
|
2014
|
+
});
|
|
2015
|
+
|
|
2016
|
+
keysCmd
|
|
2017
|
+
.command('list')
|
|
2018
|
+
.description('List stored key names')
|
|
2019
|
+
.action(() => {
|
|
2020
|
+
const km = new KeyManager();
|
|
2021
|
+
const names = km.list();
|
|
2022
|
+
if (names.length === 0) {
|
|
2023
|
+
console.log(`${icon.info} No keys stored.`);
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
console.log(`\n${color.bold('Stored keys:')}`);
|
|
2027
|
+
names.forEach(n => console.log(` • ${n}`));
|
|
2028
|
+
});
|
|
2029
|
+
|
|
2030
|
+
keysCmd
|
|
2031
|
+
.command('delete')
|
|
2032
|
+
.argument('<name>', 'Key name')
|
|
2033
|
+
.description('Delete a stored key')
|
|
2034
|
+
.action((name: string) => {
|
|
2035
|
+
const km = new KeyManager();
|
|
2036
|
+
if (km.delete(name)) {
|
|
2037
|
+
console.log(`${icon.success} Key ${color.bold(name)} deleted.`);
|
|
2038
|
+
} else {
|
|
2039
|
+
console.log(`${icon.error} Key ${color.bold(name)} not found.`);
|
|
2040
|
+
}
|
|
2041
|
+
});
|
|
2042
|
+
|
|
2043
|
+
// ── Approve command ───────────────────────────────────────────
|
|
2044
|
+
|
|
2045
|
+
const approveCmd = program.command('approve').description('Manage command approvals');
|
|
2046
|
+
|
|
2047
|
+
// Singleton for CLI — in real usage this would be loaded from daemon state
|
|
2048
|
+
const approvalManager = new ApprovalManager();
|
|
2049
|
+
|
|
2050
|
+
approveCmd
|
|
2051
|
+
.command('list')
|
|
2052
|
+
.description('Show pending approval requests')
|
|
2053
|
+
.action(() => {
|
|
2054
|
+
const pending = approvalManager.getPending();
|
|
2055
|
+
if (pending.length === 0) {
|
|
2056
|
+
console.log(`${icon.info} No pending approvals.`);
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
console.log(`\n${color.bold('Pending approvals:')}`);
|
|
2060
|
+
pending.forEach(r => {
|
|
2061
|
+
console.log(` ${color.cyan(r.id.slice(0, 8))} [${r.type}] ${r.command}`);
|
|
2062
|
+
console.log(` ${color.dim(r.description)}`);
|
|
2063
|
+
});
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
approveCmd
|
|
2067
|
+
.command('allow')
|
|
2068
|
+
.argument('<id>', 'Approval request ID (prefix match)')
|
|
2069
|
+
.description('Approve a pending request')
|
|
2070
|
+
.action((id: string) => {
|
|
2071
|
+
const pending = approvalManager.getPending();
|
|
2072
|
+
const match = pending.find(r => r.id.startsWith(id));
|
|
2073
|
+
if (!match) {
|
|
2074
|
+
console.log(`${icon.error} No pending request matching ${id}`);
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
approvalManager.approve(match.id, 'cli-user');
|
|
2078
|
+
console.log(`${icon.success} Approved: ${match.command}`);
|
|
2079
|
+
});
|
|
2080
|
+
|
|
2081
|
+
approveCmd
|
|
2082
|
+
.command('deny')
|
|
2083
|
+
.argument('<id>', 'Approval request ID (prefix match)')
|
|
2084
|
+
.description('Deny a pending request')
|
|
2085
|
+
.action((id: string) => {
|
|
2086
|
+
const pending = approvalManager.getPending();
|
|
2087
|
+
const match = pending.find(r => r.id.startsWith(id));
|
|
2088
|
+
if (!match) {
|
|
2089
|
+
console.log(`${icon.error} No pending request matching ${id}`);
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
approvalManager.deny(match.id, 'cli-user');
|
|
2093
|
+
console.log(`${icon.success} Denied: ${match.command}`);
|
|
2094
|
+
});
|
|
2095
|
+
|
|
2096
|
+
// ── Traces command ────────────────────────────────────────────
|
|
2097
|
+
|
|
2098
|
+
import { Tracer, FileExporter } from './telemetry';
|
|
2099
|
+
|
|
2100
|
+
program
|
|
2101
|
+
.command('traces')
|
|
2102
|
+
.option('-l, --limit <n>', 'Number of traces to show', '20')
|
|
2103
|
+
.option('-f, --file <path>', 'Read traces from file')
|
|
2104
|
+
.description('Show recent telemetry traces')
|
|
2105
|
+
.action(async (opts: { limit: string; file?: string }) => {
|
|
2106
|
+
const limit = parseInt(opts.limit) || 20;
|
|
2107
|
+
|
|
2108
|
+
if (opts.file) {
|
|
2109
|
+
// Read from NDJSON file
|
|
2110
|
+
const fs = require('fs');
|
|
2111
|
+
if (!fs.existsSync(opts.file)) {
|
|
2112
|
+
console.log(`${icon.error} File not found: ${opts.file}`);
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
const lines = fs.readFileSync(opts.file, 'utf-8').trim().split('\n');
|
|
2116
|
+
const spans = lines.slice(-limit).map((l: string) => {
|
|
2117
|
+
try { return JSON.parse(l); } catch { return null; }
|
|
2118
|
+
}).filter(Boolean);
|
|
2119
|
+
|
|
2120
|
+
printTraceTable(spans);
|
|
2121
|
+
} else {
|
|
2122
|
+
// Try to read from Studio API
|
|
2123
|
+
try {
|
|
2124
|
+
const oad = loadOADFile();
|
|
2125
|
+
const port = 4000; // default studio port
|
|
2126
|
+
const res = await fetch(`http://localhost:${port}/api/telemetry/traces?limit=${limit}`);
|
|
2127
|
+
const data = await res.json() as any;
|
|
2128
|
+
if (data.traces && data.traces.length > 0) {
|
|
2129
|
+
console.log(`\n${color.bold('Recent Traces')} (${data.traces.length})\n`);
|
|
2130
|
+
console.log(`${'Trace ID'.padEnd(12)} ${'Root Span'.padEnd(25)} ${'Time'.padEnd(22)} ${'Spans'.padEnd(7)} ${'Status'}`);
|
|
2131
|
+
console.log(`${'─'.repeat(12)} ${'─'.repeat(25)} ${'─'.repeat(22)} ${'─'.repeat(7)} ${'─'.repeat(8)}`);
|
|
2132
|
+
for (const t of data.traces) {
|
|
2133
|
+
const time = new Date(t.startTime).toISOString().slice(0, 19).replace('T', ' ');
|
|
2134
|
+
const statusColor = t.status === 'ok' ? color.green : t.status === 'error' ? color.red : color.dim;
|
|
2135
|
+
console.log(`${color.cyan(t.traceId.slice(0, 12))} ${t.rootSpan.padEnd(25).slice(0, 25)} ${time.padEnd(22)} ${String(t.spanCount).padEnd(7)} ${statusColor(t.status)}`);
|
|
2136
|
+
}
|
|
2137
|
+
} else {
|
|
2138
|
+
console.log(`${icon.info} No traces found. Enable telemetry in your OAD: spec.telemetry.enabled: true`);
|
|
2139
|
+
}
|
|
2140
|
+
} catch {
|
|
2141
|
+
console.log(`${icon.error} Could not connect to Studio. Is it running? (opc studio)`);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
});
|
|
2145
|
+
|
|
2146
|
+
function printTraceTable(spans: any[]) {
|
|
2147
|
+
if (spans.length === 0) {
|
|
2148
|
+
console.log(`${icon.info} No traces found.`);
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
console.log(`\n${color.bold('Recent Spans')} (${spans.length})\n`);
|
|
2152
|
+
console.log(`${'Trace ID'.padEnd(12)} ${'Span'.padEnd(25)} ${'Duration'.padEnd(10)} ${'Status'}`);
|
|
2153
|
+
console.log(`${'─'.repeat(12)} ${'─'.repeat(25)} ${'─'.repeat(10)} ${'─'.repeat(8)}`);
|
|
2154
|
+
for (const s of spans) {
|
|
2155
|
+
const dur = s.endTime ? `${s.endTime - s.startTime}ms` : 'ongoing';
|
|
2156
|
+
const statusColor = s.status === 'ok' ? color.green : s.status === 'error' ? color.red : color.dim;
|
|
2157
|
+
console.log(`${color.cyan(s.traceId.slice(0, 12))} ${s.name.padEnd(25).slice(0, 25)} ${dur.padEnd(10)} ${statusColor(s.status)}`);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
// ── A2A Protocol Commands ───────────────────────────────────
|
|
2162
|
+
const a2aCmd = program.command('a2a').description('Google A2A protocol commands');
|
|
2163
|
+
|
|
2164
|
+
a2aCmd
|
|
2165
|
+
.command('serve')
|
|
2166
|
+
.option('-p, --port <port>', 'Port for A2A server', '3001')
|
|
2167
|
+
.description('Start A2A server for this agent')
|
|
2168
|
+
.action(async (opts: { port: string }) => {
|
|
2169
|
+
const port = parseInt(opts.port) || 3001;
|
|
2170
|
+
const { A2AServer } = require('./protocols/a2a');
|
|
2171
|
+
const oad = loadOADFile();
|
|
2172
|
+
const server = new A2AServer(null, { oad, port });
|
|
2173
|
+
await server.start(port);
|
|
2174
|
+
console.log(`${icon.success} A2A server running on http://localhost:${port}`);
|
|
2175
|
+
console.log(`${icon.info} Agent card: http://localhost:${port}/.well-known/agent.json`);
|
|
2176
|
+
});
|
|
2177
|
+
|
|
2178
|
+
a2aCmd
|
|
2179
|
+
.command('card')
|
|
2180
|
+
.description('Print this agent\'s A2A card')
|
|
2181
|
+
.action(() => {
|
|
2182
|
+
const { oadToAgentCard } = require('./protocols/a2a');
|
|
2183
|
+
const oad = loadOADFile();
|
|
2184
|
+
if (!oad) { console.log(`${icon.error} No agent.yaml found`); return; }
|
|
2185
|
+
const card = oadToAgentCard(oad, 'http://localhost:3001');
|
|
2186
|
+
console.log(JSON.stringify(card, null, 2));
|
|
2187
|
+
});
|
|
2188
|
+
|
|
2189
|
+
a2aCmd
|
|
2190
|
+
.command('discover')
|
|
2191
|
+
.argument('<url>', 'Remote agent URL')
|
|
2192
|
+
.description('Fetch remote agent\'s A2A card')
|
|
2193
|
+
.action(async (url: string) => {
|
|
2194
|
+
const { A2AClient } = require('./protocols/a2a');
|
|
2195
|
+
const client = new A2AClient(url);
|
|
2196
|
+
try {
|
|
2197
|
+
const card = await client.getAgentCard();
|
|
2198
|
+
console.log(JSON.stringify(card, null, 2));
|
|
2199
|
+
} catch (err: any) {
|
|
2200
|
+
console.log(`${icon.error} Failed to discover agent: ${err.message}`);
|
|
2201
|
+
}
|
|
2202
|
+
});
|
|
2203
|
+
|
|
2204
|
+
a2aCmd
|
|
2205
|
+
.command('call')
|
|
2206
|
+
.argument('<url>', 'Remote agent URL')
|
|
2207
|
+
.argument('<message>', 'Message to send')
|
|
2208
|
+
.description('Call a remote A2A agent')
|
|
2209
|
+
.action(async (url: string, message: string) => {
|
|
2210
|
+
const { A2AClient } = require('./protocols/a2a');
|
|
2211
|
+
const client = new A2AClient(url);
|
|
2212
|
+
try {
|
|
2213
|
+
const response = await client.sendText(message);
|
|
2214
|
+
console.log(response);
|
|
2215
|
+
} catch (err: any) {
|
|
2216
|
+
console.log(`${icon.error} Call failed: ${err.message}`);
|
|
2217
|
+
}
|
|
2218
|
+
});
|
|
2219
|
+
|
|
2220
|
+
function loadOADFile(): any {
|
|
2221
|
+
const fs = require('fs');
|
|
2222
|
+
const yaml = require('js-yaml');
|
|
2223
|
+
for (const name of ['agent.yaml', 'agent.yml']) {
|
|
2224
|
+
if (fs.existsSync(name)) {
|
|
2225
|
+
return yaml.load(fs.readFileSync(name, 'utf-8'));
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
return null;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// ── MCP Server Commands ────────────────────────────────────
|
|
2232
|
+
const mcpCmd = program.command('mcp').description('MCP server commands — expose agent as MCP tools');
|
|
2233
|
+
|
|
2234
|
+
mcpCmd
|
|
2235
|
+
.command('serve')
|
|
2236
|
+
.option('--http <port>', 'Start HTTP+SSE mode on given port')
|
|
2237
|
+
.description('Start MCP server (stdio by default, --http for HTTP+SSE)')
|
|
2238
|
+
.action(async (opts: { http?: string }) => {
|
|
2239
|
+
const { MCPServer } = require('./protocols/mcp');
|
|
2240
|
+
const { agentToMCPTools, agentToMCPResources } = require('./protocols/mcp');
|
|
2241
|
+
const oad = loadOADFile();
|
|
2242
|
+
const agentName = oad?.metadata?.name || 'opc-agent';
|
|
2243
|
+
const server = new MCPServer({
|
|
2244
|
+
name: agentName,
|
|
2245
|
+
version: oad?.metadata?.version || '1.0.0',
|
|
2246
|
+
});
|
|
2247
|
+
// Register tools from OAD or defaults
|
|
2248
|
+
const { agentToMCPTools: toTools } = require('./protocols/mcp/agent-tools');
|
|
2249
|
+
const mockAgent = { name: agentName, config: { name: agentName } };
|
|
2250
|
+
const tools = toTools(mockAgent);
|
|
2251
|
+
for (const t of tools) server.addTool(t);
|
|
2252
|
+
|
|
2253
|
+
if (opts.http) {
|
|
2254
|
+
const port = parseInt(opts.http) || 3002;
|
|
2255
|
+
await server.serveHTTP(port);
|
|
2256
|
+
console.log(`${icon.success} MCP server (HTTP+SSE) running on http://localhost:${port}`);
|
|
2257
|
+
console.log(`${icon.info} SSE endpoint: http://localhost:${port}/sse`);
|
|
2258
|
+
console.log(`${icon.info} Message endpoint: http://localhost:${port}/message`);
|
|
2259
|
+
console.log(`${icon.info} Tools: ${server.getToolCount()}`);
|
|
2260
|
+
} else {
|
|
2261
|
+
console.error(`${icon.success} MCP server (stdio) started — ${server.getToolCount()} tools`);
|
|
2262
|
+
await server.serveStdio();
|
|
2263
|
+
}
|
|
2264
|
+
});
|
|
2265
|
+
|
|
2266
|
+
mcpCmd
|
|
2267
|
+
.command('tools')
|
|
2268
|
+
.description('List MCP tools that would be exposed')
|
|
2269
|
+
.action(() => {
|
|
2270
|
+
const { agentToMCPTools } = require('./protocols/mcp/agent-tools');
|
|
2271
|
+
const oad = loadOADFile();
|
|
2272
|
+
const agentName = oad?.metadata?.name || 'opc-agent';
|
|
2273
|
+
const tools = agentToMCPTools({ name: agentName });
|
|
2274
|
+
console.log(`\n${icon.gear} MCP Tools for ${color.cyan(agentName)}:\n`);
|
|
2275
|
+
for (const t of tools) {
|
|
2276
|
+
const required = t.inputSchema?.required?.join(', ') || 'none';
|
|
2277
|
+
console.log(` ${color.green(t.name.padEnd(20))} ${t.description}`);
|
|
2278
|
+
console.log(` ${' '.repeat(20)} Required: ${color.dim(required)}`);
|
|
2279
|
+
}
|
|
2280
|
+
console.log();
|
|
2281
|
+
});
|