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.
Files changed (115) hide show
  1. package/README.md +666 -0
  2. package/SOUL.md +104 -0
  3. package/config/hooks.json +14 -0
  4. package/config/mcp.json +145 -0
  5. package/package.json +86 -0
  6. package/skills/.gitkeep +0 -0
  7. package/skills/apple-notes.md +193 -0
  8. package/skills/apple-reminders.md +189 -0
  9. package/skills/camsnap.md +162 -0
  10. package/skills/coding.md +14 -0
  11. package/skills/documents.md +13 -0
  12. package/skills/email.md +13 -0
  13. package/skills/gif-search.md +196 -0
  14. package/skills/healthcheck.md +225 -0
  15. package/skills/image-gen.md +147 -0
  16. package/skills/model-usage.md +182 -0
  17. package/skills/obsidian.md +207 -0
  18. package/skills/pdf.md +211 -0
  19. package/skills/research.md +13 -0
  20. package/skills/skill-creator.md +142 -0
  21. package/skills/spotify.md +149 -0
  22. package/skills/summarize.md +230 -0
  23. package/skills/things.md +199 -0
  24. package/skills/tmux.md +204 -0
  25. package/skills/trello.md +183 -0
  26. package/skills/video-frames.md +202 -0
  27. package/skills/weather.md +127 -0
  28. package/src/a2a/A2AClient.js +136 -0
  29. package/src/a2a/A2AServer.js +316 -0
  30. package/src/a2a/AgentCard.js +79 -0
  31. package/src/agents/SubAgentManager.js +369 -0
  32. package/src/agents/Supervisor.js +192 -0
  33. package/src/channels/BaseChannel.js +104 -0
  34. package/src/channels/DiscordChannel.js +288 -0
  35. package/src/channels/EmailChannel.js +172 -0
  36. package/src/channels/GoogleChatChannel.js +316 -0
  37. package/src/channels/HttpChannel.js +26 -0
  38. package/src/channels/LineChannel.js +168 -0
  39. package/src/channels/SignalChannel.js +186 -0
  40. package/src/channels/SlackChannel.js +329 -0
  41. package/src/channels/TeamsChannel.js +272 -0
  42. package/src/channels/TelegramChannel.js +347 -0
  43. package/src/channels/WhatsAppChannel.js +219 -0
  44. package/src/channels/index.js +198 -0
  45. package/src/cli.js +1267 -0
  46. package/src/config/agentProfiles.js +120 -0
  47. package/src/config/channels.js +32 -0
  48. package/src/config/default.js +206 -0
  49. package/src/config/models.js +123 -0
  50. package/src/config/permissions.js +167 -0
  51. package/src/core/AgentLoop.js +446 -0
  52. package/src/core/Compaction.js +143 -0
  53. package/src/core/CostTracker.js +116 -0
  54. package/src/core/EventBus.js +46 -0
  55. package/src/core/Task.js +67 -0
  56. package/src/core/TaskQueue.js +206 -0
  57. package/src/core/TaskRunner.js +226 -0
  58. package/src/daemon/DaemonManager.js +301 -0
  59. package/src/hooks/HookRunner.js +230 -0
  60. package/src/index.js +482 -0
  61. package/src/mcp/MCPAgentRunner.js +112 -0
  62. package/src/mcp/MCPClient.js +186 -0
  63. package/src/mcp/MCPManager.js +412 -0
  64. package/src/models/ModelRouter.js +180 -0
  65. package/src/safety/AuditLog.js +135 -0
  66. package/src/safety/CircuitBreaker.js +126 -0
  67. package/src/safety/FilesystemGuard.js +169 -0
  68. package/src/safety/GitRollback.js +139 -0
  69. package/src/safety/HumanApproval.js +156 -0
  70. package/src/safety/InputSanitizer.js +72 -0
  71. package/src/safety/PermissionGuard.js +83 -0
  72. package/src/safety/Sandbox.js +70 -0
  73. package/src/safety/SecretScanner.js +100 -0
  74. package/src/safety/SecretVault.js +250 -0
  75. package/src/scheduler/Heartbeat.js +115 -0
  76. package/src/scheduler/Scheduler.js +228 -0
  77. package/src/services/models/outputSchema.js +15 -0
  78. package/src/services/openai.js +25 -0
  79. package/src/services/sessions.js +65 -0
  80. package/src/setup/theme.js +110 -0
  81. package/src/setup/wizard.js +788 -0
  82. package/src/skills/SkillLoader.js +168 -0
  83. package/src/storage/TaskStore.js +69 -0
  84. package/src/systemPrompt.js +526 -0
  85. package/src/tenants/TenantContext.js +19 -0
  86. package/src/tenants/TenantManager.js +379 -0
  87. package/src/tools/ToolRegistry.js +141 -0
  88. package/src/tools/applyPatch.js +144 -0
  89. package/src/tools/browserAutomation.js +223 -0
  90. package/src/tools/createDocument.js +265 -0
  91. package/src/tools/cronTool.js +105 -0
  92. package/src/tools/editFile.js +139 -0
  93. package/src/tools/executeCommand.js +123 -0
  94. package/src/tools/glob.js +67 -0
  95. package/src/tools/grep.js +121 -0
  96. package/src/tools/imageAnalysis.js +120 -0
  97. package/src/tools/index.js +173 -0
  98. package/src/tools/listDirectory.js +47 -0
  99. package/src/tools/manageAgents.js +47 -0
  100. package/src/tools/manageMCP.js +159 -0
  101. package/src/tools/memory.js +478 -0
  102. package/src/tools/messageChannel.js +45 -0
  103. package/src/tools/projectTracker.js +259 -0
  104. package/src/tools/readFile.js +52 -0
  105. package/src/tools/screenCapture.js +112 -0
  106. package/src/tools/searchContent.js +76 -0
  107. package/src/tools/searchFiles.js +75 -0
  108. package/src/tools/sendEmail.js +118 -0
  109. package/src/tools/sendFile.js +63 -0
  110. package/src/tools/textToSpeech.js +161 -0
  111. package/src/tools/transcribeAudio.js +82 -0
  112. package/src/tools/useMCP.js +29 -0
  113. package/src/tools/webFetch.js +150 -0
  114. package/src/tools/webSearch.js +134 -0
  115. 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
+ }