omnikey-cli 1.0.28 → 1.0.29
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 +68 -19
- package/backend-dist/agent/agentPrompts.js +6 -1
- package/backend-dist/agent/agentServer.js +47 -17
- package/backend-dist/agent/imageTool.js +167 -0
- package/backend-dist/agent/utils.js +13 -1
- package/backend-dist/ai-client.js +79 -0
- package/backend-dist/index.js +13 -5
- package/backend-dist/models/scheduledJob.js +97 -0
- package/backend-dist/scheduledJobExecutor.js +199 -0
- package/backend-dist/scheduledJobRoutes.js +186 -0
- package/dist/index.js +20 -0
- package/dist/onboard.js +7 -6
- package/dist/scheduleJob.js +268 -0
- package/package.json +1 -1
- package/src/index.ts +25 -0
- package/src/onboard.ts +8 -6
- package/src/scheduleJob.ts +309 -0
package/backend-dist/index.js
CHANGED
|
@@ -13,10 +13,13 @@ const featureRoutes_1 = require("./featureRoutes");
|
|
|
13
13
|
const db_1 = require("./db");
|
|
14
14
|
const logger_1 = require("./logger");
|
|
15
15
|
const taskInstructionRoutes_1 = require("./taskInstructionRoutes");
|
|
16
|
+
const scheduledJobRoutes_1 = require("./scheduledJobRoutes");
|
|
17
|
+
const scheduledJobExecutor_1 = require("./scheduledJobExecutor");
|
|
16
18
|
const config_1 = require("./config");
|
|
17
19
|
const agentServer_1 = require("./agent/agentServer");
|
|
18
|
-
// Importing AgentSession ensures the
|
|
20
|
+
// Importing AgentSession and ScheduledJob ensures the models are registered with Sequelize before initDatabase().
|
|
19
21
|
require("./models/agentSession");
|
|
22
|
+
require("./models/scheduledJob");
|
|
20
23
|
const bucket_adapter_1 = require("./bucket-adapter");
|
|
21
24
|
const app = (0, express_1.default)();
|
|
22
25
|
const PORT = Number(config_1.config.port);
|
|
@@ -28,6 +31,7 @@ app.use(express_1.default.static(path_1.default.join(process.cwd(), 'public')));
|
|
|
28
31
|
app.use('/api/subscription', (0, subscriptionRoutes_1.createSubscriptionRouter)(logger_1.logger));
|
|
29
32
|
app.use('/api/feature', (0, featureRoutes_1.createFeatureRouter)());
|
|
30
33
|
app.use('/api/instructions', (0, taskInstructionRoutes_1.taskInstructionRouter)());
|
|
34
|
+
app.use('/api/scheduled-jobs', (0, scheduledJobRoutes_1.scheduledJobRouter)());
|
|
31
35
|
app.use('/api/agent', (0, agentServer_1.createAgentRouter)());
|
|
32
36
|
app.get('/macos/download', (_req, res) => {
|
|
33
37
|
const dmgPath = path_1.default.join(process.cwd(), 'macOS', 'OmniKeyAI.dmg');
|
|
@@ -70,8 +74,8 @@ app.get('/macos/appcast', (req, res) => {
|
|
|
70
74
|
const appcastUrl = `${baseUrl}/macos/appcast`;
|
|
71
75
|
// These should match the values embedded into the macOS app
|
|
72
76
|
// Info.plist in macOS/build_release_dmg.sh.
|
|
73
|
-
const bundleVersion = '
|
|
74
|
-
const shortVersion = '1.0.
|
|
77
|
+
const bundleVersion = '23';
|
|
78
|
+
const shortVersion = '1.0.22';
|
|
75
79
|
const xml = `<?xml version="1.0" encoding="utf-8"?>
|
|
76
80
|
<rss version="2.0"
|
|
77
81
|
xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"
|
|
@@ -99,7 +103,7 @@ app.get('/macos/appcast', (req, res) => {
|
|
|
99
103
|
// ── Windows distribution endpoints ───────────────────────────────────────────
|
|
100
104
|
// These should match the values in windows/OmniKey.Windows.csproj
|
|
101
105
|
// <Version> and windows/build_release_zip.ps1 $APP_VERSION.
|
|
102
|
-
const WIN_VERSION = '1.
|
|
106
|
+
const WIN_VERSION = '1.9';
|
|
103
107
|
const WIN_ZIP_FILENAME = 'OmniKeyAI-windows-win-x64.zip';
|
|
104
108
|
const WIN_ZIP_PATH = path_1.default.join(process.cwd(), 'windows', WIN_ZIP_FILENAME);
|
|
105
109
|
// Serves the pre-built ZIP produced by windows/build_release_zip.ps1.
|
|
@@ -141,7 +145,7 @@ app.get('/windows/update', (req, res) => {
|
|
|
141
145
|
version: WIN_VERSION,
|
|
142
146
|
downloadUrl: `${baseUrl}/windows/download`,
|
|
143
147
|
fileSize,
|
|
144
|
-
releaseNotes: `What's new in ${WIN_VERSION}\n\n• OmniAgent session management — choose to start a new session or resume an existing one each time you run @omniAgent. Save a default to skip the picker automatically on future runs.\n• History button in the OmniAgent window — change your default session at any time without re-running the agent.\n• OmniAgent Session tray menu item — open session settings directly from the system tray.\n• Left-clicking the tray icon now opens the menu (previously right-click only).`,
|
|
148
|
+
releaseNotes: `What's new in ${WIN_VERSION}\n\n• New cron job automation (Scheduled Jobs) — create recurring jobs with cron-style schedules or one-time jobs to run prompts automatically in the background.\n• Scheduled Jobs controls — add jobs, activate/deactivate them, run now on demand, refresh status, and view last-run history in the app.\n• OmniAgent session management — choose to start a new session or resume an existing one each time you run @omniAgent. Save a default to skip the picker automatically on future runs.\n• History button in the OmniAgent window — change your default session at any time without re-running the agent.\n• OmniAgent Session tray menu item — open session settings directly from the system tray.\n• Left-clicking the tray icon now opens the menu (previously right-click only).\n• Manual updated with detailed OmniAgent, session management, web search provider, and LLM provider documentation.`,
|
|
145
149
|
});
|
|
146
150
|
});
|
|
147
151
|
app.get('/downloads/stats', async (_req, res) => {
|
|
@@ -167,6 +171,7 @@ async function start() {
|
|
|
167
171
|
server = app.listen(PORT, () => {
|
|
168
172
|
logger_1.logger.info(`Enhancer API listening on http://localhost:${PORT}`, {
|
|
169
173
|
isSelfHosted: config_1.config.isSelfHosted,
|
|
174
|
+
aiProvider: config_1.config.aiProvider,
|
|
170
175
|
});
|
|
171
176
|
});
|
|
172
177
|
// Attach the WebSocket-based agent server to the existing HTTP
|
|
@@ -175,6 +180,9 @@ async function start() {
|
|
|
175
180
|
if (server) {
|
|
176
181
|
(0, agentServer_1.attachAgentWebSocketServer)(server);
|
|
177
182
|
}
|
|
183
|
+
if (config_1.config.isSelfHosted) {
|
|
184
|
+
(0, scheduledJobExecutor_1.startScheduledJobExecutor)();
|
|
185
|
+
}
|
|
178
186
|
}
|
|
179
187
|
catch (err) {
|
|
180
188
|
logger_1.logger.error('Failed to start server due to DB error.', { error: err });
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ScheduledJob = void 0;
|
|
7
|
+
const sequelize_1 = require("sequelize");
|
|
8
|
+
const cuid_1 = __importDefault(require("cuid"));
|
|
9
|
+
const db_1 = require("../db");
|
|
10
|
+
const subscription_1 = require("./subscription");
|
|
11
|
+
class ScheduledJob extends sequelize_1.Model {
|
|
12
|
+
}
|
|
13
|
+
exports.ScheduledJob = ScheduledJob;
|
|
14
|
+
ScheduledJob.init({
|
|
15
|
+
id: {
|
|
16
|
+
type: sequelize_1.DataTypes.STRING,
|
|
17
|
+
primaryKey: true,
|
|
18
|
+
allowNull: false,
|
|
19
|
+
defaultValue: () => (0, cuid_1.default)(),
|
|
20
|
+
},
|
|
21
|
+
subscriptionId: {
|
|
22
|
+
type: sequelize_1.DataTypes.STRING,
|
|
23
|
+
allowNull: false,
|
|
24
|
+
field: 'subscription_id',
|
|
25
|
+
references: {
|
|
26
|
+
model: subscription_1.Subscription,
|
|
27
|
+
key: 'id',
|
|
28
|
+
},
|
|
29
|
+
onDelete: 'CASCADE',
|
|
30
|
+
onUpdate: 'CASCADE',
|
|
31
|
+
},
|
|
32
|
+
label: {
|
|
33
|
+
type: sequelize_1.DataTypes.STRING(200),
|
|
34
|
+
allowNull: false,
|
|
35
|
+
},
|
|
36
|
+
prompt: {
|
|
37
|
+
type: sequelize_1.DataTypes.TEXT,
|
|
38
|
+
allowNull: false,
|
|
39
|
+
},
|
|
40
|
+
cronExpression: {
|
|
41
|
+
type: sequelize_1.DataTypes.STRING,
|
|
42
|
+
allowNull: true,
|
|
43
|
+
field: 'cron_expression',
|
|
44
|
+
},
|
|
45
|
+
runAt: {
|
|
46
|
+
type: sequelize_1.DataTypes.DATE,
|
|
47
|
+
allowNull: true,
|
|
48
|
+
field: 'run_at',
|
|
49
|
+
},
|
|
50
|
+
isActive: {
|
|
51
|
+
type: sequelize_1.DataTypes.BOOLEAN,
|
|
52
|
+
allowNull: false,
|
|
53
|
+
defaultValue: true,
|
|
54
|
+
field: 'is_active',
|
|
55
|
+
},
|
|
56
|
+
lastRunAt: {
|
|
57
|
+
type: sequelize_1.DataTypes.DATE,
|
|
58
|
+
allowNull: true,
|
|
59
|
+
field: 'last_run_at',
|
|
60
|
+
},
|
|
61
|
+
nextRunAt: {
|
|
62
|
+
type: sequelize_1.DataTypes.DATE,
|
|
63
|
+
allowNull: true,
|
|
64
|
+
field: 'next_run_at',
|
|
65
|
+
},
|
|
66
|
+
sessionId: {
|
|
67
|
+
type: sequelize_1.DataTypes.STRING,
|
|
68
|
+
allowNull: true,
|
|
69
|
+
field: 'session_id',
|
|
70
|
+
},
|
|
71
|
+
lastRunSessionId: {
|
|
72
|
+
type: sequelize_1.DataTypes.STRING,
|
|
73
|
+
allowNull: true,
|
|
74
|
+
field: 'last_run_session_id',
|
|
75
|
+
},
|
|
76
|
+
platform: {
|
|
77
|
+
type: sequelize_1.DataTypes.STRING,
|
|
78
|
+
allowNull: true,
|
|
79
|
+
},
|
|
80
|
+
}, {
|
|
81
|
+
sequelize: db_1.sequelize,
|
|
82
|
+
tableName: 'scheduled_jobs',
|
|
83
|
+
modelName: 'ScheduledJob',
|
|
84
|
+
indexes: [
|
|
85
|
+
{
|
|
86
|
+
fields: ['subscription_id', 'next_run_at'],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
subscription_1.Subscription.hasMany(ScheduledJob, {
|
|
91
|
+
foreignKey: 'subscriptionId',
|
|
92
|
+
as: 'scheduledJobs',
|
|
93
|
+
});
|
|
94
|
+
ScheduledJob.belongsTo(subscription_1.Subscription, {
|
|
95
|
+
foreignKey: 'subscriptionId',
|
|
96
|
+
as: 'subscription',
|
|
97
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.computeNextRunAt = computeNextRunAt;
|
|
7
|
+
exports.startScheduledJobExecutor = startScheduledJobExecutor;
|
|
8
|
+
exports.executeJob = executeJob;
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const promises_1 = require("fs/promises");
|
|
11
|
+
const os_1 = require("os");
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const util_1 = require("util");
|
|
14
|
+
const cuid_1 = __importDefault(require("cuid"));
|
|
15
|
+
const sequelize_1 = require("sequelize");
|
|
16
|
+
const cron_parser_1 = require("cron-parser");
|
|
17
|
+
const scheduledJob_1 = require("./models/scheduledJob");
|
|
18
|
+
const subscription_1 = require("./models/subscription");
|
|
19
|
+
const logger_1 = require("./logger");
|
|
20
|
+
const agentServer_1 = require("./agent/agentServer");
|
|
21
|
+
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
22
|
+
const SHELL_SCRIPT_RE = /<shell_script>([\s\S]*?)<\/shell_script>/;
|
|
23
|
+
const FINAL_ANSWER_RE = /<final_answer>/;
|
|
24
|
+
// Maximum time a single job may run before it is forcibly cancelled.
|
|
25
|
+
const JOB_TIMEOUT_MS = 10 * 60 * 1000;
|
|
26
|
+
// Cron jobs get more turns than interactive sessions so multi-step tasks
|
|
27
|
+
// (web research → shell commands → final answer) can complete unattended.
|
|
28
|
+
const MAX_CRON_TURNS = 20;
|
|
29
|
+
function computeNextRunAt(cronExpression, runAt) {
|
|
30
|
+
if (cronExpression) {
|
|
31
|
+
try {
|
|
32
|
+
const interval = (0, cron_parser_1.parseExpression)(cronExpression, { currentDate: new Date() });
|
|
33
|
+
return interval.next().toDate();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (runAt && runAt > new Date()) {
|
|
40
|
+
return runAt;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
function startScheduledJobExecutor() {
|
|
45
|
+
logger_1.logger.info('Scheduled job executor started.');
|
|
46
|
+
void executeDueJobs();
|
|
47
|
+
setInterval(() => void executeDueJobs(), 60000);
|
|
48
|
+
}
|
|
49
|
+
async function executeDueJobs() {
|
|
50
|
+
try {
|
|
51
|
+
const now = new Date();
|
|
52
|
+
const dueJobs = await scheduledJob_1.ScheduledJob.findAll({
|
|
53
|
+
where: {
|
|
54
|
+
nextRunAt: { [sequelize_1.Op.lte]: now },
|
|
55
|
+
isActive: true,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
if (dueJobs.length > 0) {
|
|
59
|
+
logger_1.logger.info(`Executing ${dueJobs.length} due scheduled job(s).`);
|
|
60
|
+
}
|
|
61
|
+
for (const job of dueJobs) {
|
|
62
|
+
void executeJob(job).catch((err) => {
|
|
63
|
+
logger_1.logger.error('Scheduled job execution failed.', { jobId: job.id, error: err });
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
logger_1.logger.error('Error polling for due scheduled jobs.', { error: err });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Runs the script in the user's login shell so PATH and profile env-vars are
|
|
72
|
+
// present — identical to how the desktop apps open a terminal. Writing to a
|
|
73
|
+
// temp file avoids quoting/escaping issues with multi-line scripts.
|
|
74
|
+
async function runScript(script) {
|
|
75
|
+
const isWin = process.platform === 'win32';
|
|
76
|
+
const userHome = process.env.HOME ?? process.env.USERPROFILE ?? process.cwd();
|
|
77
|
+
const userShell = isWin ? (process.env.COMSPEC ?? 'cmd.exe') : (process.env.SHELL ?? '/bin/zsh');
|
|
78
|
+
const ext = isWin ? '.bat' : '.sh';
|
|
79
|
+
const tmpFile = path_1.default.join((0, os_1.tmpdir)(), `cron_${(0, cuid_1.default)()}${ext}`);
|
|
80
|
+
try {
|
|
81
|
+
if (isWin) {
|
|
82
|
+
await (0, promises_1.writeFile)(tmpFile, `@echo off\r\n${script}`, 'utf8');
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
await (0, promises_1.writeFile)(tmpFile, script, { encoding: 'utf8', mode: 0o700 });
|
|
86
|
+
}
|
|
87
|
+
// -l = login shell → sources ~/.zprofile / ~/.bash_profile etc.
|
|
88
|
+
const command = isWin ? `"${tmpFile}"` : `"${userShell}" -l "${tmpFile}"`;
|
|
89
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
90
|
+
timeout: 60000,
|
|
91
|
+
cwd: userHome,
|
|
92
|
+
env: process.env,
|
|
93
|
+
});
|
|
94
|
+
const combined = [stdout, stderr ? `STDERR:\n${stderr}` : ''].filter(Boolean).join('\n').trim();
|
|
95
|
+
return { output: combined || '(no output)', isError: false };
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const combined = [err.stdout ?? '', err.stderr ?? ''].filter(Boolean).join('\n').trim();
|
|
99
|
+
return { output: combined || err.message || 'Command failed', isError: true };
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
(0, promises_1.unlink)(tmpFile).catch(() => { });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function runCronJob(job, subscription, sessionId) {
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
let settled = false;
|
|
108
|
+
const settle = (err) => {
|
|
109
|
+
if (settled)
|
|
110
|
+
return;
|
|
111
|
+
settled = true;
|
|
112
|
+
clearTimeout(timeoutHandle);
|
|
113
|
+
err ? reject(err) : resolve();
|
|
114
|
+
};
|
|
115
|
+
const timeoutHandle = setTimeout(() => settle(new Error(`Cron job ${job.id} timed out after ${JOB_TIMEOUT_MS / 60000} minutes`)), JOB_TIMEOUT_MS);
|
|
116
|
+
const send = (msg) => {
|
|
117
|
+
if (settled)
|
|
118
|
+
return;
|
|
119
|
+
void (async () => {
|
|
120
|
+
const content = msg.content ?? '';
|
|
121
|
+
if (msg.is_error) {
|
|
122
|
+
logger_1.logger.error('Cron job: agent returned error.', {
|
|
123
|
+
jobId: job.id,
|
|
124
|
+
content: content.slice(0, 300),
|
|
125
|
+
});
|
|
126
|
+
settle(new Error(`Agent error: ${content.slice(0, 200)}`));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const scriptMatch = SHELL_SCRIPT_RE.exec(content);
|
|
130
|
+
if (scriptMatch) {
|
|
131
|
+
const script = scriptMatch[1].trim();
|
|
132
|
+
logger_1.logger.info('Cron job: executing shell script.', { jobId: job.id });
|
|
133
|
+
const { output, isError } = await runScript(script);
|
|
134
|
+
logger_1.logger.info('Cron job: shell script finished.', {
|
|
135
|
+
jobId: job.id,
|
|
136
|
+
isError,
|
|
137
|
+
outputLength: output.length,
|
|
138
|
+
});
|
|
139
|
+
if (settled)
|
|
140
|
+
return;
|
|
141
|
+
(0, agentServer_1.runAgentTurn)(sessionId, subscription, {
|
|
142
|
+
session_id: sessionId,
|
|
143
|
+
sender: 'user',
|
|
144
|
+
content: output,
|
|
145
|
+
is_terminal_output: true,
|
|
146
|
+
is_error: isError,
|
|
147
|
+
}, send, logger_1.logger, { maxTurns: MAX_CRON_TURNS }).catch((err) => settle(err instanceof Error ? err : new Error(String(err))));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (FINAL_ANSWER_RE.test(content)) {
|
|
151
|
+
logger_1.logger.info('Cron job: received final answer.', { jobId: job.id });
|
|
152
|
+
settle();
|
|
153
|
+
}
|
|
154
|
+
})();
|
|
155
|
+
};
|
|
156
|
+
(0, agentServer_1.runAgentTurn)(sessionId, subscription, {
|
|
157
|
+
session_id: sessionId,
|
|
158
|
+
sender: 'user',
|
|
159
|
+
content: job.prompt,
|
|
160
|
+
platform: job.platform ?? undefined,
|
|
161
|
+
}, send, logger_1.logger, { maxTurns: MAX_CRON_TURNS }).catch((err) => settle(err instanceof Error ? err : new Error(String(err))));
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
async function executeJob(job) {
|
|
165
|
+
logger_1.logger.info('Executing scheduled job.', { jobId: job.id, label: job.label });
|
|
166
|
+
const subscription = await subscription_1.Subscription.findByPk(job.subscriptionId);
|
|
167
|
+
if (!subscription) {
|
|
168
|
+
logger_1.logger.error('Subscription not found for scheduled job; skipping.', {
|
|
169
|
+
jobId: job.id,
|
|
170
|
+
subscriptionId: job.subscriptionId,
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const sessionId = (0, cuid_1.default)();
|
|
175
|
+
try {
|
|
176
|
+
await runCronJob(job, subscription, sessionId);
|
|
177
|
+
logger_1.logger.info('Scheduled job completed.', { jobId: job.id, label: job.label });
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
logger_1.logger.error('Scheduled job failed.', { jobId: job.id, label: job.label, error: err });
|
|
181
|
+
// Fall through — always update lastRunAt so the next poll does not re-run immediately.
|
|
182
|
+
}
|
|
183
|
+
const now = new Date();
|
|
184
|
+
if (job.cronExpression) {
|
|
185
|
+
await job.update({
|
|
186
|
+
lastRunAt: now,
|
|
187
|
+
nextRunAt: computeNextRunAt(job.cronExpression, null),
|
|
188
|
+
lastRunSessionId: sessionId,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
await job.update({
|
|
193
|
+
lastRunAt: now,
|
|
194
|
+
isActive: false,
|
|
195
|
+
nextRunAt: null,
|
|
196
|
+
lastRunSessionId: sessionId,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.scheduledJobRouter = scheduledJobRouter;
|
|
7
|
+
const express_1 = __importDefault(require("express"));
|
|
8
|
+
const zod_1 = __importDefault(require("zod"));
|
|
9
|
+
const authMiddleware_1 = require("./authMiddleware");
|
|
10
|
+
const scheduledJob_1 = require("./models/scheduledJob");
|
|
11
|
+
const scheduledJobExecutor_1 = require("./scheduledJobExecutor");
|
|
12
|
+
const CRON_REGEX = /^(\S+\s){4}\S+$/;
|
|
13
|
+
const jobSchema = zod_1.default.object({
|
|
14
|
+
label: zod_1.default.string().min(1).max(200),
|
|
15
|
+
prompt: zod_1.default.string().min(1),
|
|
16
|
+
cronExpression: zod_1.default.string().regex(CRON_REGEX, 'Invalid cron expression (must be 5 fields)').optional(),
|
|
17
|
+
runAt: zod_1.default.string().optional(),
|
|
18
|
+
isActive: zod_1.default.boolean().optional(),
|
|
19
|
+
sessionId: zod_1.default.string().nullable().optional(),
|
|
20
|
+
platform: zod_1.default.string().optional(),
|
|
21
|
+
});
|
|
22
|
+
function formatJob(job) {
|
|
23
|
+
return {
|
|
24
|
+
id: job.id,
|
|
25
|
+
label: job.label,
|
|
26
|
+
prompt: job.prompt,
|
|
27
|
+
cronExpression: job.cronExpression,
|
|
28
|
+
runAt: job.runAt,
|
|
29
|
+
isActive: job.isActive,
|
|
30
|
+
lastRunAt: job.lastRunAt,
|
|
31
|
+
nextRunAt: job.nextRunAt,
|
|
32
|
+
sessionId: job.sessionId,
|
|
33
|
+
lastRunSessionId: job.lastRunSessionId,
|
|
34
|
+
platform: job.platform,
|
|
35
|
+
createdAt: job.createdAt,
|
|
36
|
+
updatedAt: job.updatedAt,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function scheduledJobRouter() {
|
|
40
|
+
const router = express_1.default.Router();
|
|
41
|
+
router.get('/', authMiddleware_1.authMiddleware, async (req, res) => {
|
|
42
|
+
const { logger, subscription } = res.locals;
|
|
43
|
+
try {
|
|
44
|
+
const jobs = await scheduledJob_1.ScheduledJob.findAll({
|
|
45
|
+
where: { subscriptionId: subscription.id },
|
|
46
|
+
order: [['next_run_at', 'ASC NULLS LAST']],
|
|
47
|
+
});
|
|
48
|
+
res.json({ jobs: jobs.map(formatJob) });
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
logger.error('Error retrieving scheduled jobs.', { error: err });
|
|
52
|
+
res.status(500).json({ error: 'Failed to retrieve scheduled jobs.' });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
router.post('/', authMiddleware_1.authMiddleware, async (req, res) => {
|
|
56
|
+
const { logger, subscription } = res.locals;
|
|
57
|
+
try {
|
|
58
|
+
const parsed = jobSchema.parse(req.body);
|
|
59
|
+
const hasCron = !!parsed.cronExpression;
|
|
60
|
+
const hasRunAt = !!parsed.runAt;
|
|
61
|
+
if (!hasCron && !hasRunAt) {
|
|
62
|
+
return res.status(400).json({ error: 'Either cronExpression or runAt is required.' });
|
|
63
|
+
}
|
|
64
|
+
if (hasCron && hasRunAt) {
|
|
65
|
+
return res.status(400).json({ error: 'Provide either cronExpression or runAt, not both.' });
|
|
66
|
+
}
|
|
67
|
+
let runAt = null;
|
|
68
|
+
if (hasRunAt) {
|
|
69
|
+
runAt = new Date(parsed.runAt);
|
|
70
|
+
if (isNaN(runAt.getTime())) {
|
|
71
|
+
return res.status(400).json({ error: 'Invalid runAt date.' });
|
|
72
|
+
}
|
|
73
|
+
if (runAt <= new Date()) {
|
|
74
|
+
return res.status(400).json({ error: 'runAt must be in the future.' });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const nextRunAt = (0, scheduledJobExecutor_1.computeNextRunAt)(parsed.cronExpression ?? null, runAt);
|
|
78
|
+
const job = await scheduledJob_1.ScheduledJob.create({
|
|
79
|
+
subscriptionId: subscription.id,
|
|
80
|
+
label: parsed.label,
|
|
81
|
+
prompt: parsed.prompt,
|
|
82
|
+
cronExpression: parsed.cronExpression ?? null,
|
|
83
|
+
runAt,
|
|
84
|
+
isActive: parsed.isActive ?? true,
|
|
85
|
+
nextRunAt,
|
|
86
|
+
sessionId: parsed.sessionId ?? null,
|
|
87
|
+
platform: parsed.platform ?? null,
|
|
88
|
+
});
|
|
89
|
+
res.status(201).json(formatJob(job));
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
logger.error('Error creating scheduled job.', { error: err });
|
|
93
|
+
if (err instanceof zod_1.default.ZodError) {
|
|
94
|
+
return res.status(400).json({ error: 'Invalid job data.' });
|
|
95
|
+
}
|
|
96
|
+
res.status(500).json({ error: 'Failed to create scheduled job.' });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
router.put('/:id', authMiddleware_1.authMiddleware, async (req, res) => {
|
|
100
|
+
const { logger, subscription } = res.locals;
|
|
101
|
+
const { id } = req.params;
|
|
102
|
+
try {
|
|
103
|
+
const parsed = jobSchema.partial().parse(req.body);
|
|
104
|
+
const job = await scheduledJob_1.ScheduledJob.findOne({
|
|
105
|
+
where: { id, subscriptionId: subscription.id },
|
|
106
|
+
});
|
|
107
|
+
if (!job) {
|
|
108
|
+
return res.status(404).json({ error: 'Scheduled job not found.' });
|
|
109
|
+
}
|
|
110
|
+
const cronExpression = parsed.cronExpression !== undefined ? parsed.cronExpression ?? null : job.cronExpression;
|
|
111
|
+
let runAt = job.runAt;
|
|
112
|
+
if (parsed.runAt !== undefined) {
|
|
113
|
+
if (parsed.runAt) {
|
|
114
|
+
runAt = new Date(parsed.runAt);
|
|
115
|
+
if (isNaN(runAt.getTime())) {
|
|
116
|
+
return res.status(400).json({ error: 'Invalid runAt date.' });
|
|
117
|
+
}
|
|
118
|
+
if (runAt <= new Date()) {
|
|
119
|
+
return res.status(400).json({ error: 'runAt must be in the future.' });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
runAt = null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const nextRunAt = (0, scheduledJobExecutor_1.computeNextRunAt)(cronExpression, runAt);
|
|
127
|
+
await job.update({
|
|
128
|
+
label: parsed.label ?? job.label,
|
|
129
|
+
prompt: parsed.prompt ?? job.prompt,
|
|
130
|
+
cronExpression,
|
|
131
|
+
runAt,
|
|
132
|
+
isActive: parsed.isActive ?? job.isActive,
|
|
133
|
+
nextRunAt,
|
|
134
|
+
sessionId: parsed.sessionId !== undefined ? (parsed.sessionId ?? null) : job.sessionId,
|
|
135
|
+
platform: parsed.platform ?? job.platform,
|
|
136
|
+
});
|
|
137
|
+
res.json(formatJob(job));
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
logger.error('Error updating scheduled job.', { error: err });
|
|
141
|
+
if (err instanceof zod_1.default.ZodError) {
|
|
142
|
+
return res.status(400).json({ error: 'Invalid job data.' });
|
|
143
|
+
}
|
|
144
|
+
res.status(500).json({ error: 'Failed to update scheduled job.' });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
router.delete('/:id', authMiddleware_1.authMiddleware, async (req, res) => {
|
|
148
|
+
const { logger, subscription } = res.locals;
|
|
149
|
+
const { id } = req.params;
|
|
150
|
+
try {
|
|
151
|
+
const job = await scheduledJob_1.ScheduledJob.findOne({
|
|
152
|
+
where: { id, subscriptionId: subscription.id },
|
|
153
|
+
});
|
|
154
|
+
if (!job) {
|
|
155
|
+
return res.status(404).json({ error: 'Scheduled job not found.' });
|
|
156
|
+
}
|
|
157
|
+
await job.destroy();
|
|
158
|
+
res.status(204).send();
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
logger.error('Error deleting scheduled job.', { error: err });
|
|
162
|
+
res.status(500).json({ error: 'Failed to delete scheduled job.' });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
router.post('/:id/run-now', authMiddleware_1.authMiddleware, async (req, res) => {
|
|
166
|
+
const { logger, subscription } = res.locals;
|
|
167
|
+
const { id } = req.params;
|
|
168
|
+
try {
|
|
169
|
+
const job = await scheduledJob_1.ScheduledJob.findOne({
|
|
170
|
+
where: { id, subscriptionId: subscription.id },
|
|
171
|
+
});
|
|
172
|
+
if (!job) {
|
|
173
|
+
return res.status(404).json({ error: 'Scheduled job not found.' });
|
|
174
|
+
}
|
|
175
|
+
void (0, scheduledJobExecutor_1.executeJob)(job).catch((err) => {
|
|
176
|
+
logger.error('run-now execution failed.', { jobId: job.id, error: err });
|
|
177
|
+
});
|
|
178
|
+
res.json(formatJob(job));
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
logger.error('Error triggering scheduled job.', { error: err });
|
|
182
|
+
res.status(500).json({ error: 'Failed to trigger scheduled job.' });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
return router;
|
|
186
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const showLogs_1 = require("./showLogs");
|
|
|
11
11
|
const showConfig_1 = require("./showConfig");
|
|
12
12
|
const setConfig_1 = require("./setConfig");
|
|
13
13
|
const grantBrowserAccess_1 = require("./grantBrowserAccess");
|
|
14
|
+
const scheduleJob_1 = require("./scheduleJob");
|
|
14
15
|
const program = new commander_1.Command();
|
|
15
16
|
program
|
|
16
17
|
.name('omnikey')
|
|
@@ -94,4 +95,23 @@ program
|
|
|
94
95
|
.action(async () => {
|
|
95
96
|
await (0, grantBrowserAccess_1.reopenBrowserDebugProfile)();
|
|
96
97
|
});
|
|
98
|
+
const scheduleCmd = program
|
|
99
|
+
.command('schedule')
|
|
100
|
+
.description('Manage scheduled prompt jobs');
|
|
101
|
+
scheduleCmd
|
|
102
|
+
.command('add')
|
|
103
|
+
.description('Add a new scheduled job')
|
|
104
|
+
.action(async () => { await (0, scheduleJob_1.scheduleAdd)(); });
|
|
105
|
+
scheduleCmd
|
|
106
|
+
.command('list')
|
|
107
|
+
.description('List all scheduled jobs')
|
|
108
|
+
.action(async () => { await (0, scheduleJob_1.scheduleList)(); });
|
|
109
|
+
scheduleCmd
|
|
110
|
+
.command('remove')
|
|
111
|
+
.description('Remove a scheduled job')
|
|
112
|
+
.action(async () => { await (0, scheduleJob_1.scheduleRemove)(); });
|
|
113
|
+
scheduleCmd
|
|
114
|
+
.command('run-now <id>')
|
|
115
|
+
.description('Immediately run a scheduled job by ID')
|
|
116
|
+
.action(async (id) => { await (0, scheduleJob_1.scheduleRunNow)(id); });
|
|
97
117
|
program.parseAsync(process.argv);
|
package/dist/onboard.js
CHANGED
|
@@ -14,11 +14,12 @@ const AI_PROVIDERS = [
|
|
|
14
14
|
{ name: 'Google Gemini (gemini-2.5-flash / gemini-2.5-pro)', value: 'gemini' },
|
|
15
15
|
];
|
|
16
16
|
const SEARCH_PROVIDERS = [
|
|
17
|
-
{ name: 'Skip
|
|
18
|
-
{ name: '
|
|
19
|
-
{ name: '
|
|
20
|
-
{ name: '
|
|
21
|
-
{ name: '
|
|
17
|
+
{ name: 'Skip', value: 'skip' },
|
|
18
|
+
{ name: 'DuckDuckGo', value: 'duckduckgo' },
|
|
19
|
+
{ name: 'Serper', value: 'serper' },
|
|
20
|
+
{ name: 'Brave Search', value: 'brave' },
|
|
21
|
+
{ name: 'Tavily', value: 'tavily' },
|
|
22
|
+
{ name: 'SearXNG', value: 'searxng' },
|
|
22
23
|
];
|
|
23
24
|
const AI_PROVIDER_KEY_ENV = {
|
|
24
25
|
openai: 'OPENAI_API_KEY',
|
|
@@ -59,7 +60,7 @@ async function onboard() {
|
|
|
59
60
|
{
|
|
60
61
|
type: 'list',
|
|
61
62
|
name: 'provider',
|
|
62
|
-
message: 'Select a web search provider for the AI agent
|
|
63
|
+
message: 'Select a web search provider for the AI agent. Supported providers: DuckDuckGo, Serper, Brave Search, Tavily, SearXNG:',
|
|
63
64
|
choices: SEARCH_PROVIDERS,
|
|
64
65
|
default: 'skip',
|
|
65
66
|
},
|