opc-agent 1.4.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.
Files changed (198) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +91 -32
  3. package/dist/channels/email.d.ts +32 -26
  4. package/dist/channels/email.js +239 -62
  5. package/dist/channels/feishu.d.ts +21 -6
  6. package/dist/channels/feishu.js +225 -126
  7. package/dist/channels/telegram.d.ts +30 -9
  8. package/dist/channels/telegram.js +125 -33
  9. package/dist/channels/websocket.d.ts +46 -3
  10. package/dist/channels/websocket.js +306 -37
  11. package/dist/channels/wechat.d.ts +33 -13
  12. package/dist/channels/wechat.js +229 -42
  13. package/dist/cli.js +1127 -19
  14. package/dist/core/a2a.d.ts +17 -0
  15. package/dist/core/a2a.js +43 -1
  16. package/dist/core/agent.d.ts +39 -0
  17. package/dist/core/agent.js +228 -3
  18. package/dist/core/runtime.d.ts +7 -0
  19. package/dist/core/runtime.js +205 -2
  20. package/dist/core/sandbox.d.ts +26 -0
  21. package/dist/core/sandbox.js +117 -0
  22. package/dist/core/scheduler.d.ts +52 -0
  23. package/dist/core/scheduler.js +168 -0
  24. package/dist/core/subagent.d.ts +28 -0
  25. package/dist/core/subagent.js +65 -0
  26. package/dist/core/workflow-graph.d.ts +93 -0
  27. package/dist/core/workflow-graph.js +247 -0
  28. package/dist/daemon.d.ts +3 -0
  29. package/dist/daemon.js +134 -0
  30. package/dist/doctor.d.ts +15 -0
  31. package/dist/doctor.js +183 -0
  32. package/dist/eval/index.d.ts +65 -0
  33. package/dist/eval/index.js +191 -0
  34. package/dist/index.d.ts +37 -6
  35. package/dist/index.js +75 -3
  36. package/dist/plugins/content-filter.d.ts +7 -0
  37. package/dist/plugins/content-filter.js +25 -0
  38. package/dist/plugins/index.d.ts +42 -0
  39. package/dist/plugins/index.js +108 -2
  40. package/dist/plugins/logger.d.ts +6 -0
  41. package/dist/plugins/logger.js +20 -0
  42. package/dist/plugins/rate-limiter.d.ts +7 -0
  43. package/dist/plugins/rate-limiter.js +35 -0
  44. package/dist/protocols/a2a/client.d.ts +25 -0
  45. package/dist/protocols/a2a/client.js +115 -0
  46. package/dist/protocols/a2a/index.d.ts +6 -0
  47. package/dist/protocols/a2a/index.js +12 -0
  48. package/dist/protocols/a2a/server.d.ts +41 -0
  49. package/dist/protocols/a2a/server.js +295 -0
  50. package/dist/protocols/a2a/types.d.ts +91 -0
  51. package/dist/protocols/a2a/types.js +15 -0
  52. package/dist/protocols/a2a/utils.d.ts +6 -0
  53. package/dist/protocols/a2a/utils.js +47 -0
  54. package/dist/protocols/agui/client.d.ts +10 -0
  55. package/dist/protocols/agui/client.js +75 -0
  56. package/dist/protocols/agui/index.d.ts +4 -0
  57. package/dist/protocols/agui/index.js +25 -0
  58. package/dist/protocols/agui/server.d.ts +37 -0
  59. package/dist/protocols/agui/server.js +191 -0
  60. package/dist/protocols/agui/types.d.ts +107 -0
  61. package/dist/protocols/agui/types.js +17 -0
  62. package/dist/protocols/index.d.ts +2 -0
  63. package/dist/protocols/index.js +19 -0
  64. package/dist/protocols/mcp/agent-tools.d.ts +11 -0
  65. package/dist/protocols/mcp/agent-tools.js +129 -0
  66. package/dist/protocols/mcp/index.d.ts +5 -0
  67. package/dist/protocols/mcp/index.js +11 -0
  68. package/dist/protocols/mcp/server.d.ts +31 -0
  69. package/dist/protocols/mcp/server.js +248 -0
  70. package/dist/protocols/mcp/types.d.ts +92 -0
  71. package/dist/protocols/mcp/types.js +17 -0
  72. package/dist/providers/index.d.ts +5 -1
  73. package/dist/providers/index.js +16 -9
  74. package/dist/publish/index.d.ts +45 -0
  75. package/dist/publish/index.js +350 -0
  76. package/dist/schema/oad.d.ts +859 -67
  77. package/dist/schema/oad.js +47 -3
  78. package/dist/security/approval.d.ts +36 -0
  79. package/dist/security/approval.js +113 -0
  80. package/dist/security/index.d.ts +4 -0
  81. package/dist/security/index.js +8 -0
  82. package/dist/security/keys.d.ts +16 -0
  83. package/dist/security/keys.js +117 -0
  84. package/dist/skills/auto-learn.d.ts +28 -0
  85. package/dist/skills/auto-learn.js +257 -0
  86. package/dist/studio/server.d.ts +63 -0
  87. package/dist/studio/server.js +625 -0
  88. package/dist/studio-ui/index.html +662 -0
  89. package/dist/telemetry/index.d.ts +93 -0
  90. package/dist/telemetry/index.js +285 -0
  91. package/dist/tools/builtin/datetime.d.ts +3 -0
  92. package/dist/tools/builtin/datetime.js +44 -0
  93. package/dist/tools/builtin/file.d.ts +3 -0
  94. package/dist/tools/builtin/file.js +151 -0
  95. package/dist/tools/builtin/index.d.ts +15 -0
  96. package/dist/tools/builtin/index.js +30 -0
  97. package/dist/tools/builtin/shell.d.ts +3 -0
  98. package/dist/tools/builtin/shell.js +43 -0
  99. package/dist/tools/builtin/web.d.ts +3 -0
  100. package/dist/tools/builtin/web.js +37 -0
  101. package/dist/tools/mcp-client.d.ts +24 -0
  102. package/dist/tools/mcp-client.js +119 -0
  103. package/package.json +5 -3
  104. package/scripts/install.ps1 +31 -0
  105. package/scripts/install.sh +40 -0
  106. package/src/channels/email.ts +351 -177
  107. package/src/channels/feishu.ts +349 -236
  108. package/src/channels/telegram.ts +212 -90
  109. package/src/channels/websocket.ts +399 -87
  110. package/src/channels/wechat.ts +329 -149
  111. package/src/cli.ts +1201 -20
  112. package/src/core/a2a.ts +60 -0
  113. package/src/core/agent.ts +420 -152
  114. package/src/core/runtime.ts +174 -0
  115. package/src/core/sandbox.ts +143 -0
  116. package/src/core/scheduler.ts +187 -0
  117. package/src/core/subagent.ts +98 -0
  118. package/src/core/workflow-graph.ts +365 -0
  119. package/src/daemon.ts +96 -0
  120. package/src/doctor.ts +156 -0
  121. package/src/eval/index.ts +211 -0
  122. package/src/eval/suites/basic.json +16 -0
  123. package/src/eval/suites/memory.json +12 -0
  124. package/src/eval/suites/safety.json +14 -0
  125. package/src/index.ts +65 -6
  126. package/src/plugins/content-filter.ts +23 -0
  127. package/src/plugins/index.ts +133 -2
  128. package/src/plugins/logger.ts +18 -0
  129. package/src/plugins/rate-limiter.ts +38 -0
  130. package/src/protocols/a2a/client.ts +132 -0
  131. package/src/protocols/a2a/index.ts +8 -0
  132. package/src/protocols/a2a/server.ts +333 -0
  133. package/src/protocols/a2a/types.ts +88 -0
  134. package/src/protocols/a2a/utils.ts +50 -0
  135. package/src/protocols/agui/client.ts +83 -0
  136. package/src/protocols/agui/index.ts +4 -0
  137. package/src/protocols/agui/server.ts +218 -0
  138. package/src/protocols/agui/types.ts +153 -0
  139. package/src/protocols/index.ts +2 -0
  140. package/src/protocols/mcp/agent-tools.ts +134 -0
  141. package/src/protocols/mcp/index.ts +8 -0
  142. package/src/protocols/mcp/server.ts +262 -0
  143. package/src/protocols/mcp/types.ts +69 -0
  144. package/src/providers/index.ts +354 -339
  145. package/src/publish/index.ts +376 -0
  146. package/src/schema/oad.ts +204 -154
  147. package/src/security/approval.ts +131 -0
  148. package/src/security/index.ts +3 -0
  149. package/src/security/keys.ts +87 -0
  150. package/src/skills/auto-learn.ts +262 -0
  151. package/src/studio/server.ts +629 -0
  152. package/src/studio-ui/index.html +662 -0
  153. package/src/telemetry/index.ts +324 -0
  154. package/src/tools/builtin/datetime.ts +41 -0
  155. package/src/tools/builtin/file.ts +107 -0
  156. package/src/tools/builtin/index.ts +28 -0
  157. package/src/tools/builtin/shell.ts +43 -0
  158. package/src/tools/builtin/web.ts +35 -0
  159. package/src/tools/mcp-client.ts +131 -0
  160. package/src/types/agent-workstation.d.ts +2 -0
  161. package/tests/a2a-protocol.test.ts +285 -0
  162. package/tests/agui-protocol.test.ts +246 -0
  163. package/tests/auto-learn.test.ts +105 -0
  164. package/tests/builtin-tools.test.ts +83 -0
  165. package/tests/channels/discord.test.ts +79 -0
  166. package/tests/channels/email.test.ts +148 -0
  167. package/tests/channels/feishu.test.ts +123 -0
  168. package/tests/channels/telegram.test.ts +129 -0
  169. package/tests/channels/websocket.test.ts +53 -0
  170. package/tests/channels/wechat.test.ts +170 -0
  171. package/tests/chat-cli.test.ts +160 -0
  172. package/tests/cli.test.ts +46 -0
  173. package/tests/daemon.test.ts +135 -0
  174. package/tests/deepbrain-wire.test.ts +234 -0
  175. package/tests/doctor.test.ts +38 -0
  176. package/tests/eval.test.ts +173 -0
  177. package/tests/init-role.test.ts +124 -0
  178. package/tests/mcp-client.test.ts +92 -0
  179. package/tests/mcp-server.test.ts +178 -0
  180. package/tests/plugin-a2a-enhanced.test.ts +230 -0
  181. package/tests/publish.test.ts +231 -0
  182. package/tests/scheduler.test.ts +200 -0
  183. package/tests/security-enhanced.test.ts +233 -0
  184. package/tests/skill-learner.test.ts +161 -0
  185. package/tests/studio.test.ts +229 -0
  186. package/tests/subagent.test.ts +193 -0
  187. package/tests/telegram-discord.test.ts +60 -0
  188. package/tests/telemetry.test.ts +186 -0
  189. package/tests/tools/builtin-extended.test.ts +138 -0
  190. package/tests/workflow-graph.test.ts +279 -0
  191. package/tutorial/customer-service-agent/README.md +612 -0
  192. package/tutorial/customer-service-agent/SOUL.md +26 -0
  193. package/tutorial/customer-service-agent/agent.yaml +63 -0
  194. package/tutorial/customer-service-agent/package.json +19 -0
  195. package/tutorial/customer-service-agent/src/index.ts +69 -0
  196. package/tutorial/customer-service-agent/src/skills/faq.ts +27 -0
  197. package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -0
  198. package/tutorial/customer-service-agent/tsconfig.json +14 -0
