opc-agent 4.1.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.md +20 -20
- package/.github/ISSUE_TEMPLATE/feature_request.md +14 -14
- package/.github/PULL_REQUEST_TEMPLATE.md +13 -13
- package/CHANGELOG.md +48 -48
- package/CONTRIBUTING.md +36 -36
- package/README.zh-CN.md +497 -497
- package/dist/channels/wechat.js +6 -6
- package/dist/deploy/index.js +56 -56
- package/dist/studio/server.js +30 -1
- package/dist/studio-ui/index.html +230 -10
- package/dist/ui/components.js +105 -105
- package/examples/README.md +22 -22
- package/examples/basic-agent.ts +90 -90
- package/examples/brain-integration.ts +71 -71
- package/examples/multi-channel.ts +74 -74
- package/fix-sidebar.mjs +188 -188
- package/install.ps1 +154 -154
- package/install.sh +164 -164
- package/package.json +1 -1
- package/scripts/install.ps1 +31 -31
- package/scripts/install.sh +40 -40
- package/serve-studio.js +13 -13
- package/serve-test.js +25 -25
- package/src/channels/dingtalk.ts +46 -46
- package/src/channels/email.ts +351 -351
- package/src/channels/feishu.ts +349 -349
- package/src/channels/googlechat.ts +42 -42
- package/src/channels/imessage.ts +31 -31
- package/src/channels/irc.ts +82 -82
- package/src/channels/line.ts +32 -32
- package/src/channels/matrix.ts +33 -33
- package/src/channels/mattermost.ts +57 -57
- package/src/channels/msteams.ts +32 -32
- package/src/channels/nostr.ts +32 -32
- package/src/channels/qq.ts +33 -33
- package/src/channels/signal.ts +32 -32
- package/src/channels/sms.ts +33 -33
- package/src/channels/telegram.ts +616 -616
- package/src/channels/twitch.ts +65 -65
- package/src/channels/voice-call.ts +100 -100
- package/src/channels/websocket.ts +399 -399
- package/src/channels/wechat.ts +329 -329
- package/src/channels/whatsapp.ts +32 -32
- package/src/cli/chat.ts +99 -99
- package/src/cli/setup.ts +314 -314
- package/src/core/agent.ts +476 -476
- package/src/core/api-server.ts +277 -277
- package/src/core/audio.ts +98 -98
- package/src/core/collaboration.ts +275 -275
- package/src/core/context-discovery.ts +85 -85
- package/src/core/context-refs.ts +140 -140
- package/src/core/gateway.ts +106 -106
- package/src/core/heartbeat.ts +51 -51
- package/src/core/hooks.ts +105 -105
- package/src/core/ide-bridge.ts +133 -133
- package/src/core/node-network.ts +86 -86
- package/src/core/profiles.ts +122 -122
- package/src/core/scheduler.ts +187 -187
- package/src/core/session-manager.ts +137 -137
- package/src/core/subagent.ts +98 -98
- package/src/core/vision.ts +180 -180
- package/src/core/workflow-graph.ts +365 -365
- package/src/daemon.ts +96 -96
- package/src/deploy/index.ts +255 -255
- package/src/doctor.ts +156 -156
- package/src/eval/index.ts +211 -211
- package/src/eval/suites/basic.json +16 -16
- package/src/eval/suites/memory.json +12 -12
- package/src/eval/suites/safety.json +14 -14
- package/src/hub/brain-seed.ts +54 -54
- package/src/hub/client.ts +60 -60
- package/src/mcp/servers/calculator-mcp.ts +65 -65
- package/src/mcp/servers/crypto-mcp.ts +73 -73
- package/src/mcp/servers/database-mcp.ts +72 -72
- package/src/mcp/servers/datetime-mcp.ts +69 -69
- package/src/mcp/servers/filesystem.ts +66 -66
- package/src/mcp/servers/github-mcp.ts +58 -58
- package/src/mcp/servers/index.ts +63 -63
- package/src/mcp/servers/json-mcp.ts +102 -102
- package/src/mcp/servers/memory-mcp.ts +56 -56
- package/src/mcp/servers/regex-mcp.ts +53 -53
- package/src/mcp/servers/web-mcp.ts +49 -49
- package/src/memory/context-compressor.ts +189 -189
- package/src/memory/seed-loader.ts +212 -212
- package/src/memory/user-profiler.ts +215 -215
- package/src/plugins/content-filter.ts +23 -23
- package/src/plugins/logger.ts +18 -18
- package/src/plugins/rate-limiter.ts +38 -38
- package/src/protocols/a2a/client.ts +132 -132
- package/src/protocols/a2a/index.ts +8 -8
- package/src/protocols/a2a/server.ts +333 -333
- package/src/protocols/a2a/types.ts +88 -88
- package/src/protocols/a2a/utils.ts +50 -50
- package/src/protocols/agui/client.ts +83 -83
- package/src/protocols/agui/index.ts +4 -4
- package/src/protocols/agui/server.ts +218 -218
- package/src/protocols/agui/types.ts +153 -153
- package/src/protocols/index.ts +2 -2
- package/src/protocols/mcp/agent-tools.ts +134 -134
- package/src/protocols/mcp/index.ts +8 -8
- package/src/protocols/mcp/server.ts +262 -262
- package/src/protocols/mcp/types.ts +69 -69
- package/src/providers/index.ts +632 -632
- package/src/publish/index.ts +376 -376
- package/src/scheduler/cron-engine.ts +191 -191
- package/src/scheduler/index.ts +2 -2
- package/src/schema/oad.ts +217 -217
- package/src/security/approval.ts +131 -131
- package/src/security/approvals.ts +143 -143
- package/src/security/elevated.ts +105 -105
- package/src/security/guardrails.ts +248 -248
- package/src/security/index.ts +9 -9
- package/src/security/keys.ts +87 -87
- package/src/security/secrets.ts +129 -129
- package/src/skills/builtin/index.ts +408 -408
- package/src/skills/marketplace.ts +113 -113
- package/src/skills/types.ts +42 -42
- package/src/studio/server.ts +31 -1
- package/src/studio/templates-data.ts +178 -178
- package/src/studio-ui/index.html +230 -10
- package/src/telemetry/index.ts +324 -324
- package/src/tools/builtin/browser.ts +299 -299
- package/src/tools/builtin/datetime.ts +41 -41
- package/src/tools/builtin/file.ts +107 -107
- package/src/tools/builtin/home-assistant.ts +116 -116
- package/src/tools/builtin/rl-tools.ts +243 -243
- package/src/tools/builtin/shell.ts +43 -43
- package/src/tools/builtin/vision.ts +64 -64
- package/src/tools/builtin/web-search.ts +126 -126
- package/src/tools/builtin/web.ts +35 -35
- package/src/tools/document-processor.ts +213 -213
- package/src/tools/image-generator.ts +150 -150
- package/src/tools/integrations/calendar.ts +73 -73
- package/src/tools/integrations/code-exec.ts +39 -39
- package/src/tools/integrations/csv-analyzer.ts +92 -92
- package/src/tools/integrations/database.ts +44 -44
- package/src/tools/integrations/email-send.ts +76 -76
- package/src/tools/integrations/git-tool.ts +42 -42
- package/src/tools/integrations/github-tool.ts +76 -76
- package/src/tools/integrations/image-gen.ts +56 -56
- package/src/tools/integrations/index.ts +92 -92
- package/src/tools/integrations/jira.ts +83 -83
- package/src/tools/integrations/notion.ts +71 -71
- package/src/tools/integrations/npm-tool.ts +48 -48
- package/src/tools/integrations/pdf-reader.ts +58 -58
- package/src/tools/integrations/slack.ts +65 -65
- package/src/tools/integrations/summarizer.ts +49 -49
- package/src/tools/integrations/translator.ts +48 -48
- package/src/tools/integrations/trello.ts +60 -60
- package/src/tools/integrations/vector-search.ts +42 -42
- package/src/tools/integrations/web-scraper.ts +47 -47
- package/src/tools/integrations/web-search.ts +58 -58
- package/src/tools/integrations/webhook.ts +38 -38
- package/src/tools/mcp-client.ts +131 -131
- package/src/tools/web-scraper.ts +179 -179
- package/src/tools/web-search.ts +180 -180
- package/src/ui/components.ts +127 -127
- package/srv-out.txt +1 -1
- package/templates/ecommerce-assistant/README.md +45 -45
- package/templates/ecommerce-assistant/oad.yaml +47 -47
- package/templates/tech-support/README.md +43 -43
- package/templates/tech-support/oad.yaml +45 -45
- package/test-agent/Dockerfile +9 -9
- package/test-agent/README.md +50 -50
- package/test-agent/agent.yaml +23 -23
- package/test-agent/docker-compose.yml +11 -11
- package/test-agent/oad.yaml +31 -31
- package/test-agent/package-lock.json +1492 -1492
- package/test-agent/package.json +17 -17
- package/test-agent/src/index.ts +24 -24
- package/test-agent/src/skills/echo.ts +15 -15
- package/test-agent/tsconfig.json +24 -24
- package/test-full.js +43 -43
- package/test-sidebar.js +22 -22
- package/test-studio3.js +75 -75
- package/test-studio4.js +41 -41
- package/tests/a2a-protocol.test.ts +285 -285
- package/tests/agui-protocol.test.ts +246 -246
- package/tests/api-server.test.ts +148 -148
- package/tests/approvals.test.ts +89 -89
- package/tests/audio.test.ts +40 -40
- package/tests/brain-seed-extended.test.ts +490 -490
- package/tests/brain-seed.test.ts +239 -239
- package/tests/browser.test.ts +179 -179
- package/tests/channels/discord.test.ts +79 -79
- package/tests/channels/email.test.ts +148 -148
- package/tests/channels/feishu.test.ts +123 -123
- package/tests/channels/telegram.test.ts +129 -129
- package/tests/channels/websocket.test.ts +53 -53
- package/tests/channels/wechat.test.ts +170 -170
- package/tests/channels-extra.test.ts +45 -45
- package/tests/chat-cli.test.ts +160 -160
- package/tests/cli.test.ts +46 -46
- package/tests/context-compressor.test.ts +172 -172
- package/tests/context-refs.test.ts +121 -121
- package/tests/cron-engine.test.ts +101 -101
- package/tests/daemon.test.ts +135 -135
- package/tests/deepbrain-wire.test.ts +234 -234
- package/tests/deploy-and-dag.test.ts +196 -196
- package/tests/doctor.test.ts +38 -38
- package/tests/document-processor.test.ts +69 -69
- package/tests/e2e-nocode.test.ts +442 -442
- package/tests/elevated.test.ts +69 -69
- package/tests/eval.test.ts +173 -173
- package/tests/gateway.test.ts +63 -63
- package/tests/guardrails.test.ts +177 -177
- package/tests/home-assistant.test.ts +40 -40
- package/tests/hooks.test.ts +79 -79
- package/tests/ide-bridge.test.ts +38 -38
- package/tests/image-generator.test.ts +84 -84
- package/tests/init-role.test.ts +124 -124
- package/tests/integrations.test.ts +249 -249
- package/tests/mcp-client.test.ts +92 -92
- package/tests/mcp-server.test.ts +178 -178
- package/tests/mcp-servers.test.ts +260 -260
- package/tests/node-network.test.ts +74 -74
- package/tests/plugin-a2a-enhanced.test.ts +230 -230
- package/tests/profiles.test.ts +61 -61
- package/tests/publish.test.ts +231 -231
- package/tests/rl-tools.test.ts +93 -93
- package/tests/sandbox-manager.test.ts +46 -46
- package/tests/scheduler.test.ts +200 -200
- package/tests/secrets.test.ts +107 -107
- package/tests/security-enhanced.test.ts +233 -233
- package/tests/settings-api.test.ts +148 -148
- package/tests/setup.test.ts +73 -73
- package/tests/subagent.test.ts +193 -193
- package/tests/telegram-discord.test.ts +60 -60
- package/tests/telemetry.test.ts +186 -186
- package/tests/user-profiler.test.ts +169 -169
- package/tests/v090-features.test.ts +254 -254
- package/tests/vision.test.ts +61 -61
- package/tests/voice-call.test.ts +47 -47
- package/tests/voice-enhanced.test.ts +169 -169
- package/tests/voice-interaction.test.ts +38 -38
- package/tests/web-search.test.ts +155 -155
- package/tests/workflow-graph.test.ts +279 -279
- package/tutorial/customer-service-agent/README.md +612 -612
- package/tutorial/customer-service-agent/SOUL.md +26 -26
- package/tutorial/customer-service-agent/agent.yaml +63 -63
- package/tutorial/customer-service-agent/package.json +19 -19
- package/tutorial/customer-service-agent/src/index.ts +69 -69
- package/tutorial/customer-service-agent/src/skills/faq.ts +27 -27
- package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -22
- package/tutorial/customer-service-agent/tsconfig.json +14 -14
package/src/core/scheduler.ts
CHANGED
|
@@ -1,187 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import * as crypto from 'crypto';
|
|
4
|
-
import type { Message } from './types';
|
|
5
|
-
|
|
6
|
-
export interface Session {
|
|
7
|
-
id: string;
|
|
8
|
-
agentId: string;
|
|
9
|
-
channel: string;
|
|
10
|
-
messages: Message[];
|
|
11
|
-
metadata: Record<string, any>;
|
|
12
|
-
createdAt: number;
|
|
13
|
-
lastActivity: number;
|
|
14
|
-
parentId?: string;
|
|
15
|
-
compactedAt?: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class SessionManager {
|
|
19
|
-
private sessions: Map<string, Session> = new Map();
|
|
20
|
-
private storageDir: string;
|
|
21
|
-
|
|
22
|
-
constructor(storageDir?: string) {
|
|
23
|
-
this.storageDir = storageDir || path.join(process.env.HOME || process.env.USERPROFILE || '~', '.opc', 'sessions');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
create(agentId: string, channel: string, parentId?: string): Session {
|
|
27
|
-
const session: Session = {
|
|
28
|
-
id: crypto.randomUUID(),
|
|
29
|
-
agentId,
|
|
30
|
-
channel,
|
|
31
|
-
messages: [],
|
|
32
|
-
metadata: {},
|
|
33
|
-
createdAt: Date.now(),
|
|
34
|
-
lastActivity: Date.now(),
|
|
35
|
-
parentId,
|
|
36
|
-
};
|
|
37
|
-
this.sessions.set(session.id, session);
|
|
38
|
-
return session;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
get(id: string): Session | null {
|
|
42
|
-
return this.sessions.get(id) || null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
list(filter?: { agentId?: string; channel?: string; active?: boolean }): Session[] {
|
|
46
|
-
let result = Array.from(this.sessions.values());
|
|
47
|
-
if (filter?.agentId) result = result.filter(s => s.agentId === filter.agentId);
|
|
48
|
-
if (filter?.channel) result = result.filter(s => s.channel === filter.channel);
|
|
49
|
-
if (filter?.active !== undefined) {
|
|
50
|
-
const cutoff = Date.now() - 30 * 60 * 1000; // 30 min
|
|
51
|
-
result = result.filter(s => filter.active ? s.lastActivity > cutoff : s.lastActivity <= cutoff);
|
|
52
|
-
}
|
|
53
|
-
return result;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
addMessage(sessionId: string, message: Message): void {
|
|
57
|
-
const session = this.sessions.get(sessionId);
|
|
58
|
-
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
59
|
-
session.messages.push(message);
|
|
60
|
-
session.lastActivity = Date.now();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async compact(sessionId: string, brain?: any): Promise<void> {
|
|
64
|
-
const session = this.sessions.get(sessionId);
|
|
65
|
-
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
66
|
-
if (brain && typeof brain.compress === 'function') {
|
|
67
|
-
const compressed = await brain.compress(session.messages);
|
|
68
|
-
session.messages = [{ id: 'compacted', role: 'system', content: compressed, timestamp: Date.now() }];
|
|
69
|
-
} else {
|
|
70
|
-
// Simple: keep first and last 5 messages
|
|
71
|
-
if (session.messages.length > 10) {
|
|
72
|
-
const first = session.messages.slice(0, 2);
|
|
73
|
-
const last = session.messages.slice(-5);
|
|
74
|
-
session.messages = [...first, { id: 'compacted', role: 'system', content: `[${session.messages.length - 7} messages compacted]`, timestamp: Date.now() }, ...last];
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
session.compactedAt = Date.now();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
prune(maxAge: number): number {
|
|
81
|
-
const cutoff = Date.now() - maxAge;
|
|
82
|
-
let pruned = 0;
|
|
83
|
-
for (const [id, session] of this.sessions) {
|
|
84
|
-
if (session.lastActivity < cutoff) {
|
|
85
|
-
this.sessions.delete(id);
|
|
86
|
-
pruned++;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return pruned;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
getLineage(sessionId: string): Session[] {
|
|
93
|
-
const lineage: Session[] = [];
|
|
94
|
-
let current = this.sessions.get(sessionId);
|
|
95
|
-
while (current) {
|
|
96
|
-
lineage.unshift(current);
|
|
97
|
-
current = current.parentId ? this.sessions.get(current.parentId) || undefined : undefined;
|
|
98
|
-
}
|
|
99
|
-
return lineage;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
fork(sessionId: string): Session {
|
|
103
|
-
const parent = this.sessions.get(sessionId);
|
|
104
|
-
if (!parent) throw new Error(`Session ${sessionId} not found`);
|
|
105
|
-
return this.create(parent.agentId, parent.channel, parent.id);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export(sessionId: string): string {
|
|
109
|
-
const session = this.sessions.get(sessionId);
|
|
110
|
-
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
111
|
-
const lines = [`# Session ${session.id}`, `Agent: ${session.agentId} | Channel: ${session.channel}`, `Created: ${new Date(session.createdAt).toISOString()}`, ''];
|
|
112
|
-
for (const msg of session.messages) {
|
|
113
|
-
lines.push(`**${msg.role}** (${new Date(msg.timestamp).toISOString()}):`);
|
|
114
|
-
lines.push(msg.content);
|
|
115
|
-
lines.push('');
|
|
116
|
-
}
|
|
117
|
-
return lines.join('\n');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
save(): void {
|
|
121
|
-
if (!fs.existsSync(this.storageDir)) {
|
|
122
|
-
fs.mkdirSync(this.storageDir, { recursive: true });
|
|
123
|
-
}
|
|
124
|
-
for (const [id, session] of this.sessions) {
|
|
125
|
-
fs.writeFileSync(path.join(this.storageDir, `${id}.json`), JSON.stringify(session, null, 2));
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
load(): void {
|
|
130
|
-
if (!fs.existsSync(this.storageDir)) return;
|
|
131
|
-
const files = fs.readdirSync(this.storageDir).filter(f => f.endsWith('.json'));
|
|
132
|
-
for (const file of files) {
|
|
133
|
-
const data = JSON.parse(fs.readFileSync(path.join(this.storageDir, file), 'utf-8'));
|
|
134
|
-
this.sessions.set(data.id, data);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
4
|
+
import type { Message } from './types';
|
|
5
|
+
|
|
6
|
+
export interface Session {
|
|
7
|
+
id: string;
|
|
8
|
+
agentId: string;
|
|
9
|
+
channel: string;
|
|
10
|
+
messages: Message[];
|
|
11
|
+
metadata: Record<string, any>;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
lastActivity: number;
|
|
14
|
+
parentId?: string;
|
|
15
|
+
compactedAt?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class SessionManager {
|
|
19
|
+
private sessions: Map<string, Session> = new Map();
|
|
20
|
+
private storageDir: string;
|
|
21
|
+
|
|
22
|
+
constructor(storageDir?: string) {
|
|
23
|
+
this.storageDir = storageDir || path.join(process.env.HOME || process.env.USERPROFILE || '~', '.opc', 'sessions');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
create(agentId: string, channel: string, parentId?: string): Session {
|
|
27
|
+
const session: Session = {
|
|
28
|
+
id: crypto.randomUUID(),
|
|
29
|
+
agentId,
|
|
30
|
+
channel,
|
|
31
|
+
messages: [],
|
|
32
|
+
metadata: {},
|
|
33
|
+
createdAt: Date.now(),
|
|
34
|
+
lastActivity: Date.now(),
|
|
35
|
+
parentId,
|
|
36
|
+
};
|
|
37
|
+
this.sessions.set(session.id, session);
|
|
38
|
+
return session;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get(id: string): Session | null {
|
|
42
|
+
return this.sessions.get(id) || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
list(filter?: { agentId?: string; channel?: string; active?: boolean }): Session[] {
|
|
46
|
+
let result = Array.from(this.sessions.values());
|
|
47
|
+
if (filter?.agentId) result = result.filter(s => s.agentId === filter.agentId);
|
|
48
|
+
if (filter?.channel) result = result.filter(s => s.channel === filter.channel);
|
|
49
|
+
if (filter?.active !== undefined) {
|
|
50
|
+
const cutoff = Date.now() - 30 * 60 * 1000; // 30 min
|
|
51
|
+
result = result.filter(s => filter.active ? s.lastActivity > cutoff : s.lastActivity <= cutoff);
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
addMessage(sessionId: string, message: Message): void {
|
|
57
|
+
const session = this.sessions.get(sessionId);
|
|
58
|
+
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
59
|
+
session.messages.push(message);
|
|
60
|
+
session.lastActivity = Date.now();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async compact(sessionId: string, brain?: any): Promise<void> {
|
|
64
|
+
const session = this.sessions.get(sessionId);
|
|
65
|
+
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
66
|
+
if (brain && typeof brain.compress === 'function') {
|
|
67
|
+
const compressed = await brain.compress(session.messages);
|
|
68
|
+
session.messages = [{ id: 'compacted', role: 'system', content: compressed, timestamp: Date.now() }];
|
|
69
|
+
} else {
|
|
70
|
+
// Simple: keep first and last 5 messages
|
|
71
|
+
if (session.messages.length > 10) {
|
|
72
|
+
const first = session.messages.slice(0, 2);
|
|
73
|
+
const last = session.messages.slice(-5);
|
|
74
|
+
session.messages = [...first, { id: 'compacted', role: 'system', content: `[${session.messages.length - 7} messages compacted]`, timestamp: Date.now() }, ...last];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
session.compactedAt = Date.now();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
prune(maxAge: number): number {
|
|
81
|
+
const cutoff = Date.now() - maxAge;
|
|
82
|
+
let pruned = 0;
|
|
83
|
+
for (const [id, session] of this.sessions) {
|
|
84
|
+
if (session.lastActivity < cutoff) {
|
|
85
|
+
this.sessions.delete(id);
|
|
86
|
+
pruned++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return pruned;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getLineage(sessionId: string): Session[] {
|
|
93
|
+
const lineage: Session[] = [];
|
|
94
|
+
let current = this.sessions.get(sessionId);
|
|
95
|
+
while (current) {
|
|
96
|
+
lineage.unshift(current);
|
|
97
|
+
current = current.parentId ? this.sessions.get(current.parentId) || undefined : undefined;
|
|
98
|
+
}
|
|
99
|
+
return lineage;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fork(sessionId: string): Session {
|
|
103
|
+
const parent = this.sessions.get(sessionId);
|
|
104
|
+
if (!parent) throw new Error(`Session ${sessionId} not found`);
|
|
105
|
+
return this.create(parent.agentId, parent.channel, parent.id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export(sessionId: string): string {
|
|
109
|
+
const session = this.sessions.get(sessionId);
|
|
110
|
+
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
111
|
+
const lines = [`# Session ${session.id}`, `Agent: ${session.agentId} | Channel: ${session.channel}`, `Created: ${new Date(session.createdAt).toISOString()}`, ''];
|
|
112
|
+
for (const msg of session.messages) {
|
|
113
|
+
lines.push(`**${msg.role}** (${new Date(msg.timestamp).toISOString()}):`);
|
|
114
|
+
lines.push(msg.content);
|
|
115
|
+
lines.push('');
|
|
116
|
+
}
|
|
117
|
+
return lines.join('\n');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
save(): void {
|
|
121
|
+
if (!fs.existsSync(this.storageDir)) {
|
|
122
|
+
fs.mkdirSync(this.storageDir, { recursive: true });
|
|
123
|
+
}
|
|
124
|
+
for (const [id, session] of this.sessions) {
|
|
125
|
+
fs.writeFileSync(path.join(this.storageDir, `${id}.json`), JSON.stringify(session, null, 2));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
load(): void {
|
|
130
|
+
if (!fs.existsSync(this.storageDir)) return;
|
|
131
|
+
const files = fs.readdirSync(this.storageDir).filter(f => f.endsWith('.json'));
|
|
132
|
+
for (const file of files) {
|
|
133
|
+
const data = JSON.parse(fs.readFileSync(path.join(this.storageDir, file), 'utf-8'));
|
|
134
|
+
this.sessions.set(data.id, data);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|