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.
- package/CHANGELOG.md +25 -0
- package/README.md +91 -32
- package/dist/channels/telegram.d.ts +30 -9
- package/dist/channels/telegram.js +125 -33
- package/dist/cli.js +415 -8
- package/dist/core/agent.d.ts +23 -0
- package/dist/core/agent.js +120 -3
- package/dist/core/runtime.d.ts +1 -0
- package/dist/core/runtime.js +44 -0
- package/dist/core/scheduler.d.ts +52 -0
- package/dist/core/scheduler.js +168 -0
- package/dist/core/subagent.d.ts +28 -0
- package/dist/core/subagent.js +65 -0
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +134 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +17 -1
- package/dist/providers/index.d.ts +5 -1
- package/dist/providers/index.js +16 -9
- package/dist/schema/oad.d.ts +179 -4
- package/dist/schema/oad.js +12 -1
- package/dist/skills/auto-learn.d.ts +28 -0
- package/dist/skills/auto-learn.js +257 -0
- package/dist/tools/builtin/datetime.d.ts +3 -0
- package/dist/tools/builtin/datetime.js +44 -0
- package/dist/tools/builtin/file.d.ts +3 -0
- package/dist/tools/builtin/file.js +151 -0
- package/dist/tools/builtin/index.d.ts +15 -0
- package/dist/tools/builtin/index.js +30 -0
- package/dist/tools/builtin/shell.d.ts +3 -0
- package/dist/tools/builtin/shell.js +43 -0
- package/dist/tools/builtin/web.d.ts +3 -0
- package/dist/tools/builtin/web.js +37 -0
- package/dist/tools/mcp-client.d.ts +24 -0
- package/dist/tools/mcp-client.js +119 -0
- package/package.json +1 -1
- package/src/channels/telegram.ts +212 -90
- package/src/cli.ts +418 -8
- package/src/core/agent.ts +295 -152
- package/src/core/runtime.ts +47 -0
- package/src/core/scheduler.ts +187 -0
- package/src/core/subagent.ts +98 -0
- package/src/daemon.ts +96 -0
- package/src/index.ts +11 -0
- package/src/providers/index.ts +354 -339
- package/src/schema/oad.ts +167 -154
- package/src/skills/auto-learn.ts +262 -0
- package/src/tools/builtin/datetime.ts +41 -0
- package/src/tools/builtin/file.ts +107 -0
- package/src/tools/builtin/index.ts +28 -0
- package/src/tools/builtin/shell.ts +43 -0
- package/src/tools/builtin/web.ts +35 -0
- package/src/tools/mcp-client.ts +131 -0
- package/tests/auto-learn.test.ts +105 -0
- package/tests/builtin-tools.test.ts +83 -0
- package/tests/cli.test.ts +46 -0
- package/tests/subagent.test.ts +130 -0
- 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('
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|