daemora 1.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/README.md +666 -0
- package/SOUL.md +104 -0
- package/config/hooks.json +14 -0
- package/config/mcp.json +145 -0
- package/package.json +86 -0
- package/skills/.gitkeep +0 -0
- package/skills/apple-notes.md +193 -0
- package/skills/apple-reminders.md +189 -0
- package/skills/camsnap.md +162 -0
- package/skills/coding.md +14 -0
- package/skills/documents.md +13 -0
- package/skills/email.md +13 -0
- package/skills/gif-search.md +196 -0
- package/skills/healthcheck.md +225 -0
- package/skills/image-gen.md +147 -0
- package/skills/model-usage.md +182 -0
- package/skills/obsidian.md +207 -0
- package/skills/pdf.md +211 -0
- package/skills/research.md +13 -0
- package/skills/skill-creator.md +142 -0
- package/skills/spotify.md +149 -0
- package/skills/summarize.md +230 -0
- package/skills/things.md +199 -0
- package/skills/tmux.md +204 -0
- package/skills/trello.md +183 -0
- package/skills/video-frames.md +202 -0
- package/skills/weather.md +127 -0
- package/src/a2a/A2AClient.js +136 -0
- package/src/a2a/A2AServer.js +316 -0
- package/src/a2a/AgentCard.js +79 -0
- package/src/agents/SubAgentManager.js +369 -0
- package/src/agents/Supervisor.js +192 -0
- package/src/channels/BaseChannel.js +104 -0
- package/src/channels/DiscordChannel.js +288 -0
- package/src/channels/EmailChannel.js +172 -0
- package/src/channels/GoogleChatChannel.js +316 -0
- package/src/channels/HttpChannel.js +26 -0
- package/src/channels/LineChannel.js +168 -0
- package/src/channels/SignalChannel.js +186 -0
- package/src/channels/SlackChannel.js +329 -0
- package/src/channels/TeamsChannel.js +272 -0
- package/src/channels/TelegramChannel.js +347 -0
- package/src/channels/WhatsAppChannel.js +219 -0
- package/src/channels/index.js +198 -0
- package/src/cli.js +1267 -0
- package/src/config/agentProfiles.js +120 -0
- package/src/config/channels.js +32 -0
- package/src/config/default.js +206 -0
- package/src/config/models.js +123 -0
- package/src/config/permissions.js +167 -0
- package/src/core/AgentLoop.js +446 -0
- package/src/core/Compaction.js +143 -0
- package/src/core/CostTracker.js +116 -0
- package/src/core/EventBus.js +46 -0
- package/src/core/Task.js +67 -0
- package/src/core/TaskQueue.js +206 -0
- package/src/core/TaskRunner.js +226 -0
- package/src/daemon/DaemonManager.js +301 -0
- package/src/hooks/HookRunner.js +230 -0
- package/src/index.js +482 -0
- package/src/mcp/MCPAgentRunner.js +112 -0
- package/src/mcp/MCPClient.js +186 -0
- package/src/mcp/MCPManager.js +412 -0
- package/src/models/ModelRouter.js +180 -0
- package/src/safety/AuditLog.js +135 -0
- package/src/safety/CircuitBreaker.js +126 -0
- package/src/safety/FilesystemGuard.js +169 -0
- package/src/safety/GitRollback.js +139 -0
- package/src/safety/HumanApproval.js +156 -0
- package/src/safety/InputSanitizer.js +72 -0
- package/src/safety/PermissionGuard.js +83 -0
- package/src/safety/Sandbox.js +70 -0
- package/src/safety/SecretScanner.js +100 -0
- package/src/safety/SecretVault.js +250 -0
- package/src/scheduler/Heartbeat.js +115 -0
- package/src/scheduler/Scheduler.js +228 -0
- package/src/services/models/outputSchema.js +15 -0
- package/src/services/openai.js +25 -0
- package/src/services/sessions.js +65 -0
- package/src/setup/theme.js +110 -0
- package/src/setup/wizard.js +788 -0
- package/src/skills/SkillLoader.js +168 -0
- package/src/storage/TaskStore.js +69 -0
- package/src/systemPrompt.js +526 -0
- package/src/tenants/TenantContext.js +19 -0
- package/src/tenants/TenantManager.js +379 -0
- package/src/tools/ToolRegistry.js +141 -0
- package/src/tools/applyPatch.js +144 -0
- package/src/tools/browserAutomation.js +223 -0
- package/src/tools/createDocument.js +265 -0
- package/src/tools/cronTool.js +105 -0
- package/src/tools/editFile.js +139 -0
- package/src/tools/executeCommand.js +123 -0
- package/src/tools/glob.js +67 -0
- package/src/tools/grep.js +121 -0
- package/src/tools/imageAnalysis.js +120 -0
- package/src/tools/index.js +173 -0
- package/src/tools/listDirectory.js +47 -0
- package/src/tools/manageAgents.js +47 -0
- package/src/tools/manageMCP.js +159 -0
- package/src/tools/memory.js +478 -0
- package/src/tools/messageChannel.js +45 -0
- package/src/tools/projectTracker.js +259 -0
- package/src/tools/readFile.js +52 -0
- package/src/tools/screenCapture.js +112 -0
- package/src/tools/searchContent.js +76 -0
- package/src/tools/searchFiles.js +75 -0
- package/src/tools/sendEmail.js +118 -0
- package/src/tools/sendFile.js +63 -0
- package/src/tools/textToSpeech.js +161 -0
- package/src/tools/transcribeAudio.js +82 -0
- package/src/tools/useMCP.js +29 -0
- package/src/tools/webFetch.js +150 -0
- package/src/tools/webSearch.js +134 -0
- package/src/tools/writeFile.js +26 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync, createHash } from "crypto";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { config } from "../config/default.js";
|
|
5
|
+
import eventBus from "../core/EventBus.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Secret Vault — encrypted secret storage.
|
|
9
|
+
*
|
|
10
|
+
* All API keys, tokens, and credentials are encrypted at rest using
|
|
11
|
+
* AES-256-GCM with a user-provided master passphrase.
|
|
12
|
+
*
|
|
13
|
+
* Security model:
|
|
14
|
+
* - User provides a master passphrase during setup
|
|
15
|
+
* - Passphrase → scrypt → 256-bit key (with per-vault salt)
|
|
16
|
+
* - Each secret encrypted individually with unique IV
|
|
17
|
+
* - Even if filesystem is compromised, secrets can't be read without passphrase
|
|
18
|
+
* - Vault file: data/.vault.enc (encrypted JSON)
|
|
19
|
+
* - No plaintext API keys anywhere on disk
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* vault.unlock("user-passphrase")
|
|
23
|
+
* vault.set("OPENAI_API_KEY", "sk-...")
|
|
24
|
+
* const key = vault.get("OPENAI_API_KEY")
|
|
25
|
+
* vault.lock()
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const VAULT_FILE = ".vault.enc";
|
|
29
|
+
const SALT_FILE = ".vault.salt";
|
|
30
|
+
const ALGORITHM = "aes-256-gcm";
|
|
31
|
+
const SCRYPT_N = 16384;
|
|
32
|
+
const SCRYPT_R = 8;
|
|
33
|
+
const SCRYPT_P = 1;
|
|
34
|
+
const KEY_LENGTH = 32;
|
|
35
|
+
|
|
36
|
+
class SecretVault {
|
|
37
|
+
constructor() {
|
|
38
|
+
this.vaultPath = join(config.dataDir, VAULT_FILE);
|
|
39
|
+
this.saltPath = join(config.dataDir, SALT_FILE);
|
|
40
|
+
this.encryptionKey = null;
|
|
41
|
+
this.secrets = null; // decrypted secrets in memory (only while unlocked)
|
|
42
|
+
this.unlocked = false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Unlock the vault with a master passphrase.
|
|
47
|
+
* Must be called before get/set operations.
|
|
48
|
+
*/
|
|
49
|
+
unlock(passphrase) {
|
|
50
|
+
if (!passphrase || passphrase.length < 8) {
|
|
51
|
+
throw new Error("Passphrase must be at least 8 characters");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Get or create salt
|
|
55
|
+
let salt;
|
|
56
|
+
if (existsSync(this.saltPath)) {
|
|
57
|
+
salt = readFileSync(this.saltPath);
|
|
58
|
+
} else {
|
|
59
|
+
salt = randomBytes(32);
|
|
60
|
+
mkdirSync(config.dataDir, { recursive: true });
|
|
61
|
+
writeFileSync(this.saltPath, salt);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Derive key from passphrase
|
|
65
|
+
this.encryptionKey = scryptSync(passphrase, salt, KEY_LENGTH, {
|
|
66
|
+
N: SCRYPT_N,
|
|
67
|
+
r: SCRYPT_R,
|
|
68
|
+
p: SCRYPT_P,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Load existing vault or create empty
|
|
72
|
+
if (existsSync(this.vaultPath)) {
|
|
73
|
+
try {
|
|
74
|
+
const encrypted = readFileSync(this.vaultPath, "utf-8");
|
|
75
|
+
const decrypted = this._decrypt(encrypted);
|
|
76
|
+
this.secrets = JSON.parse(decrypted);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
"Failed to unlock vault. Wrong passphrase or corrupted vault file."
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
this.secrets = {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.unlocked = true;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Lock the vault — clear decrypted secrets from memory.
|
|
92
|
+
*/
|
|
93
|
+
lock() {
|
|
94
|
+
this.secrets = null;
|
|
95
|
+
this.encryptionKey = null;
|
|
96
|
+
this.unlocked = false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Store a secret in the vault.
|
|
101
|
+
*/
|
|
102
|
+
set(key, value) {
|
|
103
|
+
this._ensureUnlocked();
|
|
104
|
+
this.secrets[key] = value;
|
|
105
|
+
this._save();
|
|
106
|
+
eventBus.emitEvent("vault:secret_stored", { key });
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Retrieve a secret from the vault.
|
|
112
|
+
*/
|
|
113
|
+
get(key) {
|
|
114
|
+
this._ensureUnlocked();
|
|
115
|
+
return this.secrets[key] || null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Delete a secret from the vault.
|
|
120
|
+
*/
|
|
121
|
+
delete(key) {
|
|
122
|
+
this._ensureUnlocked();
|
|
123
|
+
delete this.secrets[key];
|
|
124
|
+
this._save();
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* List all secret keys (NOT values).
|
|
130
|
+
*/
|
|
131
|
+
list() {
|
|
132
|
+
this._ensureUnlocked();
|
|
133
|
+
return Object.keys(this.secrets).map((key) => ({
|
|
134
|
+
key,
|
|
135
|
+
length: this.secrets[key]?.length || 0,
|
|
136
|
+
preview: `${this.secrets[key]?.slice(0, 4)}...`,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if vault exists (has been set up).
|
|
142
|
+
*/
|
|
143
|
+
exists() {
|
|
144
|
+
return existsSync(this.vaultPath);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if vault is unlocked.
|
|
149
|
+
*/
|
|
150
|
+
isUnlocked() {
|
|
151
|
+
return this.unlocked;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Import secrets from .env file into the vault.
|
|
156
|
+
* After import, the .env values can be removed.
|
|
157
|
+
*/
|
|
158
|
+
importFromEnv(envPath) {
|
|
159
|
+
this._ensureUnlocked();
|
|
160
|
+
|
|
161
|
+
if (!existsSync(envPath)) {
|
|
162
|
+
throw new Error(`.env file not found: ${envPath}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const content = readFileSync(envPath, "utf-8");
|
|
166
|
+
const lines = content.split("\n");
|
|
167
|
+
let imported = 0;
|
|
168
|
+
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
const trimmed = line.trim();
|
|
171
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
172
|
+
|
|
173
|
+
const eqIdx = trimmed.indexOf("=");
|
|
174
|
+
if (eqIdx <= 0) continue;
|
|
175
|
+
|
|
176
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
177
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
178
|
+
|
|
179
|
+
// Remove surrounding quotes
|
|
180
|
+
if (
|
|
181
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
182
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
183
|
+
) {
|
|
184
|
+
value = value.slice(1, -1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Only import non-empty secret-looking values
|
|
188
|
+
if (value && value.length >= 8) {
|
|
189
|
+
this.secrets[key] = value;
|
|
190
|
+
imported++;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this._save();
|
|
195
|
+
return imported;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get all secrets as environment variables (for process.env injection).
|
|
200
|
+
*/
|
|
201
|
+
getAsEnv() {
|
|
202
|
+
this._ensureUnlocked();
|
|
203
|
+
return { ...this.secrets };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ===== Private methods =====
|
|
207
|
+
|
|
208
|
+
_ensureUnlocked() {
|
|
209
|
+
if (!this.unlocked || !this.secrets) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
"Vault is locked. Call vault.unlock(passphrase) first."
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
_save() {
|
|
217
|
+
const json = JSON.stringify(this.secrets);
|
|
218
|
+
const encrypted = this._encrypt(json);
|
|
219
|
+
writeFileSync(this.vaultPath, encrypted, "utf-8");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_encrypt(plaintext) {
|
|
223
|
+
const iv = randomBytes(16);
|
|
224
|
+
const cipher = createCipheriv(ALGORITHM, this.encryptionKey, iv);
|
|
225
|
+
let encrypted = cipher.update(plaintext, "utf-8", "hex");
|
|
226
|
+
encrypted += cipher.final("hex");
|
|
227
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
228
|
+
// Format: iv:authTag:ciphertext
|
|
229
|
+
return `${iv.toString("hex")}:${authTag}:${encrypted}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_decrypt(encryptedStr) {
|
|
233
|
+
const parts = encryptedStr.split(":");
|
|
234
|
+
if (parts.length !== 3) {
|
|
235
|
+
throw new Error("Invalid vault format");
|
|
236
|
+
}
|
|
237
|
+
const [ivHex, authTagHex, ciphertext] = parts;
|
|
238
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
239
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
240
|
+
|
|
241
|
+
const decipher = createDecipheriv(ALGORITHM, this.encryptionKey, iv);
|
|
242
|
+
decipher.setAuthTag(authTag);
|
|
243
|
+
let decrypted = decipher.update(ciphertext, "hex", "utf-8");
|
|
244
|
+
decrypted += decipher.final("utf-8");
|
|
245
|
+
return decrypted;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const secretVault = new SecretVault();
|
|
250
|
+
export default secretVault;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { config } from "../config/default.js";
|
|
4
|
+
import taskQueue from "../core/TaskQueue.js";
|
|
5
|
+
import eventBus from "../core/EventBus.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Heartbeat — periodic proactive check.
|
|
9
|
+
*
|
|
10
|
+
* Reads HEARTBEAT.md for user-defined checks.
|
|
11
|
+
* Every N minutes, creates a task: "Check status per HEARTBEAT.md"
|
|
12
|
+
* If nothing notable → "All clear" (no notification).
|
|
13
|
+
* If something needs attention → sends result to configured channel.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
class Heartbeat {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.timer = null;
|
|
19
|
+
this.running = false;
|
|
20
|
+
this.heartbeatPath = join(config.rootDir, "HEARTBEAT.md");
|
|
21
|
+
this.intervalMinutes = config.heartbeatIntervalMinutes;
|
|
22
|
+
this.lastCheck = null;
|
|
23
|
+
this.checkCount = 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Start the heartbeat.
|
|
28
|
+
*/
|
|
29
|
+
start() {
|
|
30
|
+
if (!config.daemonMode) {
|
|
31
|
+
console.log(`[Heartbeat] Skipped — daemon mode not enabled`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!existsSync(this.heartbeatPath)) {
|
|
36
|
+
console.log(`[Heartbeat] No HEARTBEAT.md found — heartbeat disabled`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.running = true;
|
|
41
|
+
this.timer = setInterval(
|
|
42
|
+
() => this.check(),
|
|
43
|
+
this.intervalMinutes * 60 * 1000
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
console.log(
|
|
47
|
+
`[Heartbeat] Started (every ${this.intervalMinutes} minutes)`
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Run first check after 1 minute
|
|
51
|
+
setTimeout(() => this.check(), 60000);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Stop the heartbeat.
|
|
56
|
+
*/
|
|
57
|
+
stop() {
|
|
58
|
+
this.running = false;
|
|
59
|
+
if (this.timer) {
|
|
60
|
+
clearInterval(this.timer);
|
|
61
|
+
this.timer = null;
|
|
62
|
+
}
|
|
63
|
+
console.log(`[Heartbeat] Stopped`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Run a heartbeat check.
|
|
68
|
+
*/
|
|
69
|
+
async check() {
|
|
70
|
+
if (!this.running) return;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const instructions = readFileSync(this.heartbeatPath, "utf-8");
|
|
74
|
+
this.checkCount++;
|
|
75
|
+
this.lastCheck = new Date().toISOString();
|
|
76
|
+
|
|
77
|
+
console.log(`[Heartbeat] Check #${this.checkCount}...`);
|
|
78
|
+
|
|
79
|
+
const prompt = `You are running a periodic heartbeat check. Review the following instructions and check each item. If everything looks fine, just respond "All clear." If something needs attention, describe what you found.
|
|
80
|
+
|
|
81
|
+
Instructions from HEARTBEAT.md:
|
|
82
|
+
${instructions}
|
|
83
|
+
|
|
84
|
+
Current time: ${new Date().toISOString()}`;
|
|
85
|
+
|
|
86
|
+
taskQueue.enqueue({
|
|
87
|
+
input: prompt,
|
|
88
|
+
channel: "heartbeat",
|
|
89
|
+
sessionId: null,
|
|
90
|
+
priority: 2,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
eventBus.emitEvent("heartbeat:check", {
|
|
94
|
+
checkNumber: this.checkCount,
|
|
95
|
+
});
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.log(`[Heartbeat] Error: ${error.message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get stats.
|
|
103
|
+
*/
|
|
104
|
+
stats() {
|
|
105
|
+
return {
|
|
106
|
+
running: this.running,
|
|
107
|
+
intervalMinutes: this.intervalMinutes,
|
|
108
|
+
lastCheck: this.lastCheck,
|
|
109
|
+
checkCount: this.checkCount,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const heartbeat = new Heartbeat();
|
|
115
|
+
export default heartbeat;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import cron from "node-cron";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { v4 as uuidv4 } from "uuid";
|
|
5
|
+
import { config } from "../config/default.js";
|
|
6
|
+
import taskQueue from "../core/TaskQueue.js";
|
|
7
|
+
import eventBus from "../core/EventBus.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Scheduler — cron-based task scheduling.
|
|
11
|
+
*
|
|
12
|
+
* Schedules stored in data/schedules.json.
|
|
13
|
+
* Each schedule creates a Task when its cron expression triggers.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
class Scheduler {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.schedules = new Map();
|
|
19
|
+
this.cronJobs = new Map();
|
|
20
|
+
this.schedulesPath = join(config.dataDir, "schedules.json");
|
|
21
|
+
this.running = false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Start the scheduler — load and activate all schedules.
|
|
26
|
+
*/
|
|
27
|
+
start() {
|
|
28
|
+
this.loadSchedules();
|
|
29
|
+
|
|
30
|
+
for (const [id, schedule] of this.schedules) {
|
|
31
|
+
if (schedule.enabled) {
|
|
32
|
+
this.activateSchedule(id, schedule);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.running = true;
|
|
37
|
+
console.log(
|
|
38
|
+
`[Scheduler] Started — ${this.schedules.size} schedule(s) loaded`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Stop the scheduler — cancel all cron jobs.
|
|
44
|
+
*/
|
|
45
|
+
stop() {
|
|
46
|
+
for (const [id, job] of this.cronJobs) {
|
|
47
|
+
job.stop();
|
|
48
|
+
}
|
|
49
|
+
this.cronJobs.clear();
|
|
50
|
+
this.running = false;
|
|
51
|
+
console.log(`[Scheduler] Stopped`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a new schedule.
|
|
56
|
+
* @param {object} options
|
|
57
|
+
* @param {string} options.cronExpression - Cron expression (e.g., "0 9 * * *" = daily at 9 AM)
|
|
58
|
+
* @param {string} options.taskInput - The task/message to send when triggered
|
|
59
|
+
* @param {string} [options.channel] - Channel to use (default: "scheduler")
|
|
60
|
+
* @param {string} [options.model] - Model to use
|
|
61
|
+
* @param {string} [options.name] - Human-readable name
|
|
62
|
+
*/
|
|
63
|
+
create({ cronExpression, taskInput, channel, model, name }) {
|
|
64
|
+
if (!cron.validate(cronExpression)) {
|
|
65
|
+
throw new Error(`Invalid cron expression: ${cronExpression}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const id = uuidv4();
|
|
69
|
+
const schedule = {
|
|
70
|
+
id,
|
|
71
|
+
name: name || `Schedule ${id.slice(0, 8)}`,
|
|
72
|
+
cronExpression,
|
|
73
|
+
taskInput,
|
|
74
|
+
channel: channel || "scheduler",
|
|
75
|
+
model: model || null,
|
|
76
|
+
enabled: true,
|
|
77
|
+
createdAt: new Date().toISOString(),
|
|
78
|
+
lastRun: null,
|
|
79
|
+
runCount: 0,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
this.schedules.set(id, schedule);
|
|
83
|
+
this.saveSchedules();
|
|
84
|
+
this.activateSchedule(id, schedule);
|
|
85
|
+
|
|
86
|
+
eventBus.emitEvent("schedule:created", { id, name: schedule.name });
|
|
87
|
+
console.log(
|
|
88
|
+
`[Scheduler] Created: "${schedule.name}" (${cronExpression})`
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return schedule;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Update an existing schedule (patch — only fields provided are changed).
|
|
96
|
+
* @param {string} id - Schedule ID (full or prefix)
|
|
97
|
+
* @param {object} patch - Fields to update: cronExpression, taskInput, name, enabled
|
|
98
|
+
*/
|
|
99
|
+
update(id, patch) {
|
|
100
|
+
// Support short ID prefix matching
|
|
101
|
+
const fullId = id.length < 36
|
|
102
|
+
? [...this.schedules.keys()].find((k) => k.startsWith(id))
|
|
103
|
+
: id;
|
|
104
|
+
|
|
105
|
+
const schedule = this.schedules.get(fullId);
|
|
106
|
+
if (!schedule) throw new Error(`Schedule not found: ${id}`);
|
|
107
|
+
|
|
108
|
+
// Update cron expression — requires restarting the cron job
|
|
109
|
+
if (patch.cronExpression && patch.cronExpression !== schedule.cronExpression) {
|
|
110
|
+
if (!cron.validate(patch.cronExpression)) {
|
|
111
|
+
throw new Error(`Invalid cron expression: ${patch.cronExpression}`);
|
|
112
|
+
}
|
|
113
|
+
const job = this.cronJobs.get(fullId);
|
|
114
|
+
if (job) { job.stop(); this.cronJobs.delete(fullId); }
|
|
115
|
+
schedule.cronExpression = patch.cronExpression;
|
|
116
|
+
if (schedule.enabled) this.activateSchedule(fullId, schedule);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (patch.taskInput !== undefined) schedule.taskInput = patch.taskInput;
|
|
120
|
+
if (patch.name !== undefined) schedule.name = patch.name;
|
|
121
|
+
|
|
122
|
+
// Enable / disable
|
|
123
|
+
if (patch.enabled === false && schedule.enabled !== false) {
|
|
124
|
+
const job = this.cronJobs.get(fullId);
|
|
125
|
+
if (job) { job.stop(); this.cronJobs.delete(fullId); }
|
|
126
|
+
schedule.enabled = false;
|
|
127
|
+
} else if (patch.enabled === true && schedule.enabled !== true) {
|
|
128
|
+
schedule.enabled = true;
|
|
129
|
+
if (!this.cronJobs.has(fullId)) this.activateSchedule(fullId, schedule);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.saveSchedules();
|
|
133
|
+
console.log(`[Scheduler] Updated: "${schedule.name}" (${schedule.cronExpression})`);
|
|
134
|
+
return schedule;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Delete a schedule.
|
|
139
|
+
*/
|
|
140
|
+
delete(id) {
|
|
141
|
+
const job = this.cronJobs.get(id);
|
|
142
|
+
if (job) {
|
|
143
|
+
job.stop();
|
|
144
|
+
this.cronJobs.delete(id);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.schedules.delete(id);
|
|
148
|
+
this.saveSchedules();
|
|
149
|
+
console.log(`[Scheduler] Deleted schedule: ${id}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* List all schedules.
|
|
154
|
+
*/
|
|
155
|
+
list() {
|
|
156
|
+
return [...this.schedules.values()];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Activate a cron job for a schedule.
|
|
161
|
+
*/
|
|
162
|
+
activateSchedule(id, schedule) {
|
|
163
|
+
const job = cron.schedule(schedule.cronExpression, () => {
|
|
164
|
+
this.triggerSchedule(id);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
this.cronJobs.set(id, job);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Trigger a scheduled task.
|
|
172
|
+
*/
|
|
173
|
+
triggerSchedule(id) {
|
|
174
|
+
const schedule = this.schedules.get(id);
|
|
175
|
+
if (!schedule || !schedule.enabled) return;
|
|
176
|
+
|
|
177
|
+
console.log(`[Scheduler] Triggering: "${schedule.name}"`);
|
|
178
|
+
|
|
179
|
+
taskQueue.enqueue({
|
|
180
|
+
input: schedule.taskInput,
|
|
181
|
+
channel: schedule.channel,
|
|
182
|
+
model: schedule.model,
|
|
183
|
+
sessionId: null,
|
|
184
|
+
priority: 3, // Scheduled tasks get slightly higher priority
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
schedule.lastRun = new Date().toISOString();
|
|
188
|
+
schedule.runCount++;
|
|
189
|
+
this.saveSchedules();
|
|
190
|
+
|
|
191
|
+
eventBus.emitEvent("schedule:triggered", {
|
|
192
|
+
id,
|
|
193
|
+
name: schedule.name,
|
|
194
|
+
runCount: schedule.runCount,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Load schedules from disk.
|
|
200
|
+
*/
|
|
201
|
+
loadSchedules() {
|
|
202
|
+
if (!existsSync(this.schedulesPath)) return;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const data = JSON.parse(readFileSync(this.schedulesPath, "utf-8"));
|
|
206
|
+
for (const schedule of data) {
|
|
207
|
+
this.schedules.set(schedule.id, schedule);
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.log(`[Scheduler] Error loading schedules: ${error.message}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Save schedules to disk.
|
|
216
|
+
*/
|
|
217
|
+
saveSchedules() {
|
|
218
|
+
try {
|
|
219
|
+
const data = [...this.schedules.values()];
|
|
220
|
+
writeFileSync(this.schedulesPath, JSON.stringify(data, null, 2), "utf-8");
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.log(`[Scheduler] Error saving schedules: ${error.message}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const scheduler = new Scheduler();
|
|
228
|
+
export default scheduler;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const outputSchema = z.object({
|
|
4
|
+
type: z.enum(["text", "tool_call"]),
|
|
5
|
+
text_content: z.string().nullable(),
|
|
6
|
+
tool_call: z
|
|
7
|
+
.object({
|
|
8
|
+
tool_name: z.string(),
|
|
9
|
+
params: z.array(z.string()),
|
|
10
|
+
})
|
|
11
|
+
.nullable(),
|
|
12
|
+
finalResponse: z.boolean(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export default outputSchema;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backward-compatible wrapper around the new AgentLoop.
|
|
3
|
+
*
|
|
4
|
+
* The original runAgent() interface is preserved so existing code
|
|
5
|
+
* (index.js POST /chat) continues to work without changes.
|
|
6
|
+
*
|
|
7
|
+
* New code should import from src/core/AgentLoop.js directly.
|
|
8
|
+
*/
|
|
9
|
+
import { runAgentLoop } from "../core/AgentLoop.js";
|
|
10
|
+
|
|
11
|
+
export async function runAgent({ messages, systemPrompt, tools, modelId = null }) {
|
|
12
|
+
const result = await runAgentLoop({
|
|
13
|
+
messages,
|
|
14
|
+
systemPrompt,
|
|
15
|
+
tools,
|
|
16
|
+
modelId,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Return in the same shape the old code expects
|
|
20
|
+
return {
|
|
21
|
+
text: result.text,
|
|
22
|
+
messages: result.messages,
|
|
23
|
+
cost: result.cost,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync } from "fs";
|
|
3
|
+
import { config } from "../config/default.js";
|
|
4
|
+
|
|
5
|
+
const SESSIONS_DIR = config.sessionsDir;
|
|
6
|
+
|
|
7
|
+
// Ensure sessions directory exists
|
|
8
|
+
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
9
|
+
|
|
10
|
+
const sessions = new Map();
|
|
11
|
+
|
|
12
|
+
export function createSession(existingId = null) {
|
|
13
|
+
const sessionId = existingId || uuidv4();
|
|
14
|
+
const session = {
|
|
15
|
+
sessionId,
|
|
16
|
+
createdAt: new Date().toISOString(),
|
|
17
|
+
messages: [],
|
|
18
|
+
};
|
|
19
|
+
sessions.set(sessionId, session);
|
|
20
|
+
return session;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getSession(sessionId) {
|
|
24
|
+
// check memory first
|
|
25
|
+
if (sessions.has(sessionId)) {
|
|
26
|
+
return sessions.get(sessionId);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// fallback: try loading from disk
|
|
30
|
+
const filePath = `${SESSIONS_DIR}/${sessionId}.json`;
|
|
31
|
+
if (existsSync(filePath)) {
|
|
32
|
+
try {
|
|
33
|
+
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
34
|
+
sessions.set(sessionId, data);
|
|
35
|
+
console.log(`Session ${sessionId} restored from disk`);
|
|
36
|
+
return data;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.log(`Failed to restore session ${sessionId}: ${error.message}`);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function appendMessage(sessionId, role, content) {
|
|
47
|
+
const session = sessions.get(sessionId);
|
|
48
|
+
if (!session) return null;
|
|
49
|
+
session.messages.push({ role, content, timestamp: new Date().toISOString() });
|
|
50
|
+
saveSession(session);
|
|
51
|
+
return session;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function setMessages(sessionId, messages) {
|
|
55
|
+
const session = sessions.get(sessionId);
|
|
56
|
+
if (!session) return null;
|
|
57
|
+
session.messages = messages;
|
|
58
|
+
saveSession(session);
|
|
59
|
+
return session;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function saveSession(session) {
|
|
63
|
+
const filePath = `${SESSIONS_DIR}/${session.sessionId}.json`;
|
|
64
|
+
writeFileSync(filePath, JSON.stringify(session, null, 2));
|
|
65
|
+
}
|