opc-agent 1.3.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +14 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +13 -0
  4. package/.github/workflows/ci.yml +24 -0
  5. package/CHANGELOG.md +48 -63
  6. package/CONTRIBUTING.md +21 -60
  7. package/README.md +284 -348
  8. package/README.zh-CN.md +415 -415
  9. package/dist/channels/slack.js +93 -10
  10. package/dist/channels/telegram.d.ts +30 -9
  11. package/dist/channels/telegram.js +125 -33
  12. package/dist/channels/web.d.ts +10 -0
  13. package/dist/channels/web.js +33 -2
  14. package/dist/cli.js +667 -65
  15. package/dist/core/agent.d.ts +23 -0
  16. package/dist/core/agent.js +120 -3
  17. package/dist/core/runtime.d.ts +5 -0
  18. package/dist/core/runtime.js +71 -0
  19. package/dist/core/scheduler.d.ts +52 -0
  20. package/dist/core/scheduler.js +168 -0
  21. package/dist/core/subagent.d.ts +28 -0
  22. package/dist/core/subagent.js +65 -0
  23. package/dist/daemon.d.ts +3 -0
  24. package/dist/daemon.js +134 -0
  25. package/dist/deploy/hermes.js +22 -22
  26. package/dist/deploy/openclaw.js +31 -40
  27. package/dist/index.d.ts +10 -10
  28. package/dist/index.js +22 -15
  29. package/dist/providers/index.d.ts +6 -2
  30. package/dist/providers/index.js +22 -9
  31. package/dist/schema/oad.d.ts +180 -6
  32. package/dist/schema/oad.js +12 -1
  33. package/dist/skills/auto-learn.d.ts +28 -0
  34. package/dist/skills/auto-learn.js +257 -0
  35. package/dist/templates/code-reviewer.d.ts +0 -8
  36. package/dist/templates/code-reviewer.js +5 -9
  37. package/dist/templates/customer-service.d.ts +0 -8
  38. package/dist/templates/customer-service.js +2 -6
  39. package/dist/templates/data-analyst.d.ts +0 -8
  40. package/dist/templates/data-analyst.js +5 -9
  41. package/dist/templates/knowledge-base.d.ts +0 -8
  42. package/dist/templates/knowledge-base.js +2 -6
  43. package/dist/templates/sales-assistant.d.ts +0 -8
  44. package/dist/templates/sales-assistant.js +4 -8
  45. package/dist/templates/teacher.d.ts +0 -8
  46. package/dist/templates/teacher.js +6 -10
  47. package/dist/tools/builtin/datetime.d.ts +3 -0
  48. package/dist/tools/builtin/datetime.js +44 -0
  49. package/dist/tools/builtin/file.d.ts +3 -0
  50. package/dist/tools/builtin/file.js +151 -0
  51. package/dist/tools/builtin/index.d.ts +15 -0
  52. package/dist/tools/builtin/index.js +30 -0
  53. package/dist/tools/builtin/shell.d.ts +3 -0
  54. package/dist/tools/builtin/shell.js +43 -0
  55. package/dist/tools/builtin/web.d.ts +3 -0
  56. package/dist/tools/builtin/web.js +37 -0
  57. package/dist/tools/mcp-client.d.ts +24 -0
  58. package/dist/tools/mcp-client.js +119 -0
  59. package/dist/traces/index.d.ts +49 -0
  60. package/dist/traces/index.js +102 -0
  61. package/docs/.vitepress/config.ts +103 -103
  62. package/docs/api/cli.md +48 -48
  63. package/docs/api/oad-schema.md +64 -64
  64. package/docs/api/sdk.md +80 -80
  65. package/docs/guide/concepts.md +51 -51
  66. package/docs/guide/configuration.md +79 -79
  67. package/docs/guide/deployment.md +42 -42
  68. package/docs/guide/getting-started.md +44 -44
  69. package/docs/guide/templates.md +28 -28
  70. package/docs/guide/testing.md +84 -84
  71. package/docs/index.md +27 -27
  72. package/docs/zh/api/cli.md +54 -54
  73. package/docs/zh/api/oad-schema.md +87 -87
  74. package/docs/zh/api/sdk.md +102 -102
  75. package/docs/zh/guide/concepts.md +104 -104
  76. package/docs/zh/guide/configuration.md +135 -135
  77. package/docs/zh/guide/deployment.md +81 -81
  78. package/docs/zh/guide/getting-started.md +82 -82
  79. package/docs/zh/guide/templates.md +84 -84
  80. package/docs/zh/guide/testing.md +88 -88
  81. package/docs/zh/index.md +27 -27
  82. package/examples/README.md +22 -0
  83. package/examples/basic-agent.ts +90 -0
  84. package/examples/brain-integration.ts +71 -0
  85. package/examples/customer-service-demo/README.md +90 -90
  86. package/examples/customer-service-demo/oad.yaml +107 -107
  87. package/examples/multi-channel.ts +74 -0
  88. package/package.json +1 -1
  89. package/src/analytics/index.ts +66 -66
  90. package/src/channels/discord.ts +192 -192
  91. package/src/channels/email.ts +177 -177
  92. package/src/channels/feishu.ts +236 -236
  93. package/src/channels/index.ts +15 -15
  94. package/src/channels/slack.ts +217 -160
  95. package/src/channels/telegram.ts +155 -33
  96. package/src/channels/voice.ts +106 -106
  97. package/src/channels/web.ts +38 -2
  98. package/src/channels/webhook.ts +199 -199
  99. package/src/channels/websocket.ts +87 -87
  100. package/src/channels/wechat.ts +149 -149
  101. package/src/cli.ts +697 -63
  102. package/src/core/a2a.ts +143 -143
  103. package/src/core/agent.ts +146 -3
  104. package/src/core/analytics-engine.ts +186 -186
  105. package/src/core/auth.ts +57 -57
  106. package/src/core/cache.ts +141 -141
  107. package/src/core/compose.ts +77 -77
  108. package/src/core/config.ts +14 -14
  109. package/src/core/errors.ts +148 -148
  110. package/src/core/hitl.ts +138 -138
  111. package/src/core/logger.ts +57 -57
  112. package/src/core/orchestrator.ts +215 -215
  113. package/src/core/performance.ts +187 -187
  114. package/src/core/rate-limiter.ts +128 -128
  115. package/src/core/room.ts +109 -109
  116. package/src/core/runtime.ts +230 -152
  117. package/src/core/sandbox.ts +101 -101
  118. package/src/core/scheduler.ts +187 -0
  119. package/src/core/security.ts +171 -171
  120. package/src/core/subagent.ts +98 -0
  121. package/src/core/types.ts +68 -68
  122. package/src/core/versioning.ts +106 -106
  123. package/src/core/watch.ts +178 -178
  124. package/src/core/workflow.ts +235 -235
  125. package/src/daemon.ts +96 -0
  126. package/src/deploy/hermes.ts +156 -156
  127. package/src/deploy/openclaw.ts +190 -200
  128. package/src/i18n/index.ts +216 -216
  129. package/src/index.ts +14 -10
  130. package/src/memory/deepbrain.ts +108 -108
  131. package/src/memory/index.ts +34 -34
  132. package/src/plugins/index.ts +208 -208
  133. package/src/providers/index.ts +354 -331
  134. package/src/schema/oad.ts +14 -2
  135. package/src/skills/auto-learn.ts +262 -0
  136. package/src/skills/base.ts +16 -16
  137. package/src/skills/document.ts +100 -100
  138. package/src/skills/http.ts +35 -35
  139. package/src/skills/index.ts +27 -27
  140. package/src/skills/scheduler.ts +80 -80
  141. package/src/skills/webhook-trigger.ts +59 -59
  142. package/src/templates/code-reviewer.ts +30 -34
  143. package/src/templates/customer-service.ts +76 -80
  144. package/src/templates/data-analyst.ts +66 -70
  145. package/src/templates/executive-assistant.ts +71 -71
  146. package/src/templates/financial-advisor.ts +60 -60
  147. package/src/templates/knowledge-base.ts +27 -31
  148. package/src/templates/legal-assistant.ts +71 -71
  149. package/src/templates/sales-assistant.ts +75 -79
  150. package/src/templates/teacher.ts +75 -79
  151. package/src/testing/index.ts +181 -181
  152. package/src/tools/builtin/datetime.ts +41 -0
  153. package/src/tools/builtin/file.ts +107 -0
  154. package/src/tools/builtin/index.ts +28 -0
  155. package/src/tools/builtin/shell.ts +43 -0
  156. package/src/tools/builtin/web.ts +35 -0
  157. package/src/tools/calculator.ts +73 -73
  158. package/src/tools/datetime.ts +149 -149
  159. package/src/tools/json-transform.ts +187 -187
  160. package/src/tools/mcp-client.ts +131 -0
  161. package/src/tools/mcp.ts +76 -76
  162. package/src/tools/text-analysis.ts +116 -116
  163. package/src/traces/index.ts +132 -0
  164. package/templates/Dockerfile +15 -15
  165. package/templates/code-reviewer/README.md +27 -27
  166. package/templates/code-reviewer/oad.yaml +41 -41
  167. package/templates/customer-service/README.md +22 -22
  168. package/templates/customer-service/oad.yaml +36 -36
  169. package/templates/docker-compose.yml +21 -21
  170. package/templates/ecommerce-assistant/README.md +45 -45
  171. package/templates/ecommerce-assistant/oad.yaml +47 -47
  172. package/templates/knowledge-base/README.md +28 -28
  173. package/templates/knowledge-base/oad.yaml +38 -38
  174. package/templates/sales-assistant/README.md +26 -26
  175. package/templates/sales-assistant/oad.yaml +43 -43
  176. package/templates/tech-support/README.md +43 -43
  177. package/templates/tech-support/oad.yaml +45 -45
  178. package/test-agent/Dockerfile +9 -0
  179. package/test-agent/README.md +50 -0
  180. package/test-agent/agent.yaml +23 -0
  181. package/test-agent/docker-compose.yml +11 -0
  182. package/test-agent/oad.yaml +31 -0
  183. package/test-agent/package-lock.json +1492 -0
  184. package/test-agent/package.json +18 -0
  185. package/test-agent/src/index.ts +24 -0
  186. package/test-agent/src/skills/echo.ts +15 -0
  187. package/test-agent/tsconfig.json +25 -0
  188. package/tests/a2a.test.ts +66 -66
  189. package/tests/agent.test.ts +72 -72
  190. package/tests/analytics.test.ts +50 -50
  191. package/tests/auto-learn.test.ts +105 -0
  192. package/tests/builtin-tools.test.ts +83 -0
  193. package/tests/channel.test.ts +39 -39
  194. package/tests/cli.test.ts +46 -0
  195. package/tests/e2e.test.ts +134 -134
  196. package/tests/errors.test.ts +83 -83
  197. package/tests/hitl.test.ts +71 -71
  198. package/tests/i18n.test.ts +41 -41
  199. package/tests/mcp.test.ts +54 -54
  200. package/tests/oad.test.ts +68 -68
  201. package/tests/performance.test.ts +115 -115
  202. package/tests/plugin.test.ts +74 -74
  203. package/tests/room.test.ts +106 -106
  204. package/tests/runtime.test.ts +42 -42
  205. package/tests/sandbox.test.ts +46 -46
  206. package/tests/security.test.ts +60 -60
  207. package/tests/subagent.test.ts +130 -0
  208. package/tests/telegram-discord.test.ts +60 -0
  209. package/tests/templates.test.ts +77 -77
  210. package/tests/v070.test.ts +76 -76
  211. package/tests/versioning.test.ts +75 -75
  212. package/tests/voice.test.ts +61 -61
  213. package/tests/webhook.test.ts +29 -29
  214. package/tests/workflow.test.ts +143 -143
  215. package/tsconfig.json +19 -19
  216. package/vitest.config.ts +9 -9
  217. package/dist/core/dashboard.d.ts +0 -35
  218. package/dist/core/dashboard.js +0 -157
  219. package/dist/core/priority.d.ts +0 -52
  220. package/dist/core/priority.js +0 -102
  221. package/src/core/dashboard.ts +0 -219
  222. package/src/core/priority.ts +0 -140
  223. package/src/dtv/data.ts +0 -29
  224. package/src/dtv/trust.ts +0 -43
  225. package/src/dtv/value.ts +0 -47
  226. package/src/marketplace/index.ts +0 -223
