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
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Simple cron scheduler — no external dependencies.
3
+ * Supports cron expressions: star, star-slash-N, M-N, M,N for minute/hour/day/month/weekday.
4
+ */
5
+
6
+ export interface CronJob {
7
+ id: string;
8
+ name: string;
9
+ schedule: string;
10
+ task: string;
11
+ enabled: boolean;
12
+ lastRun?: Date;
13
+ nextRun?: Date;
14
+ }
15
+
16
+ type CronField = { type: 'any' } | { type: 'every'; step: number } | { type: 'list'; values: number[] };
17
+
18
+ interface ParsedCron {
19
+ minute: CronField;
20
+ hour: CronField;
21
+ dayOfMonth: CronField;
22
+ month: CronField;
23
+ dayOfWeek: CronField;
24
+ }
25
+
26
+ function parseField(field: string, min: number, max: number): CronField {
27
+ if (field === '*') return { type: 'any' };
28
+ if (field.startsWith('*/')) {
29
+ const step = parseInt(field.slice(2), 10);
30
+ if (isNaN(step) || step <= 0) throw new Error(`Invalid cron step: ${field}`);
31
+ return { type: 'every', step };
32
+ }
33
+ // Could be comma-separated, each part could be a range
34
+ const values: number[] = [];
35
+ for (const part of field.split(',')) {
36
+ if (part.includes('-')) {
37
+ const [a, b] = part.split('-').map(Number);
38
+ if (isNaN(a) || isNaN(b)) throw new Error(`Invalid cron range: ${part}`);
39
+ for (let i = a; i <= b; i++) values.push(i);
40
+ } else {
41
+ const n = parseInt(part, 10);
42
+ if (isNaN(n)) throw new Error(`Invalid cron value: ${part}`);
43
+ values.push(n);
44
+ }
45
+ }
46
+ return { type: 'list', values };
47
+ }
48
+
49
+ export function parseCron(expr: string): ParsedCron {
50
+ const parts = expr.trim().split(/\s+/);
51
+ if (parts.length !== 5) throw new Error(`Invalid cron expression (need 5 fields): ${expr}`);
52
+ return {
53
+ minute: parseField(parts[0], 0, 59),
54
+ hour: parseField(parts[1], 0, 23),
55
+ dayOfMonth: parseField(parts[2], 1, 31),
56
+ month: parseField(parts[3], 1, 12),
57
+ dayOfWeek: parseField(parts[4], 0, 6),
58
+ };
59
+ }
60
+
61
+ function fieldMatches(field: CronField, value: number): boolean {
62
+ switch (field.type) {
63
+ case 'any': return true;
64
+ case 'every': return value % field.step === 0;
65
+ case 'list': return field.values.includes(value);
66
+ }
67
+ }
68
+
69
+ export function cronMatches(parsed: ParsedCron, date: Date): boolean {
70
+ return (
71
+ fieldMatches(parsed.minute, date.getMinutes()) &&
72
+ fieldMatches(parsed.hour, date.getHours()) &&
73
+ fieldMatches(parsed.dayOfMonth, date.getDate()) &&
74
+ fieldMatches(parsed.month, date.getMonth() + 1) &&
75
+ fieldMatches(parsed.dayOfWeek, date.getDay())
76
+ );
77
+ }
78
+
79
+ /** Compute approximate next run (scans forward up to 48h). */
80
+ function computeNextRun(parsed: ParsedCron, from: Date): Date | undefined {
81
+ const d = new Date(from);
82
+ d.setSeconds(0, 0);
83
+ d.setMinutes(d.getMinutes() + 1);
84
+ const limit = 48 * 60; // 48 hours in minutes
85
+ for (let i = 0; i < limit; i++) {
86
+ if (cronMatches(parsed, d)) return new Date(d);
87
+ d.setMinutes(d.getMinutes() + 1);
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ export type JobHandler = (job: CronJob) => void | Promise<void>;
93
+
94
+ export class Scheduler {
95
+ private jobs = new Map<string, CronJob>();
96
+ private parsed = new Map<string, ParsedCron>();
97
+ private timer: ReturnType<typeof setInterval> | null = null;
98
+ private handler: JobHandler;
99
+
100
+ constructor(handler: JobHandler) {
101
+ this.handler = handler;
102
+ }
103
+
104
+ addJob(job: CronJob): void {
105
+ const p = parseCron(job.schedule);
106
+ this.parsed.set(job.id, p);
107
+ job.nextRun = computeNextRun(p, new Date()) ?? undefined;
108
+ this.jobs.set(job.id, job);
109
+ }
110
+
111
+ removeJob(id: string): void {
112
+ this.jobs.delete(id);
113
+ this.parsed.delete(id);
114
+ }
115
+
116
+ enableJob(id: string): void {
117
+ const job = this.jobs.get(id);
118
+ if (job) job.enabled = true;
119
+ }
120
+
121
+ disableJob(id: string): void {
122
+ const job = this.jobs.get(id);
123
+ if (job) job.enabled = false;
124
+ }
125
+
126
+ getJobs(): CronJob[] {
127
+ return Array.from(this.jobs.values());
128
+ }
129
+
130
+ getJob(id: string): CronJob | undefined {
131
+ return this.jobs.get(id);
132
+ }
133
+
134
+ /** Run a specific job immediately */
135
+ async runJob(id: string): Promise<boolean> {
136
+ const job = this.jobs.get(id);
137
+ if (!job) return false;
138
+ job.lastRun = new Date();
139
+ await this.handler(job);
140
+ const parsed = this.parsed.get(id);
141
+ if (parsed) job.nextRun = computeNextRun(parsed, new Date());
142
+ return true;
143
+ }
144
+
145
+ start(): void {
146
+ if (this.timer) return;
147
+ // Check every 60 seconds
148
+ this.timer = setInterval(() => this.tick(), 60_000);
149
+ // Also tick immediately
150
+ this.tick();
151
+ }
152
+
153
+ stop(): void {
154
+ if (this.timer) {
155
+ clearInterval(this.timer);
156
+ this.timer = null;
157
+ }
158
+ }
159
+
160
+ private tick(): void {
161
+ const now = new Date();
162
+ for (const [id, job] of this.jobs) {
163
+ if (!job.enabled) continue;
164
+ const parsed = this.parsed.get(id);
165
+ if (!parsed) continue;
166
+ if (cronMatches(parsed, now)) {
167
+ // Avoid double-fire: check lastRun isn't same minute
168
+ if (job.lastRun) {
169
+ const last = job.lastRun;
170
+ if (last.getFullYear() === now.getFullYear() &&
171
+ last.getMonth() === now.getMonth() &&
172
+ last.getDate() === now.getDate() &&
173
+ last.getHours() === now.getHours() &&
174
+ last.getMinutes() === now.getMinutes()) {
175
+ continue;
176
+ }
177
+ }
178
+ job.lastRun = new Date(now);
179
+ job.nextRun = computeNextRun(parsed, now);
180
+ // Fire and forget (log errors)
181
+ Promise.resolve(this.handler(job)).catch((err) => {
182
+ console.error(`[scheduler] Job "${job.name}" failed:`, err);
183
+ });
184
+ }
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,98 @@
1
+ import { BaseAgent } from './agent';
2
+ import { InMemoryStore } from '../memory';
3
+ import type { Message } from './types';
4
+
5
+ export interface SubAgentConfig {
6
+ name: string;
7
+ task: string;
8
+ systemPrompt?: string;
9
+ provider?: string;
10
+ model?: string;
11
+ timeout?: number;
12
+ isolated?: boolean;
13
+ }
14
+
15
+ export interface SubAgentResult {
16
+ id: string;
17
+ name: string;
18
+ status: 'completed' | 'failed' | 'timeout';
19
+ result: string;
20
+ duration: number;
21
+ }
22
+
23
+ interface SubAgentEntry {
24
+ agent: BaseAgent;
25
+ status: string;
26
+ name: string;
27
+ }
28
+
29
+ export class SubAgentManager {
30
+ private agents: Map<string, SubAgentEntry> = new Map();
31
+
32
+ async spawn(config: SubAgentConfig, parentProvider?: any): Promise<SubAgentResult> {
33
+ const id = `sub_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
34
+ const timeout = config.timeout ?? 300000;
35
+ const isolated = config.isolated !== false;
36
+
37
+ const agent = new BaseAgent({
38
+ name: config.name,
39
+ systemPrompt: config.systemPrompt ?? 'You are a helpful sub-agent.',
40
+ provider: config.provider ?? 'openai',
41
+ model: config.model,
42
+ memory: isolated ? new InMemoryStore() : undefined,
43
+ });
44
+
45
+ this.agents.set(id, { agent, status: 'running', name: config.name });
46
+
47
+ const message: Message = {
48
+ id: `msg_${Date.now()}`,
49
+ role: 'user',
50
+ content: config.task,
51
+ timestamp: Date.now(),
52
+ metadata: { subAgentId: id },
53
+ };
54
+
55
+ const start = Date.now();
56
+
57
+ try {
58
+ const result = await Promise.race([
59
+ agent.handleMessage(message),
60
+ new Promise<never>((_, reject) =>
61
+ setTimeout(() => reject(new Error('SubAgent timeout')), timeout),
62
+ ),
63
+ ]);
64
+
65
+ const duration = Date.now() - start;
66
+ this.agents.set(id, { agent, status: 'completed', name: config.name });
67
+
68
+ return { id, name: config.name, status: 'completed', result: result.content, duration };
69
+ } catch (err) {
70
+ const duration = Date.now() - start;
71
+ const isTimeout = (err as Error).message.includes('timeout');
72
+ const status = isTimeout ? 'timeout' : 'failed';
73
+ this.agents.set(id, { agent, status, name: config.name });
74
+
75
+ return { id, name: config.name, status, result: (err as Error).message, duration };
76
+ }
77
+ }
78
+
79
+ async spawnParallel(configs: SubAgentConfig[], parentProvider?: any): Promise<SubAgentResult[]> {
80
+ return Promise.all(configs.map((c) => this.spawn(c, parentProvider)));
81
+ }
82
+
83
+ list(): Array<{ id: string; name: string; status: string }> {
84
+ return Array.from(this.agents.entries()).map(([id, entry]) => ({
85
+ id,
86
+ name: entry.name,
87
+ status: entry.status,
88
+ }));
89
+ }
90
+
91
+ kill(id: string): boolean {
92
+ const entry = this.agents.get(id);
93
+ if (!entry) return false;
94
+ entry.status = 'killed';
95
+ this.agents.set(id, entry);
96
+ return true;
97
+ }
98
+ }
package/src/daemon.ts ADDED
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Daemon entry point — spawned by `opc start` as a detached background process.
4
+ * Loads agent.yaml, creates runtime, starts all channels, writes heartbeat.
5
+ */
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import { AgentRuntime } from './core/runtime';
9
+
10
+ const OPC_DIR = path.resolve('.opc');
11
+ const HEARTBEAT_FILE = path.join(OPC_DIR, 'heartbeat');
12
+ const LOG_FILE = path.join(OPC_DIR, 'agent.log');
13
+ const PID_FILE = path.join(OPC_DIR, 'agent.pid');
14
+ const HEARTBEAT_INTERVAL = 30_000;
15
+
16
+ function ensureDir(dir: string) {
17
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
18
+ }
19
+
20
+ function log(msg: string) {
21
+ const line = `[${new Date().toISOString()}] ${msg}\n`;
22
+ try { fs.appendFileSync(LOG_FILE, line); } catch { /* ignore */ }
23
+ }
24
+
25
+ async function main() {
26
+ ensureDir(OPC_DIR);
27
+
28
+ // Redirect stdout/stderr to log file
29
+ const logStream = fs.createWriteStream(LOG_FILE, { flags: 'a' });
30
+ process.stdout.write = logStream.write.bind(logStream) as any;
31
+ process.stderr.write = logStream.write.bind(logStream) as any;
32
+
33
+ // Write PID
34
+ fs.writeFileSync(PID_FILE, String(process.pid));
35
+ log(`Daemon started, PID=${process.pid}`);
36
+
37
+ // Write start time for uptime calculation
38
+ fs.writeFileSync(path.join(OPC_DIR, 'started'), String(Date.now()));
39
+
40
+ // Heartbeat
41
+ const heartbeatTimer = setInterval(() => {
42
+ try { fs.writeFileSync(HEARTBEAT_FILE, String(Date.now())); } catch { /* ignore */ }
43
+ }, HEARTBEAT_INTERVAL);
44
+ fs.writeFileSync(HEARTBEAT_FILE, String(Date.now()));
45
+
46
+ // Load .env
47
+ const envPath = path.resolve('.env');
48
+ if (fs.existsSync(envPath)) {
49
+ try {
50
+ const content = fs.readFileSync(envPath, 'utf-8');
51
+ for (const line of content.split('\n')) {
52
+ const trimmed = line.trim();
53
+ if (!trimmed || trimmed.startsWith('#')) continue;
54
+ const eqIdx = trimmed.indexOf('=');
55
+ if (eqIdx === -1) continue;
56
+ const key = trimmed.slice(0, eqIdx).trim();
57
+ const value = trimmed.slice(eqIdx + 1).trim();
58
+ if (!process.env[key]) process.env[key] = value;
59
+ }
60
+ } catch { /* ignore */ }
61
+ }
62
+
63
+ // Determine config file
64
+ const configFile = fs.existsSync('agent.yaml') ? 'agent.yaml' : 'oad.yaml';
65
+
66
+ const runtime = new AgentRuntime();
67
+ await runtime.loadConfig(configFile);
68
+ await runtime.initialize();
69
+ await runtime.start();
70
+
71
+ log(`Agent running (config=${configFile})`);
72
+
73
+ // Graceful shutdown
74
+ const shutdown = async (signal: string) => {
75
+ log(`Received ${signal}, shutting down...`);
76
+ clearInterval(heartbeatTimer);
77
+ await runtime.stop();
78
+ try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
79
+ log('Daemon stopped');
80
+ process.exit(0);
81
+ };
82
+
83
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
84
+ process.on('SIGINT', () => shutdown('SIGINT'));
85
+
86
+ // On Windows, handle the message-based kill
87
+ process.on('message', (msg) => {
88
+ if (msg === 'shutdown') shutdown('message:shutdown');
89
+ });
90
+ }
91
+
92
+ main().catch((err) => {
93
+ log(`Fatal error: ${err instanceof Error ? err.message : String(err)}`);
94
+ try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
95
+ process.exit(1);
96
+ });
package/src/index.ts CHANGED
@@ -76,6 +76,8 @@ export { SchedulerSkill } from './skills/scheduler';
76
76
  export type { ScheduledTask } from './skills/scheduler';
77
77
  export { DocumentSkill } from './skills/document';
78
78
  export type { DocumentChunk } from './skills/document';
79
+ export { SkillLearner, skillToMarkdown, parseSkillMarkdown } from './skills/auto-learn';
80
+ export type { LearnedSkill } from './skills/auto-learn';
79
81
 
80
82
  // v0.9.0 modules
81
83
  export { runTests, loadTestCases, formatReport } from './testing';
@@ -113,3 +115,12 @@ export type { StreamChunk, StreamOptions } from './core/streaming';
113
115
  // v1.3.0 modules
114
116
  export { TraceCollector, ConsoleExporter, DeepBrainExporter } from './traces';
115
117
  export type { Span, SpanEvent, TraceExporter } from './traces';
118
+
119
+ // v1.4.0 modules
120
+ export { Scheduler, parseCron, cronMatches } from './core/scheduler';
121
+ export type { CronJob, JobHandler } from './core/scheduler';
122
+
123
+ // v1.5.0 — built-in tools + MCP client
124
+ export { getBuiltinTools, getBuiltinToolsByName } from './tools/builtin';
125
+ export { MCPClient } from './tools/mcp-client';
126
+ export type { MCPServerConfig } from './tools/mcp-client';