package/src/cli.ts CHANGED
@@ -29,7 +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';
33
+ import { Scheduler } from './core/scheduler';
34
+ import type { CronJob } from './core/scheduler';
32
35
  import type { Span } from './traces';
36
+ import { spawn } from 'child_process';
37
+ import { searchRoles, getPopularRoles, getRole, getCategories } from 'agent-workstation';
33
38
 
34
39
  const program = new Command();
35
40
 
@@ -94,7 +99,7 @@ async function select(question: string, options: { value: string; label: string
94
99
  program
95
100
  .name('opc')
96
101
  .description('OPC Agent - Open Agent Framework for business workstations')
97
- .version('1.4.0');
102
+ .version('2.0.0');
98
103
 
99
104
  // ── Init command ─────────────────────────────────────────────
100
105
 
@@ -104,9 +109,226 @@ program
104
109
  .argument('[name]', 'Project name')
105
110
  .option('-t, --template <template>', 'Template to use')
106
111
  .option('-y, --yes', 'Skip prompts, use defaults')
107
- .action(async (nameArg: string | undefined, opts: { template?: string; yes?: boolean }) => {
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 }) => {
108
115
  console.log(`\n${icon.rocket} ${color.bold('OPC Agent - Create New Project')}\n`);
109
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
+
110
332
  const name = opts.yes ? (nameArg ?? 'my-agent') : (nameArg ?? await promptUser('Project name', 'my-agent'));
111
333
  const template = opts.yes
112
334
  ? (opts.template ?? 'customer-service')
@@ -166,15 +388,24 @@ spec:
166
388
  path.join(dir, 'src', 'index.ts'),
167
389
  `import { AgentRuntime } from 'opc-agent';
168
390
  import { EchoSkill } from './skills/echo';
391
+ import { readFileSync, existsSync } from 'fs';
169
392
 
170
393
  async function main() {
171
394
  const runtime = new AgentRuntime();
172
395
 
173
396
  // Load OAD config
174
- await runtime.loadConfig('./agent.yaml');
397
+ const config = await runtime.loadConfig('./agent.yaml');
398
+
399
+ // Load personality and context files
400
+ const soul = existsSync('./SOUL.md') ? readFileSync('./SOUL.md', 'utf-8') : '';
401
+ const context = existsSync('./CONTEXT.md') ? readFileSync('./CONTEXT.md', 'utf-8') : '';
402
+ if (soul || context) {
403
+ const fullPrompt = [soul, context, config.spec.systemPrompt].filter(Boolean).join('\\n\\n');
404
+ config.spec.systemPrompt = fullPrompt;
405
+ }
175
406
 
176
407
  // Initialize agent with channels, memory, etc.
177
- const agent = await runtime.initialize();
408
+ const agent = await runtime.initialize(config);
178
409
 
179
410
  // Register custom skills
180
411
  runtime.registerSkill(new EchoSkill());
@@ -384,10 +615,59 @@ Edit \`agent.yaml\` to customize your agent's personality, skills, and behavior.
384
615
  `,
385
616
  );
386
617
 
618
+ // SOUL.md — agent personality
619
+ const createdDate = new Date().toISOString().split('T')[0];
620
+ fs.writeFileSync(
621
+ path.join(dir, 'SOUL.md'),
622
+ `# ${name} Personality
623
+
624
+ ## Identity
625
+ - Name: ${name}
626
+ - Role: AI Assistant
627
+ - Created: ${createdDate}
628
+
629
+ ## Personality Traits
630
+ - Helpful and professional
631
+ - Concise but thorough
632
+ - Friendly tone
633
+
634
+ ## Communication Style
635
+ - Use clear, simple language
636
+ - Be direct — answer the question first, then explain
637
+ - Use markdown formatting when helpful
638
+
639
+ ## Rules
640
+ - Always be honest about limitations
641
+ - Ask for clarification when the request is ambiguous
642
+ - Never make up information
643
+ `,
644
+ );
645
+
646
+ // CONTEXT.md — project context
647
+ fs.writeFileSync(
648
+ path.join(dir, 'CONTEXT.md'),
649
+ `# Project Context
650
+
651
+ ## About This Agent
652
+ ${name} is an AI agent built with OPC Agent Framework.
653
+
654
+ ## Knowledge Base
655
+ Add project-specific context here. The agent reads this file
656
+ on startup to understand the project context.
657
+
658
+ ## Important Notes
659
+ - Add domain knowledge here
660
+ - Add FAQ items here
661
+ - Add company policies here
662
+ `,
663
+ );
664
+
387
665
  console.log(`\n${icon.success} Created agent project: ${color.bold(name + '/')}`);
388
666
  console.log(` ${icon.file} agent.yaml - Agent definition (OAD)`);
389
667
  console.log(` ${icon.file} src/index.ts - Entry point`);
390
668
  console.log(` ${icon.file} src/skills/echo.ts - Example skill`);
669
+ console.log(` ${icon.file} SOUL.md - Agent personality`);
670
+ console.log(` ${icon.file} CONTEXT.md - Project context`);
391
671
  console.log(` ${icon.file} package.json - Dependencies`);
392
672
  console.log(` ${icon.file} tsconfig.json - TypeScript config`);
393
673
  console.log(` ${icon.file} .env.example - Environment template`);
@@ -400,6 +680,9 @@ Edit \`agent.yaml\` to customize your agent's personality, skills, and behavior.
400
680
  console.log(` 2. npm install`);
401
681
  console.log(` 3. npx tsx src/index.ts ${color.dim('# or: npx opc run')}`);
402
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`);
403
686
  });