package/src/cli.ts CHANGED
@@ -27,9 +27,12 @@ import { WorkflowEngine } from './core/workflow';
27
27
  import { VersionManager } from './core/versioning';
28
28
  import { createProvider } from './providers';
29
29
  import { KnowledgeBase } from './core/knowledge';
30
- import { publishAgent, installAgent } from './marketplace';
31
30
 
32
31
  import { PluginManager, createLoggingPlugin, createAnalyticsPlugin, createRateLimitPlugin } from './plugins';
32
+ import { Scheduler } from './core/scheduler';
33
+ import type { CronJob } from './core/scheduler';
34
+ import type { Span } from './traces';
35
+ import { spawn } from 'child_process';
33
36
 
34
37
  const program = new Command();
35
38
 
@@ -94,7 +97,7 @@ async function select(question: string, options: { value: string; label: string
94
97
  program
95
98
  .name('opc')
96
99
  .description('OPC Agent - Open Agent Framework for business workstations')
97
- .version('1.0.0');
100
+ .version('2.0.0');
98
101
 
99
102
  // ── Init command ─────────────────────────────────────────────
100
103
 
@@ -119,6 +122,8 @@ program
119
122
  }
120
123
 
121
124
  fs.mkdirSync(dir, { recursive: true });
125
+ fs.mkdirSync(path.join(dir, 'src', 'skills'), { recursive: true });
126
+
122
127
  const factory = TEMPLATES[template]?.factory ?? createCustomerServiceConfig;
123
128
  const config = factory();
124
129
  config.metadata.name = name;
@@ -130,6 +135,122 @@ program
130
135
 
131
136
  fs.writeFileSync(path.join(dir, 'oad.yaml'), yaml.dump(config, { lineWidth: 120 }));
132
137
 
138
+ // agent.yaml — standalone OAD config for runtime usage
139
+ fs.writeFileSync(
140
+ path.join(dir, 'agent.yaml'),
141
+ `apiVersion: opc/v1
142
+ kind: Agent
143
+ metadata:
144
+ name: ${name}
145
+ version: 1.0.0
146
+ description: My AI Agent
147
+ spec:
148
+ model: qwen2.5
149
+ provider:
150
+ default: ollama
151
+ systemPrompt: |
152
+ You are a helpful AI assistant named ${name}.
153
+ Be concise, helpful, and friendly.
154
+ channels:
155
+ - type: web
156
+ port: 3000
157
+ memory:
158
+ shortTerm: true
159
+ longTerm:
160
+ provider: deepbrain
161
+ skills:
162
+ - name: echo
163
+ description: Echo test skill
164
+ `,
165
+ );
166
+
167
+ // src/index.ts — entry point
168
+ fs.writeFileSync(
169
+ path.join(dir, 'src', 'index.ts'),
170
+ `import { AgentRuntime } from 'opc-agent';
171
+ import { EchoSkill } from './skills/echo';
172
+ import { readFileSync, existsSync } from 'fs';
173
+
174
+ async function main() {
175
+ const runtime = new AgentRuntime();
176
+
177
+ // Load OAD config
178
+ const config = await runtime.loadConfig('./agent.yaml');
179
+
180
+ // Load personality and context files
181
+ const soul = existsSync('./SOUL.md') ? readFileSync('./SOUL.md', 'utf-8') : '';
182
+ const context = existsSync('./CONTEXT.md') ? readFileSync('./CONTEXT.md', 'utf-8') : '';
183
+ if (soul || context) {
184
+ const fullPrompt = [soul, context, config.spec.systemPrompt].filter(Boolean).join('\\n\\n');
185
+ config.spec.systemPrompt = fullPrompt;
186
+ }
187
+
188
+ // Initialize agent with channels, memory, etc.
189
+ const agent = await runtime.initialize(config);
190
+
191
+ // Register custom skills
192
+ runtime.registerSkill(new EchoSkill());
193
+
194
+ // Start serving
195
+ await runtime.start();
196
+
197
+ console.log('🤖 Agent is running!');
198
+ console.log(' Web UI: http://localhost:3000');
199
+ console.log(' Press Ctrl+C to stop');
200
+ }
201
+
202
+ main().catch(console.error);
203
+ `,
204
+ );
205
+
206
+ // src/skills/echo.ts — example skill
207
+ fs.writeFileSync(
208
+ path.join(dir, 'src', 'skills', 'echo.ts'),
209
+ `import { BaseSkill } from 'opc-agent';
210
+ import type { AgentContext, Message, SkillResult } from 'opc-agent';
211
+
212
+ export class EchoSkill extends BaseSkill {
213
+ name = 'echo';
214
+ description = 'Echo back the message (test skill)';
215
+
216
+ async execute(context: AgentContext, message: Message): Promise<SkillResult> {
217
+ if (message.content.toLowerCase().startsWith('/echo ')) {
218
+ const text = message.content.slice(6);
219
+ return this.match(\`🔊 Echo: \${text}\`);
220
+ }
221
+ return this.noMatch();
222
+ }
223
+ }
224
+ `,
225
+ );
226
+
227
+ // tsconfig.json
228
+ fs.writeFileSync(
229
+ path.join(dir, 'tsconfig.json'),
230
+ JSON.stringify(
231
+ {
232
+ compilerOptions: {
233
+ target: 'ES2022',
234
+ module: 'commonjs',
235
+ lib: ['ES2022'],
236
+ outDir: 'dist',
237
+ rootDir: 'src',
238
+ strict: true,
239
+ esModuleInterop: true,
240
+ skipLibCheck: true,
241
+ forceConsistentCasingInFileNames: true,
242
+ resolveJsonModule: true,
243
+ declaration: true,
244
+ sourceMap: true,
245
+ },
246
+ include: ['src/**/*'],
247
+ exclude: ['node_modules', 'dist'],
248
+ },
249
+ null,
250
+ 2,
251
+ ),
252
+ );
253
+
133
254
  // .env.example
134
255
  fs.writeFileSync(
135
256
  path.join(dir, '.env.example'),
@@ -142,9 +263,9 @@ OPC_LLM_MODEL=gpt-4o-mini
142
263
  # OPC_LLM_BASE_URL=https://api.deepseek.com/v1
143
264
  # OPC_LLM_MODEL=deepseek-chat
144
265
 
145
- # For local Ollama:
266
+ # For local Ollama (default in agent.yaml):
146
267
  # OPC_LLM_BASE_URL=http://localhost:11434/v1
147
- # OPC_LLM_MODEL=llama3
268
+ # OPC_LLM_MODEL=qwen2.5
148
269
  `,
149
270
  );
150
271
 
@@ -167,10 +288,16 @@ OPC_LLM_MODEL=gpt-4o-mini
167
288
  private: true,
168
289
  scripts: {
169
290
  start: 'opc run',
291
+ dev: 'opc dev',
170
292
  chat: 'opc chat',
293
+ build: 'tsc',
171
294
  },
172
295
  dependencies: {
173
- 'opc-agent': '^0.5.0',
296
+ 'opc-agent': '^1.3.0',
297
+ },
298
+ devDependencies: {
299
+ typescript: '^5.5.0',
300
+ tsx: '^4.0.0',
174
301
  },
175
302
  },
176
303
  null,
@@ -179,7 +306,7 @@ OPC_LLM_MODEL=gpt-4o-mini
179
306
  );
180
307
 
181
308
  // .gitignore
182
- fs.writeFileSync(path.join(dir, '.gitignore'), 'node_modules\n.env\n.opc-knowledge.json\ndata/\n');
309
+ fs.writeFileSync(path.join(dir, '.gitignore'), 'node_modules\ndist\n.env\n.opc-knowledge.json\ndata/\n');
183
310
 
184
311
  // Dockerfile
185
312
  fs.writeFileSync(
@@ -188,7 +315,8 @@ OPC_LLM_MODEL=gpt-4o-mini
188
315
  WORKDIR /app
189
316
  COPY package.json package-lock.json* ./
190
317
  RUN npm ci --production 2>/dev/null || npm install --production
191
- COPY oad.yaml .env* ./
318
+ COPY oad.yaml agent.yaml .env* ./
319
+ COPY src/ ./src/
192
320
  COPY prompts/ ./prompts/ 2>/dev/null || true
193
321
  EXPOSE 3000
194
322
  CMD ["npx", "opc", "run"]
@@ -207,7 +335,7 @@ services:
207
335
  env_file:
208
336
  - .env
209
337
  volumes:
210
- - ./oad.yaml:/app/oad.yaml:ro
338
+ - ./agent.yaml:/app/agent.yaml:ro
211
339
  restart: unless-stopped
212
340
  `,
213
341
  );
@@ -221,53 +349,118 @@ Created with [OPC Agent](https://github.com/Deepleaper/opc-agent) using the \`${
221
349
 
222
350
  ## Quick Start
223
351
 
224
- 1. **Set your API key:**
352
+ 1. **Install dependencies:**
225
353
  \`\`\`bash
226
- # Edit .env and add your API key
227
- cp .env.example .env
228
- # Then edit .env with your actual key
354
+ npm install
229
355
  \`\`\`
230
356
 
231
- 2. **Install dependencies:**
357
+ 2. **Run with Ollama (default):**
232
358
  \`\`\`bash
233
- npm install
359
+ # Make sure Ollama is running with qwen2.5 model
360
+ ollama pull qwen2.5
361
+ npx tsx src/index.ts
234
362
  \`\`\`
235
363
 
236
- 3. **Start the web server:**
364
+ 3. **Or use OpenAI/other providers:**
237
365
  \`\`\`bash
366
+ # Edit .env and set your API key
238
367
  npx opc run
239
368
  \`\`\`
240
369
 
241
370
  4. **Open browser:** [http://localhost:3000](http://localhost:3000)
242
371
 
243
- ## CLI Chat
372
+ ## Development
244
373
 
245
374
  \`\`\`bash
246
- npx opc chat
375
+ npx opc dev # Hot-reload mode
376
+ npx opc chat # CLI chat
377
+ \`\`\`
378
+
379
+ ## Project Structure
380
+
381
+ \`\`\`
382
+ ${name}/
383
+ ├── agent.yaml # OAD agent config (used by src/index.ts)
384
+ ├── oad.yaml # OAD config (used by opc CLI)
385
+ ├── src/
386
+ │ ├── index.ts # Entry point
387
+ │ └── skills/
388
+ │ └── echo.ts # Example skill
389
+ ├── package.json
390
+ └── tsconfig.json
247
391
  \`\`\`
248
392
 
249
393
  ## Configuration
250
394
 
251
- Edit \`oad.yaml\` to customize your agent's personality, skills, and behavior.
395
+ Edit \`agent.yaml\` to customize your agent's personality, skills, and behavior.
396
+ `,
397
+ );
398
+
399
+ // SOUL.md — agent personality
400
+ const createdDate = new Date().toISOString().split('T')[0];
401
+ fs.writeFileSync(
402
+ path.join(dir, 'SOUL.md'),
403
+ `# ${name} Personality
404
+
405
+ ## Identity
406
+ - Name: ${name}
407
+ - Role: AI Assistant
408
+ - Created: ${createdDate}
409
+
410
+ ## Personality Traits
411
+ - Helpful and professional
412
+ - Concise but thorough
413
+ - Friendly tone
414
+
415
+ ## Communication Style
416
+ - Use clear, simple language
417
+ - Be direct — answer the question first, then explain
418
+ - Use markdown formatting when helpful
419
+
420
+ ## Rules
421
+ - Always be honest about limitations
422
+ - Ask for clarification when the request is ambiguous
423
+ - Never make up information
424
+ `,
425
+ );
426
+
427
+ // CONTEXT.md — project context
428
+ fs.writeFileSync(
429
+ path.join(dir, 'CONTEXT.md'),
430
+ `# Project Context
431
+
432
+ ## About This Agent
433
+ ${name} is an AI agent built with OPC Agent Framework.
434
+
435
+ ## Knowledge Base
436
+ Add project-specific context here. The agent reads this file
437
+ on startup to understand the project context.
438
+
439
+ ## Important Notes
440
+ - Add domain knowledge here
441
+ - Add FAQ items here
442
+ - Add company policies here
252
443
  `,
253
444
  );
254
445
 
255
446
  console.log(`\n${icon.success} Created agent project: ${color.bold(name + '/')}`);
256
- console.log(` ${icon.file} oad.yaml - Agent definition`);
257
- console.log(` ${icon.file} package.json - Dependencies`);
258
- console.log(` ${icon.file} .env.example - Environment template`);
259
- console.log(` ${icon.file} .env - Environment config (edit this!)`);
447
+ console.log(` ${icon.file} agent.yaml - Agent definition (OAD)`);
448
+ console.log(` ${icon.file} src/index.ts - Entry point`);
449
+ console.log(` ${icon.file} src/skills/echo.ts - Example skill`);
450
+ console.log(` ${icon.file} SOUL.md - Agent personality`);
451
+ console.log(` ${icon.file} CONTEXT.md - Project context`);
452
+ console.log(` ${icon.file} package.json - Dependencies`);
453
+ console.log(` ${icon.file} tsconfig.json - TypeScript config`);
454
+ console.log(` ${icon.file} .env.example - Environment template`);
260
455
  console.log(` ${icon.file} .gitignore`);
261
456
  console.log(` ${icon.file} Dockerfile`);
262
- console.log(` ${icon.file} docker-compose.yml`);
263
457
  console.log(` ${icon.file} README.md`);
264
458
  console.log(`\n Template: ${color.cyan(template)}`);
265
459
  console.log(`\n${color.bold('Next steps:')}`);
266
460
  console.log(` 1. cd ${name}`);
267
- console.log(` 2. Edit .env — set your OPC_LLM_API_KEY`);
268
- console.log(` 3. npm install`);
269
- console.log(` 4. npx opc run`);
270
- console.log(` 5. Open http://localhost:3000\n`);
461
+ console.log(` 2. npm install`);
462
+ console.log(` 3. npx tsx src/index.ts ${color.dim('# or: npx opc run')}`);
463
+ console.log(` 4. Open http://localhost:3000\n`);
271
464
  });
