@newsails/veil-cli 1.0.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/.veil/agents/analyst/AGENT.md +21 -0
- package/.veil/agents/analyst/agent.json +23 -0
- package/.veil/agents/assistant/AGENT.md +15 -0
- package/.veil/agents/assistant/agent.json +19 -0
- package/.veil/agents/coder/AGENT.md +18 -0
- package/.veil/agents/coder/agent.json +19 -0
- package/.veil/agents/hello/AGENT.md +5 -0
- package/.veil/agents/hello/agent.json +13 -0
- package/.veil/agents/writer/AGENT.md +12 -0
- package/.veil/agents/writer/agent.json +17 -0
- package/.veil/memory/MEMORY.md +343 -0
- package/.veil/memory/agents/analyst/MEMORY.md +55 -0
- package/.veil/memory/agents/hello/MEMORY.md +12 -0
- package/.veil/runtime.pid +1 -0
- package/.veil/settings.json +10 -0
- package/.veil-studio/studio.db +0 -0
- package/.veil-studio/studio.db-shm +0 -0
- package/.veil-studio/studio.db-wal +0 -0
- package/PLAN/01-vision.md +26 -0
- package/PLAN/02-tech-stack.md +94 -0
- package/PLAN/03-agents.md +232 -0
- package/PLAN/04-runtime.md +171 -0
- package/PLAN/05-tools.md +211 -0
- package/PLAN/06-communication.md +243 -0
- package/PLAN/07-storage.md +218 -0
- package/PLAN/08-api-cli.md +153 -0
- package/PLAN/09-permissions.md +108 -0
- package/PLAN/10-ably.md +105 -0
- package/PLAN/11-file-formats.md +442 -0
- package/PLAN/12-folder-structure.md +205 -0
- package/PLAN/13-operations.md +212 -0
- package/PLAN/README.md +23 -0
- package/README.md +128 -0
- package/REPORT.md +174 -0
- package/TODO.md +45 -0
- package/ai-tests/FRONTEND_PROMPT.md +220 -0
- package/ai-tests/Research & Planning.md +814 -0
- package/ai-tests/prompt-001-basic-api.md +230 -0
- package/ai-tests/prompt-002-basic-flows.md +230 -0
- package/ai-tests/prompt-003-agent-behaviors.md +220 -0
- package/api/middleware.js +60 -0
- package/api/routes/agents.js +193 -0
- package/api/routes/chat.js +93 -0
- package/api/routes/completions.js +122 -0
- package/api/routes/daemons.js +80 -0
- package/api/routes/memory.js +169 -0
- package/api/routes/models.js +40 -0
- package/api/routes/remote-methods.js +74 -0
- package/api/routes/sessions.js +208 -0
- package/api/routes/settings.js +108 -0
- package/api/routes/system.js +50 -0
- package/api/routes/tasks.js +270 -0
- package/api/server.js +120 -0
- package/cli/formatter.js +70 -0
- package/cli/index.js +443 -0
- package/cli/parser.js +113 -0
- package/config/config.json +10 -0
- package/config/models.json +6826 -0
- package/core/agent.js +329 -0
- package/core/cancel.js +38 -0
- package/core/compaction.js +176 -0
- package/core/events.js +13 -0
- package/core/loop.js +564 -0
- package/core/memory.js +51 -0
- package/core/prompt.js +185 -0
- package/core/queue.js +96 -0
- package/core/registry.js +291 -0
- package/core/remote-methods.js +124 -0
- package/core/router.js +386 -0
- package/core/running-sessions.js +18 -0
- package/docs/api/01-system.md +84 -0
- package/docs/api/02-agents.md +374 -0
- package/docs/api/03-chat.md +269 -0
- package/docs/api/04-tasks.md +470 -0
- package/docs/api/05-sessions.md +444 -0
- package/docs/api/06-daemons.md +142 -0
- package/docs/api/07-memory.md +186 -0
- package/docs/api/08-settings.md +133 -0
- package/docs/api/09-models.md +119 -0
- package/docs/api/09-websocket.md +350 -0
- package/docs/api/10-completions.md +134 -0
- package/docs/api/README.md +116 -0
- package/docs/guide/01-quickstart.md +220 -0
- package/docs/guide/02-folder-structure.md +185 -0
- package/docs/guide/03-configuration.md +252 -0
- package/docs/guide/04-agents.md +267 -0
- package/docs/guide/05-cli.md +290 -0
- package/docs/guide/06-tools.md +643 -0
- package/docs/guide/07-permissions.md +236 -0
- package/docs/guide/08-memory.md +139 -0
- package/docs/guide/09-multi-agent.md +271 -0
- package/docs/guide/10-daemons.md +226 -0
- package/docs/guide/README.md +53 -0
- package/docs/index.html +623 -0
- package/examples/README.md +151 -0
- package/examples/agents/assistant/AGENT.md +31 -0
- package/examples/agents/assistant/SOUL.md +9 -0
- package/examples/agents/assistant/agent.json +74 -0
- package/examples/agents/hello/AGENT.md +15 -0
- package/examples/agents/hello/agent.json +14 -0
- package/examples/agents/monitor/AGENT.md +51 -0
- package/examples/agents/monitor/agent.json +33 -0
- package/examples/agents/monitor/heartbeats/monitor.md +24 -0
- package/examples/agents/orchestrator/AGENT.md +70 -0
- package/examples/agents/orchestrator/agent.json +30 -0
- package/examples/agents/researcher/AGENT.md +52 -0
- package/examples/agents/researcher/agent.json +49 -0
- package/examples/agents/researcher/skills/web-research.md +28 -0
- package/examples/skills/code-review.md +72 -0
- package/examples/skills/summarise.md +59 -0
- package/examples/skills/web-research.md +42 -0
- package/examples/tools/word-count/index.js +27 -0
- package/examples/tools/word-count/tool.json +18 -0
- package/infrastructure/database.js +563 -0
- package/infrastructure/scheduler.js +122 -0
- package/llm/client.js +206 -0
- package/migrations/001-initial.sql +121 -0
- package/migrations/002-debuggability.sql +13 -0
- package/migrations/003-drop-orphaned-columns.sql +72 -0
- package/migrations/004-session-message-token-fields.sql +78 -0
- package/migrations/005-session-thinking.sql +5 -0
- package/package.json +30 -0
- package/schemas/agent.json +143 -0
- package/schemas/settings.json +111 -0
- package/scripts/fetch-models.js +93 -0
- package/session-debug-scenario.md +248 -0
- package/settings/fields.js +52 -0
- package/system-prompts/base-core.md +7 -0
- package/system-prompts/environment.md +13 -0
- package/system-prompts/reminders/anti-drift.md +6 -0
- package/system-prompts/reminders/stall-recovery.md +10 -0
- package/system-prompts/safety-rules.md +25 -0
- package/system-prompts/task-heuristics.md +27 -0
- package/test/client.js +71 -0
- package/test/integration/01-health.test.js +25 -0
- package/test/integration/02-agents.test.js +80 -0
- package/test/integration/03-chat-hello.test.js +48 -0
- package/test/integration/04-chat-multiturn.test.js +61 -0
- package/test/integration/05-chat-writer.test.js +48 -0
- package/test/integration/06-task-basic.test.js +68 -0
- package/test/integration/07-task-tools.test.js +74 -0
- package/test/integration/08-task-code-analysis.test.js +69 -0
- package/test/integration/09-memory-analyst.test.js +63 -0
- package/test/integration/10-task-advanced.test.js +85 -0
- package/test/integration/11-sessions-advanced.test.js +84 -0
- package/test/integration/12-assistant-chat-tools.test.js +75 -0
- package/test/integration/13-edge-cases.test.js +99 -0
- package/test/integration/14-cancel.test.js +62 -0
- package/test/integration/15-debug.test.js +106 -0
- package/test/integration/16-memory-api.test.js +83 -0
- package/test/integration/17-settings-api.test.js +41 -0
- package/test/integration/18-tool-search-activation.test.js +119 -0
- package/test/results/.gitkeep +0 -0
- package/test/runner.js +206 -0
- package/test/smoke.js +216 -0
- package/tools/agent_message.js +85 -0
- package/tools/agent_send.js +80 -0
- package/tools/agent_spawn.js +44 -0
- package/tools/bash.js +49 -0
- package/tools/edit_file.js +41 -0
- package/tools/glob.js +64 -0
- package/tools/grep.js +82 -0
- package/tools/list_dir.js +63 -0
- package/tools/log_write.js +31 -0
- package/tools/memory_read.js +38 -0
- package/tools/memory_search.js +65 -0
- package/tools/memory_write.js +42 -0
- package/tools/read_file.js +48 -0
- package/tools/sleep.js +22 -0
- package/tools/task_create.js +41 -0
- package/tools/task_respond.js +37 -0
- package/tools/task_spawn.js +64 -0
- package/tools/task_status.js +39 -0
- package/tools/task_subscribe.js +37 -0
- package/tools/todo_read.js +26 -0
- package/tools/todo_write.js +38 -0
- package/tools/tool_activate.js +24 -0
- package/tools/tool_search.js +24 -0
- package/tools/web_fetch.js +50 -0
- package/tools/web_search.js +52 -0
- package/tools/write_file.js +28 -0
- package/ui/api.js +190 -0
- package/ui/app.js +281 -0
- package/ui/index.html +382 -0
- package/ui/views/agents.js +377 -0
- package/ui/views/chat.js +610 -0
- package/ui/views/connection.js +96 -0
- package/ui/views/daemons.js +129 -0
- package/ui/views/feed.js +194 -0
- package/ui/views/memory.js +263 -0
- package/ui/views/models.js +146 -0
- package/ui/views/sessions.js +314 -0
- package/ui/views/settings.js +142 -0
- package/ui/views/tasks.js +415 -0
- package/utils/context.js +49 -0
- package/utils/id.js +16 -0
- package/utils/models.js +88 -0
- package/utils/paths.js +213 -0
- package/utils/settings.js +172 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const paths = require('../utils/paths');
|
|
6
|
+
const { generateId } = require('../utils/id');
|
|
7
|
+
const { getContextLimit } = require('../utils/models');
|
|
8
|
+
|
|
9
|
+
let _db = null;
|
|
10
|
+
const CURRENT_SCHEMA_VERSION = 1;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Open (or return cached) the SQLite database.
|
|
14
|
+
* Runs migrations on every call (idempotent IF NOT EXISTS).
|
|
15
|
+
* @returns {import('better-sqlite3').Database}
|
|
16
|
+
*/
|
|
17
|
+
function getDb() {
|
|
18
|
+
if (_db) return _db;
|
|
19
|
+
|
|
20
|
+
const dbPath = paths.getDbPath();
|
|
21
|
+
const dbDir = path.dirname(dbPath);
|
|
22
|
+
if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
|
|
23
|
+
|
|
24
|
+
const Database = require('better-sqlite3');
|
|
25
|
+
_db = new Database(dbPath);
|
|
26
|
+
|
|
27
|
+
_db.pragma('journal_mode = WAL');
|
|
28
|
+
_db.pragma('synchronous = NORMAL');
|
|
29
|
+
_db.pragma('foreign_keys = ON');
|
|
30
|
+
|
|
31
|
+
runMigrations(_db);
|
|
32
|
+
return _db;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Run all migrations. SQL uses IF NOT EXISTS so this is always safe.
|
|
37
|
+
* @param {import('better-sqlite3').Database} db
|
|
38
|
+
*/
|
|
39
|
+
function runMigrations(db) {
|
|
40
|
+
// Bootstrap: create schema_version before we can check it
|
|
41
|
+
db.exec(`CREATE TABLE IF NOT EXISTS schema_version (
|
|
42
|
+
version INTEGER PRIMARY KEY,
|
|
43
|
+
applied_at TEXT NOT NULL
|
|
44
|
+
)`);
|
|
45
|
+
|
|
46
|
+
const migrationsDir = path.join(__dirname, '..', 'migrations');
|
|
47
|
+
const files = fs.readdirSync(migrationsDir).filter(f => f.endsWith('.sql')).sort();
|
|
48
|
+
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
const versionMatch = file.match(/^(\d+)/);
|
|
51
|
+
if (!versionMatch) continue;
|
|
52
|
+
const version = parseInt(versionMatch[1], 10);
|
|
53
|
+
|
|
54
|
+
const alreadyApplied = db.prepare(
|
|
55
|
+
'SELECT version FROM schema_version WHERE version = ?'
|
|
56
|
+
).get(version);
|
|
57
|
+
|
|
58
|
+
if (!alreadyApplied) {
|
|
59
|
+
const sql = fs.readFileSync(path.join(migrationsDir, file), 'utf8');
|
|
60
|
+
try {
|
|
61
|
+
db.transaction(() => {
|
|
62
|
+
db.exec(sql);
|
|
63
|
+
db.prepare('INSERT OR IGNORE INTO schema_version (version, applied_at) VALUES (?, ?)').run(
|
|
64
|
+
version, new Date().toISOString()
|
|
65
|
+
);
|
|
66
|
+
})();
|
|
67
|
+
} catch (err) {
|
|
68
|
+
// Table-rename migrations (003, 004) can fail on existing DBs whose schema is
|
|
69
|
+
// already partially updated. ensureColumn() below is the authoritative patcher
|
|
70
|
+
// and will add every required column regardless, so we log and continue.
|
|
71
|
+
console.warn(`[db] Migration ${file} skipped (schema already partially applied): ${err.message}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Ensure ALL required columns exist (idempotent schema patching for legacy DBs)
|
|
77
|
+
// Sessions table
|
|
78
|
+
ensureColumn(db, 'sessions', 'instance_folder', 'TEXT');
|
|
79
|
+
ensureColumn(db, 'sessions', 'title', 'TEXT');
|
|
80
|
+
ensureColumn(db, 'sessions', 'message_count', 'INTEGER NOT NULL DEFAULT 0');
|
|
81
|
+
ensureColumn(db, 'sessions', 'total_input_tokens', 'INTEGER NOT NULL DEFAULT 0');
|
|
82
|
+
ensureColumn(db, 'sessions', 'total_output_tokens', 'INTEGER NOT NULL DEFAULT 0');
|
|
83
|
+
ensureColumn(db, 'sessions', 'total_cache_tokens', 'INTEGER NOT NULL DEFAULT 0');
|
|
84
|
+
ensureColumn(db, 'sessions', 'context_size', 'INTEGER NOT NULL DEFAULT 0');
|
|
85
|
+
ensureColumn(db, 'sessions', 'context_size_limit', 'INTEGER');
|
|
86
|
+
ensureColumn(db, 'sessions', 'cost', 'REAL NOT NULL DEFAULT 0');
|
|
87
|
+
ensureColumn(db, 'sessions', 'compaction_count', 'INTEGER NOT NULL DEFAULT 0');
|
|
88
|
+
ensureColumn(db, 'sessions', 'model_thinking', 'TEXT');
|
|
89
|
+
|
|
90
|
+
// Tasks table
|
|
91
|
+
ensureColumn(db, 'tasks', 'tags', 'TEXT');
|
|
92
|
+
ensureColumn(db, 'tasks', 'token_budget', 'INTEGER');
|
|
93
|
+
ensureColumn(db, 'tasks', 'max_iterations', 'INTEGER');
|
|
94
|
+
ensureColumn(db, 'tasks', 'max_duration_seconds', 'INTEGER');
|
|
95
|
+
ensureColumn(db, 'tasks', 'instance_folder', 'TEXT');
|
|
96
|
+
ensureColumn(db, 'tasks', 'iterations', 'INTEGER NOT NULL DEFAULT 0');
|
|
97
|
+
ensureColumn(db, 'tasks', 'token_input', 'INTEGER NOT NULL DEFAULT 0');
|
|
98
|
+
ensureColumn(db, 'tasks', 'token_output', 'INTEGER NOT NULL DEFAULT 0');
|
|
99
|
+
ensureColumn(db, 'tasks', 'token_cache', 'INTEGER NOT NULL DEFAULT 0');
|
|
100
|
+
ensureColumn(db, 'tasks', 'started_at', 'TEXT');
|
|
101
|
+
ensureColumn(db, 'tasks', 'finished_at', 'TEXT');
|
|
102
|
+
|
|
103
|
+
// Messages table
|
|
104
|
+
ensureColumn(db, 'messages', 'tool_calls', 'TEXT');
|
|
105
|
+
ensureColumn(db, 'messages', 'tool_call_id', 'TEXT');
|
|
106
|
+
ensureColumn(db, 'messages', 'model_key', 'TEXT');
|
|
107
|
+
ensureColumn(db, 'messages', 'input_tokens', 'INTEGER');
|
|
108
|
+
ensureColumn(db, 'messages', 'output_tokens', 'INTEGER');
|
|
109
|
+
ensureColumn(db, 'messages', 'cache_tokens', 'INTEGER');
|
|
110
|
+
ensureColumn(db, 'messages', 'cost', 'REAL');
|
|
111
|
+
|
|
112
|
+
// Agent messages table
|
|
113
|
+
ensureColumn(db, 'agent_messages', 'skill', 'TEXT');
|
|
114
|
+
ensureColumn(db, 'agent_messages', 'correlation_id', 'TEXT');
|
|
115
|
+
ensureColumn(db, 'agent_messages', 'response', 'TEXT');
|
|
116
|
+
ensureColumn(db, 'agent_messages', 'delivered_at', 'TEXT');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Add a column if it doesn't exist (SQLite doesn't support IF NOT EXISTS for ALTER TABLE).
|
|
121
|
+
* @param {import('better-sqlite3').Database} db
|
|
122
|
+
* @param {string} table
|
|
123
|
+
* @param {string} column
|
|
124
|
+
* @param {string} type
|
|
125
|
+
*/
|
|
126
|
+
function ensureColumn(db, table, column, type) {
|
|
127
|
+
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
128
|
+
if (!columns.some(c => c.name === column)) {
|
|
129
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Close the database (for graceful shutdown).
|
|
135
|
+
*/
|
|
136
|
+
function closeDb() {
|
|
137
|
+
if (_db) {
|
|
138
|
+
_db.close();
|
|
139
|
+
_db = null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Session queries ──────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {{ agentName: string, mode: string, instanceFolder: string, model: string }} opts
|
|
147
|
+
* @returns {string} sessionId
|
|
148
|
+
*/
|
|
149
|
+
function createSession({ agentName, mode, instanceFolder, model, modelThinking }) {
|
|
150
|
+
const db = getDb();
|
|
151
|
+
const id = generateId('sess_');
|
|
152
|
+
const now = new Date().toISOString();
|
|
153
|
+
const contextSizeLimit = model ? (getContextLimit(model) || null) : null;
|
|
154
|
+
db.prepare(`
|
|
155
|
+
INSERT INTO sessions (id, agent_name, mode, instance_folder, status, model, model_thinking, context_size_limit, created_at, updated_at)
|
|
156
|
+
VALUES (?, ?, ?, ?, 'active', ?, ?, ?, ?, ?)
|
|
157
|
+
`).run(id, agentName, mode, instanceFolder || null, model || null,
|
|
158
|
+
modelThinking ? JSON.stringify(modelThinking) : null, contextSizeLimit, now, now);
|
|
159
|
+
return id;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {string} sessionId
|
|
164
|
+
* @returns {Object|null}
|
|
165
|
+
*/
|
|
166
|
+
function getSession(sessionId) {
|
|
167
|
+
return getDb().prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId) || null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {{ instanceFolder?: string, agentName?: string, status?: string, limit?: number, cursor?: string }} opts
|
|
172
|
+
* @returns {Object[]}
|
|
173
|
+
*/
|
|
174
|
+
function listSessions({ instanceFolder, agentName, status, limit = 20, cursor } = {}) {
|
|
175
|
+
const db = getDb();
|
|
176
|
+
let query = 'SELECT * FROM sessions WHERE 1=1';
|
|
177
|
+
const params = [];
|
|
178
|
+
if (instanceFolder) { query += ' AND instance_folder = ?'; params.push(instanceFolder); }
|
|
179
|
+
if (agentName) { query += ' AND agent_name = ?'; params.push(agentName); }
|
|
180
|
+
if (status) { query += ' AND status = ?'; params.push(status); }
|
|
181
|
+
if (cursor) { query += ' AND id < ?'; params.push(cursor); }
|
|
182
|
+
query += ' ORDER BY created_at DESC LIMIT ?';
|
|
183
|
+
params.push(limit);
|
|
184
|
+
return db.prepare(query).all(...params);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {string} sessionId
|
|
189
|
+
* @param {Object} updates - Partial session fields to update
|
|
190
|
+
*/
|
|
191
|
+
function updateSession(sessionId, updates) {
|
|
192
|
+
const db = getDb();
|
|
193
|
+
const fields = Object.keys(updates).map(k => `${camelToSnake(k)} = ?`).join(', ');
|
|
194
|
+
const values = Object.values(updates);
|
|
195
|
+
db.prepare(`UPDATE sessions SET ${fields}, updated_at = ? WHERE id = ?`)
|
|
196
|
+
.run(...values, new Date().toISOString(), sessionId);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @param {string} sessionId
|
|
201
|
+
*/
|
|
202
|
+
function closeSession(sessionId) {
|
|
203
|
+
updateSession(sessionId, { status: 'closed' });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Hard delete a session and all its messages/todos (via CASCADE).
|
|
208
|
+
* @param {string} sessionId
|
|
209
|
+
*/
|
|
210
|
+
function deleteSession(sessionId) {
|
|
211
|
+
getDb().prepare('DELETE FROM sessions WHERE id = ?').run(sessionId);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Reset a session — clear all messages and token counts, keep session metadata.
|
|
216
|
+
* @param {string} sessionId
|
|
217
|
+
*/
|
|
218
|
+
function resetSession(sessionId) {
|
|
219
|
+
const db = getDb();
|
|
220
|
+
db.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId);
|
|
221
|
+
db.prepare(`
|
|
222
|
+
UPDATE sessions
|
|
223
|
+
SET message_count = 0, total_input_tokens = 0, total_output_tokens = 0, total_cache_tokens = 0,
|
|
224
|
+
context_size = 0, cost = 0, updated_at = ?
|
|
225
|
+
WHERE id = ?
|
|
226
|
+
`).run(new Date().toISOString(), sessionId);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── Message queries ──────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @param {{ sessionId: string, role: string, content?: string, toolCalls?: any[], toolCallId?: string, modelKey?: string, inputTokens?: number, outputTokens?: number, cacheTokens?: number, cost?: number }} msg
|
|
233
|
+
*/
|
|
234
|
+
function addMessage({ sessionId, role, content, toolCalls, toolCallId, modelKey, inputTokens, outputTokens, cacheTokens, cost }) {
|
|
235
|
+
const db = getDb();
|
|
236
|
+
const now = new Date().toISOString();
|
|
237
|
+
const { lastInsertRowid } = db.prepare(`
|
|
238
|
+
INSERT INTO messages (session_id, role, content, tool_calls, tool_call_id, model_key, input_tokens, output_tokens, cache_tokens, cost, created_at)
|
|
239
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
240
|
+
`).run(
|
|
241
|
+
sessionId, role,
|
|
242
|
+
content || null,
|
|
243
|
+
toolCalls ? JSON.stringify(toolCalls) : null,
|
|
244
|
+
toolCallId || null,
|
|
245
|
+
modelKey || null,
|
|
246
|
+
inputTokens || null,
|
|
247
|
+
outputTokens || null,
|
|
248
|
+
cacheTokens || null,
|
|
249
|
+
cost || null,
|
|
250
|
+
now
|
|
251
|
+
);
|
|
252
|
+
db.prepare('UPDATE sessions SET message_count = message_count + 1, updated_at = ? WHERE id = ?')
|
|
253
|
+
.run(now, sessionId);
|
|
254
|
+
return lastInsertRowid;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* @param {string} sessionId
|
|
259
|
+
* @param {{ limit?: number, offset?: number }} opts
|
|
260
|
+
* @returns {Object[]}
|
|
261
|
+
*/
|
|
262
|
+
function getMessages(sessionId, { limit = 1000, offset = 0 } = {}) {
|
|
263
|
+
const rows = getDb().prepare(
|
|
264
|
+
'SELECT * FROM messages WHERE session_id = ? ORDER BY id ASC LIMIT ? OFFSET ?'
|
|
265
|
+
).all(sessionId, limit, offset);
|
|
266
|
+
return rows.map(r => ({
|
|
267
|
+
...r,
|
|
268
|
+
tool_calls: r.tool_calls ? JSON.parse(r.tool_calls) : null,
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Task queries ─────────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* @param {{ agentName: string, input: string, priority?: string, tags?: string[], maxIterations?: number, maxDurationSeconds?: number, parentTaskId?: string, instanceFolder?: string, sessionId?: string }} opts
|
|
276
|
+
* @returns {string} taskId
|
|
277
|
+
*/
|
|
278
|
+
function createTask({ agentName, input, priority = 'normal', tags, maxIterations, maxDurationSeconds, tokenBudget, parentTaskId, instanceFolder, sessionId }) {
|
|
279
|
+
const db = getDb();
|
|
280
|
+
const id = generateId('task_');
|
|
281
|
+
const now = new Date().toISOString();
|
|
282
|
+
db.prepare(`
|
|
283
|
+
INSERT INTO tasks (id, agent_name, status, priority, parent_task_id, session_id, input, tags, max_iterations, max_duration_seconds, token_budget, instance_folder, created_at, updated_at)
|
|
284
|
+
VALUES (?, ?, 'pending', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
285
|
+
`).run(
|
|
286
|
+
id, agentName, priority, parentTaskId || null, sessionId || null,
|
|
287
|
+
typeof input === 'string' ? input : JSON.stringify(input),
|
|
288
|
+
tags ? JSON.stringify(tags) : null,
|
|
289
|
+
maxIterations || null, maxDurationSeconds || null, tokenBudget || null,
|
|
290
|
+
instanceFolder || null, now, now
|
|
291
|
+
);
|
|
292
|
+
return id;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* @param {string} taskId
|
|
297
|
+
* @returns {Object|null}
|
|
298
|
+
*/
|
|
299
|
+
function getTask(taskId) {
|
|
300
|
+
const row = getDb().prepare(`
|
|
301
|
+
SELECT t.*, (SELECT COUNT(*) FROM task_events WHERE task_id = t.id) AS event_count
|
|
302
|
+
FROM tasks t WHERE t.id = ?
|
|
303
|
+
`).get(taskId);
|
|
304
|
+
if (!row) return null;
|
|
305
|
+
return parseTaskRow(row);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* @param {{ instanceFolder?: string, agentName?: string, status?: string, priority?: string, tags?: string, limit?: number, cursor?: string }} opts
|
|
310
|
+
* @returns {Object[]}
|
|
311
|
+
*/
|
|
312
|
+
function listTasks({ instanceFolder, agentName, status, priority, limit = 20, cursor } = {}) {
|
|
313
|
+
const db = getDb();
|
|
314
|
+
let query = 'SELECT * FROM tasks WHERE 1=1';
|
|
315
|
+
const params = [];
|
|
316
|
+
if (instanceFolder) { query += ' AND instance_folder = ?'; params.push(instanceFolder); }
|
|
317
|
+
if (agentName) { query += ' AND agent_name = ?'; params.push(agentName); }
|
|
318
|
+
if (status) { query += ' AND status = ?'; params.push(status); }
|
|
319
|
+
if (priority) { query += ' AND priority = ?'; params.push(priority); }
|
|
320
|
+
if (cursor) { query += ' AND id < ?'; params.push(cursor); }
|
|
321
|
+
query += ' ORDER BY created_at DESC LIMIT ?';
|
|
322
|
+
params.push(limit);
|
|
323
|
+
return db.prepare(query).all(...params).map(parseTaskRow);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* @param {string} taskId
|
|
328
|
+
* @param {Object} updates
|
|
329
|
+
*/
|
|
330
|
+
function updateTask(taskId, updates) {
|
|
331
|
+
const db = getDb();
|
|
332
|
+
const snakeUpdates = {};
|
|
333
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
334
|
+
snakeUpdates[camelToSnake(k)] = v;
|
|
335
|
+
}
|
|
336
|
+
const fields = Object.keys(snakeUpdates).map(k => `${k} = ?`).join(', ');
|
|
337
|
+
const values = Object.values(snakeUpdates);
|
|
338
|
+
db.prepare(`UPDATE tasks SET ${fields}, updated_at = ? WHERE id = ?`)
|
|
339
|
+
.run(...values, new Date().toISOString(), taskId);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Hard-delete a task and all its associated rows (events, context, subscriptions via CASCADE).
|
|
344
|
+
* @param {string} taskId
|
|
345
|
+
*/
|
|
346
|
+
function deleteTask(taskId) {
|
|
347
|
+
getDb().prepare('DELETE FROM tasks WHERE id = ?').run(taskId);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── Task context (LLM context snapshots for debuggability) ──────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Save a snapshot of the LLM context (messages + tools) for a task.
|
|
354
|
+
* Upserts — always overwrites with the latest snapshot.
|
|
355
|
+
* @param {string} taskId
|
|
356
|
+
* @param {{ messages: Object[], tools: string[], iteration: number }} opts
|
|
357
|
+
*/
|
|
358
|
+
function saveTaskContext(taskId, { messages, tools, iteration }) {
|
|
359
|
+
getDb().prepare(`
|
|
360
|
+
INSERT INTO task_context (task_id, messages, tools, iteration, updated_at)
|
|
361
|
+
VALUES (?, ?, ?, ?, ?)
|
|
362
|
+
ON CONFLICT(task_id) DO UPDATE
|
|
363
|
+
SET messages = excluded.messages, tools = excluded.tools, iteration = excluded.iteration, updated_at = excluded.updated_at
|
|
364
|
+
`).run(taskId, JSON.stringify(messages), JSON.stringify(tools || []), iteration, new Date().toISOString());
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Get the most recent LLM context snapshot for a task.
|
|
369
|
+
* @param {string} taskId
|
|
370
|
+
* @returns {Object|null}
|
|
371
|
+
*/
|
|
372
|
+
function getTaskContext(taskId) {
|
|
373
|
+
const row = getDb().prepare('SELECT * FROM task_context WHERE task_id = ?').get(taskId);
|
|
374
|
+
if (!row) return null;
|
|
375
|
+
return {
|
|
376
|
+
messages: JSON.parse(row.messages),
|
|
377
|
+
tools: JSON.parse(row.tools || '[]'),
|
|
378
|
+
iteration: row.iteration,
|
|
379
|
+
updatedAt: row.updated_at,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── Task events ──────────────────────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* @param {{ taskId: string, type: string, data?: Object }} opts
|
|
387
|
+
*/
|
|
388
|
+
function addTaskEvent({ taskId, type, data }) {
|
|
389
|
+
getDb().prepare(
|
|
390
|
+
'INSERT INTO task_events (task_id, type, data, created_at) VALUES (?, ?, ?, ?)'
|
|
391
|
+
).run(taskId, type, data ? JSON.stringify(data) : null, new Date().toISOString());
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* @param {string} taskId
|
|
396
|
+
* @returns {Object[]}
|
|
397
|
+
*/
|
|
398
|
+
function getTaskEvents(taskId, { since = 0, limit = 500 } = {}) {
|
|
399
|
+
return getDb().prepare(
|
|
400
|
+
'SELECT * FROM task_events WHERE task_id = ? AND id > ? ORDER BY id ASC LIMIT ?'
|
|
401
|
+
).all(taskId, since, limit)
|
|
402
|
+
.map(r => ({ ...r, data: r.data ? JSON.parse(r.data) : null }));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ── Todos ────────────────────────────────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* @param {string} sessionId
|
|
409
|
+
* @param {Object[]} items
|
|
410
|
+
*/
|
|
411
|
+
function saveTodos(sessionId, items) {
|
|
412
|
+
const db = getDb();
|
|
413
|
+
const now = new Date().toISOString();
|
|
414
|
+
db.prepare(`
|
|
415
|
+
INSERT INTO todos (session_id, items, updated_at) VALUES (?, ?, ?)
|
|
416
|
+
ON CONFLICT(session_id) DO UPDATE SET items = excluded.items, updated_at = excluded.updated_at
|
|
417
|
+
`).run(sessionId, JSON.stringify(items), now);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* @param {string} sessionId
|
|
422
|
+
* @returns {Object[]}
|
|
423
|
+
*/
|
|
424
|
+
function getTodos(sessionId) {
|
|
425
|
+
const row = getDb().prepare('SELECT items FROM todos WHERE session_id = ?').get(sessionId);
|
|
426
|
+
return row ? JSON.parse(row.items) : [];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ── Agent messages ────────────────────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* @param {{ targetAgent: string, targetSessionId?: string, fromAgent: string, content: string, followup?: boolean, skill?: string, correlationId?: string }} opts
|
|
433
|
+
* @returns {string} messageId
|
|
434
|
+
*/
|
|
435
|
+
function enqueueAgentMessage({ targetAgent, targetSessionId, fromAgent, content, followup = false, skill, correlationId }) {
|
|
436
|
+
const id = generateId('amsg_');
|
|
437
|
+
const now = new Date().toISOString();
|
|
438
|
+
getDb().prepare(`
|
|
439
|
+
INSERT INTO agent_messages (id, target_agent, target_session_id, from_agent, content, followup, skill, status, correlation_id, created_at)
|
|
440
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)
|
|
441
|
+
`).run(id, targetAgent, targetSessionId || null, fromAgent, content, followup ? 1 : 0, skill || null, correlationId || null, now);
|
|
442
|
+
return id;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* @param {string} targetAgent
|
|
447
|
+
* @returns {Object[]}
|
|
448
|
+
*/
|
|
449
|
+
function getPendingAgentMessages(targetAgent) {
|
|
450
|
+
return getDb().prepare(
|
|
451
|
+
"SELECT * FROM agent_messages WHERE target_agent = ? AND status = 'pending' ORDER BY id ASC"
|
|
452
|
+
).all(targetAgent);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* @param {string} messageId
|
|
457
|
+
* @param {string} [response]
|
|
458
|
+
*/
|
|
459
|
+
function markAgentMessageDelivered(messageId, response) {
|
|
460
|
+
getDb().prepare(
|
|
461
|
+
'UPDATE agent_messages SET status = ?, response = ?, delivered_at = ? WHERE id = ?'
|
|
462
|
+
).run('delivered', response || null, new Date().toISOString(), messageId);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ── Task subscriptions ───────────────────────────────────────────────────────
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* @param {{ taskId: string, subscriberSessionId: string, subscriberAgent: string }} opts
|
|
469
|
+
*/
|
|
470
|
+
function createTaskSubscription({ taskId, subscriberSessionId, subscriberAgent }) {
|
|
471
|
+
getDb().prepare(`
|
|
472
|
+
INSERT INTO task_subscriptions (task_id, subscriber_session_id, subscriber_agent, status, created_at)
|
|
473
|
+
VALUES (?, ?, ?, 'pending', ?)
|
|
474
|
+
`).run(taskId, subscriberSessionId, subscriberAgent, new Date().toISOString());
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* @param {string} taskId
|
|
479
|
+
* @returns {Object[]}
|
|
480
|
+
*/
|
|
481
|
+
function getPendingSubscriptions(taskId) {
|
|
482
|
+
return getDb().prepare(
|
|
483
|
+
"SELECT * FROM task_subscriptions WHERE task_id = ? AND status = 'pending'"
|
|
484
|
+
).all(taskId);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* @param {number} subscriptionId
|
|
489
|
+
*/
|
|
490
|
+
function markSubscriptionDelivered(subscriptionId) {
|
|
491
|
+
getDb().prepare(
|
|
492
|
+
'UPDATE task_subscriptions SET status = ?, delivered_at = ? WHERE id = ?'
|
|
493
|
+
).run('delivered', new Date().toISOString(), subscriptionId);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Convert camelCase to snake_case.
|
|
500
|
+
* @param {string} str
|
|
501
|
+
* @returns {string}
|
|
502
|
+
*/
|
|
503
|
+
function camelToSnake(str) {
|
|
504
|
+
return str.replace(/[A-Z]/g, c => `_${c.toLowerCase()}`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Parse a task row from the DB, JSON-decoding fields.
|
|
509
|
+
* @param {Object} row
|
|
510
|
+
* @returns {Object}
|
|
511
|
+
*/
|
|
512
|
+
function parseTaskRow(row) {
|
|
513
|
+
return {
|
|
514
|
+
...row,
|
|
515
|
+
tags: row.tags ? JSON.parse(row.tags) : [],
|
|
516
|
+
input: row.input ? tryJson(row.input) : null,
|
|
517
|
+
output: row.output || null,
|
|
518
|
+
error: row.error ? tryJson(row.error) : null,
|
|
519
|
+
tokenBudget: row.token_budget ?? null,
|
|
520
|
+
eventCount: row.event_count ?? undefined,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function tryJson(str) {
|
|
525
|
+
try { return JSON.parse(str); } catch { return str; }
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Mark pending/processing tasks as failed (used on startup recovery).
|
|
530
|
+
* @param {string} instanceFolder
|
|
531
|
+
*/
|
|
532
|
+
function recoverStaleTasks(instanceFolder) {
|
|
533
|
+
const db = getDb();
|
|
534
|
+
const now = new Date().toISOString();
|
|
535
|
+
db.prepare(`
|
|
536
|
+
UPDATE tasks SET status = 'failed', error = 'Recovered: server was restarted', finished_at = ?, updated_at = ?
|
|
537
|
+
WHERE status IN ('processing') AND instance_folder = ?
|
|
538
|
+
`).run(now, now, instanceFolder);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
module.exports = {
|
|
542
|
+
getDb,
|
|
543
|
+
closeDb,
|
|
544
|
+
runMigrations,
|
|
545
|
+
// Sessions
|
|
546
|
+
createSession, getSession, listSessions, updateSession, closeSession, deleteSession, resetSession,
|
|
547
|
+
// Messages
|
|
548
|
+
addMessage, getMessages,
|
|
549
|
+
// Tasks
|
|
550
|
+
createTask, getTask, listTasks, updateTask, deleteTask,
|
|
551
|
+
// Task context
|
|
552
|
+
saveTaskContext, getTaskContext,
|
|
553
|
+
// Task events
|
|
554
|
+
addTaskEvent, getTaskEvents,
|
|
555
|
+
// Todos
|
|
556
|
+
saveTodos, getTodos,
|
|
557
|
+
// Agent messages
|
|
558
|
+
enqueueAgentMessage, getPendingAgentMessages, markAgentMessageDelivered,
|
|
559
|
+
// Subscriptions
|
|
560
|
+
createTaskSubscription, getPendingSubscriptions, markSubscriptionDelivered,
|
|
561
|
+
// Startup
|
|
562
|
+
recoverStaleTasks,
|
|
563
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const cron = require('node-cron');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Daemon scheduler — manages cron jobs for daemon-mode agents.
|
|
7
|
+
* State is in-memory, rebuilt from agent.json on restart.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a new scheduler instance.
|
|
12
|
+
* @returns {Object} Scheduler API
|
|
13
|
+
*/
|
|
14
|
+
function createScheduler() {
|
|
15
|
+
const _jobs = new Map(); // agentName → { task: CronTask, lastRun: Date|null, status: string }
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Start a daemon cron job for an agent.
|
|
19
|
+
* @param {{ agentName: string, agent: Object, cwd: string, settings: Object }} opts
|
|
20
|
+
*/
|
|
21
|
+
function startDaemon({ agentName, agent, cwd, settings }) {
|
|
22
|
+
if (_jobs.has(agentName)) {
|
|
23
|
+
stopDaemon(agentName);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const cronExpr = agent.modes?.daemon?.cron;
|
|
27
|
+
if (!cronExpr) {
|
|
28
|
+
console.warn(`[scheduler] Agent "${agentName}" has no daemon cron schedule`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!cron.validate(cronExpr)) {
|
|
33
|
+
console.error(`[scheduler] Invalid cron schedule for "${agentName}": ${cronExpr}`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const conflictPolicy = agent.modes?.daemon?.conflictPolicy || 'skip';
|
|
38
|
+
let isRunning = false;
|
|
39
|
+
|
|
40
|
+
const task = cron.schedule(cronExpr, async () => {
|
|
41
|
+
if (isRunning) {
|
|
42
|
+
if (conflictPolicy === 'skip') {
|
|
43
|
+
console.log(`[scheduler] Daemon "${agentName}" tick skipped (still running)`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (conflictPolicy === 'queue') {
|
|
47
|
+
// Wait for current run to finish — handled by cron itself
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// 'restart' — cancel current and start new (not easily doable with node-cron, just skip)
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
isRunning = true;
|
|
55
|
+
const state = _jobs.get(agentName);
|
|
56
|
+
if (state) {
|
|
57
|
+
state.lastRun = new Date();
|
|
58
|
+
state.status = 'running';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const { runDaemonTick } = require('../core/router');
|
|
63
|
+
await runDaemonTick({ agentName, cwd, settings });
|
|
64
|
+
if (state) state.status = 'idle';
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(`[scheduler] Daemon "${agentName}" tick error: ${err.message}`);
|
|
67
|
+
if (state) state.status = 'error';
|
|
68
|
+
} finally {
|
|
69
|
+
isRunning = false;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
_jobs.set(agentName, {
|
|
74
|
+
task,
|
|
75
|
+
cron: cronExpr,
|
|
76
|
+
lastRun: null,
|
|
77
|
+
status: 'idle',
|
|
78
|
+
agentName,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
console.log(`[scheduler] Daemon "${agentName}" started (cron: ${cronExpr})`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Stop a daemon cron job.
|
|
86
|
+
* @param {string} agentName
|
|
87
|
+
*/
|
|
88
|
+
function stopDaemon(agentName) {
|
|
89
|
+
const job = _jobs.get(agentName);
|
|
90
|
+
if (job) {
|
|
91
|
+
job.task.stop();
|
|
92
|
+
_jobs.delete(agentName);
|
|
93
|
+
console.log(`[scheduler] Daemon "${agentName}" stopped`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Stop all daemon cron jobs.
|
|
99
|
+
*/
|
|
100
|
+
function stopAll() {
|
|
101
|
+
for (const agentName of _jobs.keys()) {
|
|
102
|
+
stopDaemon(agentName);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* List all active daemons.
|
|
108
|
+
* @returns {Object[]}
|
|
109
|
+
*/
|
|
110
|
+
function listDaemons() {
|
|
111
|
+
return Array.from(_jobs.values()).map(j => ({
|
|
112
|
+
agentName: j.agentName,
|
|
113
|
+
cron: j.cron,
|
|
114
|
+
lastRun: j.lastRun ? j.lastRun.toISOString() : null,
|
|
115
|
+
status: j.status,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { startDaemon, stopDaemon, stopAll, listDaemons };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { createScheduler };
|