404
687
 
405
688
  // ── Chat command ─────────────────────────────────────────────
@@ -414,28 +697,118 @@ program
414
697
 
415
698
  let systemPrompt = 'You are a helpful AI agent.';
416
699
  let model: string | undefined;
700
+ let agentName = 'Agent';
701
+ let agentVersion = '1.0.0';
702
+ let providerName = 'openai';
703
+ let skillNames: string[] = [];
704
+
705
+ // Try loading SOUL.md and CONTEXT.md for enriched system prompt
706
+ const soulPath = path.resolve('SOUL.md');
707
+ const contextPath = path.resolve('CONTEXT.md');
708
+ const soulContent = fs.existsSync(soulPath) ? fs.readFileSync(soulPath, 'utf-8') : '';
709
+ const contextContent = fs.existsSync(contextPath) ? fs.readFileSync(contextPath, 'utf-8') : '';
417
710
 
418
711
  try {
419
712
  const raw = fs.readFileSync(opts.file, 'utf-8');
420
713
  const config = yaml.load(raw) as any;
421
714
  if (config?.spec?.systemPrompt) systemPrompt = config.spec.systemPrompt;
422
715
  if (config?.spec?.model) model = config.spec.model;
423
- console.log(`\n${icon.gear} Loaded agent: ${color.bold(config?.metadata?.name ?? 'unknown')}`);
716
+ if (config?.metadata?.name) agentName = config.metadata.name;
717
+ if (config?.metadata?.version) agentVersion = config.metadata.version;
718
+ if (config?.spec?.provider?.default) providerName = config.spec.provider.default;
719
+ if (config?.spec?.skills) skillNames = config.spec.skills.map((s: any) => s.name);
424
720
  } catch {
425
- console.log(`\n${icon.info} No oad.yaml found, using defaults.`);
721
+ // No config file, use defaults
426
722
  }