272
465
 
273
466
  // ── Chat command ─────────────────────────────────────────────
@@ -282,28 +475,118 @@ program
282
475
 
283
476
  let systemPrompt = 'You are a helpful AI agent.';
284
477
  let model: string | undefined;
478
+ let agentName = 'Agent';
479
+ let agentVersion = '1.0.0';
480
+ let providerName = 'openai';
481
+ let skillNames: string[] = [];
482
+
483
+ // Try loading SOUL.md and CONTEXT.md for enriched system prompt
484
+ const soulPath = path.resolve('SOUL.md');
485
+ const contextPath = path.resolve('CONTEXT.md');
486
+ const soulContent = fs.existsSync(soulPath) ? fs.readFileSync(soulPath, 'utf-8') : '';
487
+ const contextContent = fs.existsSync(contextPath) ? fs.readFileSync(contextPath, 'utf-8') : '';
285
488
 
286
489
  try {
287
490
  const raw = fs.readFileSync(opts.file, 'utf-8');
288
491
  const config = yaml.load(raw) as any;
289
492
  if (config?.spec?.systemPrompt) systemPrompt = config.spec.systemPrompt;
290
493
  if (config?.spec?.model) model = config.spec.model;
291
- console.log(`\n${icon.gear} Loaded agent: ${color.bold(config?.metadata?.name ?? 'unknown')}`);
494
+ if (config?.metadata?.name) agentName = config.metadata.name;
495
+ if (config?.metadata?.version) agentVersion = config.metadata.version;
496
+ if (config?.spec?.provider?.default) providerName = config.spec.provider.default;
497
+ if (config?.spec?.skills) skillNames = config.spec.skills.map((s: any) => s.name);
292
498
  } catch {
293
- console.log(`\n${icon.info} No oad.yaml found, using defaults.`);
499
+ // No config file, use defaults
294
500
  }
