morpheus-cli 0.4.0 → 0.4.2
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 +88 -0
- package/dist/config/manager.js +32 -0
- package/dist/config/schemas.js +5 -0
- package/dist/devkit/adapters/shell.js +80 -0
- package/dist/devkit/index.js +10 -0
- package/dist/devkit/registry.js +12 -0
- package/dist/devkit/tools/filesystem.js +219 -0
- package/dist/devkit/tools/git.js +210 -0
- package/dist/devkit/tools/network.js +158 -0
- package/dist/devkit/tools/packages.js +73 -0
- package/dist/devkit/tools/processes.js +130 -0
- package/dist/devkit/tools/shell.js +94 -0
- package/dist/devkit/tools/system.js +132 -0
- package/dist/devkit/types.js +1 -0
- package/dist/devkit/utils.js +45 -0
- package/dist/http/api.js +122 -0
- package/dist/runtime/apoc.js +110 -0
- package/dist/runtime/memory/sati/index.js +2 -2
- package/dist/runtime/memory/sati/service.js +3 -2
- package/dist/runtime/memory/sqlite.js +98 -28
- package/dist/runtime/oracle.js +4 -1
- package/dist/runtime/providers/factory.js +85 -80
- package/dist/runtime/telephonist.js +19 -1
- package/dist/runtime/tools/apoc-tool.js +43 -0
- package/dist/runtime/tools/index.js +1 -0
- package/dist/types/config.js +6 -0
- package/dist/ui/assets/index-CjlkpcsE.js +109 -0
- package/dist/ui/assets/index-LrqT6MpO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/index-CwvCMGLo.css +0 -1
- package/dist/ui/assets/index-D9REy_tK.js +0 -109
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { tool } from '@langchain/core/tools';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { ShellAdapter } from '../adapters/shell.js';
|
|
4
|
+
import { registerToolFactory } from '../registry.js';
|
|
5
|
+
import { platform } from 'os';
|
|
6
|
+
export function createSystemTools(ctx) {
|
|
7
|
+
const shell = ShellAdapter.create();
|
|
8
|
+
const isWindows = platform() === 'win32';
|
|
9
|
+
const isMac = platform() === 'darwin';
|
|
10
|
+
return [
|
|
11
|
+
tool(async ({ title, message, urgency }) => {
|
|
12
|
+
try {
|
|
13
|
+
if (isWindows) {
|
|
14
|
+
// PowerShell toast notification
|
|
15
|
+
const ps = `[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime] | Out-Null; $template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02); $template.GetElementsByTagName('text')[0].AppendChild($template.CreateTextNode('${title}')); $template.GetElementsByTagName('text')[1].AppendChild($template.CreateTextNode('${message}')); [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Morpheus').Show([Windows.UI.Notifications.ToastNotification]::new($template))`;
|
|
16
|
+
await shell.run('powershell', ['-Command', ps], { cwd: ctx.working_dir, timeout_ms: 5_000 });
|
|
17
|
+
}
|
|
18
|
+
else if (isMac) {
|
|
19
|
+
await shell.run('osascript', ['-e', `display notification "${message}" with title "${title}"`], {
|
|
20
|
+
cwd: ctx.working_dir, timeout_ms: 5_000,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
// Linux — notify-send
|
|
25
|
+
const args = [title, message];
|
|
26
|
+
if (urgency)
|
|
27
|
+
args.unshift(`-u`, urgency);
|
|
28
|
+
await shell.run('notify-send', args, { cwd: ctx.working_dir, timeout_ms: 5_000 });
|
|
29
|
+
}
|
|
30
|
+
return JSON.stringify({ success: true });
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
return JSON.stringify({ success: false, error: err.message });
|
|
34
|
+
}
|
|
35
|
+
}, {
|
|
36
|
+
name: 'notify',
|
|
37
|
+
description: 'Send a desktop notification.',
|
|
38
|
+
schema: z.object({
|
|
39
|
+
title: z.string(),
|
|
40
|
+
message: z.string(),
|
|
41
|
+
urgency: z.enum(['low', 'normal', 'critical']).optional().describe('Linux urgency level'),
|
|
42
|
+
}),
|
|
43
|
+
}),
|
|
44
|
+
tool(async () => {
|
|
45
|
+
try {
|
|
46
|
+
let result;
|
|
47
|
+
if (isWindows) {
|
|
48
|
+
result = await shell.run('powershell', ['-Command', 'Get-Clipboard'], {
|
|
49
|
+
cwd: ctx.working_dir, timeout_ms: 5_000,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else if (isMac) {
|
|
53
|
+
result = await shell.run('pbpaste', [], { cwd: ctx.working_dir, timeout_ms: 5_000 });
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
result = await shell.run('xclip', ['-selection', 'clipboard', '-o'], {
|
|
57
|
+
cwd: ctx.working_dir, timeout_ms: 5_000,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return JSON.stringify({ success: result.exitCode === 0, content: result.stdout });
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
return JSON.stringify({ success: false, error: err.message });
|
|
64
|
+
}
|
|
65
|
+
}, {
|
|
66
|
+
name: 'read_clipboard',
|
|
67
|
+
description: 'Read the current clipboard contents.',
|
|
68
|
+
schema: z.object({}),
|
|
69
|
+
}),
|
|
70
|
+
tool(async ({ content }) => {
|
|
71
|
+
try {
|
|
72
|
+
let result;
|
|
73
|
+
if (isWindows) {
|
|
74
|
+
result = await shell.run('powershell', ['-Command', `Set-Clipboard -Value '${content.replace(/'/g, "''")}'`], {
|
|
75
|
+
cwd: ctx.working_dir, timeout_ms: 5_000,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else if (isMac) {
|
|
79
|
+
result = await shell.run('sh', ['-c', `printf '%s' '${content.replace(/'/g, "'\\''")}' | pbcopy`], {
|
|
80
|
+
cwd: ctx.working_dir, timeout_ms: 5_000,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
result = await shell.run('sh', ['-c', `printf '%s' '${content.replace(/'/g, "'\\''")}' | xclip -selection clipboard`], {
|
|
85
|
+
cwd: ctx.working_dir, timeout_ms: 5_000,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return JSON.stringify({ success: result.exitCode === 0 });
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
return JSON.stringify({ success: false, error: err.message });
|
|
92
|
+
}
|
|
93
|
+
}, {
|
|
94
|
+
name: 'write_clipboard',
|
|
95
|
+
description: 'Write content to the clipboard.',
|
|
96
|
+
schema: z.object({ content: z.string() }),
|
|
97
|
+
}),
|
|
98
|
+
tool(async ({ url }) => {
|
|
99
|
+
try {
|
|
100
|
+
const open = isWindows ? 'start' : isMac ? 'open' : 'xdg-open';
|
|
101
|
+
const result = await shell.run(isWindows ? 'cmd' : open, isWindows ? ['/c', 'start', url] : [url], {
|
|
102
|
+
cwd: ctx.working_dir, timeout_ms: 5_000,
|
|
103
|
+
});
|
|
104
|
+
return JSON.stringify({ success: result.exitCode === 0, url });
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
return JSON.stringify({ success: false, error: err.message });
|
|
108
|
+
}
|
|
109
|
+
}, {
|
|
110
|
+
name: 'open_url',
|
|
111
|
+
description: 'Open a URL in the default browser.',
|
|
112
|
+
schema: z.object({ url: z.string() }),
|
|
113
|
+
}),
|
|
114
|
+
tool(async ({ file_path }) => {
|
|
115
|
+
try {
|
|
116
|
+
const open = isWindows ? 'start' : isMac ? 'open' : 'xdg-open';
|
|
117
|
+
const result = await shell.run(isWindows ? 'cmd' : open, isWindows ? ['/c', 'start', '""', file_path] : [file_path], {
|
|
118
|
+
cwd: ctx.working_dir, timeout_ms: 5_000,
|
|
119
|
+
});
|
|
120
|
+
return JSON.stringify({ success: result.exitCode === 0, file_path });
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
return JSON.stringify({ success: false, error: err.message });
|
|
124
|
+
}
|
|
125
|
+
}, {
|
|
126
|
+
name: 'open_file',
|
|
127
|
+
description: 'Open a file with the default application.',
|
|
128
|
+
schema: z.object({ file_path: z.string() }),
|
|
129
|
+
}),
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
registerToolFactory(createSystemTools);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MAX_OUTPUT_BYTES = 50 * 1024; // 50 KB
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { MAX_OUTPUT_BYTES } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Truncates a string to MAX_OUTPUT_BYTES (50 KB) if needed.
|
|
5
|
+
* Returns a UTF-8-safe truncation with a note when truncated.
|
|
6
|
+
*/
|
|
7
|
+
export function truncateOutput(output) {
|
|
8
|
+
const bytes = Buffer.byteLength(output, 'utf8');
|
|
9
|
+
if (bytes <= MAX_OUTPUT_BYTES)
|
|
10
|
+
return output;
|
|
11
|
+
const truncated = Buffer.from(output).subarray(0, MAX_OUTPUT_BYTES).toString('utf8');
|
|
12
|
+
return truncated + `\n\n[OUTPUT TRUNCATED: ${bytes} bytes total, showing first ${MAX_OUTPUT_BYTES} bytes]`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Returns true if filePath is inside dir (or equal to dir).
|
|
16
|
+
* Both paths are resolved before comparison.
|
|
17
|
+
*/
|
|
18
|
+
export function isWithinDir(filePath, dir) {
|
|
19
|
+
const resolved = path.resolve(filePath);
|
|
20
|
+
const resolvedDir = path.resolve(dir);
|
|
21
|
+
return resolved === resolvedDir || resolved.startsWith(resolvedDir + path.sep);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Extracts the binary base name from a command string.
|
|
25
|
+
* Handles full paths (/usr/bin/node, C:\bin\node.exe) and plain names.
|
|
26
|
+
*/
|
|
27
|
+
export function extractBinaryName(command) {
|
|
28
|
+
// Take first token (before any space), then get the basename, strip extension
|
|
29
|
+
const firstToken = command.split(/\s+/)[0] ?? command;
|
|
30
|
+
const base = path.basename(firstToken);
|
|
31
|
+
return base.replace(/\.(exe|cmd|bat|sh|ps1)$/i, '').toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Checks if a command is allowed based on the allowlist.
|
|
35
|
+
* Empty allowlist means ALL commands are allowed (Merovingian mode).
|
|
36
|
+
*/
|
|
37
|
+
export function isCommandAllowed(command, allowedCommands) {
|
|
38
|
+
if (allowedCommands.length === 0)
|
|
39
|
+
return true;
|
|
40
|
+
const binary = extractBinaryName(command);
|
|
41
|
+
return allowedCommands.some(allowed => {
|
|
42
|
+
const allowedBinary = extractBinaryName(allowed);
|
|
43
|
+
return allowedBinary === binary;
|
|
44
|
+
});
|
|
45
|
+
}
|
package/dist/http/api.js
CHANGED
|
@@ -213,6 +213,82 @@ export function createApiRouter(oracle) {
|
|
|
213
213
|
res.status(500).json({ error: error.message });
|
|
214
214
|
}
|
|
215
215
|
});
|
|
216
|
+
// --- Model Pricing ---
|
|
217
|
+
const ModelPricingSchema = z.object({
|
|
218
|
+
provider: z.string().min(1),
|
|
219
|
+
model: z.string().min(1),
|
|
220
|
+
input_price_per_1m: z.number().nonnegative(),
|
|
221
|
+
output_price_per_1m: z.number().nonnegative()
|
|
222
|
+
});
|
|
223
|
+
router.get('/model-pricing', (req, res) => {
|
|
224
|
+
try {
|
|
225
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
|
|
226
|
+
const entries = h.listModelPricing();
|
|
227
|
+
h.close();
|
|
228
|
+
res.json(entries);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
res.status(500).json({ error: error.message });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
router.post('/model-pricing', (req, res) => {
|
|
235
|
+
const parsed = ModelPricingSchema.safeParse(req.body);
|
|
236
|
+
if (!parsed.success) {
|
|
237
|
+
return res.status(400).json({ error: 'Invalid payload', details: parsed.error.issues });
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
|
|
241
|
+
h.upsertModelPricing(parsed.data);
|
|
242
|
+
h.close();
|
|
243
|
+
res.json({ success: true });
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
res.status(500).json({ error: error.message });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
router.put('/model-pricing/:provider/:model', (req, res) => {
|
|
250
|
+
const { provider, model } = req.params;
|
|
251
|
+
const partial = z.object({
|
|
252
|
+
input_price_per_1m: z.number().nonnegative().optional(),
|
|
253
|
+
output_price_per_1m: z.number().nonnegative().optional()
|
|
254
|
+
}).safeParse(req.body);
|
|
255
|
+
if (!partial.success) {
|
|
256
|
+
return res.status(400).json({ error: 'Invalid payload', details: partial.error.issues });
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
|
|
260
|
+
const existing = h.listModelPricing().find(e => e.provider === provider && e.model === model);
|
|
261
|
+
if (!existing) {
|
|
262
|
+
h.close();
|
|
263
|
+
return res.status(404).json({ error: 'Pricing entry not found' });
|
|
264
|
+
}
|
|
265
|
+
h.upsertModelPricing({
|
|
266
|
+
provider,
|
|
267
|
+
model,
|
|
268
|
+
input_price_per_1m: partial.data.input_price_per_1m ?? existing.input_price_per_1m,
|
|
269
|
+
output_price_per_1m: partial.data.output_price_per_1m ?? existing.output_price_per_1m
|
|
270
|
+
});
|
|
271
|
+
h.close();
|
|
272
|
+
res.json({ success: true });
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
res.status(500).json({ error: error.message });
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
router.delete('/model-pricing/:provider/:model', (req, res) => {
|
|
279
|
+
const { provider, model } = req.params;
|
|
280
|
+
try {
|
|
281
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
|
|
282
|
+
const changes = h.deleteModelPricing(provider, model);
|
|
283
|
+
h.close();
|
|
284
|
+
if (changes === 0)
|
|
285
|
+
return res.status(404).json({ error: 'Pricing entry not found' });
|
|
286
|
+
res.json({ success: true });
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
res.status(500).json({ error: error.message });
|
|
290
|
+
}
|
|
291
|
+
});
|
|
216
292
|
// Calculate diff between two objects
|
|
217
293
|
const getDiff = (obj1, obj2, prefix = '') => {
|
|
218
294
|
const changes = [];
|
|
@@ -310,6 +386,52 @@ export function createApiRouter(oracle) {
|
|
|
310
386
|
res.status(500).json({ error: error.message });
|
|
311
387
|
}
|
|
312
388
|
});
|
|
389
|
+
// Apoc config endpoints
|
|
390
|
+
router.get('/config/apoc', (req, res) => {
|
|
391
|
+
try {
|
|
392
|
+
const apocConfig = configManager.getApocConfig();
|
|
393
|
+
res.json(apocConfig);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
res.status(500).json({ error: error.message });
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
router.post('/config/apoc', async (req, res) => {
|
|
400
|
+
try {
|
|
401
|
+
const config = configManager.get();
|
|
402
|
+
await configManager.save({ ...config, apoc: req.body });
|
|
403
|
+
const display = DisplayManager.getInstance();
|
|
404
|
+
display.log('Apoc configuration updated via UI', {
|
|
405
|
+
source: 'Zaion',
|
|
406
|
+
level: 'info'
|
|
407
|
+
});
|
|
408
|
+
res.json({ success: true });
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
if (error.name === 'ZodError') {
|
|
412
|
+
res.status(400).json({ error: 'Validation failed', details: error.errors });
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
res.status(500).json({ error: error.message });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
router.delete('/config/apoc', async (req, res) => {
|
|
420
|
+
try {
|
|
421
|
+
const config = configManager.get();
|
|
422
|
+
const { apoc: _apoc, ...restConfig } = config;
|
|
423
|
+
await configManager.save(restConfig);
|
|
424
|
+
const display = DisplayManager.getInstance();
|
|
425
|
+
display.log('Apoc configuration removed via UI (falling back to Oracle config)', {
|
|
426
|
+
source: 'Zaion',
|
|
427
|
+
level: 'info'
|
|
428
|
+
});
|
|
429
|
+
res.json({ success: true });
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
res.status(500).json({ error: error.message });
|
|
433
|
+
}
|
|
434
|
+
});
|
|
313
435
|
// Sati memories endpoints
|
|
314
436
|
router.get('/sati/memories', async (req, res) => {
|
|
315
437
|
try {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
|
2
|
+
import { ConfigManager } from "../config/manager.js";
|
|
3
|
+
import { ProviderFactory } from "./providers/factory.js";
|
|
4
|
+
import { ProviderError } from "./errors.js";
|
|
5
|
+
import { DisplayManager } from "./display.js";
|
|
6
|
+
import { buildDevKit } from "../devkit/index.js";
|
|
7
|
+
/**
|
|
8
|
+
* Apoc is a subagent of Oracle specialized in devtools operations.
|
|
9
|
+
* It receives delegated tasks from Oracle and executes them using DevKit tools
|
|
10
|
+
* (filesystem, shell, git, network, processes, packages, system).
|
|
11
|
+
*
|
|
12
|
+
* Oracle calls Apoc via the `apoc_delegate` tool when the user requests
|
|
13
|
+
* dev-related tasks such as running commands, reading/writing files,
|
|
14
|
+
* managing git, or inspecting system state.
|
|
15
|
+
*/
|
|
16
|
+
export class Apoc {
|
|
17
|
+
static instance = null;
|
|
18
|
+
agent;
|
|
19
|
+
config;
|
|
20
|
+
display = DisplayManager.getInstance();
|
|
21
|
+
constructor(config) {
|
|
22
|
+
this.config = config || ConfigManager.getInstance().get();
|
|
23
|
+
}
|
|
24
|
+
static getInstance(config) {
|
|
25
|
+
if (!Apoc.instance) {
|
|
26
|
+
Apoc.instance = new Apoc(config);
|
|
27
|
+
}
|
|
28
|
+
return Apoc.instance;
|
|
29
|
+
}
|
|
30
|
+
static resetInstance() {
|
|
31
|
+
Apoc.instance = null;
|
|
32
|
+
}
|
|
33
|
+
async initialize() {
|
|
34
|
+
const apocConfig = this.config.apoc || this.config.llm;
|
|
35
|
+
console.log(`Apoc configuration: ${JSON.stringify(apocConfig)}`);
|
|
36
|
+
const working_dir = this.config.apoc?.working_dir || process.cwd();
|
|
37
|
+
const timeout_ms = this.config.apoc?.timeout_ms || 30_000;
|
|
38
|
+
// Import all devkit tool factories (side-effect registration)
|
|
39
|
+
await import("../devkit/index.js");
|
|
40
|
+
const tools = buildDevKit({
|
|
41
|
+
working_dir,
|
|
42
|
+
allowed_commands: [], // no restriction — Oracle is trusted orchestrator
|
|
43
|
+
timeout_ms,
|
|
44
|
+
});
|
|
45
|
+
this.display.log(`Apoc initialized with ${tools.length} DevKit tools (working_dir: ${working_dir})`, { source: "Apoc" });
|
|
46
|
+
try {
|
|
47
|
+
this.agent = await ProviderFactory.createBare(apocConfig, tools);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
throw new ProviderError(apocConfig.provider, err, "Apoc subagent initialization failed");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Execute a devtools task delegated by Oracle.
|
|
55
|
+
* @param task Natural language task description
|
|
56
|
+
* @param context Optional additional context from the ongoing conversation
|
|
57
|
+
*/
|
|
58
|
+
async execute(task, context) {
|
|
59
|
+
if (!this.agent) {
|
|
60
|
+
await this.initialize();
|
|
61
|
+
}
|
|
62
|
+
this.display.log(`Executing delegated task: ${task.slice(0, 80)}...`, {
|
|
63
|
+
source: "Apoc",
|
|
64
|
+
});
|
|
65
|
+
const systemMessage = new SystemMessage(`
|
|
66
|
+
You are Apoc, a specialized devtools subagent within the Morpheus system.
|
|
67
|
+
|
|
68
|
+
You are called by Oracle when the user needs dev operations performed.
|
|
69
|
+
Your job is to execute the requested task accurately using your available tools.
|
|
70
|
+
|
|
71
|
+
Available capabilities:
|
|
72
|
+
- Read, write, append, and delete files
|
|
73
|
+
- Execute shell commands
|
|
74
|
+
- Inspect and manage processes
|
|
75
|
+
- Run git operations (status, log, diff, clone, commit, etc.)
|
|
76
|
+
- Perform network operations (curl, DNS, ping)
|
|
77
|
+
- Manage packages (npm, yarn)
|
|
78
|
+
- Inspect system information
|
|
79
|
+
|
|
80
|
+
OPERATING RULES:
|
|
81
|
+
1. Use tools to accomplish the task. Do not speculate.
|
|
82
|
+
2. Always verify results after execution.
|
|
83
|
+
3. Report clearly what was done and what the result was.
|
|
84
|
+
4. If something fails, report the error and what you tried.
|
|
85
|
+
5. Stay focused on the delegated task only.
|
|
86
|
+
|
|
87
|
+
${context ? `CONTEXT FROM ORACLE:\n${context}` : ""}
|
|
88
|
+
`);
|
|
89
|
+
const userMessage = new HumanMessage(task);
|
|
90
|
+
const messages = [systemMessage, userMessage];
|
|
91
|
+
try {
|
|
92
|
+
const response = await this.agent.invoke({ messages });
|
|
93
|
+
const lastMessage = response.messages[response.messages.length - 1];
|
|
94
|
+
const content = typeof lastMessage.content === "string"
|
|
95
|
+
? lastMessage.content
|
|
96
|
+
: JSON.stringify(lastMessage.content);
|
|
97
|
+
this.display.log("Apoc task completed.", { source: "Apoc" });
|
|
98
|
+
return content;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
throw new ProviderError(this.config.apoc?.provider || this.config.llm.provider, err, "Apoc task execution failed");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Reload with updated config (called when settings change) */
|
|
105
|
+
async reload() {
|
|
106
|
+
this.config = ConfigManager.getInstance().get();
|
|
107
|
+
this.agent = undefined;
|
|
108
|
+
await this.initialize();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -41,7 +41,7 @@ export class SatiMemoryMiddleware {
|
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
async afterAgent(generatedResponse, history) {
|
|
44
|
+
async afterAgent(generatedResponse, history, userSessionId) {
|
|
45
45
|
try {
|
|
46
46
|
await this.service.evaluateAndPersist([
|
|
47
47
|
...history.slice(-5).map(m => ({
|
|
@@ -49,7 +49,7 @@ export class SatiMemoryMiddleware {
|
|
|
49
49
|
content: m.content.toString()
|
|
50
50
|
})),
|
|
51
51
|
{ role: 'assistant', content: generatedResponse }
|
|
52
|
-
]);
|
|
52
|
+
], userSessionId);
|
|
53
53
|
}
|
|
54
54
|
catch (error) {
|
|
55
55
|
display.log(`Error in afterAgent: ${error}`, { source: 'Sati' });
|
|
@@ -50,7 +50,7 @@ export class SatiService {
|
|
|
50
50
|
}))
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
-
async evaluateAndPersist(conversation) {
|
|
53
|
+
async evaluateAndPersist(conversation, userSessionId) {
|
|
54
54
|
try {
|
|
55
55
|
const satiConfig = ConfigManager.getInstance().getSatiConfig();
|
|
56
56
|
if (!satiConfig)
|
|
@@ -74,7 +74,8 @@ export class SatiService {
|
|
|
74
74
|
new SystemMessage(SATI_EVALUATION_PROMPT),
|
|
75
75
|
new HumanMessage(JSON.stringify(inputPayload, null, 2))
|
|
76
76
|
];
|
|
77
|
-
const
|
|
77
|
+
const satiSessionId = userSessionId ? `sati-evaluation-${userSessionId}` : 'sati-evaluation';
|
|
78
|
+
const history = new SQLiteChatMessageHistory({ sessionId: satiSessionId });
|
|
78
79
|
try {
|
|
79
80
|
const inputMsg = new ToolMessage({
|
|
80
81
|
content: JSON.stringify(inputPayload, null, 2),
|
|
@@ -14,6 +14,9 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
14
14
|
sessionId;
|
|
15
15
|
limit;
|
|
16
16
|
titleSet = false; // cache: skip setSessionTitleIfNeeded after title is set
|
|
17
|
+
get currentSessionId() {
|
|
18
|
+
return this.sessionId;
|
|
19
|
+
}
|
|
17
20
|
constructor(fields) {
|
|
18
21
|
super();
|
|
19
22
|
this.sessionId = fields.sessionId && fields.sessionId !== '' ? fields.sessionId : '';
|
|
@@ -108,9 +111,10 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
108
111
|
total_tokens INTEGER,
|
|
109
112
|
cache_read_tokens INTEGER,
|
|
110
113
|
provider TEXT,
|
|
111
|
-
model TEXT
|
|
114
|
+
model TEXT,
|
|
115
|
+
audio_duration_seconds REAL
|
|
112
116
|
);
|
|
113
|
-
|
|
117
|
+
|
|
114
118
|
CREATE INDEX IF NOT EXISTS idx_messages_session_id
|
|
115
119
|
ON messages(session_id);
|
|
116
120
|
|
|
@@ -130,6 +134,33 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
130
134
|
embedding_status TEXT CHECK (embedding_status IN ('none', 'pending', 'embedded', 'failed')) NOT NULL DEFAULT 'none'
|
|
131
135
|
);
|
|
132
136
|
|
|
137
|
+
CREATE TABLE IF NOT EXISTS model_pricing (
|
|
138
|
+
provider TEXT NOT NULL,
|
|
139
|
+
model TEXT NOT NULL,
|
|
140
|
+
input_price_per_1m REAL NOT NULL DEFAULT 0,
|
|
141
|
+
output_price_per_1m REAL NOT NULL DEFAULT 0,
|
|
142
|
+
PRIMARY KEY (provider, model)
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
INSERT OR IGNORE INTO model_pricing (provider, model, input_price_per_1m, output_price_per_1m) VALUES
|
|
146
|
+
('anthropic', 'claude-opus-4-6', 15.0, 75.0),
|
|
147
|
+
('anthropic', 'claude-sonnet-4-5-20250929', 3.0, 15.0),
|
|
148
|
+
('anthropic', 'claude-haiku-4-5-20251001', 0.8, 4.0),
|
|
149
|
+
('anthropic', 'claude-3-5-sonnet-20241022', 3.0, 15.0),
|
|
150
|
+
('anthropic', 'claude-3-5-haiku-20241022', 0.8, 4.0),
|
|
151
|
+
('anthropic', 'claude-3-opus-20240229', 15.0, 75.0),
|
|
152
|
+
('openai', 'gpt-4o', 2.5, 10.0),
|
|
153
|
+
('openai', 'gpt-4o-mini', 0.15, 0.6),
|
|
154
|
+
('openai', 'gpt-4-turbo', 10.0, 30.0),
|
|
155
|
+
('openai', 'gpt-3.5-turbo', 0.5, 1.5),
|
|
156
|
+
('openai', 'o1', 15.0, 60.0),
|
|
157
|
+
('openai', 'o1-mini', 3.0, 12.0),
|
|
158
|
+
('google', 'gemini-2.5-flash', 0.15, 0.6),
|
|
159
|
+
('google', 'gemini-2.5-flash-lite', 0.075, 0.3),
|
|
160
|
+
('google', 'gemini-2.0-flash', 0.1, 0.4),
|
|
161
|
+
('google', 'gemini-1.5-pro', 1.25, 5.0),
|
|
162
|
+
('google', 'gemini-1.5-flash', 0.075, 0.3);
|
|
163
|
+
|
|
133
164
|
`);
|
|
134
165
|
this.migrateTable();
|
|
135
166
|
}
|
|
@@ -151,13 +182,15 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
151
182
|
'total_tokens',
|
|
152
183
|
'cache_read_tokens',
|
|
153
184
|
'provider',
|
|
154
|
-
'model'
|
|
185
|
+
'model',
|
|
186
|
+
'audio_duration_seconds'
|
|
155
187
|
];
|
|
156
188
|
const integerColumns = new Set(['input_tokens', 'output_tokens', 'total_tokens', 'cache_read_tokens']);
|
|
189
|
+
const realColumns = new Set(['audio_duration_seconds']);
|
|
157
190
|
for (const col of newColumns) {
|
|
158
191
|
if (!columns.has(col)) {
|
|
159
192
|
try {
|
|
160
|
-
const type = integerColumns.has(col) ? 'INTEGER' : 'TEXT';
|
|
193
|
+
const type = integerColumns.has(col) ? 'INTEGER' : realColumns.has(col) ? 'REAL' : 'TEXT';
|
|
161
194
|
this.db.exec(`ALTER TABLE messages ADD COLUMN ${col} ${type}`);
|
|
162
195
|
}
|
|
163
196
|
catch (e) {
|
|
@@ -296,6 +329,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
296
329
|
// Extract provider metadata
|
|
297
330
|
const provider = anyMsg.provider_metadata?.provider ?? null;
|
|
298
331
|
const model = anyMsg.provider_metadata?.model ?? null;
|
|
332
|
+
const audioDurationSeconds = usage?.audio_duration_seconds ?? null;
|
|
299
333
|
// Handle special content serialization for Tools
|
|
300
334
|
let finalContent = "";
|
|
301
335
|
if (type === 'ai' && (message.tool_calls?.length ?? 0) > 0) {
|
|
@@ -318,8 +352,8 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
318
352
|
? message.content
|
|
319
353
|
: JSON.stringify(message.content);
|
|
320
354
|
}
|
|
321
|
-
const stmt = this.db.prepare("INSERT INTO messages (session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
322
|
-
stmt.run(this.sessionId, type, finalContent, Date.now(), inputTokens, outputTokens, totalTokens, cacheReadTokens, provider, model);
|
|
355
|
+
const stmt = this.db.prepare("INSERT INTO messages (session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model, audio_duration_seconds) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
356
|
+
stmt.run(this.sessionId, type, finalContent, Date.now(), inputTokens, outputTokens, totalTokens, cacheReadTokens, provider, model, audioDurationSeconds);
|
|
323
357
|
// Verificar se a sessão tem título e definir automaticamente se necessário
|
|
324
358
|
await this.setSessionTitleIfNeeded();
|
|
325
359
|
}
|
|
@@ -346,7 +380,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
346
380
|
async addMessages(messages) {
|
|
347
381
|
if (messages.length === 0)
|
|
348
382
|
return;
|
|
349
|
-
const stmt = this.db.prepare("INSERT INTO messages (session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
383
|
+
const stmt = this.db.prepare("INSERT INTO messages (session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model, audio_duration_seconds) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
350
384
|
const insertAll = this.db.transaction((msgs) => {
|
|
351
385
|
for (const message of msgs) {
|
|
352
386
|
let type;
|
|
@@ -373,7 +407,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
373
407
|
else {
|
|
374
408
|
finalContent = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
|
|
375
409
|
}
|
|
376
|
-
stmt.run(this.sessionId, type, finalContent, Date.now(), usage?.input_tokens ?? null, usage?.output_tokens ?? null, usage?.total_tokens ?? null, usage?.input_token_details?.cache_read ?? usage?.cache_read_tokens ?? null, anyMsg.provider_metadata?.provider ?? null, anyMsg.provider_metadata?.model ?? null);
|
|
410
|
+
stmt.run(this.sessionId, type, finalContent, Date.now(), usage?.input_tokens ?? null, usage?.output_tokens ?? null, usage?.total_tokens ?? null, usage?.input_token_details?.cache_read ?? usage?.cache_read_tokens ?? null, anyMsg.provider_metadata?.provider ?? null, anyMsg.provider_metadata?.model ?? null, usage?.audio_duration_seconds ?? null);
|
|
377
411
|
}
|
|
378
412
|
});
|
|
379
413
|
try {
|
|
@@ -440,9 +474,18 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
440
474
|
try {
|
|
441
475
|
const stmt = this.db.prepare("SELECT SUM(input_tokens) as totalInput, SUM(output_tokens) as totalOutput FROM messages");
|
|
442
476
|
const row = stmt.get();
|
|
477
|
+
// Calculate total estimated cost by summing per-model costs
|
|
478
|
+
const costStmt = this.db.prepare(`SELECT
|
|
479
|
+
SUM((COALESCE(m.input_tokens, 0) / 1000000.0) * p.input_price_per_1m
|
|
480
|
+
+ (COALESCE(m.output_tokens, 0) / 1000000.0) * p.output_price_per_1m) as totalCost
|
|
481
|
+
FROM messages m
|
|
482
|
+
INNER JOIN model_pricing p ON p.provider = m.provider AND p.model = COALESCE(m.model, 'unknown')
|
|
483
|
+
WHERE m.provider IS NOT NULL`);
|
|
484
|
+
const costRow = costStmt.get();
|
|
443
485
|
return {
|
|
444
486
|
totalInputTokens: row.totalInput || 0,
|
|
445
|
-
totalOutputTokens: row.totalOutput || 0
|
|
487
|
+
totalOutputTokens: row.totalOutput || 0,
|
|
488
|
+
totalEstimatedCostUsd: costRow.totalCost ?? null
|
|
446
489
|
};
|
|
447
490
|
}
|
|
448
491
|
catch (error) {
|
|
@@ -474,31 +517,58 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
474
517
|
*/
|
|
475
518
|
async getUsageStatsByProviderAndModel() {
|
|
476
519
|
try {
|
|
477
|
-
const stmt = this.db.prepare(`SELECT
|
|
478
|
-
provider,
|
|
479
|
-
COALESCE(model, 'unknown') as model,
|
|
480
|
-
SUM(input_tokens) as totalInputTokens,
|
|
481
|
-
SUM(output_tokens) as totalOutputTokens,
|
|
482
|
-
SUM(total_tokens) as totalTokens,
|
|
483
|
-
COUNT(*) as messageCount
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
520
|
+
const stmt = this.db.prepare(`SELECT
|
|
521
|
+
m.provider,
|
|
522
|
+
COALESCE(m.model, 'unknown') as model,
|
|
523
|
+
SUM(m.input_tokens) as totalInputTokens,
|
|
524
|
+
SUM(m.output_tokens) as totalOutputTokens,
|
|
525
|
+
SUM(m.total_tokens) as totalTokens,
|
|
526
|
+
COUNT(*) as messageCount,
|
|
527
|
+
COALESCE(SUM(m.audio_duration_seconds), 0) as totalAudioSeconds,
|
|
528
|
+
p.input_price_per_1m,
|
|
529
|
+
p.output_price_per_1m
|
|
530
|
+
FROM messages m
|
|
531
|
+
LEFT JOIN model_pricing p ON p.provider = m.provider AND p.model = COALESCE(m.model, 'unknown')
|
|
532
|
+
WHERE m.provider IS NOT NULL
|
|
533
|
+
GROUP BY m.provider, COALESCE(m.model, 'unknown')
|
|
534
|
+
ORDER BY m.provider, m.model`);
|
|
488
535
|
const rows = stmt.all();
|
|
489
|
-
return rows.map((row) =>
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
536
|
+
return rows.map((row) => {
|
|
537
|
+
const inputTokens = row.totalInputTokens || 0;
|
|
538
|
+
const outputTokens = row.totalOutputTokens || 0;
|
|
539
|
+
let estimatedCostUsd = null;
|
|
540
|
+
if (row.input_price_per_1m != null && row.output_price_per_1m != null) {
|
|
541
|
+
estimatedCostUsd = (inputTokens / 1_000_000) * row.input_price_per_1m
|
|
542
|
+
+ (outputTokens / 1_000_000) * row.output_price_per_1m;
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
provider: row.provider,
|
|
546
|
+
model: row.model,
|
|
547
|
+
totalInputTokens: inputTokens,
|
|
548
|
+
totalOutputTokens: outputTokens,
|
|
549
|
+
totalTokens: row.totalTokens || 0,
|
|
550
|
+
messageCount: row.messageCount || 0,
|
|
551
|
+
totalAudioSeconds: row.totalAudioSeconds || 0,
|
|
552
|
+
estimatedCostUsd
|
|
553
|
+
};
|
|
554
|
+
});
|
|
497
555
|
}
|
|
498
556
|
catch (error) {
|
|
499
557
|
throw new Error(`Failed to get grouped usage stats: ${error}`);
|
|
500
558
|
}
|
|
501
559
|
}
|
|
560
|
+
// --- Model Pricing CRUD ---
|
|
561
|
+
listModelPricing() {
|
|
562
|
+
const rows = this.db.prepare('SELECT provider, model, input_price_per_1m, output_price_per_1m FROM model_pricing ORDER BY provider, model').all();
|
|
563
|
+
return rows;
|
|
564
|
+
}
|
|
565
|
+
upsertModelPricing(entry) {
|
|
566
|
+
this.db.prepare('INSERT INTO model_pricing (provider, model, input_price_per_1m, output_price_per_1m) VALUES (?, ?, ?, ?) ON CONFLICT(provider, model) DO UPDATE SET input_price_per_1m = excluded.input_price_per_1m, output_price_per_1m = excluded.output_price_per_1m').run(entry.provider, entry.model, entry.input_price_per_1m, entry.output_price_per_1m);
|
|
567
|
+
}
|
|
568
|
+
deleteModelPricing(provider, model) {
|
|
569
|
+
const result = this.db.prepare('DELETE FROM model_pricing WHERE provider = ? AND model = ?').run(provider, model);
|
|
570
|
+
return result.changes;
|
|
571
|
+
}
|
|
502
572
|
/**
|
|
503
573
|
* Clears all messages for the current session from the database.
|
|
504
574
|
*/
|