427
723
 
724
+ // Prepend SOUL.md and CONTEXT.md to system prompt
725
+ systemPrompt = [soulContent, contextContent, systemPrompt].filter(Boolean).join('\n\n');
726
+
428
727
  const provider = createProvider('openai', model);
429
728
  const history: { role: 'user' | 'assistant' | 'system'; content: string }[] = [];
430
729
 
431
- console.log(`${color.dim('Type your message. Press Ctrl+C to exit.')}\n`);
730
+ // Print startup banner
731
+ const bannerLines = [
732
+ '╔══════════════════════════════════════╗',
733
+ '║ 🤖 OPC Agent — Interactive Chat ║',
734
+ `║ Agent: ${(agentName + ' v' + agentVersion).padEnd(27)}║`,
735
+ `║ Model: ${((providerName + '/' + (model ?? 'default')).slice(0, 27)).padEnd(27)}║`,
736
+ `║ Skills: ${(String(skillNames.length) + ' loaded').padEnd(26)}║`,
737
+ '║ Type /help for commands ║',
738
+ '╚══════════════════════════════════════╝',
739
+ ];
740
+ console.log('\n' + color.cyan(bannerLines.join('\n')) + '\n');
741
+
742
+ if (soulContent) console.log(` ${icon.info} Loaded SOUL.md`);
743
+ if (contextContent) console.log(` ${icon.info} Loaded CONTEXT.md`);
744
+ if (soulContent || contextContent) console.log();
745
+
746
+ const rl = readline.createInterface({
747
+ input: process.stdin,
748
+ output: process.stdout,
749
+ historySize: 100,
750
+ });
751
+
752
+ const handleSlashCommand = (cmd: string): boolean => {
753
+ const lower = cmd.toLowerCase().trim();
754
+ if (lower === '/quit' || lower === '/exit') {
755
+ console.log(`\n${color.dim('Goodbye! 👋')}`);
756
+ process.exit(0);
757
+ }
758
+ if (lower === '/help') {
759
+ console.log(`\n ${color.bold('Available commands:')}`);
760
+ console.log(` ${color.cyan('/help')} — Show this help`);
761
+ console.log(` ${color.cyan('/quit')} — Exit chat (/exit also works)`);
762
+ console.log(` ${color.cyan('/clear')} — Clear conversation history`);
763
+ console.log(` ${color.cyan('/skills')} — List registered skills`);
764
+ console.log(` ${color.cyan('/memory')} — Show memory stats`);
765
+ console.log(` ${color.cyan('/info')} — Show agent info\n`);
766
+ return true;
767
+ }
768
+ if (lower === '/clear') {
769
+ history.length = 0;
770
+ console.log(`\n ${icon.success} Conversation history cleared.\n`);
771
+ return true;
772
+ }
773
+ if (lower === '/skills') {
774
+ if (skillNames.length === 0) {
775
+ console.log(`\n ${icon.info} No skills registered.\n`);
776
+ } else {
777
+ console.log(`\n ${color.bold('Registered skills:')}`);
778
+ skillNames.forEach((s) => console.log(` • ${color.cyan(s)}`));
779
+ console.log();
780
+ }
781
+ return true;
782
+ }
783
+ if (lower === '/memory') {
784
+ console.log(`\n ${color.bold('Memory stats:')}`);
785
+ console.log(` Messages in history: ${color.cyan(String(history.length))}`);
786
+ console.log(` Characters: ${color.cyan(String(history.reduce((a, m) => a + m.content.length, 0)))}\n`);
787
+ return true;
788
+ }
789
+ if (lower === '/info') {
790
+ console.log(`\n ${color.bold('Agent Info:')}`);
791
+ console.log(` Name: ${color.cyan(agentName)}`);
792
+ console.log(` Version: ${color.cyan(agentVersion)}`);
793
+ console.log(` Provider: ${color.cyan(providerName)}`);
794
+ console.log(` Model: ${color.cyan(model ?? 'default')}`);
795
+ console.log(` Skills: ${color.cyan(String(skillNames.length))}\n`);
796
+ return true;
797
+ }
798
+ return false;
799
+ };
432
800
 