295
501
 
502
+ // Prepend SOUL.md and CONTEXT.md to system prompt
503
+ systemPrompt = [soulContent, contextContent, systemPrompt].filter(Boolean).join('\n\n');
504
+
296
505
  const provider = createProvider('openai', model);
297
506
  const history: { role: 'user' | 'assistant' | 'system'; content: string }[] = [];
298
507
 
299
- console.log(`${color.dim('Type your message. Press Ctrl+C to exit.')}\n`);
508
+ // Print startup banner
509
+ const bannerLines = [
510
+ '╔══════════════════════════════════════╗',
511
+ '║ 🤖 OPC Agent — Interactive Chat ║',
512
+ `║ Agent: ${(agentName + ' v' + agentVersion).padEnd(27)}║`,
513
+ `║ Model: ${((providerName + '/' + (model ?? 'default')).slice(0, 27)).padEnd(27)}║`,
514
+ `║ Skills: ${(String(skillNames.length) + ' loaded').padEnd(26)}║`,
515
+ '║ Type /help for commands ║',
516
+ '╚══════════════════════════════════════╝',
517
+ ];
518
+ console.log('\n' + color.cyan(bannerLines.join('\n')) + '\n');
519
+
520
+ if (soulContent) console.log(` ${icon.info} Loaded SOUL.md`);
521
+ if (contextContent) console.log(` ${icon.info} Loaded CONTEXT.md`);
522
+ if (soulContent || contextContent) console.log();
523
+
524
+ const rl = readline.createInterface({
525
+ input: process.stdin,
526
+ output: process.stdout,
527
+ historySize: 100,
528
+ });
529
+
530
+ const handleSlashCommand = (cmd: string): boolean => {
531
+ const lower = cmd.toLowerCase().trim();
532
+ if (lower === '/quit' || lower === '/exit') {
533
+ console.log(`\n${color.dim('Goodbye! 👋')}`);
534
+ process.exit(0);
535
+ }
536
+ if (lower === '/help') {
537
+ console.log(`\n ${color.bold('Available commands:')}`);
538
+ console.log(` ${color.cyan('/help')} — Show this help`);
539
+ console.log(` ${color.cyan('/quit')} — Exit chat (/exit also works)`);
540
+ console.log(` ${color.cyan('/clear')} — Clear conversation history`);
541
+ console.log(` ${color.cyan('/skills')} — List registered skills`);
542
+ console.log(` ${color.cyan('/memory')} — Show memory stats`);
543
+ console.log(` ${color.cyan('/info')} — Show agent info\n`);
544
+ return true;
545
+ }
546
+ if (lower === '/clear') {
547
+ history.length = 0;
548
+ console.log(`\n ${icon.success} Conversation history cleared.\n`);
549
+ return true;
550
+ }
551
+ if (lower === '/skills') {
552
+ if (skillNames.length === 0) {
553
+ console.log(`\n ${icon.info} No skills registered.\n`);
554
+ } else {
555
+ console.log(`\n ${color.bold('Registered skills:')}`);
556
+ skillNames.forEach((s) => console.log(` • ${color.cyan(s)}`));
557
+ console.log();
558
+ }
559
+ return true;
560
+ }
561
+ if (lower === '/memory') {
562
+ console.log(`\n ${color.bold('Memory stats:')}`);
563
+ console.log(` Messages in history: ${color.cyan(String(history.length))}`);
564
+ console.log(` Characters: ${color.cyan(String(history.reduce((a, m) => a + m.content.length, 0)))}\n`);
565
+ return true;
566
+ }
567
+ if (lower === '/info') {
568
+ console.log(`\n ${color.bold('Agent Info:')}`);
569
+ console.log(` Name: ${color.cyan(agentName)}`);
570
+ console.log(` Version: ${color.cyan(agentVersion)}`);
571
+ console.log(` Provider: ${color.cyan(providerName)}`);
572
+ console.log(` Model: ${color.cyan(model ?? 'default')}`);
573
+ console.log(` Skills: ${color.cyan(String(skillNames.length))}\n`);
574
+ return true;
575
+ }
576
+ return false;
577
+ };
300
578
 
