morpheus-cli 0.9.33 → 0.9.40
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/dist/channels/telegram.js +1 -1
- package/dist/cli/commands/start.js +7 -0
- package/dist/config/paths.js +1 -0
- package/dist/config/schemas.js +7 -0
- package/dist/http/api.js +5 -0
- package/dist/http/routers/model-presets.js +169 -0
- package/dist/http/routers/oauth.js +93 -0
- package/dist/http/server.js +4 -0
- package/dist/runtime/memory/sqlite.js +96 -19
- package/dist/runtime/oauth/manager.js +218 -0
- package/dist/runtime/oauth/provider.js +90 -0
- package/dist/runtime/oauth/store.js +118 -0
- package/dist/runtime/oauth/types.js +1 -0
- package/dist/runtime/providers/factory.js +93 -1
- package/dist/runtime/tools/cache.js +66 -47
- package/dist/ui/assets/{AuditDashboard-rap15I-4.js → AuditDashboard-EvtKjy5H.js} +1 -1
- package/dist/ui/assets/{Chat-CnuRZFBT.js → Chat-yptierPt.js} +3 -3
- package/dist/ui/assets/{Chronos-C81_HP6e.js → Chronos-BA77MYbp.js} +1 -1
- package/dist/ui/assets/{ConfirmationModal-CT_v8cAi.js → ConfirmationModal-NOZr-ipQ.js} +1 -1
- package/dist/ui/assets/{Dashboard-0VfNJ9BZ.js → Dashboard-ly1MJiB4.js} +1 -1
- package/dist/ui/assets/{DeleteConfirmationModal-P2foiqon.js → DeleteConfirmationModal-2HMraacH.js} +1 -1
- package/dist/ui/assets/{Documents-C8UfbcGD.js → Documents-C31fAm0Z.js} +2 -2
- package/dist/ui/assets/{Logs-qdsCW9u9.js → Logs-BiajoLAB.js} +1 -1
- package/dist/ui/assets/{MCPManager-CaZLnrKz.js → MCPManager-DS9jfiZT.js} +1 -1
- package/dist/ui/assets/ModelPresets-CxhKcalw.js +1 -0
- package/dist/ui/assets/{ModelPricing-C73OfGhc.js → ModelPricing-CN8flHnP.js} +1 -1
- package/dist/ui/assets/{Notifications-CwqeagwF.js → Notifications-BfP1_CM3.js} +1 -1
- package/dist/ui/assets/{Pagination-3P6KG-u6.js → Pagination-Doam4_qd.js} +1 -1
- package/dist/ui/assets/SatiMemories-Bk4_ubo7.js +1 -0
- package/dist/ui/assets/{SessionAudit-Cykp4Sv_.js → SessionAudit-D3E6QSQw.js} +2 -2
- package/dist/ui/assets/Settings-3VBK8muv.js +49 -0
- package/dist/ui/assets/{Skills-B6io4GZh.js → Skills-Dp0_GwiW.js} +1 -1
- package/dist/ui/assets/{Smiths-XoDzX1K0.js → Smiths-COTgI2R4.js} +1 -1
- package/dist/ui/assets/{Tasks-vui0C_76.js → Tasks-COe4lIJ7.js} +1 -1
- package/dist/ui/assets/{TrinityDatabases-Dp71dyUn.js → TrinityDatabases-BEU4mmyW.js} +1 -1
- package/dist/ui/assets/{UsageStats-Dz4LXfr4.js → UsageStats-BTmDeG2V.js} +1 -1
- package/dist/ui/assets/{WebhookManager-CC4Mbo2v.js → WebhookManager-FQVyKyW-.js} +2 -2
- package/dist/ui/assets/{agents-DV1oGA7P.js → agents-B6e9N0QI.js} +1 -1
- package/dist/ui/assets/{audit-DnegNntR.js → audit-giQG2WRk.js} +1 -1
- package/dist/ui/assets/{chronos-BDlP8kzg.js → chronos-sweaRcNj.js} +1 -1
- package/dist/ui/assets/{config-BhjCL4aM.js → config-CbUdj76n.js} +1 -1
- package/dist/ui/assets/index-CRPT77Yo.css +1 -0
- package/dist/ui/assets/{index-C3qfojVn.js → index-yu2c4ry1.js} +7 -7
- package/dist/ui/assets/{mcp-uYhIyjyx.js → mcp-v64BBpUk.js} +1 -1
- package/dist/ui/assets/modelPresets-BaNh-gxn.js +1 -0
- package/dist/ui/assets/{skills-_9hplz7d.js → skills-ClRXBlVt.js} +1 -1
- package/dist/ui/assets/{stats-BwaWi9yN.js → stats-nI-89hEX.js} +1 -1
- package/dist/ui/assets/{useCurrency-RBarItCC.js → useCurrency-D5An8I2f.js} +1 -1
- package/dist/ui/assets/vendor-icons-LSkmAkBj.js +1 -0
- package/dist/ui/index.html +3 -3
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/SatiMemories-CVhOdyAk.js +0 -1
- package/dist/ui/assets/Settings-DnyG6tDx.js +0 -49
- package/dist/ui/assets/index-gx__iEcl.css +0 -1
- package/dist/ui/assets/vendor-icons-tocJCdt5.js +0 -1
|
@@ -1408,7 +1408,7 @@ export class TelegramAdapter {
|
|
|
1408
1408
|
}
|
|
1409
1409
|
async handleStartCommand(ctx, user) {
|
|
1410
1410
|
const welcomeMessage = `
|
|
1411
|
-
Hello, @${user}! I am ${this.config.get().agent.name},
|
|
1411
|
+
Hello, @${user}! I am ${this.config.get().agent.name},
|
|
1412
1412
|
|
|
1413
1413
|
I am your local AI operator/agent. Here are the commands you can use:
|
|
1414
1414
|
|
|
@@ -23,6 +23,7 @@ import { TaskWorker } from '../../runtime/tasks/worker.js';
|
|
|
23
23
|
import { TaskNotifier } from '../../runtime/tasks/notifier.js';
|
|
24
24
|
import { ChronosWorker } from '../../runtime/chronos/worker.js';
|
|
25
25
|
import { ChronosRepository } from '../../runtime/chronos/repository.js';
|
|
26
|
+
import { OAuthManager } from '../../runtime/oauth/manager.js';
|
|
26
27
|
import { SkillRegistry } from '../../runtime/skills/index.js';
|
|
27
28
|
import { MCPToolCache } from '../../runtime/tools/cache.js';
|
|
28
29
|
import { SmithRegistry } from '../../runtime/smiths/registry.js';
|
|
@@ -244,6 +245,12 @@ export const startCommand = new Command('start')
|
|
|
244
245
|
// Use CLI port if provided and valid, otherwise fallback to config or default
|
|
245
246
|
const port = parseInt(options.port) || config.ui.port || 3333;
|
|
246
247
|
httpServer.start(port);
|
|
248
|
+
// Initialize OAuth manager with the HTTP port (needed for redirect URI)
|
|
249
|
+
const oauthManager = OAuthManager.getInstance(port);
|
|
250
|
+
oauthManager.setNotifyFn(async (serverName, url) => {
|
|
251
|
+
const msg = `🔐 MCP *${serverName}* requires OAuth authorization.\n\nClick to authenticate:\n${url.toString()}`;
|
|
252
|
+
ChannelRegistry.broadcast(msg);
|
|
253
|
+
});
|
|
247
254
|
}
|
|
248
255
|
catch (e) {
|
|
249
256
|
display.log(chalk.red(`Failed to start Web UI: ${e.message}`), { source: 'Zaion' });
|
package/dist/config/paths.js
CHANGED
|
@@ -22,4 +22,5 @@ export const PATHS = {
|
|
|
22
22
|
trinityDb: path.join(MORPHEUS_ROOT, 'memory', 'trinity.db'),
|
|
23
23
|
satiDb: path.join(MORPHEUS_ROOT, 'memory', 'sati-memory.db'),
|
|
24
24
|
linkDb: path.join(MORPHEUS_ROOT, 'memory', 'link.db'),
|
|
25
|
+
oauthTokens: path.join(MORPHEUS_ROOT, 'oauth-tokens.json'),
|
|
25
26
|
};
|
package/dist/config/schemas.js
CHANGED
|
@@ -154,6 +154,12 @@ export const ConfigSchema = z.object({
|
|
|
154
154
|
retention: z.string().default(DEFAULT_CONFIG.logging.retention),
|
|
155
155
|
}).default(DEFAULT_CONFIG.logging),
|
|
156
156
|
});
|
|
157
|
+
export const OAuth2ConfigSchema = z.object({
|
|
158
|
+
grant_type: z.enum(['client_credentials', 'authorization_code']).default('authorization_code'),
|
|
159
|
+
client_id: z.string().optional(),
|
|
160
|
+
client_secret: z.string().optional(),
|
|
161
|
+
scope: z.string().optional(),
|
|
162
|
+
});
|
|
157
163
|
export const MCPServerConfigSchema = z.discriminatedUnion('transport', [
|
|
158
164
|
z.object({
|
|
159
165
|
transport: z.literal('stdio'),
|
|
@@ -166,6 +172,7 @@ export const MCPServerConfigSchema = z.discriminatedUnion('transport', [
|
|
|
166
172
|
transport: z.literal('http'),
|
|
167
173
|
url: z.string().url('Valid URL is required for http transport'),
|
|
168
174
|
headers: z.record(z.string(), z.string()).optional().default({}),
|
|
175
|
+
oauth2: OAuth2ConfigSchema.optional(),
|
|
169
176
|
args: z.array(z.string()).optional().default([]),
|
|
170
177
|
env: z.record(z.string(), z.string()).optional().default({}),
|
|
171
178
|
_comment: z.string().optional(),
|
package/dist/http/api.js
CHANGED
|
@@ -24,6 +24,7 @@ import { createDangerRouter } from './routers/danger.js';
|
|
|
24
24
|
import { createLinkRouter } from './routers/link.js';
|
|
25
25
|
import { createAgentsRouter } from './routers/agents.js';
|
|
26
26
|
import { createDisplayRouter } from './routers/display.js';
|
|
27
|
+
import { createModelPresetsRouter } from './routers/model-presets.js';
|
|
27
28
|
import { getActiveEnvOverrides } from '../config/precedence.js';
|
|
28
29
|
import { hotReloadConfig, getRestartRequiredChanges } from '../runtime/hot-reload.js';
|
|
29
30
|
import { AuditRepository } from '../runtime/audit/repository.js';
|
|
@@ -61,6 +62,10 @@ export function createApiRouter(oracle, chronosWorker) {
|
|
|
61
62
|
router.use('/agents', createAgentsRouter());
|
|
62
63
|
// Mount Display Stream router
|
|
63
64
|
router.use('/display', createDisplayRouter());
|
|
65
|
+
// Mount Model Presets router
|
|
66
|
+
router.use('/model-presets', createModelPresetsRouter());
|
|
67
|
+
// NOTE: OAuth router is mounted in server.ts BEFORE auth middleware
|
|
68
|
+
// so the callback endpoint is publicly accessible (browser redirect).
|
|
64
69
|
// --- Session Management ---
|
|
65
70
|
router.get('/sessions', async (req, res) => {
|
|
66
71
|
try {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
import { SQLiteChatMessageHistory } from '../../runtime/memory/sqlite.js';
|
|
5
|
+
import { encrypt, safeDecrypt, looksLikeEncrypted, canEncrypt } from '../../runtime/trinity-crypto.js';
|
|
6
|
+
const PresetBodySchema = z.object({
|
|
7
|
+
name: z.string().min(1).max(100).trim(),
|
|
8
|
+
provider: z.string().min(1),
|
|
9
|
+
model: z.string().min(1),
|
|
10
|
+
api_key: z.string().optional().nullable(),
|
|
11
|
+
base_url: z.string().optional().nullable(),
|
|
12
|
+
temperature: z.number().min(0).max(2).optional().nullable(),
|
|
13
|
+
max_tokens: z.number().int().positive().optional().nullable(),
|
|
14
|
+
});
|
|
15
|
+
function isNameConflict(err) {
|
|
16
|
+
const msg = String(err?.message ?? '');
|
|
17
|
+
return msg.includes('UNIQUE constraint failed: model_presets.name');
|
|
18
|
+
}
|
|
19
|
+
export function createModelPresetsRouter() {
|
|
20
|
+
const router = Router();
|
|
21
|
+
// GET /api/model-presets — list all (api_key masked)
|
|
22
|
+
router.get('/', (req, res) => {
|
|
23
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'presets-api' });
|
|
24
|
+
try {
|
|
25
|
+
const rows = h.listModelPresets();
|
|
26
|
+
const result = rows.map(({ api_key, ...rest }) => ({
|
|
27
|
+
...rest,
|
|
28
|
+
has_api_key: !!api_key,
|
|
29
|
+
}));
|
|
30
|
+
res.json(result);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
res.status(500).json({ error: err.message });
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
h.close();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
// GET /api/model-presets/:id/decrypt — returns decrypted api_key (must be before /:id)
|
|
40
|
+
router.get('/:id/decrypt', (req, res) => {
|
|
41
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'presets-api' });
|
|
42
|
+
try {
|
|
43
|
+
const preset = h.getModelPreset(req.params.id);
|
|
44
|
+
if (!preset)
|
|
45
|
+
return res.status(404).json({ error: 'Preset not found.' });
|
|
46
|
+
if (!preset.api_key)
|
|
47
|
+
return res.json({ api_key: null });
|
|
48
|
+
if (looksLikeEncrypted(preset.api_key)) {
|
|
49
|
+
if (!canEncrypt()) {
|
|
50
|
+
return res.json({ api_key: null, error: 'MORPHEUS_SECRET is not set — cannot decrypt.' });
|
|
51
|
+
}
|
|
52
|
+
return res.json({ api_key: safeDecrypt(preset.api_key) });
|
|
53
|
+
}
|
|
54
|
+
// Plaintext stored (MORPHEUS_SECRET was not set when saved)
|
|
55
|
+
res.json({ api_key: preset.api_key });
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
res.status(500).json({ error: err.message });
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
h.close();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// GET /api/model-presets/:id — single preset (api_key masked)
|
|
65
|
+
router.get('/:id', (req, res) => {
|
|
66
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'presets-api' });
|
|
67
|
+
try {
|
|
68
|
+
const preset = h.getModelPreset(req.params.id);
|
|
69
|
+
if (!preset)
|
|
70
|
+
return res.status(404).json({ error: 'Preset not found.' });
|
|
71
|
+
const { api_key, ...rest } = preset;
|
|
72
|
+
res.json({ ...rest, has_api_key: !!api_key });
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
res.status(500).json({ error: err.message });
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
h.close();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
// POST /api/model-presets — create
|
|
82
|
+
router.post('/', (req, res) => {
|
|
83
|
+
const parsed = PresetBodySchema.safeParse(req.body);
|
|
84
|
+
if (!parsed.success)
|
|
85
|
+
return res.status(400).json({ error: parsed.error.issues });
|
|
86
|
+
const { name, provider, model, api_key, base_url, temperature, max_tokens } = parsed.data;
|
|
87
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'presets-api' });
|
|
88
|
+
try {
|
|
89
|
+
const now = new Date().toISOString();
|
|
90
|
+
let storedKey = null;
|
|
91
|
+
if (api_key) {
|
|
92
|
+
storedKey = canEncrypt() ? encrypt(api_key) : api_key;
|
|
93
|
+
}
|
|
94
|
+
const id = randomUUID();
|
|
95
|
+
h.upsertModelPreset({ id, name, provider, model, api_key: storedKey, base_url: base_url ?? null, temperature: temperature ?? null, max_tokens: max_tokens ?? null, created_at: now, updated_at: now });
|
|
96
|
+
res.status(201).json({ id, success: true });
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
if (isNameConflict(err))
|
|
100
|
+
return res.status(409).json({ error: 'A preset with that name already exists.' });
|
|
101
|
+
res.status(500).json({ error: err.message });
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
h.close();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
// PUT /api/model-presets/:id — update
|
|
108
|
+
router.put('/:id', (req, res) => {
|
|
109
|
+
const parsed = PresetBodySchema.safeParse(req.body);
|
|
110
|
+
if (!parsed.success)
|
|
111
|
+
return res.status(400).json({ error: parsed.error.issues });
|
|
112
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'presets-api' });
|
|
113
|
+
try {
|
|
114
|
+
const existing = h.getModelPreset(req.params.id);
|
|
115
|
+
if (!existing)
|
|
116
|
+
return res.status(404).json({ error: 'Preset not found.' });
|
|
117
|
+
const { name, provider, model, api_key, base_url, temperature, max_tokens } = parsed.data;
|
|
118
|
+
// api_key update logic:
|
|
119
|
+
// - absent from body (undefined): keep existing
|
|
120
|
+
// - null or "": clear
|
|
121
|
+
// - non-empty string: encrypt and replace
|
|
122
|
+
let storedKey = existing.api_key ?? null;
|
|
123
|
+
if ('api_key' in req.body) {
|
|
124
|
+
if (!api_key) {
|
|
125
|
+
storedKey = null; // clear
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
storedKey = canEncrypt() ? encrypt(api_key) : api_key;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
h.upsertModelPreset({
|
|
132
|
+
id: req.params.id,
|
|
133
|
+
name, provider, model,
|
|
134
|
+
api_key: storedKey,
|
|
135
|
+
base_url: base_url ?? null,
|
|
136
|
+
temperature: temperature ?? null,
|
|
137
|
+
max_tokens: max_tokens ?? null,
|
|
138
|
+
created_at: existing.created_at,
|
|
139
|
+
updated_at: new Date().toISOString(),
|
|
140
|
+
});
|
|
141
|
+
res.json({ success: true });
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
if (isNameConflict(err))
|
|
145
|
+
return res.status(409).json({ error: 'A preset with that name already exists.' });
|
|
146
|
+
res.status(500).json({ error: err.message });
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
h.close();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
// DELETE /api/model-presets/:id
|
|
153
|
+
router.delete('/:id', (req, res) => {
|
|
154
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'presets-api' });
|
|
155
|
+
try {
|
|
156
|
+
const changes = h.deleteModelPreset(req.params.id);
|
|
157
|
+
if (changes === 0)
|
|
158
|
+
return res.status(404).json({ error: 'Preset not found.' });
|
|
159
|
+
res.json({ success: true });
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
res.status(500).json({ error: err.message });
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
h.close();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
return router;
|
|
169
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { OAuthManager } from '../../runtime/oauth/manager.js';
|
|
3
|
+
import { MCPToolCache } from '../../runtime/tools/cache.js';
|
|
4
|
+
import { DisplayManager } from '../../runtime/display.js';
|
|
5
|
+
const display = DisplayManager.getInstance();
|
|
6
|
+
export function createOAuthRouter() {
|
|
7
|
+
const router = Router();
|
|
8
|
+
/**
|
|
9
|
+
* GET /api/oauth/callback?code=...&state=...
|
|
10
|
+
* Receives the OAuth redirect after user authorizes in their browser.
|
|
11
|
+
*/
|
|
12
|
+
router.get('/callback', async (req, res) => {
|
|
13
|
+
const { code, state } = req.query;
|
|
14
|
+
if (!code || typeof code !== 'string') {
|
|
15
|
+
res.status(400).send(renderHtml('Authorization Failed', 'Missing authorization code. Please try again.', false));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const oauthManager = OAuthManager.getInstance();
|
|
20
|
+
const result = await oauthManager.finishAuth(code, state);
|
|
21
|
+
display.log(`OAuth callback: '${result.serverName}' authorized (${result.toolCount} tools)`, {
|
|
22
|
+
level: 'info',
|
|
23
|
+
source: 'OAuth',
|
|
24
|
+
});
|
|
25
|
+
// Trigger MCP tool reload in background
|
|
26
|
+
MCPToolCache.getInstance().reload().catch(err => {
|
|
27
|
+
display.log(`Failed to reload MCP tools after OAuth: ${err}`, {
|
|
28
|
+
level: 'warning',
|
|
29
|
+
source: 'OAuth',
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
res.send(renderHtml('Authorization Successful', `MCP server <strong>${result.serverName}</strong> has been authorized. ` +
|
|
33
|
+
`${result.toolCount} tools are now available. You can close this window.`, true));
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
display.log(`OAuth callback failed: ${error.message}`, {
|
|
37
|
+
level: 'warning',
|
|
38
|
+
source: 'OAuth',
|
|
39
|
+
});
|
|
40
|
+
res.status(500).send(renderHtml('Authorization Failed', `Error: ${error.message}`, false));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* GET /api/oauth/status
|
|
45
|
+
* Returns OAuth status for all MCP servers with OAuth data.
|
|
46
|
+
*/
|
|
47
|
+
router.get('/status', async (_req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const oauthManager = OAuthManager.getInstance();
|
|
50
|
+
const statuses = oauthManager.getStatus();
|
|
51
|
+
res.json({ servers: statuses });
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
res.status(500).json({ error: error.message });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
/**
|
|
58
|
+
* DELETE /api/oauth/tokens/:name
|
|
59
|
+
* Revoke and remove stored OAuth token for an MCP server.
|
|
60
|
+
*/
|
|
61
|
+
router.delete('/tokens/:name', async (req, res) => {
|
|
62
|
+
try {
|
|
63
|
+
const oauthManager = OAuthManager.getInstance();
|
|
64
|
+
oauthManager.revokeToken(req.params.name);
|
|
65
|
+
res.json({ ok: true, message: `Token revoked for '${req.params.name}'` });
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
res.status(500).json({ error: error.message });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return router;
|
|
72
|
+
}
|
|
73
|
+
function renderHtml(title, message, success) {
|
|
74
|
+
const color = success ? '#22c55e' : '#ef4444';
|
|
75
|
+
const icon = success ? '✓' : '✗';
|
|
76
|
+
return `<!DOCTYPE html>
|
|
77
|
+
<html><head><title>Morpheus OAuth - ${title}</title>
|
|
78
|
+
<style>
|
|
79
|
+
body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center;
|
|
80
|
+
min-height: 100vh; margin: 0; background: #0a0a0a; color: #e0e0e0; }
|
|
81
|
+
.card { text-align: center; padding: 2rem; border: 1px solid ${color}; border-radius: 12px;
|
|
82
|
+
max-width: 400px; background: #111; }
|
|
83
|
+
.icon { font-size: 3rem; color: ${color}; margin-bottom: 1rem; }
|
|
84
|
+
h1 { font-size: 1.25rem; margin: 0 0 1rem; }
|
|
85
|
+
p { color: #999; line-height: 1.5; margin: 0; }
|
|
86
|
+
strong { color: #e0e0e0; }
|
|
87
|
+
</style></head>
|
|
88
|
+
<body><div class="card">
|
|
89
|
+
<div class="icon">${icon}</div>
|
|
90
|
+
<h1>${title}</h1>
|
|
91
|
+
<p>${message}</p>
|
|
92
|
+
</div></body></html>`;
|
|
93
|
+
}
|
package/dist/http/server.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createApiRouter } from './api.js';
|
|
|
9
9
|
import { createWebhooksRouter } from './webhooks-router.js';
|
|
10
10
|
import { authMiddleware } from './middleware/auth.js';
|
|
11
11
|
import { WebhookDispatcher } from '../runtime/webhooks/dispatcher.js';
|
|
12
|
+
import { createOAuthRouter } from './routers/oauth.js';
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
14
|
const __dirname = path.dirname(__filename);
|
|
14
15
|
export class HttpServer {
|
|
@@ -55,6 +56,9 @@ export class HttpServer {
|
|
|
55
56
|
// The trigger endpoint is public (validated via x-api-key header internally).
|
|
56
57
|
// All other webhook management endpoints apply authMiddleware internally.
|
|
57
58
|
this.app.use('/api/webhooks', createWebhooksRouter());
|
|
59
|
+
// OAuth callback — public (browser redirect from OAuth provider, no API key).
|
|
60
|
+
// Status/revoke endpoints remain auth-guarded inside the /api block.
|
|
61
|
+
this.app.use('/api/oauth', createOAuthRouter());
|
|
58
62
|
this.app.use('/api', authMiddleware, createApiRouter(this.oracle, this.chronosWorker));
|
|
59
63
|
// Serve static frontend from compiled output
|
|
60
64
|
const uiPath = path.resolve(__dirname, '../ui');
|
|
@@ -207,6 +207,19 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
207
207
|
('google', 'gemini-1.5-pro', 1.25, 5.0),
|
|
208
208
|
('google', 'gemini-1.5-flash', 0.075, 0.3);
|
|
209
209
|
|
|
210
|
+
CREATE TABLE IF NOT EXISTS model_presets (
|
|
211
|
+
id TEXT PRIMARY KEY,
|
|
212
|
+
name TEXT UNIQUE NOT NULL,
|
|
213
|
+
provider TEXT NOT NULL,
|
|
214
|
+
model TEXT NOT NULL,
|
|
215
|
+
api_key TEXT,
|
|
216
|
+
base_url TEXT,
|
|
217
|
+
temperature REAL,
|
|
218
|
+
max_tokens INTEGER,
|
|
219
|
+
created_at TEXT NOT NULL,
|
|
220
|
+
updated_at TEXT NOT NULL
|
|
221
|
+
);
|
|
222
|
+
|
|
210
223
|
`);
|
|
211
224
|
this.migrateTable();
|
|
212
225
|
}
|
|
@@ -268,52 +281,90 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
268
281
|
catch (error) {
|
|
269
282
|
console.warn(`[SQLite] model_pricing migration failed: ${error}`);
|
|
270
283
|
}
|
|
284
|
+
// Ensure model_presets table exists for databases created before this feature
|
|
285
|
+
try {
|
|
286
|
+
this.db.exec(`
|
|
287
|
+
CREATE TABLE IF NOT EXISTS model_presets (
|
|
288
|
+
id TEXT PRIMARY KEY,
|
|
289
|
+
name TEXT UNIQUE NOT NULL,
|
|
290
|
+
provider TEXT NOT NULL,
|
|
291
|
+
model TEXT NOT NULL,
|
|
292
|
+
api_key TEXT,
|
|
293
|
+
base_url TEXT,
|
|
294
|
+
temperature REAL,
|
|
295
|
+
max_tokens INTEGER,
|
|
296
|
+
created_at TEXT NOT NULL,
|
|
297
|
+
updated_at TEXT NOT NULL
|
|
298
|
+
)
|
|
299
|
+
`);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
console.warn(`[SQLite] model_presets migration failed: ${error}`);
|
|
303
|
+
}
|
|
271
304
|
}
|
|
272
305
|
/**
|
|
273
306
|
* Removes orphaned ToolMessages and incomplete tool-call groups that can
|
|
274
|
-
* appear when the LIMIT clause truncates the message window mid-sequence
|
|
307
|
+
* appear when the LIMIT clause truncates the message window mid-sequence,
|
|
308
|
+
* or when a previous session ended mid-execution leaving dangling tool calls.
|
|
275
309
|
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
310
|
+
* Handles both ends of the window:
|
|
311
|
+
* START — orphaned ToolMessages / AIMessages whose tool responses were cut off
|
|
312
|
+
* END — AIMessages with unanswered tool_calls (session ended mid-execution),
|
|
313
|
+
* which would cause Gemini to reject the next human turn
|
|
280
314
|
*/
|
|
281
315
|
sanitizeMessageWindow(messages) {
|
|
282
316
|
if (messages.length === 0)
|
|
283
317
|
return messages;
|
|
284
318
|
// Work in chronological order (reverse of DESC) for easier reasoning.
|
|
285
319
|
const chrono = [...messages].reverse();
|
|
286
|
-
//
|
|
320
|
+
// ── START sanitization ─────────────────────────────────────────────────
|
|
321
|
+
// Drop leading ToolMessages that have no preceding AIMessage with tool_calls.
|
|
287
322
|
let startIdx = 0;
|
|
288
323
|
while (startIdx < chrono.length && chrono[startIdx] instanceof ToolMessage) {
|
|
289
324
|
startIdx++;
|
|
290
325
|
}
|
|
291
|
-
//
|
|
326
|
+
// Drop a leading AIMessage that has tool_calls but whose ToolMessage
|
|
292
327
|
// responses were trimmed (they would have been before it in the DB).
|
|
293
328
|
if (startIdx < chrono.length && chrono[startIdx] instanceof AIMessage) {
|
|
294
329
|
const ai = chrono[startIdx];
|
|
295
330
|
if (ai.tool_calls && ai.tool_calls.length > 0) {
|
|
296
|
-
// Check if ALL tool_call responses exist after this AIMessage
|
|
297
331
|
const toolCallIds = ai.tool_calls.map((tc) => tc.id).filter(Boolean);
|
|
298
332
|
const remaining = chrono.slice(startIdx + 1);
|
|
299
|
-
|
|
300
|
-
for (let i = 0; i < toolCallIds.length; i++) {
|
|
301
|
-
const hasResponse = remaining.some((m) => m instanceof ToolMessage && m.tool_call_id === toolCallIds[i]);
|
|
302
|
-
if (!hasResponse) {
|
|
303
|
-
allFound = false;
|
|
304
|
-
break;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
333
|
+
const allFound = toolCallIds.every((id) => remaining.some((m) => m instanceof ToolMessage && m.tool_call_id === id));
|
|
307
334
|
if (!allFound)
|
|
308
335
|
startIdx++;
|
|
309
336
|
}
|
|
310
337
|
}
|
|
311
|
-
|
|
338
|
+
// ── END sanitization ───────────────────────────────────────────────────
|
|
339
|
+
// Walk backwards from the end and strip any trailing AIMessage that has
|
|
340
|
+
// unanswered tool_calls (session ended mid-execution). Gemini requires that
|
|
341
|
+
// a function_call turn is ALWAYS immediately followed by a function_response
|
|
342
|
+
// turn — a human turn after a dangling tool call causes a 400 error.
|
|
343
|
+
let endIdx = chrono.length;
|
|
344
|
+
while (endIdx > startIdx) {
|
|
345
|
+
const last = chrono[endIdx - 1];
|
|
346
|
+
if (!(last instanceof AIMessage))
|
|
347
|
+
break;
|
|
348
|
+
const ai = last;
|
|
349
|
+
if (!ai.tool_calls || ai.tool_calls.length === 0)
|
|
350
|
+
break;
|
|
351
|
+
// This AIMessage has tool_calls — check if all responses exist before it
|
|
352
|
+
const toolCallIds = ai.tool_calls.map((tc) => tc.id).filter(Boolean);
|
|
353
|
+
const before = chrono.slice(startIdx, endIdx - 1);
|
|
354
|
+
const allAnswered = toolCallIds.every((id) => before.some((m) => m instanceof ToolMessage && m.tool_call_id === id));
|
|
355
|
+
if (allAnswered)
|
|
356
|
+
break; // complete group — keep it
|
|
357
|
+
// Incomplete: strip this AIMessage and any trailing ToolMessages after it
|
|
358
|
+
endIdx--;
|
|
359
|
+
while (endIdx > startIdx && chrono[endIdx - 1] instanceof ToolMessage) {
|
|
360
|
+
endIdx--;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (startIdx === 0 && endIdx === chrono.length) {
|
|
312
364
|
// No sanitization needed — return original DESC order.
|
|
313
365
|
return messages;
|
|
314
366
|
}
|
|
315
|
-
|
|
316
|
-
const sanitized = chrono.slice(startIdx);
|
|
367
|
+
const sanitized = chrono.slice(startIdx, endIdx);
|
|
317
368
|
sanitized.reverse();
|
|
318
369
|
return sanitized;
|
|
319
370
|
}
|
|
@@ -800,6 +851,32 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
800
851
|
const result = this.db.prepare('DELETE FROM model_pricing WHERE provider = ? AND model = ?').run(provider, model);
|
|
801
852
|
return result.changes;
|
|
802
853
|
}
|
|
854
|
+
// --- Model Presets CRUD ---
|
|
855
|
+
listModelPresets() {
|
|
856
|
+
return this.db.prepare('SELECT id, name, provider, model, api_key, base_url, temperature, max_tokens, created_at, updated_at FROM model_presets ORDER BY name').all();
|
|
857
|
+
}
|
|
858
|
+
getModelPreset(id) {
|
|
859
|
+
return this.db.prepare('SELECT id, name, provider, model, api_key, base_url, temperature, max_tokens, created_at, updated_at FROM model_presets WHERE id = ?').get(id);
|
|
860
|
+
}
|
|
861
|
+
upsertModelPreset(entry) {
|
|
862
|
+
const now = new Date().toISOString();
|
|
863
|
+
this.db.prepare(`
|
|
864
|
+
INSERT INTO model_presets (id, name, provider, model, api_key, base_url, temperature, max_tokens, created_at, updated_at)
|
|
865
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
866
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
867
|
+
name = excluded.name,
|
|
868
|
+
provider = excluded.provider,
|
|
869
|
+
model = excluded.model,
|
|
870
|
+
api_key = excluded.api_key,
|
|
871
|
+
base_url = excluded.base_url,
|
|
872
|
+
temperature = excluded.temperature,
|
|
873
|
+
max_tokens = excluded.max_tokens,
|
|
874
|
+
updated_at = excluded.updated_at
|
|
875
|
+
`).run(entry.id, entry.name, entry.provider, entry.model, entry.api_key ?? null, entry.base_url ?? null, entry.temperature ?? null, entry.max_tokens ?? null, entry.created_at ?? now, now);
|
|
876
|
+
}
|
|
877
|
+
deleteModelPreset(id) {
|
|
878
|
+
return this.db.prepare('DELETE FROM model_presets WHERE id = ?').run(id).changes;
|
|
879
|
+
}
|
|
803
880
|
/**
|
|
804
881
|
* Clears all messages for the current session from the database.
|
|
805
882
|
*/
|