omnikey-cli 1.0.35 → 1.0.37
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.
|
@@ -17,28 +17,17 @@ ${hasTaskInstructions
|
|
|
17
17
|
- Default to a \`<shell_script>\` for anything involving the machine, network, files, processes, env vars, or system state — never answer from training data alone.
|
|
18
18
|
- **Read vs write:** For open-ended/ambiguous requests run safe read-only commands first to understand the current state. When the user **explicitly** asks to create, update, delete, configure, or run something — do it directly; no need to ask for confirmation unless the scope is genuinely unclear.
|
|
19
19
|
- **Package installation:** Install any package required to complete the task. Include the install step as its own phase so you can confirm it succeeded before building on it. Prefer project-local or user scope; avoid \`sudo\`/admin unless the user explicitly asks.
|
|
20
|
-
${config_1.config.browserDebugPort !== undefined
|
|
20
|
+
${config_1.config.browserDebugPort !== undefined
|
|
21
|
+
? `- **Browser automation:** Use browser automation proactively when needed to complete the task.
|
|
22
|
+
- Do NOT wait for explicit user wording like "use browser" if interaction is obviously required to get the final result.
|
|
23
|
+
- If \`web_search\` / \`web_fetch\` do not provide enough usable context (blocked pages, incomplete data, client-rendered content, auth walls, dynamic tables, hidden details, repeated low-value fetch results), immediately switch to Playwright-based browser interaction.
|
|
24
|
+
- Generate \`<shell_script>\` blocks that use Node.js and \`playwright-core\` — one phase at a time (phasing rules below apply).
|
|
21
25
|
- **Phase 1 — ensure deps:** Check and install \`playwright-core\` if missing:
|
|
22
26
|
\`node -e "require('/tmp/playwright-runner/node_modules/playwright-core')" 2>/dev/null || npm install --prefix /tmp/playwright-runner playwright-core --silent\`
|
|
23
|
-
- **Phase 2 — connect & navigate:**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
browser = await chromium.connectOverCDP('http://localhost:${config_1.config.browserDebugPort}');
|
|
29
|
-
const pages = browser.contexts().flatMap(c => c.pages());
|
|
30
|
-
page = pages.find(p => p.url().startsWith(TARGET_URL)) ?? null;
|
|
31
|
-
if (page) { await page.bringToFront(); }
|
|
32
|
-
else { page = await browser.contexts()[0].newPage(); await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 15000 }); }
|
|
33
|
-
} catch {
|
|
34
|
-
const ctx = await chromium.launchPersistentContext('${config_1.config.browserDebugUserDataDir}', { executablePath: '${config_1.config.browserDebugExecutable}', headless: false });
|
|
35
|
-
browser = ctx;
|
|
36
|
-
page = ctx.pages().find(p => p.url().startsWith(TARGET_URL)) ?? await ctx.newPage();
|
|
37
|
-
if (!page.url().startsWith(TARGET_URL)) await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
38
|
-
}
|
|
39
|
-
\`\`\`
|
|
40
|
-
- **Phase 3+ — one action per script:** Each subsequent script reconnects the same way, finds the already-open tab, performs exactly one action (click / type / select / screenshot / read text), prints the result, then calls \`browser.disconnect()\` (CDP) or just exits (profile launch — leaves the window open).
|
|
41
|
-
- Always inline Node.js via a bash heredoc so the script is self-contained. Print structured output to stdout so it returns as \`TERMINAL OUTPUT:\`.` : ''}
|
|
27
|
+
- **Phase 2 — connect & navigate:** Connect to the running browser via CDP at \`http://localhost:${config_1.config.browserDebugPort}\`. If CDP fails, fall back to launching a persistent context using the debug profile at \`${config_1.config.browserDebugUserDataDir}\` with the executable at \`${config_1.config.browserDebugExecutable}\` (headless: false). Once connected, navigate to any URL required by the task — open any page needed, reusing an existing tab if the URL already matches or creating a new one if not. There is no restriction on which sites or pages you can visit; open whatever is necessary to complete the task.
|
|
28
|
+
- **Phase 3+ — one action per script:** Each subsequent script reconnects via the same CDP endpoint (\`http://localhost:${config_1.config.browserDebugPort}\`) or profile fallback, finds the already-open tab (or reopens it), performs exactly one action (click, type, select, scroll, screenshot, read text, extract data, fill forms, etc.), prints the result to stdout, then calls \`browser.disconnect()\` (CDP) or exits (profile launch). You may perform any interaction the task requires — reading content, extracting structured data, submitting forms, navigating between pages, or capturing screenshots.
|
|
29
|
+
- Always inline Node.js via a bash heredoc so the script is self-contained. Print structured output to stdout so it returns as \`TERMINAL OUTPUT:\`.`
|
|
30
|
+
: ''}
|
|
42
31
|
- Use ${!isWindows ? 'bash (macOS/Linux)' : 'PowerShell'}. Every script must be self-contained and ready to run as-is.
|
|
43
32
|
- Skip the script only for purely factual/conversational requests with no live data dependency (e.g. "what is 2+2").
|
|
44
33
|
|
|
@@ -171,10 +171,6 @@ async function runToolLoop(initialResult, session, sessionId, send, log, tools,
|
|
|
171
171
|
return result;
|
|
172
172
|
}
|
|
173
173
|
const aiModel = (0, ai_client_1.getDefaultModel)(config_1.config.aiProvider, 'smart');
|
|
174
|
-
// In-memory cache: sessionId -> live SessionState. Hydrated from DB on first
|
|
175
|
-
// access and written back after each turn so restarts resume correctly.
|
|
176
|
-
const sessionMessages = new Map();
|
|
177
|
-
const MAX_TURNS = 20;
|
|
178
174
|
// ─── DB helpers ───────────────────────────────────────────────────────────────
|
|
179
175
|
async function persistSessionToDB(sessionId, state) {
|
|
180
176
|
try {
|
|
@@ -216,22 +212,7 @@ async function enforceSessionCap(subscriptionId, logger) {
|
|
|
216
212
|
}
|
|
217
213
|
}
|
|
218
214
|
async function getOrCreateSession(sessionId, subscription, platform, log, isCronJob = false) {
|
|
219
|
-
// 1.
|
|
220
|
-
const existing = sessionMessages.get(sessionId);
|
|
221
|
-
if (existing) {
|
|
222
|
-
log.debug('Reusing existing agent session (in-memory)', {
|
|
223
|
-
sessionId,
|
|
224
|
-
subscriptionId: existing.subscription.id,
|
|
225
|
-
turns: existing.turns,
|
|
226
|
-
});
|
|
227
|
-
return {
|
|
228
|
-
sessionState: existing,
|
|
229
|
-
hasStoredPrompt: existing.history
|
|
230
|
-
.filter((h) => h.role === 'user')
|
|
231
|
-
.some((h) => typeof h.content === 'string' && h.content.includes('<stored_instructions>')),
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
// 2. Try to resume from a persisted DB record.
|
|
215
|
+
// 1. Try to resume from a persisted DB record.
|
|
235
216
|
try {
|
|
236
217
|
const dbSession = await agentSession_1.AgentSession.findOne({
|
|
237
218
|
where: { id: sessionId, subscriptionId: subscription.id },
|
|
@@ -243,7 +224,6 @@ async function getOrCreateSession(sessionId, subscription, platform, log, isCron
|
|
|
243
224
|
history,
|
|
244
225
|
turns: dbSession.turns,
|
|
245
226
|
};
|
|
246
|
-
sessionMessages.set(sessionId, entry);
|
|
247
227
|
log.info('Resumed agent session from DB', {
|
|
248
228
|
sessionId,
|
|
249
229
|
subscriptionId: subscription.id,
|
|
@@ -263,7 +243,7 @@ async function getOrCreateSession(sessionId, subscription, platform, log, isCron
|
|
|
263
243
|
error: err,
|
|
264
244
|
});
|
|
265
245
|
}
|
|
266
|
-
//
|
|
246
|
+
// 2. Create a brand-new session and persist it to the DB.
|
|
267
247
|
const prompt = await (0, featureRoutes_1.getPromptForCommand)(log, 'task', subscription).catch((err) => {
|
|
268
248
|
log.error('Failed to get system prompt for new agent session', { error: err });
|
|
269
249
|
return '';
|
|
@@ -293,18 +273,39 @@ ${prompt}
|
|
|
293
273
|
],
|
|
294
274
|
turns: 0,
|
|
295
275
|
};
|
|
296
|
-
sessionMessages.set(sessionId, entry);
|
|
297
276
|
// Persist immediately so that GET /sessions picks it up right away.
|
|
298
277
|
try {
|
|
299
|
-
await agentSession_1.AgentSession.
|
|
300
|
-
id: sessionId,
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
278
|
+
const [dbSession, created] = await agentSession_1.AgentSession.findOrCreate({
|
|
279
|
+
where: { id: sessionId, subscriptionId: subscription.id },
|
|
280
|
+
defaults: {
|
|
281
|
+
id: sessionId,
|
|
282
|
+
subscriptionId: subscription.id,
|
|
283
|
+
title: 'New Session',
|
|
284
|
+
platform: platform ?? null,
|
|
285
|
+
historyJson: JSON.stringify(entry.history),
|
|
286
|
+
turns: 0,
|
|
287
|
+
lastActiveAt: new Date(),
|
|
288
|
+
},
|
|
307
289
|
});
|
|
290
|
+
if (!created) {
|
|
291
|
+
const history = JSON.parse(dbSession.historyJson || '[]');
|
|
292
|
+
const existingEntry = {
|
|
293
|
+
subscription,
|
|
294
|
+
history,
|
|
295
|
+
turns: dbSession.turns,
|
|
296
|
+
};
|
|
297
|
+
log.info('Reused existing agent session row from DB during create path', {
|
|
298
|
+
sessionId,
|
|
299
|
+
subscriptionId: subscription.id,
|
|
300
|
+
turns: existingEntry.turns,
|
|
301
|
+
});
|
|
302
|
+
return {
|
|
303
|
+
sessionState: existingEntry,
|
|
304
|
+
hasStoredPrompt: history
|
|
305
|
+
.filter((h) => h.role === 'user')
|
|
306
|
+
.some((h) => typeof h.content === 'string' && h.content.includes('<stored_instructions>')),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
308
309
|
// Prune oldest sessions after each creation so the cap is always respected.
|
|
309
310
|
void enforceSessionCap(subscription.id, log);
|
|
310
311
|
}
|
|
@@ -321,7 +322,7 @@ ${prompt}
|
|
|
321
322
|
hasStoredPrompt: !!prompt,
|
|
322
323
|
};
|
|
323
324
|
}
|
|
324
|
-
async function
|
|
325
|
+
async function runAgentTurnInternal(sessionId, subscription, clientMessage, send, log, options) {
|
|
325
326
|
const { sessionState: session, hasStoredPrompt } = await getOrCreateSession(sessionId, subscription, clientMessage.platform, log, options?.isCronJob);
|
|
326
327
|
// Count this call as one agent iteration.
|
|
327
328
|
session.turns += 1;
|
|
@@ -330,14 +331,6 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, o
|
|
|
330
331
|
subscriptionId: subscription.id,
|
|
331
332
|
turn: session.turns,
|
|
332
333
|
});
|
|
333
|
-
const effectiveMaxTurns = options?.maxTurns ?? MAX_TURNS;
|
|
334
|
-
// On the final iteration, instruct the LLM to provide a consolidated answer.
|
|
335
|
-
if (session.turns === effectiveMaxTurns) {
|
|
336
|
-
(0, utils_1.pushToSessionHistory)(logger_1.logger, session, {
|
|
337
|
-
role: 'system',
|
|
338
|
-
content: 'Provide a single, final, concise answer based on the entire conversation so far. Wrap the answer in a <final_answer>...</final_answer> block and do not ask for further input or mention additional shell scripts to run. Do not include any <shell_script> block in this response.',
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
334
|
// Append the client message as user content, marking terminal
|
|
342
335
|
// output and errors in the text so the agent can reason about them.
|
|
343
336
|
let userContent = clientMessage.content || '';
|
|
@@ -386,10 +379,7 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, o
|
|
|
386
379
|
}
|
|
387
380
|
}
|
|
388
381
|
}
|
|
389
|
-
|
|
390
|
-
// plain text <final_answer> rather than issuing another tool call.
|
|
391
|
-
const isFinalTurn = session.turns >= effectiveMaxTurns;
|
|
392
|
-
const tools = isFinalTurn ? undefined : (0, utils_1.buildAvailableTools)();
|
|
382
|
+
const tools = (0, utils_1.buildAvailableTools)();
|
|
393
383
|
const recordUsage = async (result) => {
|
|
394
384
|
const usage = result.usage;
|
|
395
385
|
if (!usage)
|
|
@@ -445,13 +435,12 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, o
|
|
|
445
435
|
log.warn('Agent LLM returned empty content; sending generic error to client.');
|
|
446
436
|
const errorMessage = 'The agent returned an empty response. Please try again.';
|
|
447
437
|
await persistSessionToDB(sessionId, session);
|
|
448
|
-
sessionMessages.delete(sessionId);
|
|
449
438
|
(0, utils_1.sendFinalAnswer)(send, sessionId, errorMessage, true);
|
|
450
439
|
return;
|
|
451
440
|
}
|
|
452
441
|
// If the model requested web tool calls, execute them and get a follow-up
|
|
453
442
|
// response before deciding what to send to the client.
|
|
454
|
-
if (
|
|
443
|
+
if (result.finish_reason === 'tool_calls') {
|
|
455
444
|
log.info('Running web tool calls to gather information', {
|
|
456
445
|
sessionId,
|
|
457
446
|
subscriptionId: subscription.id,
|
|
@@ -506,7 +495,10 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, o
|
|
|
506
495
|
'No plain text. No other format.',
|
|
507
496
|
].join('\n'),
|
|
508
497
|
});
|
|
509
|
-
|
|
498
|
+
// DB-only session state: persist before recursive handoff so the
|
|
499
|
+
// follow-up turn reads the latest history and turn count.
|
|
500
|
+
await persistSessionToDB(sessionId, session);
|
|
501
|
+
await runAgentTurnInternal(sessionId, subscription, {
|
|
510
502
|
sender: 'agent',
|
|
511
503
|
session_id: sessionId,
|
|
512
504
|
content: '',
|
|
@@ -515,15 +507,9 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, o
|
|
|
515
507
|
return;
|
|
516
508
|
}
|
|
517
509
|
}
|
|
518
|
-
// Ensure that a proper <final_answer> block is produced for the
|
|
519
|
-
// desktop clients once we reach the final turn. If the model did
|
|
520
|
-
// not emit either a <shell_script> or <final_answer> tag on the
|
|
521
|
-
// MAX_TURNS turn, we treat this as the final natural-language answer
|
|
522
|
-
// and wrap it in <final_answer> tags so the client can stop
|
|
523
|
-
// waiting and paste the result.
|
|
524
510
|
const hasShellScriptTag = content.includes('<shell_script>');
|
|
525
511
|
const hasFinalAnswerTag = content.includes('<final_answer>');
|
|
526
|
-
if (hasShellScriptTag
|
|
512
|
+
if (hasShellScriptTag) {
|
|
527
513
|
log.info('Completed agent turn. Sending back scripts, waiting for results.', {
|
|
528
514
|
sessionId,
|
|
529
515
|
subscriptionId: subscription.id,
|
|
@@ -534,6 +520,10 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, o
|
|
|
534
520
|
role: 'assistant',
|
|
535
521
|
content,
|
|
536
522
|
});
|
|
523
|
+
// Persist before sending so that if the send callback triggers a new
|
|
524
|
+
// runAgentTurn immediately (e.g. cron shell-script loop), the DB already
|
|
525
|
+
// has the updated turn count and history.
|
|
526
|
+
await persistSessionToDB(sessionId, session);
|
|
537
527
|
send({
|
|
538
528
|
session_id: sessionId,
|
|
539
529
|
sender: 'agent',
|
|
@@ -543,8 +533,8 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, o
|
|
|
543
533
|
});
|
|
544
534
|
return;
|
|
545
535
|
}
|
|
546
|
-
if (
|
|
547
|
-
log.info('Finalizing agent session after
|
|
536
|
+
if (hasFinalAnswerTag) {
|
|
537
|
+
log.info('Finalizing agent session after final answer tag', {
|
|
548
538
|
sessionId,
|
|
549
539
|
subscriptionId: subscription.id,
|
|
550
540
|
turns: session.turns,
|
|
@@ -552,7 +542,6 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, o
|
|
|
552
542
|
});
|
|
553
543
|
(0, utils_1.pushToSessionHistory)(logger_1.logger, session, { role: 'assistant', content });
|
|
554
544
|
await persistSessionToDB(sessionId, session);
|
|
555
|
-
sessionMessages.delete(sessionId);
|
|
556
545
|
send({
|
|
557
546
|
session_id: sessionId,
|
|
558
547
|
sender: 'agent',
|
|
@@ -571,7 +560,6 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, o
|
|
|
571
560
|
});
|
|
572
561
|
(0, utils_1.pushToSessionHistory)(log, session, { role: 'assistant', content });
|
|
573
562
|
await persistSessionToDB(sessionId, session);
|
|
574
|
-
sessionMessages.delete(sessionId);
|
|
575
563
|
send({
|
|
576
564
|
session_id: sessionId,
|
|
577
565
|
sender: 'agent',
|
|
@@ -583,7 +571,6 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, o
|
|
|
583
571
|
sessionId,
|
|
584
572
|
});
|
|
585
573
|
await persistSessionToDB(sessionId, session);
|
|
586
|
-
sessionMessages.delete(sessionId);
|
|
587
574
|
(0, utils_1.sendFinalAnswer)(send, sessionId, 'The agent returned an empty response. Please try again.', true);
|
|
588
575
|
}
|
|
589
576
|
}
|
|
@@ -591,10 +578,12 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, o
|
|
|
591
578
|
log.error('Agent LLM call failed', { error: err });
|
|
592
579
|
const errorMessage = 'Agent failed to call language model. Please try again later.';
|
|
593
580
|
await persistSessionToDB(sessionId, session);
|
|
594
|
-
sessionMessages.delete(sessionId);
|
|
595
581
|
(0, utils_1.sendFinalAnswer)(send, sessionId, errorMessage, true);
|
|
596
582
|
}
|
|
597
583
|
}
|
|
584
|
+
async function runAgentTurn(sessionId, subscription, clientMessage, send, log, options) {
|
|
585
|
+
await runAgentTurnInternal(sessionId, subscription, clientMessage, send, log, options);
|
|
586
|
+
}
|
|
598
587
|
function attachAgentWebSocketServer(server) {
|
|
599
588
|
const wss = new ws_1.WebSocketServer({ server, path: '/ws/omni-agent' });
|
|
600
589
|
wss.on('connection', (ws, req) => {
|
|
@@ -731,8 +720,6 @@ function createAgentRouter() {
|
|
|
731
720
|
res.status(404).json({ error: 'Session not found' });
|
|
732
721
|
return;
|
|
733
722
|
}
|
|
734
|
-
// Also remove from the in-memory cache if it was loaded.
|
|
735
|
-
sessionMessages.delete(sessionId);
|
|
736
723
|
res.status(200).json({ deleted: true });
|
|
737
724
|
}
|
|
738
725
|
catch (err) {
|
|
@@ -10,18 +10,33 @@ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
|
10
10
|
const logger_1 = require("./logger");
|
|
11
11
|
const config_1 = require("./config");
|
|
12
12
|
const subscription_1 = require("./models/subscription");
|
|
13
|
+
const SELF_HOSTED_SUBSCRIPTION_ID = 'self-hosted-local-subscription';
|
|
13
14
|
async function selfHostedSubscription() {
|
|
14
15
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// Reuse any existing self-hosted record (including legacy IDs) first.
|
|
17
|
+
const existing = await subscription_1.Subscription.findOne({ where: { isSelfHosted: true } });
|
|
18
|
+
if (existing)
|
|
19
|
+
return existing;
|
|
20
|
+
// Use a deterministic primary key so concurrent first-time requests do not
|
|
21
|
+
// create duplicate rows.
|
|
22
|
+
const [subscription, created] = await subscription_1.Subscription.findOrCreate({
|
|
23
|
+
where: { id: SELF_HOSTED_SUBSCRIPTION_ID },
|
|
24
|
+
defaults: {
|
|
25
|
+
id: SELF_HOSTED_SUBSCRIPTION_ID,
|
|
18
26
|
email: 'local-user@omnikey.ai',
|
|
19
27
|
licenseKey: 'self-hosted',
|
|
20
28
|
subscriptionStatus: 'active',
|
|
21
29
|
isSelfHosted: true,
|
|
22
|
-
}
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
if (created) {
|
|
23
33
|
logger_1.logger.info('Created self-hosted subscription record in database.');
|
|
24
34
|
}
|
|
35
|
+
// Ensure deterministic row remains flagged for self-hosted mode.
|
|
36
|
+
if (!subscription.isSelfHosted) {
|
|
37
|
+
subscription.isSelfHosted = true;
|
|
38
|
+
await subscription.save();
|
|
39
|
+
}
|
|
25
40
|
return subscription;
|
|
26
41
|
}
|
|
27
42
|
catch (err) {
|
|
@@ -31,37 +46,37 @@ async function selfHostedSubscription() {
|
|
|
31
46
|
}
|
|
32
47
|
async function authMiddleware(req, res, next) {
|
|
33
48
|
const authHeader = req.headers.authorization;
|
|
34
|
-
logger_1.logger.
|
|
49
|
+
const requestLogger = logger_1.logger.child({ traceId: (0, crypto_1.randomUUID)() });
|
|
50
|
+
res.locals.logger = requestLogger;
|
|
35
51
|
if (config_1.config.blockSaas) {
|
|
36
|
-
|
|
52
|
+
requestLogger.warn('Blocking SaaS access: rejecting request due to BLOCK_SAAS=true');
|
|
37
53
|
return res.status(403).json({ error: 'SaaS access is blocked.' });
|
|
38
54
|
}
|
|
39
55
|
if (config_1.config.isSelfHosted || !config_1.config.jwtSecret) {
|
|
40
|
-
|
|
56
|
+
requestLogger.info('Self-hosted mode: skipping auth middleware.');
|
|
41
57
|
if (config_1.config.isSelfHosted) {
|
|
42
58
|
res.locals.subscription = await selfHostedSubscription();
|
|
43
|
-
res.locals.logger = logger_1.logger;
|
|
44
59
|
}
|
|
45
60
|
return next();
|
|
46
61
|
}
|
|
47
62
|
if (!authHeader) {
|
|
48
|
-
|
|
63
|
+
requestLogger.warn('Missing Authorization header on feature route.');
|
|
49
64
|
return res.status(401).json({ error: 'Missing bearer token.' });
|
|
50
65
|
}
|
|
51
66
|
const [scheme, token] = authHeader.split(' ');
|
|
52
67
|
if (scheme !== 'Bearer' || !token) {
|
|
53
|
-
|
|
68
|
+
requestLogger.warn('Malformed Authorization header on feature route.');
|
|
54
69
|
return res.status(401).json({ error: 'Invalid authorization header.' });
|
|
55
70
|
}
|
|
56
71
|
try {
|
|
57
72
|
const decoded = jsonwebtoken_1.default.verify(token, config_1.config.jwtSecret);
|
|
58
73
|
const subscription = await subscription_1.Subscription.findByPk(decoded.sid);
|
|
59
74
|
if (!subscription) {
|
|
60
|
-
|
|
75
|
+
requestLogger.warn('Subscription not found for JWT.', { sid: decoded.sid });
|
|
61
76
|
return res.status(403).json({ error: 'Invalid or expired token.' });
|
|
62
77
|
}
|
|
63
78
|
if (subscription.subscriptionStatus == 'expired') {
|
|
64
|
-
|
|
79
|
+
requestLogger.warn('Inactive subscription for JWT.', {
|
|
65
80
|
sid: decoded.sid,
|
|
66
81
|
status: subscription.subscriptionStatus,
|
|
67
82
|
});
|
|
@@ -71,19 +86,18 @@ async function authMiddleware(req, res, next) {
|
|
|
71
86
|
if (subscription.licenseKeyExpiresAt && subscription.licenseKeyExpiresAt <= now) {
|
|
72
87
|
subscription.subscriptionStatus = 'expired';
|
|
73
88
|
await subscription.save();
|
|
74
|
-
|
|
89
|
+
requestLogger.info('Subscription key has expired during activation.', {
|
|
75
90
|
subscriptionId: subscription.id,
|
|
76
91
|
});
|
|
77
92
|
return res
|
|
78
93
|
.status(403)
|
|
79
94
|
.json({ error: 'Subscription has expired.', subscriptionStatus: 'expired' });
|
|
80
95
|
}
|
|
81
|
-
res.locals.logger = logger_1.logger;
|
|
82
96
|
res.locals.subscription = subscription;
|
|
83
97
|
next();
|
|
84
98
|
}
|
|
85
99
|
catch (err) {
|
|
86
|
-
|
|
100
|
+
requestLogger.warn('Invalid or expired JWT on feature route.', { error: err });
|
|
87
101
|
return res.status(403).json({ error: 'Invalid or expired token.' });
|
|
88
102
|
}
|
|
89
103
|
}
|
|
@@ -3,9 +3,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getDownloadCounts = getDownloadCounts;
|
|
4
4
|
exports.incrementDownloadCount = incrementDownloadCount;
|
|
5
5
|
const storage_1 = require("@google-cloud/storage");
|
|
6
|
+
const zod_1 = require("zod");
|
|
6
7
|
const logger_1 = require("../logger");
|
|
7
8
|
const config_1 = require("../config");
|
|
8
9
|
const DEFAULT_COUNTS = { macos: 0, windows: 0 };
|
|
10
|
+
const downloadCountsSchema = zod_1.z.object({
|
|
11
|
+
macos: zod_1.z.number().nonnegative().optional(),
|
|
12
|
+
windows: zod_1.z.number().nonnegative().optional(),
|
|
13
|
+
});
|
|
14
|
+
function parseDownloadCounts(raw) {
|
|
15
|
+
const json = JSON.parse(raw);
|
|
16
|
+
const parsed = downloadCountsSchema.safeParse(json);
|
|
17
|
+
if (!parsed.success) {
|
|
18
|
+
return { ...DEFAULT_COUNTS };
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
macos: parsed.data.macos ?? 0,
|
|
22
|
+
windows: parsed.data.windows ?? 0,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
9
25
|
// Initialised once at module load — uses Application Default Credentials when
|
|
10
26
|
// running on Cloud Run (or any GCP environment), and falls back to ADC from
|
|
11
27
|
// the local environment during development.
|
|
@@ -30,28 +46,57 @@ async function readCounts(bucketName, objectPath) {
|
|
|
30
46
|
return { ...DEFAULT_COUNTS };
|
|
31
47
|
}
|
|
32
48
|
const [contents] = await file.download();
|
|
33
|
-
|
|
49
|
+
return parseDownloadCounts(contents.toString('utf8'));
|
|
50
|
+
}
|
|
51
|
+
async function readCountsWithGeneration(bucketName, objectPath) {
|
|
52
|
+
const file = storage.bucket(bucketName).file(objectPath);
|
|
53
|
+
const [exists] = await file.exists();
|
|
54
|
+
if (!exists) {
|
|
55
|
+
return { counts: { ...DEFAULT_COUNTS }, generation: null, exists: false };
|
|
56
|
+
}
|
|
57
|
+
const [[metadata], [contents]] = await Promise.all([file.getMetadata(), file.download()]);
|
|
58
|
+
const counts = parseDownloadCounts(contents.toString('utf8'));
|
|
34
59
|
return {
|
|
35
|
-
|
|
36
|
-
|
|
60
|
+
counts,
|
|
61
|
+
generation: metadata.generation ?? null,
|
|
62
|
+
exists: true,
|
|
37
63
|
};
|
|
38
64
|
}
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
});
|
|
65
|
+
function isGcsPreconditionError(err) {
|
|
66
|
+
const maybe = err;
|
|
67
|
+
return (maybe?.code === 412 ||
|
|
68
|
+
maybe?.message?.includes('conditionNotMet') === true ||
|
|
69
|
+
maybe?.message?.includes('Precondition Failed') === true);
|
|
45
70
|
}
|
|
46
71
|
async function incrementDownloadCount(platform) {
|
|
47
72
|
const gcs = getGcsConfig();
|
|
48
73
|
if (!gcs)
|
|
49
74
|
return;
|
|
75
|
+
const file = storage.bucket(gcs.bucketName).file(gcs.objectPath);
|
|
76
|
+
const MAX_RETRIES = 6;
|
|
50
77
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
78
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
79
|
+
const { counts, generation, exists } = await readCountsWithGeneration(gcs.bucketName, gcs.objectPath);
|
|
80
|
+
counts[platform] += 1;
|
|
81
|
+
try {
|
|
82
|
+
await file.save(JSON.stringify(counts), {
|
|
83
|
+
contentType: 'application/json',
|
|
84
|
+
resumable: false,
|
|
85
|
+
preconditionOpts: exists
|
|
86
|
+
? { ifGenerationMatch: Number(generation) }
|
|
87
|
+
: { ifGenerationMatch: 0 },
|
|
88
|
+
});
|
|
89
|
+
logger_1.logger.info(`Download count incremented for ${platform}.`, { counts, attempt });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (isGcsPreconditionError(err) && attempt < MAX_RETRIES) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
logger_1.logger.warn(`Download count increment exhausted retries for ${platform}.`);
|
|
55
100
|
}
|
|
56
101
|
catch (err) {
|
|
57
102
|
logger_1.logger.error(`Failed to increment download count for ${platform}.`, { error: err });
|
|
@@ -144,7 +144,7 @@ function runCronJob(job, subscription, sessionId) {
|
|
|
144
144
|
content: output,
|
|
145
145
|
is_terminal_output: true,
|
|
146
146
|
is_error: isError,
|
|
147
|
-
}, send, logger_1.logger, {
|
|
147
|
+
}, send, logger_1.logger, { isCronJob: true }).catch((err) => settle(err instanceof Error ? err : new Error(String(err))));
|
|
148
148
|
return;
|
|
149
149
|
}
|
|
150
150
|
if (FINAL_ANSWER_RE.test(content)) {
|
|
@@ -158,7 +158,7 @@ function runCronJob(job, subscription, sessionId) {
|
|
|
158
158
|
sender: 'user',
|
|
159
159
|
content: job.prompt,
|
|
160
160
|
platform: job.platform ?? undefined,
|
|
161
|
-
}, send, logger_1.logger, {
|
|
161
|
+
}, send, logger_1.logger, { isCronJob: true }).catch((err) => settle(err instanceof Error ? err : new Error(String(err))));
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
164
|
async function executeJob(job) {
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public",
|
|
5
5
|
"registry": "https://registry.npmjs.org/"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.0.
|
|
7
|
+
"version": "1.0.37",
|
|
8
8
|
"description": "CLI for onboarding users to Omnikey AI and configuring OPENAI_API_KEY. Use Yarn for install/build.",
|
|
9
9
|
"engines": {
|
|
10
10
|
"node": ">=14.0.0",
|