433
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
434
801
  const ask = (): void => {
435
802
  rl.question(color.cyan('You: '), async (input) => {
436
803
  const text = input.trim();
437
804
  if (!text) { ask(); return; }
438
805
 
806
+ // Handle slash commands
807
+ if (text.startsWith('/') && handleSlashCommand(text)) {
808
+ ask();
809
+ return;
810
+ }
811
+
439
812
  history.push({ role: 'user', content: text });
440
813
 
441
814
  // Build messages for provider
@@ -472,7 +845,7 @@ program
472
845
  };
473
846
 
474
847
  rl.on('close', () => {
475
- console.log(`\n${color.dim('Goodbye!')}`);
848
+ console.log(`\n${color.dim('Goodbye! 👋')}`);
476
849
  process.exit(0);
477
850
  });
478
851
 
@@ -520,6 +893,33 @@ program
520
893
  console.log(` Model: ${s.model}`);
521
894
  console.log(` Channels: ${s.channels.map((c: any) => c.type).join(', ') || color.dim('(none)')}`);
522
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
+
523
923
  console.log();
524
924
  } catch (err) {
525
925
  console.error(`${icon.error} Failed to read OAD:`, err instanceof Error ? err.message : err);
@@ -855,23 +1255,87 @@ kbCmd.command('clear').action(() => {
855
1255
 
856
1256
  // 📦 Package commands ───────────────────────────────────
857
1257
 
1258
+ import { AgentPackager, AgentPublisher, AgentInstaller } from './publish';
1259
+
858
1260
  program
859
1261
  .command('publish')
860
- .description('Package agent for distribution')
861
- .option('-f, --file <file>', 'OAD file', 'oad.yaml')
862
- .option('-o, --output <dir>', 'Output directory', '.')
863
- .option('--include-kb', 'Include knowledge base')
864
- .action(async () => {
865
- console.log(`\n${icon.package} Agent packaging coming soon.\n`);
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`);
866
1327
  });
867
1328
 
868
1329
  program
869
1330
  .command('install')
870
- .description('Install agent from package')
871
- .argument('<source>', 'Package file path or URL')
872
- .option('-d, --dir <dir>', 'Install directory')
873
- .action(async () => {
874
- console.log(`\n${icon.package} Agent install coming soon.\n`);
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();
875
1339
  });
876
1340
 
877
1341
  // 🔌 Plugin commands ────────────────────────────────────────
@@ -919,6 +1383,77 @@ pluginCmd.command('add')
919
1383
  }
920
1384
  });
921
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
+
922
1457
  // 🔄 Migrate command ────────────────────────────────────────
923
1458
 
924
1459
  program
@@ -1096,5 +1631,651 @@ program
1096
1631
  }
1097
1632
  });
1098
1633
 
1634
+ // ── Daemon commands (start/stop/status) ─────────────────────
1635
+
1636
+ const OPC_DIR = path.resolve('.opc');
1637
+
1638
+ program
1639
+ .command('start')
1640
+ .description('Start agent as a background daemon')
1641
+ .option('-f, --file <file>', 'OAD file (agent.yaml or oad.yaml)')
1642
+ .action(async () => {
1643
+ if (!fs.existsSync(OPC_DIR)) fs.mkdirSync(OPC_DIR, { recursive: true });
1644
+ const pidFile = path.join(OPC_DIR, 'agent.pid');
1645
+
1646
+ // Check if already running
1647
+ if (fs.existsSync(pidFile)) {
1648
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
1649
+ try { process.kill(pid, 0); console.log(`${icon.warn} Agent already running (PID ${pid}).`); return; } catch { /* stale */ }
1650
+ }
1651
+
1652
+ // Find daemon entry point
1653
+ const daemonScript = path.join(__dirname, 'daemon.js');
1654
+ if (!fs.existsSync(daemonScript)) {
1655
+ console.error(`${icon.error} Daemon script not found. Run ${color.cyan('npm run build')} first.`);
1656
+ process.exit(1);
1657
+ }
1658
+
1659
+ const logFile = path.join(OPC_DIR, 'agent.log');
1660
+ const out = fs.openSync(logFile, 'a');
1661
+ const err = fs.openSync(logFile, 'a');
1662
+
1663
+ const child = spawn(process.execPath, [daemonScript], {
1664
+ detached: true,
1665
+ stdio: ['ignore', out, err],
1666
+ cwd: process.cwd(),
1667
+ env: process.env,
1668
+ });
1669
+
1670
+ child.unref();
1671
+
1672
+ // Wait briefly for PID file
1673
+ await new Promise(r => setTimeout(r, 1000));
1674
+
1675
+ if (fs.existsSync(pidFile)) {
1676
+ const pid = fs.readFileSync(pidFile, 'utf-8').trim();
1677
+ console.log(`${icon.success} Agent started (PID ${pid})`);
1678
+ console.log(` ${color.dim('Logs:')} ${logFile}`);
1679
+ console.log(` ${color.dim('Stop:')} opc stop`);
1680
+ } else {
1681
+ console.log(`${icon.success} Agent starting... (PID ${child.pid})`);
1682
+ console.log(` ${color.dim('Logs:')} ${logFile}`);
1683
+ }
1684
+ });
1685
+
1686
+ program
1687
+ .command('stop')
1688
+ .description('Stop the background daemon')
1689
+ .action(() => {
1690
+ const pidFile = path.join(OPC_DIR, 'agent.pid');
1691
+ if (!fs.existsSync(pidFile)) {
1692
+ console.log(`${icon.info} No running agent found.`);
1693
+ return;
1694
+ }
1695
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
1696
+ try {
1697
+ // On Windows, process.kill with SIGTERM may not work; use taskkill
1698
+ if (process.platform === 'win32') {
1699
+ const { execSync } = require('child_process');
1700
+ try { execSync(`taskkill /PID ${pid} /T /F`, { stdio: 'ignore' }); } catch { /* ignore */ }
1701
+ } else {
1702
+ process.kill(pid, 'SIGTERM');
1703
+ }
1704
+ console.log(`${icon.success} Sent stop signal to PID ${pid}`);
1705
+ } catch {
1706
+ console.log(`${icon.warn} Process ${pid} not found (may have already stopped).`);
1707
+ }
1708
+ try { fs.unlinkSync(pidFile); } catch { /* ignore */ }
1709
+ });
1710
+
1711
+ program
1712
+ .command('status')
1713
+ .description('Check daemon status')
1714
+ .action(() => {
1715
+ const pidFile = path.join(OPC_DIR, 'agent.pid');
1716
+ if (!fs.existsSync(pidFile)) {
1717
+ console.log(`\n Status: ${color.red('stopped')}\n`);
1718
+ return;
1719
+ }
1720
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
1721
+ let running = false;
1722
+ try { process.kill(pid, 0); running = true; } catch { /* not running */ }
1723
+
1724
+ if (!running) {
1725
+ console.log(`\n Status: ${color.red('stopped')} (stale PID file)`);
1726
+ try { fs.unlinkSync(pidFile); } catch { /* ignore */ }
1727
+ console.log();
1728
+ return;
1729
+ }
1730
+
1731
+ // Uptime
1732
+ const startedFile = path.join(OPC_DIR, 'started');
1733
+ let uptime = '';
1734
+ if (fs.existsSync(startedFile)) {
1735
+ const startedMs = parseInt(fs.readFileSync(startedFile, 'utf-8').trim(), 10);
1736
+ const secs = Math.floor((Date.now() - startedMs) / 1000);
1737
+ const h = Math.floor(secs / 3600);
1738
+ const m = Math.floor((secs % 3600) / 60);
1739
+ const s = secs % 60;
1740
+ uptime = `${h}h ${m}m ${s}s`;
1741
+ }
1742
+
1743
+ // Agent name from config
1744
+ let agentName = 'unknown';
1745
+ for (const f of ['agent.yaml', 'oad.yaml']) {
1746
+ if (fs.existsSync(f)) {
1747
+ try {
1748
+ const raw = fs.readFileSync(f, 'utf-8');
1749
+ const cfg = yaml.load(raw) as any;
1750
+ if (cfg?.metadata?.name) { agentName = cfg.metadata.name; break; }
1751
+ } catch { /* ignore */ }
1752
+ }
1753
+ }
1754
+
1755
+ console.log(`\n Status: ${color.green('running')}`);
1756
+ console.log(` PID: ${pid}`);
1757
+ console.log(` Agent: ${color.cyan(agentName)}`);
1758
+ if (uptime) console.log(` Uptime: ${uptime}`);
1759
+ console.log();
1760
+ });
1761
+
1762
+ // ── Jobs commands ────────────────────────────────────────────
1763
+
1764
+ const jobsCmd = program.command('jobs').description('Manage scheduled jobs');
1765
+
1766
+ jobsCmd
1767
+ .command('list', { isDefault: true })
1768
+ .description('List all scheduled jobs')
1769
+ .option('-f, --file <file>', 'OAD file', 'oad.yaml')
1770
+ .action(async (opts: { file: string }) => {
1771
+ const jobs = loadJobsFromConfig(opts.file);
1772
+ if (jobs.length === 0) {
1773
+ console.log(`\n${icon.info} No scheduled jobs defined in config.\n`);
1774
+ return;
1775
+ }
1776
+ console.log(`\n${icon.gear} ${color.bold('Scheduled Jobs')}\n`);
1777
+ for (const job of jobs) {
1778
+ const status = job.enabled ? color.green('enabled') : color.dim('disabled');
1779
+ const next = job.nextRun ? job.nextRun.toLocaleString() : color.dim('N/A');
1780
+ console.log(` ${color.cyan(job.id.padEnd(20))} ${job.name}`);
1781
+ console.log(` ${''.padEnd(20)} Schedule: ${color.dim(job.schedule)} | Status: ${status} | Next: ${next}`);
1782
+ console.log();
1783
+ }
1784
+ });
1785
+
1786
+ jobsCmd
1787
+ .command('run')
1788
+ .argument('<id>', 'Job ID to run')
1789
+ .option('-f, --file <file>', 'OAD file', 'oad.yaml')
1790
+ .description('Manually trigger a scheduled job')
1791
+ .action(async (id: string, opts: { file: string }) => {
1792
+ const jobs = loadJobsFromConfig(opts.file);
1793
+ const job = jobs.find(j => j.id === id || j.name === id);
1794
+ if (!job) {
1795
+ console.error(`${icon.error} Job "${id}" not found. Available: ${jobs.map(j => j.id).join(', ')}`);
1796
+ process.exit(1);
1797
+ }
1798
+ console.log(`${icon.info} Running job "${color.bold(job.name)}"...`);
1799
+ console.log(` Task: ${color.dim(job.task)}`);
1800
+ console.log(`\n${icon.warn} Manual job execution requires a running daemon. Use ${color.cyan('opc start')} first.\n`);
1801
+ });
1802
+
1803
+ function loadJobsFromConfig(file: string): CronJob[] {
1804
+ try {
1805
+ const raw = fs.readFileSync(file, 'utf-8');
1806
+ const config = yaml.load(raw) as any;
1807
+ const jobConfigs = config?.spec?.scheduler?.jobs ?? [];
1808
+ const { parseCron } = require('./core/scheduler');
1809
+ return jobConfigs.map((j: any, i: number) => {
1810
+ const id = j.id || j.name?.toLowerCase().replace(/\s+/g, '-') || `job-${i}`;
1811
+ const parsed = parseCron(j.schedule);
1812
+ // Compute next run
1813
+ const now = new Date();
1814
+ let nextRun: Date | undefined;
1815
+ const d = new Date(now);
1816
+ d.setSeconds(0, 0);
1817
+ d.setMinutes(d.getMinutes() + 1);
1818
+ for (let k = 0; k < 48 * 60; k++) {
1819
+ const { cronMatches } = require('./core/scheduler');
1820
+ if (cronMatches(parsed, d)) { nextRun = new Date(d); break; }
1821
+ d.setMinutes(d.getMinutes() + 1);
1822
+ }
1823
+ return {
1824
+ id,
1825
+ name: j.name || id,
1826
+ schedule: j.schedule,
1827
+ task: j.task || '',
1828
+ enabled: j.enabled !== false,
1829
+ nextRun,
1830
+ } as CronJob;
1831
+ });
1832
+ } catch {
1833
+ return [];
1834
+ }
1835
+ }
1836
+
1837
+ // ── Skills commands ──────────────────────────────────────────
1838
+
1839
+ const skillsCmd = program.command('skills').description('Manage learned skills');
1840
+
1841
+ skillsCmd
1842
+ .command('list', { isDefault: true })
1843
+ .description('List all learned skills')
1844
+ .option('-d, --dir <dir>', 'Skills directory', '.opc/skills')
1845
+ .action(async (opts: { dir: string }) => {
1846
+ const { SkillLearner } = await import('./skills/auto-learn');
1847
+ const learner = new SkillLearner(opts.dir);
1848
+ const skills = await learner.loadLearnedSkills();
1849
+ if (skills.length === 0) {
1850
+ console.log(`\n${icon.info} No learned skills yet.\n`);
1851
+ console.log(` Skills are auto-created from conversations when learning is enabled.`);
1852
+ console.log(` Directory: ${color.dim(path.resolve(opts.dir))}\n`);
1853
+ return;
1854
+ }
1855
+ console.log(`\n${icon.gear} ${color.bold('Learned Skills')} (${skills.length})\n`);
1856
+ for (const skill of skills) {
1857
+ console.log(` ${color.cyan(skill.name.padEnd(24))} ${skill.description}`);
1858
+ console.log(` ${''.padEnd(24)} v${skill.version} | used ${skill.usageCount}x | trigger: ${color.dim(skill.trigger)}`);
1859
+ console.log();
1860
+ }
1861
+ });
1862
+
1863
+ skillsCmd
1864
+ .command('show')
1865
+ .argument('<name>', 'Skill name')
1866
+ .option('-d, --dir <dir>', 'Skills directory', '.opc/skills')
1867
+ .description('Show details of a learned skill')
1868
+ .action(async (name: string, opts: { dir: string }) => {
1869
+ const skillPath = path.join(opts.dir, `${name}.md`);
1870
+ if (!fs.existsSync(skillPath)) {
1871
+ console.error(`${icon.error} Skill "${name}" not found at ${skillPath}`);
1872
+ process.exit(1);
1873
+ }
1874
+ const content = fs.readFileSync(skillPath, 'utf-8');
1875
+ console.log(`\n${content}`);
1876
+ });
1877
+
1878
+ skillsCmd
1879
+ .command('remove')
1880
+ .argument('<name>', 'Skill name')
1881
+ .option('-d, --dir <dir>', 'Skills directory', '.opc/skills')
1882
+ .description('Remove a learned skill')
1883
+ .action(async (name: string, opts: { dir: string }) => {
1884
+ const skillPath = path.join(opts.dir, `${name}.md`);
1885
+ if (!fs.existsSync(skillPath)) {
1886
+ console.error(`${icon.error} Skill "${name}" not found.`);
1887
+ process.exit(1);
1888
+ }
1889
+ fs.unlinkSync(skillPath);
1890
+ console.log(`${icon.success} Removed skill "${color.cyan(name)}".`);
1891
+ });
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
+
1099
1992
  program.parse();
1100
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
+ });