301
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
302
579
  const ask = (): void => {
303
580
  rl.question(color.cyan('You: '), async (input) => {
304
581
  const text = input.trim();
305
582
  if (!text) { ask(); return; }
306
583
 
584
+ // Handle slash commands
585
+ if (text.startsWith('/') && handleSlashCommand(text)) {
586
+ ask();
587
+ return;
588
+ }
589
+
307
590
  history.push({ role: 'user', content: text });
308
591
 
309
592
  // Build messages for provider
@@ -340,7 +623,7 @@ program
340
623
  };
341
624
 
342
625
  rl.on('close', () => {
343
- console.log(`\n${color.dim('Goodbye!')}`);
626
+ console.log(`\n${color.dim('Goodbye! 👋')}`);
344
627
  process.exit(0);
345
628
  });
346
629
 
@@ -721,7 +1004,7 @@ kbCmd.command('clear').action(() => {
721
1004
  console.log(`${icon.success} Knowledge base cleared.`);
722
1005
  });
723
1006
 
724
- // 📦 Marketplace commands ───────────────────────────────────
1007
+ // 📦 Package commands ───────────────────────────────────
725
1008
 
726
1009
  program
727
1010
  .command('publish')
@@ -729,23 +1012,8 @@ program
729
1012
  .option('-f, --file <file>', 'OAD file', 'oad.yaml')
730
1013
  .option('-o, --output <dir>', 'Output directory', '.')
731
1014
  .option('--include-kb', 'Include knowledge base')
732
- .action(async (opts: { file: string; output: string; includeKb?: boolean }) => {
733
- try {
734
- console.log(`\n${icon.package} Packaging agent...\n`);
735
- const result = await publishAgent({
736
- oadPath: opts.file,
737
- outputDir: opts.output,
738
- includeKnowledge: opts.includeKb,
739
- });
740
- console.log(`${icon.success} Published: ${color.bold(result.archivePath)}`);
741
- console.log(` Name: ${result.manifest.name}`);
742
- console.log(` Version: ${result.manifest.version}`);
743
- console.log(` Files: ${result.manifest.files.length}`);
744
- console.log();
745
- } catch (err) {
746
- console.error(`${icon.error} Publish failed:`, err instanceof Error ? err.message : err);
747
- process.exit(1);
748
- }
1015
+ .action(async () => {
1016
+ console.log(`\n${icon.package} Agent packaging coming soon.\n`);
749
1017
  });
750
1018
 
751
1019
  program
@@ -753,19 +1021,8 @@ program
753
1021
  .description('Install agent from package')
754
1022
  .argument('<source>', 'Package file path or URL')
755
1023
  .option('-d, --dir <dir>', 'Install directory')
756
- .action(async (source: string, opts: { dir?: string }) => {
757
- try {
758
- console.log(`\n${icon.package} Installing agent from ${color.bold(source)}...\n`);
759
- const result = await installAgent({ source, targetDir: opts.dir });
760
- console.log(`${icon.success} Installed: ${color.bold(result.manifest.name)} v${result.manifest.version}`);
761
- console.log(` Directory: ${result.dir}`);
762
- console.log(`\n${color.bold('Next steps:')}`);
763
- console.log(` cd ${result.dir}`);
764
- console.log(` opc run\n`);
765
- } catch (err) {
766
- console.error(`${icon.error} Install failed:`, err instanceof Error ? err.message : err);
767
- process.exit(1);
768
- }
1024
+ .action(async () => {
1025
+ console.log(`\n${icon.package} Agent install coming soon.\n`);
769
1026
  });
770
1027
 
771
1028
  // 🔌 Plugin commands ────────────────────────────────────────
@@ -873,4 +1130,381 @@ program
873
1130
  }
874
1131
  });
875
1132
 
1133
+ // ── Brain command ────────────────────────────────────────────
1134
+
1135
+ program
1136
+ .command('brain')
1137
+ .description('Show agent memory/brain status from DeepBrain')
1138
+ .option('--url <url>', 'DeepBrain server URL', 'http://localhost:3333')
1139
+ .action(async (opts: { url: string }) => {
1140
+ console.log(`\n${icon.gear} ${color.bold('DeepBrain Status')} — ${color.dim(opts.url)}\n`);
1141
+ try {
1142
+ const res = await fetch(`${opts.url}/api/stats`);
1143
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
1144
+ const stats = (await res.json()) as Record<string, any>;
1145
+ const rows: [string, string][] = [
1146
+ ['Total Pages', String(stats.totalPages ?? stats.pages ?? '-')],
1147
+ ['Total Chunks', String(stats.totalChunks ?? stats.chunks ?? '-')],
1148
+ ['Memory Tiers', String(stats.memoryTiers ?? stats.tiers ?? '-')],
1149
+ ['Index Size', stats.indexSize ?? '-'],
1150
+ ['Last Updated', stats.lastUpdated ?? stats.updatedAt ?? '-'],
1151
+ ];
1152
+ const maxKey = Math.max(...rows.map(([k]) => k.length));
1153
+ for (const [key, val] of rows) {
1154
+ console.log(` ${color.cyan(key.padEnd(maxKey))} ${val}`);
1155
+ }
1156
+ console.log();
1157
+ } catch (err) {
1158
+ const msg = err instanceof Error ? err.message : String(err);
1159
+ if (msg.includes('ECONNREFUSED') || msg.includes('fetch failed')) {
1160
+ console.log(` ${icon.warn} Cannot connect to DeepBrain at ${opts.url}`);
1161
+ console.log(` ${color.dim('Is the server running? Start with: deepbrain serve')}\n`);
1162
+ } else {
1163
+ console.error(` ${icon.error} ${msg}\n`);
1164
+ }
1165
+ }
1166
+ });
1167
+
1168
+ // ── Logs command ─────────────────────────────────────────────
1169
+
1170
+ program
1171
+ .command('logs')
1172
+ .description('Show recent agent traces')
1173
+ .option('-n, --limit <n>', 'Number of spans to show', '20')
1174
+ .option('-f, --follow', 'Keep watching for new spans')
1175
+ .action(async (opts: { limit: string; follow?: boolean }) => {
1176
+ const { TraceCollector } = await import('./traces');
1177
+ const collector = new TraceCollector();
1178
+ const limit = parseInt(opts.limit) || 20;
1179
+
1180
+ const printSpans = (spans: readonly Span[]) => {
1181
+ const slice = spans.slice(-limit);
1182
+ if (slice.length === 0) {
1183
+ console.log(` ${icon.info} No traces yet. Interact with the agent to generate traces.`);
1184
+ return;
1185
+ }
1186
+ for (const span of slice) {
1187
+ const duration = span.endTime
1188
+ ? `${span.endTime.getTime() - span.startTime.getTime()}ms`
1189
+ : 'ongoing';
1190
+ const statusIcon = span.status === 'ok' ? icon.success : span.status === 'error' ? icon.error : color.dim('○');
1191
+ const time = span.startTime.toLocaleTimeString();
1192
+ console.log(` ${statusIcon} ${color.dim(time)} ${color.bold(span.name)} ${color.dim(duration)}`);
1193
+ }
1194
+ };
1195
+
1196
+ console.log(`\n${icon.gear} ${color.bold('Agent Traces')}\n`);
1197
+ const spans = collector.getBufferedSpans();
1198
+ printSpans(spans);
1199
+
1200
+ if (opts.follow) {
1201
+ console.log(`\n ${color.dim('Watching for new traces... (Ctrl+C to stop)')}\n`);
1202
+ let lastCount = spans.length;
1203
+ const interval = setInterval(() => {
1204
+ const current = collector.getBufferedSpans();
1205
+ if (current.length > lastCount) {
1206
+ const newSpans = current.slice(lastCount);
1207
+ printSpans(newSpans);
1208
+ lastCount = current.length;
1209
+ }
1210
+ }, 1000);
1211
+ process.on('SIGINT', () => { clearInterval(interval); process.exit(0); });
1212
+ } else {
1213
+ console.log();
1214
+ }
1215
+ });
1216
+
1217
+ // ── Score command ────────────────────────────────────────────
1218
+
1219
+ program
1220
+ .command('score')
1221
+ .description('Show agent performance score')
1222
+ .action(async () => {
1223
+ console.log(`\n${icon.gear} ${color.bold('Agent Performance Score')}\n`);
1224
+ try {
1225
+ const engine = new AnalyticsEngine('.');
1226
+ const stats = engine.getStats();
1227
+ if (!stats || stats.totalMessages === 0) {
1228
+ console.log(` ${icon.info} No score data yet. Run the agent first.\n`);
1229
+ return;
1230
+ }
1231
+ const errorRate = stats.totalMessages > 0 ? (stats.totalErrors / stats.totalMessages) : 0;
1232
+ const rows: [string, string][] = [
1233
+ ['Total Messages', String(stats.totalMessages)],
1234
+ ['Total LLM Calls', String(stats.totalLLMCalls)],
1235
+ ['Total Tool Uses', String(stats.totalToolUses)],
1236
+ ['Avg Response Time', `${stats.avgResponseTimeMs}ms`],
1237
+ ['Error Rate', `${(errorRate * 100).toFixed(1)}%`],
1238
+ ['Token Usage', `${stats.totalTokens.total} tokens (in: ${stats.totalTokens.input}, out: ${stats.totalTokens.output})`],
1239
+ ];
1240
+ const maxKey = Math.max(...rows.map(([k]) => k.length));
1241
+ for (const [key, val] of rows) {
1242
+ console.log(` ${color.cyan(key.padEnd(maxKey))} ${val}`);
1243
+ }
1244
+ console.log();
1245
+ } catch {
1246
+ console.log(` ${icon.info} No score data yet. Run the agent first.\n`);
1247
+ }
1248
+ });
1249
+
1250
+ // ── Daemon commands (start/stop/status) ─────────────────────
1251
+
1252
+ const OPC_DIR = path.resolve('.opc');
1253
+
1254
+ program
1255
+ .command('start')
1256
+ .description('Start agent as a background daemon')
1257
+ .option('-f, --file <file>', 'OAD file (agent.yaml or oad.yaml)')
1258
+ .action(async () => {
1259
+ if (!fs.existsSync(OPC_DIR)) fs.mkdirSync(OPC_DIR, { recursive: true });
1260
+ const pidFile = path.join(OPC_DIR, 'agent.pid');
1261
+
1262
+ // Check if already running
1263
+ if (fs.existsSync(pidFile)) {
1264
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
1265
+ try { process.kill(pid, 0); console.log(`${icon.warn} Agent already running (PID ${pid}).`); return; } catch { /* stale */ }
1266
+ }
1267
+
1268
+ // Find daemon entry point
1269
+ const daemonScript = path.join(__dirname, 'daemon.js');
1270
+ if (!fs.existsSync(daemonScript)) {
1271
+ console.error(`${icon.error} Daemon script not found. Run ${color.cyan('npm run build')} first.`);
1272
+ process.exit(1);
1273
+ }
1274
+
1275
+ const logFile = path.join(OPC_DIR, 'agent.log');
1276
+ const out = fs.openSync(logFile, 'a');
1277
+ const err = fs.openSync(logFile, 'a');
1278
+
1279
+ const child = spawn(process.execPath, [daemonScript], {
1280
+ detached: true,
1281
+ stdio: ['ignore', out, err],
1282
+ cwd: process.cwd(),
1283
+ env: process.env,
1284
+ });
1285
+
1286
+ child.unref();
1287
+
1288
+ // Wait briefly for PID file
1289
+ await new Promise(r => setTimeout(r, 1000));
1290
+
1291
+ if (fs.existsSync(pidFile)) {
1292
+ const pid = fs.readFileSync(pidFile, 'utf-8').trim();
1293
+ console.log(`${icon.success} Agent started (PID ${pid})`);
1294
+ console.log(` ${color.dim('Logs:')} ${logFile}`);
1295
+ console.log(` ${color.dim('Stop:')} opc stop`);
1296
+ } else {
1297
+ console.log(`${icon.success} Agent starting... (PID ${child.pid})`);
1298
+ console.log(` ${color.dim('Logs:')} ${logFile}`);
1299
+ }
1300
+ });
1301
+
1302
+ program
1303
+ .command('stop')
1304
+ .description('Stop the background daemon')
1305
+ .action(() => {
1306
+ const pidFile = path.join(OPC_DIR, 'agent.pid');
1307
+ if (!fs.existsSync(pidFile)) {
1308
+ console.log(`${icon.info} No running agent found.`);
1309
+ return;
1310
+ }
1311
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
1312
+ try {
1313
+ // On Windows, process.kill with SIGTERM may not work; use taskkill
1314
+ if (process.platform === 'win32') {
1315
+ const { execSync } = require('child_process');
1316
+ try { execSync(`taskkill /PID ${pid} /T /F`, { stdio: 'ignore' }); } catch { /* ignore */ }
1317
+ } else {
1318
+ process.kill(pid, 'SIGTERM');
1319
+ }
1320
+ console.log(`${icon.success} Sent stop signal to PID ${pid}`);
1321
+ } catch {
1322
+ console.log(`${icon.warn} Process ${pid} not found (may have already stopped).`);
1323
+ }
1324
+ try { fs.unlinkSync(pidFile); } catch { /* ignore */ }
1325
+ });
1326
+
1327
+ program
1328
+ .command('status')
1329
+ .description('Check daemon status')
1330
+ .action(() => {
1331
+ const pidFile = path.join(OPC_DIR, 'agent.pid');
1332
+ if (!fs.existsSync(pidFile)) {
1333
+ console.log(`\n Status: ${color.red('stopped')}\n`);
1334
+ return;
1335
+ }
1336
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
1337
+ let running = false;
1338
+ try { process.kill(pid, 0); running = true; } catch { /* not running */ }
1339
+
1340
+ if (!running) {
1341
+ console.log(`\n Status: ${color.red('stopped')} (stale PID file)`);
1342
+ try { fs.unlinkSync(pidFile); } catch { /* ignore */ }
1343
+ console.log();
1344
+ return;
1345
+ }
1346
+
1347
+ // Uptime
1348
+ const startedFile = path.join(OPC_DIR, 'started');
1349
+ let uptime = '';
1350
+ if (fs.existsSync(startedFile)) {
1351
+ const startedMs = parseInt(fs.readFileSync(startedFile, 'utf-8').trim(), 10);
1352
+ const secs = Math.floor((Date.now() - startedMs) / 1000);
1353
+ const h = Math.floor(secs / 3600);
1354
+ const m = Math.floor((secs % 3600) / 60);
1355
+ const s = secs % 60;
1356
+ uptime = `${h}h ${m}m ${s}s`;
1357
+ }
1358
+
1359
+ // Agent name from config
1360
+ let agentName = 'unknown';
1361
+ for (const f of ['agent.yaml', 'oad.yaml']) {
1362
+ if (fs.existsSync(f)) {
1363
+ try {
1364
+ const raw = fs.readFileSync(f, 'utf-8');
1365
+ const cfg = yaml.load(raw) as any;
1366
+ if (cfg?.metadata?.name) { agentName = cfg.metadata.name; break; }
1367
+ } catch { /* ignore */ }
1368
+ }
1369
+ }
1370
+
1371
+ console.log(`\n Status: ${color.green('running')}`);
1372
+ console.log(` PID: ${pid}`);
1373
+ console.log(` Agent: ${color.cyan(agentName)}`);
1374
+ if (uptime) console.log(` Uptime: ${uptime}`);
1375
+ console.log();
1376
+ });
1377
+
1378
+ // ── Jobs commands ────────────────────────────────────────────
1379
+
1380
+ const jobsCmd = program.command('jobs').description('Manage scheduled jobs');
1381
+
1382
+ jobsCmd
1383
+ .command('list', { isDefault: true })
1384
+ .description('List all scheduled jobs')
1385
+ .option('-f, --file <file>', 'OAD file', 'oad.yaml')
1386
+ .action(async (opts: { file: string }) => {
1387
+ const jobs = loadJobsFromConfig(opts.file);
1388
+ if (jobs.length === 0) {
1389
+ console.log(`\n${icon.info} No scheduled jobs defined in config.\n`);
1390
+ return;
1391
+ }
1392
+ console.log(`\n${icon.gear} ${color.bold('Scheduled Jobs')}\n`);
1393
+ for (const job of jobs) {
1394
+ const status = job.enabled ? color.green('enabled') : color.dim('disabled');
1395
+ const next = job.nextRun ? job.nextRun.toLocaleString() : color.dim('N/A');
1396
+ console.log(` ${color.cyan(job.id.padEnd(20))} ${job.name}`);
1397
+ console.log(` ${''.padEnd(20)} Schedule: ${color.dim(job.schedule)} | Status: ${status} | Next: ${next}`);
1398
+ console.log();
1399
+ }
1400
+ });
1401
+
1402
+ jobsCmd
1403
+ .command('run')
1404
+ .argument('<id>', 'Job ID to run')
1405
+ .option('-f, --file <file>', 'OAD file', 'oad.yaml')
1406
+ .description('Manually trigger a scheduled job')
1407
+ .action(async (id: string, opts: { file: string }) => {
1408
+ const jobs = loadJobsFromConfig(opts.file);
1409
+ const job = jobs.find(j => j.id === id || j.name === id);
1410
+ if (!job) {
1411
+ console.error(`${icon.error} Job "${id}" not found. Available: ${jobs.map(j => j.id).join(', ')}`);
1412
+ process.exit(1);
1413
+ }
1414
+ console.log(`${icon.info} Running job "${color.bold(job.name)}"...`);
1415
+ console.log(` Task: ${color.dim(job.task)}`);
1416
+ console.log(`\n${icon.warn} Manual job execution requires a running daemon. Use ${color.cyan('opc start')} first.\n`);
1417
+ });
1418
+
1419
+ function loadJobsFromConfig(file: string): CronJob[] {
1420
+ try {
1421
+ const raw = fs.readFileSync(file, 'utf-8');
1422
+ const config = yaml.load(raw) as any;
1423
+ const jobConfigs = config?.spec?.scheduler?.jobs ?? [];
1424
+ const { parseCron } = require('./core/scheduler');
1425
+ return jobConfigs.map((j: any, i: number) => {
1426
+ const id = j.id || j.name?.toLowerCase().replace(/\s+/g, '-') || `job-${i}`;
1427
+ const parsed = parseCron(j.schedule);
1428
+ // Compute next run
1429
+ const now = new Date();
1430
+ let nextRun: Date | undefined;
1431
+ const d = new Date(now);
1432
+ d.setSeconds(0, 0);
1433
+ d.setMinutes(d.getMinutes() + 1);
1434
+ for (let k = 0; k < 48 * 60; k++) {
1435
+ const { cronMatches } = require('./core/scheduler');
1436
+ if (cronMatches(parsed, d)) { nextRun = new Date(d); break; }
1437
+ d.setMinutes(d.getMinutes() + 1);
1438
+ }
1439
+ return {
1440
+ id,
1441
+ name: j.name || id,
1442
+ schedule: j.schedule,
1443
+ task: j.task || '',
1444
+ enabled: j.enabled !== false,
1445
+ nextRun,
1446
+ } as CronJob;
1447
+ });
1448
+ } catch {
1449
+ return [];
1450
+ }
1451
+ }
1452
+
1453
+ // ── Skills commands ──────────────────────────────────────────
1454
+
1455
+ const skillsCmd = program.command('skills').description('Manage learned skills');
1456
+
1457
+ skillsCmd
1458
+ .command('list', { isDefault: true })
1459
+ .description('List all learned skills')
1460
+ .option('-d, --dir <dir>', 'Skills directory', '.opc/skills')
1461
+ .action(async (opts: { dir: string }) => {
1462
+ const { SkillLearner } = await import('./skills/auto-learn');
1463
+ const learner = new SkillLearner(opts.dir);
1464
+ const skills = await learner.loadLearnedSkills();
1465
+ if (skills.length === 0) {
1466
+ console.log(`\n${icon.info} No learned skills yet.\n`);
1467
+ console.log(` Skills are auto-created from conversations when learning is enabled.`);
1468
+ console.log(` Directory: ${color.dim(path.resolve(opts.dir))}\n`);
1469
+ return;
1470
+ }
1471
+ console.log(`\n${icon.gear} ${color.bold('Learned Skills')} (${skills.length})\n`);
1472
+ for (const skill of skills) {
1473
+ console.log(` ${color.cyan(skill.name.padEnd(24))} ${skill.description}`);
1474
+ console.log(` ${''.padEnd(24)} v${skill.version} | used ${skill.usageCount}x | trigger: ${color.dim(skill.trigger)}`);
1475
+ console.log();
1476
+ }
1477
+ });
1478
+
1479
+ skillsCmd
1480
+ .command('show')
1481
+ .argument('<name>', 'Skill name')
1482
+ .option('-d, --dir <dir>', 'Skills directory', '.opc/skills')
1483
+ .description('Show details of a learned skill')
1484
+ .action(async (name: string, opts: { dir: string }) => {
1485
+ const skillPath = path.join(opts.dir, `${name}.md`);
1486
+ if (!fs.existsSync(skillPath)) {
1487
+ console.error(`${icon.error} Skill "${name}" not found at ${skillPath}`);
1488
+ process.exit(1);
1489
+ }
1490
+ const content = fs.readFileSync(skillPath, 'utf-8');
1491
+ console.log(`\n${content}`);
1492
+ });
1493
+
1494
+ skillsCmd
1495
+ .command('remove')
1496
+ .argument('<name>', 'Skill name')
1497
+ .option('-d, --dir <dir>', 'Skills directory', '.opc/skills')
1498
+ .description('Remove a learned skill')
1499
+ .action(async (name: string, opts: { dir: string }) => {
1500
+ const skillPath = path.join(opts.dir, `${name}.md`);
1501
+ if (!fs.existsSync(skillPath)) {
1502
+ console.error(`${icon.error} Skill "${name}" not found.`);
1503
+ process.exit(1);
1504
+ }
1505
+ fs.unlinkSync(skillPath);
1506
+ console.log(`${icon.success} Removed skill "${color.cyan(name)}".`);
1507
+ });
1508
+
876
1509
  program.parse();
1510
+