opc-agent 1.4.0 → 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 (58) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +91 -32
  3. package/dist/channels/telegram.d.ts +30 -9
  4. package/dist/channels/telegram.js +125 -33
  5. package/dist/cli.js +415 -8
  6. package/dist/core/agent.d.ts +23 -0
  7. package/dist/core/agent.js +120 -3
  8. package/dist/core/runtime.d.ts +1 -0
  9. package/dist/core/runtime.js +44 -0
  10. package/dist/core/scheduler.d.ts +52 -0
  11. package/dist/core/scheduler.js +168 -0
  12. package/dist/core/subagent.d.ts +28 -0
  13. package/dist/core/subagent.js +65 -0
  14. package/dist/daemon.d.ts +3 -0
  15. package/dist/daemon.js +134 -0
  16. package/dist/index.d.ts +7 -0
  17. package/dist/index.js +17 -1
  18. package/dist/providers/index.d.ts +5 -1
  19. package/dist/providers/index.js +16 -9
  20. package/dist/schema/oad.d.ts +179 -4
  21. package/dist/schema/oad.js +12 -1
  22. package/dist/skills/auto-learn.d.ts +28 -0
  23. package/dist/skills/auto-learn.js +257 -0
  24. package/dist/tools/builtin/datetime.d.ts +3 -0
  25. package/dist/tools/builtin/datetime.js +44 -0
  26. package/dist/tools/builtin/file.d.ts +3 -0
  27. package/dist/tools/builtin/file.js +151 -0
  28. package/dist/tools/builtin/index.d.ts +15 -0
  29. package/dist/tools/builtin/index.js +30 -0
  30. package/dist/tools/builtin/shell.d.ts +3 -0
  31. package/dist/tools/builtin/shell.js +43 -0
  32. package/dist/tools/builtin/web.d.ts +3 -0
  33. package/dist/tools/builtin/web.js +37 -0
  34. package/dist/tools/mcp-client.d.ts +24 -0
  35. package/dist/tools/mcp-client.js +119 -0
  36. package/package.json +1 -1
  37. package/src/channels/telegram.ts +212 -90
  38. package/src/cli.ts +418 -8
  39. package/src/core/agent.ts +295 -152
  40. package/src/core/runtime.ts +47 -0
  41. package/src/core/scheduler.ts +187 -0
  42. package/src/core/subagent.ts +98 -0
  43. package/src/daemon.ts +96 -0
  44. package/src/index.ts +11 -0
  45. package/src/providers/index.ts +354 -339
  46. package/src/schema/oad.ts +167 -154
  47. package/src/skills/auto-learn.ts +262 -0
  48. package/src/tools/builtin/datetime.ts +41 -0
  49. package/src/tools/builtin/file.ts +107 -0
  50. package/src/tools/builtin/index.ts +28 -0
  51. package/src/tools/builtin/shell.ts +43 -0
  52. package/src/tools/builtin/web.ts +35 -0
  53. package/src/tools/mcp-client.ts +131 -0
  54. package/tests/auto-learn.test.ts +105 -0
  55. package/tests/builtin-tools.test.ts +83 -0
  56. package/tests/cli.test.ts +46 -0
  57. package/tests/subagent.test.ts +130 -0
  58. package/tests/telegram-discord.test.ts +60 -0
package/src/cli.ts CHANGED
@@ -29,7 +29,10 @@ import { createProvider } from './providers';
29
29
  import { KnowledgeBase } from './core/knowledge';
30
30
 
31
31
  import { PluginManager, createLoggingPlugin, createAnalyticsPlugin, createRateLimitPlugin } from './plugins';
32
+ import { Scheduler } from './core/scheduler';
33
+ import type { CronJob } from './core/scheduler';
32
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.4.0');
100
+ .version('2.0.0');
98
101
 
99
102
  // ── Init command ─────────────────────────────────────────────
100
103
 
@@ -166,15 +169,24 @@ spec:
166
169
  path.join(dir, 'src', 'index.ts'),
167
170
  `import { AgentRuntime } from 'opc-agent';
168
171
  import { EchoSkill } from './skills/echo';
172
+ import { readFileSync, existsSync } from 'fs';
169
173
 
170
174
  async function main() {
171
175
  const runtime = new AgentRuntime();
172
176
 
173
177
  // Load OAD config
174
- await runtime.loadConfig('./agent.yaml');
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
+ }
175
187
 
176
188
  // Initialize agent with channels, memory, etc.
177
- const agent = await runtime.initialize();
189
+ const agent = await runtime.initialize(config);
178
190
 
179
191
  // Register custom skills
180
192
  runtime.registerSkill(new EchoSkill());
@@ -384,10 +396,59 @@ Edit \`agent.yaml\` to customize your agent's personality, skills, and behavior.
384
396
  `,
385
397
  );
386
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
443
+ `,
444
+ );
445
+
387
446
  console.log(`\n${icon.success} Created agent project: ${color.bold(name + '/')}`);
388
447
  console.log(` ${icon.file} agent.yaml - Agent definition (OAD)`);
389
448
  console.log(` ${icon.file} src/index.ts - Entry point`);
390
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`);
391
452
  console.log(` ${icon.file} package.json - Dependencies`);
392
453
  console.log(` ${icon.file} tsconfig.json - TypeScript config`);
393
454
  console.log(` ${icon.file} .env.example - Environment template`);
@@ -414,28 +475,118 @@ program
414
475
 
415
476
  let systemPrompt = 'You are a helpful AI agent.';
416
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') : '';
417
488
 
418
489
  try {
419
490
  const raw = fs.readFileSync(opts.file, 'utf-8');
420
491
  const config = yaml.load(raw) as any;
421
492
  if (config?.spec?.systemPrompt) systemPrompt = config.spec.systemPrompt;
422
493
  if (config?.spec?.model) model = config.spec.model;
423
- 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);
424
498
  } catch {
425
- console.log(`\n${icon.info} No oad.yaml found, using defaults.`);
499
+ // No config file, use defaults
426
500
  }
427
501
 
502
+ // Prepend SOUL.md and CONTEXT.md to system prompt
503
+ systemPrompt = [soulContent, contextContent, systemPrompt].filter(Boolean).join('\n\n');
504
+
428
505
  const provider = createProvider('openai', model);
429
506
  const history: { role: 'user' | 'assistant' | 'system'; content: string }[] = [];
430
507
 
431
- 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
+ };
432
578
 
433
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
434
579
  const ask = (): void => {
435
580
  rl.question(color.cyan('You: '), async (input) => {
436
581
  const text = input.trim();
437
582
  if (!text) { ask(); return; }
438
583
 
584
+ // Handle slash commands
585
+ if (text.startsWith('/') && handleSlashCommand(text)) {
586
+ ask();
587
+ return;
588
+ }
589
+
439
590
  history.push({ role: 'user', content: text });
440
591
 
441
592
  // Build messages for provider
@@ -472,7 +623,7 @@ program
472
623
  };
473
624
 
474
625
  rl.on('close', () => {
475
- console.log(`\n${color.dim('Goodbye!')}`);
626
+ console.log(`\n${color.dim('Goodbye! 👋')}`);
476
627
  process.exit(0);
477
628
  });
478
629
 
@@ -1096,5 +1247,264 @@ program
1096
1247
  }
1097
1248
  });
1098
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
+
1099
1509
  program.parse();
1100
1510