morpheus-cli 0.8.6 → 0.8.8

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.
Files changed (74) hide show
  1. package/dist/channels/telegram.js +43 -0
  2. package/dist/http/api.js +49 -13
  3. package/dist/http/routers/chronos.js +12 -1
  4. package/dist/http/webhooks-router.js +11 -3
  5. package/dist/runtime/ISubagent.js +1 -0
  6. package/dist/runtime/apoc.js +49 -39
  7. package/dist/runtime/audit/repository.js +193 -6
  8. package/dist/runtime/chronos/repository.js +35 -0
  9. package/dist/runtime/keymaker.js +6 -30
  10. package/dist/runtime/memory/sati/repository.js +37 -0
  11. package/dist/runtime/memory/sqlite.js +16 -3
  12. package/dist/runtime/neo.js +78 -34
  13. package/dist/runtime/oracle.js +31 -6
  14. package/dist/runtime/skills/tool.js +25 -0
  15. package/dist/runtime/smiths/delegator.js +21 -0
  16. package/dist/runtime/subagent-utils.js +89 -0
  17. package/dist/runtime/tasks/repository.js +51 -0
  18. package/dist/runtime/tasks/worker.js +12 -2
  19. package/dist/runtime/telephonist.js +17 -9
  20. package/dist/runtime/tools/delegation-utils.js +120 -0
  21. package/dist/runtime/tools/index.js +0 -2
  22. package/dist/runtime/trinity.js +50 -34
  23. package/dist/runtime/webhooks/repository.js +31 -0
  24. package/dist/types/pagination.js +1 -0
  25. package/dist/ui/assets/AuditDashboard-5sA8Sd8S.js +1 -0
  26. package/dist/ui/assets/Chat-CjxeAQmd.js +41 -0
  27. package/dist/ui/assets/Chronos-BAjeLobF.js +1 -0
  28. package/dist/ui/assets/{ConfirmationModal-Bx-GtD9B.js → ConfirmationModal-fvgnOWTY.js} +1 -1
  29. package/dist/ui/assets/{Dashboard-DDyN_X-J.js → Dashboard-Ca5mSefz.js} +1 -1
  30. package/dist/ui/assets/{DeleteConfirmationModal-DIXbckY8.js → DeleteConfirmationModal-A8EmnHoa.js} +1 -1
  31. package/dist/ui/assets/{Logs-dzPLW45U.js → Logs-CYu7se7R.js} +1 -1
  32. package/dist/ui/assets/MCPManager-DsDA_ZVT.js +1 -0
  33. package/dist/ui/assets/ModelPricing-DnSm_Nh-.js +1 -0
  34. package/dist/ui/assets/Notifications-CiljQzvM.js +1 -0
  35. package/dist/ui/assets/Pagination-JsiwxVNQ.js +1 -0
  36. package/dist/ui/assets/SatiMemories-rnO2b0LG.js +1 -0
  37. package/dist/ui/assets/SessionAudit-Dfvhge3Z.js +9 -0
  38. package/dist/ui/assets/{Settings-DNDe62-H.js → Settings-OQlHAJoy.js} +1 -1
  39. package/dist/ui/assets/Skills-Crsybug0.js +7 -0
  40. package/dist/ui/assets/Smiths-wm90jRDT.js +1 -0
  41. package/dist/ui/assets/Tasks-C5FMu_Yu.js +1 -0
  42. package/dist/ui/assets/TrinityDatabases-BzYfecKI.js +1 -0
  43. package/dist/ui/assets/{UsageStats-doBLB7Lc.js → UsageStats-CBo2vW2n.js} +1 -1
  44. package/dist/ui/assets/{WebhookManager-D3A5pdjC.js → WebhookManager-0tDFkfHd.js} +1 -1
  45. package/dist/ui/assets/audit-B-F8XPLi.js +1 -0
  46. package/dist/ui/assets/chronos-BvMxfBQH.js +1 -0
  47. package/dist/ui/assets/{config-DX3Xb0XE.js → config-DteVgNGR.js} +1 -1
  48. package/dist/ui/assets/index-Cwqr-n0Y.js +10 -0
  49. package/dist/ui/assets/index-DcfyUdLI.css +1 -0
  50. package/dist/ui/assets/{mcp-DfhJYN14.js → mcp-DxzodOdH.js} +1 -1
  51. package/dist/ui/assets/{skills-BPjq0qV7.js → skills--hAyQnmG.js} +1 -1
  52. package/dist/ui/assets/{stats-DHCRNkJp.js → stats-Cibaisqd.js} +1 -1
  53. package/dist/ui/assets/vendor-icons-BVuQI-6R.js +1 -0
  54. package/dist/ui/index.html +3 -3
  55. package/dist/ui/sw.js +1 -1
  56. package/package.json +2 -1
  57. package/dist/runtime/tools/apoc-tool.js +0 -157
  58. package/dist/runtime/tools/neo-tool.js +0 -172
  59. package/dist/runtime/tools/trinity-tool.js +0 -157
  60. package/dist/ui/assets/Chat-CO15OnaY.js +0 -38
  61. package/dist/ui/assets/Chronos-CUZDQLh2.js +0 -1
  62. package/dist/ui/assets/MCPManager-CRHWR4S7.js +0 -1
  63. package/dist/ui/assets/ModelPricing-TRBesy0r.js +0 -1
  64. package/dist/ui/assets/Notifications-DMke7Dr7.js +0 -1
  65. package/dist/ui/assets/SatiMemories-CaLrgdZV.js +0 -1
  66. package/dist/ui/assets/SessionAudit-DedGO5XK.js +0 -9
  67. package/dist/ui/assets/Skills-KUhW7UXP.js +0 -7
  68. package/dist/ui/assets/Smiths-Btoqw4Ex.js +0 -1
  69. package/dist/ui/assets/Tasks-cwA25Hq2.js +0 -1
  70. package/dist/ui/assets/TrinityDatabases-CQhettEJ.js +0 -1
  71. package/dist/ui/assets/chronos-DlDM2UBT.js +0 -1
  72. package/dist/ui/assets/index-CQIUjucB.js +0 -10
  73. package/dist/ui/assets/index-DAh3q_hR.css +0 -1
  74. package/dist/ui/assets/vendor-icons-DLvvGkeN.js +0 -1
@@ -14,6 +14,7 @@ import { SQLiteChatMessageHistory } from '../runtime/memory/sqlite.js';
14
14
  import { SatiRepository } from '../runtime/memory/sati/repository.js';
15
15
  import { MCPManager } from '../config/mcp-manager.js';
16
16
  import { Construtor } from '../runtime/tools/factory.js';
17
+ import { AuditRepository } from '../runtime/audit/repository.js';
17
18
  function escapeHtml(text) {
18
19
  return text
19
20
  .replace(/&/g, '&')
@@ -310,8 +311,34 @@ export class TelegramAdapter {
310
311
  filePath = await this.downloadToTemp(fileLink);
311
312
  // Transcribe
312
313
  this.display.log(`Transcribing audio for @${user}...`, { source: 'Telephonist' });
314
+ const transcribeStart = Date.now();
313
315
  const { text, usage } = await this.telephonist.transcribe(filePath, 'audio/ogg', apiKey);
316
+ const transcribeDurationMs = Date.now() - transcribeStart;
314
317
  this.display.log(`Transcription success for @${user}: "${text}"`, { source: 'Telephonist', level: 'success' });
318
+ // Audit: record telephonist execution
319
+ try {
320
+ const sessionId = await this.history.getCurrentSessionOrCreate();
321
+ AuditRepository.getInstance().insert({
322
+ session_id: sessionId,
323
+ event_type: 'telephonist',
324
+ agent: 'telephonist',
325
+ provider: config.audio.provider,
326
+ model: config.audio.model,
327
+ input_tokens: usage.input_tokens ?? null,
328
+ output_tokens: usage.output_tokens ?? null,
329
+ duration_ms: transcribeDurationMs,
330
+ status: 'success',
331
+ metadata: {
332
+ audio_duration_seconds: usage.audio_duration_seconds ?? duration,
333
+ text_preview: text.slice(0, 120),
334
+ text_length: text.length,
335
+ user,
336
+ },
337
+ });
338
+ }
339
+ catch {
340
+ // Audit failure never breaks the main flow
341
+ }
315
342
  // Reply with transcription (optional, maybe just process it?)
316
343
  // The prompt says "reply with the answer".
317
344
  // "Transcribe them... and process the resulting text as a standard user prompt."
@@ -352,6 +379,22 @@ export class TelegramAdapter {
352
379
  catch (error) {
353
380
  const detail = error?.cause?.message || error?.response?.data?.error?.message || error.message;
354
381
  this.display.log(`Audio processing error for @${user}: ${detail}`, { source: 'Telephonist', level: 'error' });
382
+ try {
383
+ const sessionId = await this.history.getCurrentSessionOrCreate();
384
+ AuditRepository.getInstance().insert({
385
+ session_id: sessionId,
386
+ event_type: 'telephonist',
387
+ agent: 'telephonist',
388
+ provider: this.config.get().audio.provider,
389
+ model: this.config.get().audio.model,
390
+ duration_ms: null,
391
+ status: 'error',
392
+ metadata: { error: detail, user },
393
+ });
394
+ }
395
+ catch {
396
+ // Audit failure never breaks the main flow
397
+ }
355
398
  await ctx.reply("Sorry, I failed to process your audio message.");
356
399
  }
357
400
  finally {
package/dist/http/api.js CHANGED
@@ -178,6 +178,7 @@ export function createApiRouter(oracle, chronosWorker) {
178
178
  duration_ms: row.duration_ms ?? null,
179
179
  provider: row.provider ?? null,
180
180
  model: row.model ?? null,
181
+ audio_duration_seconds: row.audio_duration_seconds ?? null,
181
182
  sati_memories_count: null,
182
183
  };
183
184
  });
@@ -211,7 +212,16 @@ export function createApiRouter(oracle, chronosWorker) {
211
212
  sessionHistory.close();
212
213
  }
213
214
  });
214
- // --- Session Audit ---
215
+ // --- Audit ---
216
+ router.get('/audit/global', (_req, res) => {
217
+ try {
218
+ const summary = AuditRepository.getInstance().getGlobalSummary();
219
+ res.json(summary);
220
+ }
221
+ catch (err) {
222
+ res.status(500).json({ error: err.message });
223
+ }
224
+ });
215
225
  router.get('/sessions/:id/audit', (req, res) => {
216
226
  try {
217
227
  const { id } = req.params;
@@ -259,14 +269,25 @@ export function createApiRouter(oracle, chronosWorker) {
259
269
  const originChannel = req.query.origin_channel;
260
270
  const sessionId = req.query.session_id;
261
271
  const limit = req.query.limit;
272
+ const pageRaw = req.query.page;
273
+ const perPageRaw = req.query.per_page;
262
274
  const parsedStatus = typeof status === 'string' ? TaskStatusSchema.safeParse(status) : null;
263
275
  const parsedAgent = typeof agent === 'string' ? TaskAgentSchema.safeParse(agent) : null;
264
276
  const parsedOrigin = typeof originChannel === 'string' ? OriginChannelSchema.safeParse(originChannel) : null;
265
- const tasks = taskRepository.listTasks({
277
+ const filters = {
266
278
  status: parsedStatus?.success ? parsedStatus.data : undefined,
267
279
  agent: parsedAgent?.success ? parsedAgent.data : undefined,
268
280
  origin_channel: parsedOrigin?.success ? parsedOrigin.data : undefined,
269
281
  session_id: typeof sessionId === 'string' ? sessionId : undefined,
282
+ };
283
+ if (pageRaw !== undefined || perPageRaw !== undefined) {
284
+ const page = Math.max(1, parseInt(String(pageRaw ?? '1'), 10) || 1);
285
+ const per_page = Math.min(100, Math.max(1, parseInt(String(perPageRaw ?? '20'), 10) || 20));
286
+ const result = taskRepository.listTasksPaginated({ ...filters, page, per_page });
287
+ return res.json(result);
288
+ }
289
+ const tasks = taskRepository.listTasks({
290
+ ...filters,
270
291
  limit: typeof limit === 'string' ? Math.max(1, Math.min(500, Number(limit) || 200)) : 200,
271
292
  });
272
293
  res.json(tasks);
@@ -424,7 +445,8 @@ export function createApiRouter(oracle, chronosWorker) {
424
445
  provider: z.string().min(1),
425
446
  model: z.string().min(1),
426
447
  input_price_per_1m: z.number().nonnegative(),
427
- output_price_per_1m: z.number().nonnegative()
448
+ output_price_per_1m: z.number().nonnegative(),
449
+ audio_cost_per_second: z.number().nonnegative().nullable().optional(),
428
450
  });
429
451
  router.get('/model-pricing', (req, res) => {
430
452
  try {
@@ -456,7 +478,8 @@ export function createApiRouter(oracle, chronosWorker) {
456
478
  const { provider, model } = req.params;
457
479
  const partial = z.object({
458
480
  input_price_per_1m: z.number().nonnegative().optional(),
459
- output_price_per_1m: z.number().nonnegative().optional()
481
+ output_price_per_1m: z.number().nonnegative().optional(),
482
+ audio_cost_per_second: z.number().nonnegative().nullable().optional(),
460
483
  }).safeParse(req.body);
461
484
  if (!partial.success) {
462
485
  return res.status(400).json({ error: 'Invalid payload', details: partial.error.issues });
@@ -472,7 +495,10 @@ export function createApiRouter(oracle, chronosWorker) {
472
495
  provider,
473
496
  model,
474
497
  input_price_per_1m: partial.data.input_price_per_1m ?? existing.input_price_per_1m,
475
- output_price_per_1m: partial.data.output_price_per_1m ?? existing.output_price_per_1m
498
+ output_price_per_1m: partial.data.output_price_per_1m ?? existing.output_price_per_1m,
499
+ audio_cost_per_second: 'audio_cost_per_second' in partial.data
500
+ ? partial.data.audio_cost_per_second
501
+ : existing.audio_cost_per_second,
476
502
  });
477
503
  h.close();
478
504
  res.json({ success: true });
@@ -701,15 +727,25 @@ export function createApiRouter(oracle, chronosWorker) {
701
727
  router.get('/sati/memories', async (req, res) => {
702
728
  try {
703
729
  const repository = SatiRepository.getInstance();
704
- const memories = repository.getAllMemories();
705
- // Convert dates to ISO strings for JSON serialization
706
- const serializedMemories = memories.map(memory => ({
730
+ const pageRaw = req.query.page;
731
+ const perPageRaw = req.query.per_page;
732
+ const category = typeof req.query.category === 'string' && req.query.category !== 'all' ? req.query.category : undefined;
733
+ const importance = typeof req.query.importance === 'string' && req.query.importance !== 'all' ? req.query.importance : undefined;
734
+ const search = typeof req.query.search === 'string' && req.query.search.trim() ? req.query.search.trim() : undefined;
735
+ const serializeMemory = (memory) => ({
707
736
  ...memory,
708
- created_at: memory.created_at.toISOString(),
709
- updated_at: memory.updated_at.toISOString(),
710
- last_accessed_at: memory.last_accessed_at ? memory.last_accessed_at.toISOString() : null
711
- }));
712
- res.json(serializedMemories);
737
+ created_at: memory.created_at instanceof Date ? memory.created_at.toISOString() : memory.created_at,
738
+ updated_at: memory.updated_at instanceof Date ? memory.updated_at.toISOString() : memory.updated_at,
739
+ last_accessed_at: memory.last_accessed_at instanceof Date ? memory.last_accessed_at.toISOString() : (memory.last_accessed_at ?? null),
740
+ });
741
+ if (pageRaw !== undefined || perPageRaw !== undefined) {
742
+ const page = Math.max(1, parseInt(String(pageRaw ?? '1'), 10) || 1);
743
+ const per_page = Math.min(100, Math.max(1, parseInt(String(perPageRaw ?? '20'), 10) || 20));
744
+ const result = repository.getMemoriesPaginated({ category, importance, search, page, per_page });
745
+ return res.json({ ...result, data: result.data.map(serializeMemory) });
746
+ }
747
+ const memories = repository.getAllMemories();
748
+ res.json(memories.map(serializeMemory));
713
749
  }
714
750
  catch (error) {
715
751
  res.status(500).json({ error: error.message });
@@ -75,11 +75,22 @@ export function createChronosJobRouter(repo, _worker) {
75
75
  res.status(400).json({ error: err.message });
76
76
  }
77
77
  });
78
- // GET /api/chronos — list jobs
78
+ // GET /api/chronos — list jobs (paginated)
79
79
  router.get('/', (req, res) => {
80
80
  try {
81
81
  const enabled = req.query.enabled;
82
82
  const created_by = req.query.created_by;
83
+ const page = req.query.page ? Math.max(1, parseInt(String(req.query.page), 10) || 1) : undefined;
84
+ const per_page = req.query.per_page ? Math.min(100, Math.max(1, parseInt(String(req.query.per_page), 10) || 20)) : undefined;
85
+ if (page !== undefined || per_page !== undefined) {
86
+ const result = repo.listJobsPaginated({
87
+ enabled: enabled === 'true' ? true : enabled === 'false' ? false : undefined,
88
+ created_by,
89
+ page,
90
+ per_page,
91
+ });
92
+ return res.json(result);
93
+ }
83
94
  const jobs = repo.listJobs({
84
95
  enabled: enabled === 'true' ? true : enabled === 'false' ? false : undefined,
85
96
  created_by,
@@ -74,11 +74,19 @@ export function createWebhooksRouter() {
74
74
  // ─────────────────────────────────────────────────────────────────────────────
75
75
  router.get('/notifications', authMiddleware, (req, res) => {
76
76
  try {
77
- const { webhookId, unreadOnly } = req.query;
78
- const notifications = repo.listNotifications({
77
+ const { webhookId, unreadOnly, status, page: pageRaw, per_page: perPageRaw } = req.query;
78
+ const filters = {
79
79
  webhookId: typeof webhookId === 'string' ? webhookId : undefined,
80
80
  unreadOnly: unreadOnly === 'true',
81
- });
81
+ status: typeof status === 'string' && status !== 'all' ? status : undefined,
82
+ };
83
+ if (pageRaw !== undefined || perPageRaw !== undefined) {
84
+ const page = Math.max(1, parseInt(String(pageRaw ?? '1'), 10) || 1);
85
+ const per_page = Math.min(100, Math.max(1, parseInt(String(perPageRaw ?? '20'), 10) || 20));
86
+ const result = repo.listNotificationsPaginated({ ...filters, page, per_page });
87
+ return res.json(result);
88
+ }
89
+ const notifications = repo.listNotifications(filters);
82
90
  res.json(notifications);
83
91
  }
84
92
  catch (err) {
@@ -0,0 +1 @@
1
+ export {};
@@ -4,7 +4,8 @@ import { ProviderFactory } from "./providers/factory.js";
4
4
  import { ProviderError } from "./errors.js";
5
5
  import { DisplayManager } from "./display.js";
6
6
  import { buildDevKit } from "../devkit/index.js";
7
- import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
7
+ import { extractRawUsage, persistAgentMessage, buildAgentResult, emitToolAuditEvents } from "./subagent-utils.js";
8
+ import { buildDelegationTool } from "./tools/delegation-utils.js";
8
9
  /**
9
10
  * Apoc is a subagent of Oracle specialized in devtools operations.
10
11
  * It receives delegated tasks from Oracle and executes them using DevKit tools
@@ -17,6 +18,7 @@ import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
17
18
  export class Apoc {
18
19
  static instance = null;
19
20
  static currentSessionId = undefined;
21
+ static _delegateTool = null;
20
22
  agent;
21
23
  config;
22
24
  display = DisplayManager.getInstance();
@@ -38,10 +40,10 @@ export class Apoc {
38
40
  }
39
41
  static resetInstance() {
40
42
  Apoc.instance = null;
43
+ Apoc._delegateTool = null;
41
44
  }
42
45
  async initialize() {
43
46
  const apocConfig = this.config.apoc || this.config.llm;
44
- // console.log(`Apoc configuration: ${JSON.stringify(apocConfig)}`);
45
47
  const devkit = ConfigManager.getInstance().getDevKitConfig();
46
48
  const timeout_ms = devkit.timeout_ms || this.config.apoc?.timeout_ms || 30_000;
47
49
  const personality = this.config.apoc?.personality || 'pragmatic_dev';
@@ -73,8 +75,9 @@ export class Apoc {
73
75
  * @param task Natural language task description
74
76
  * @param context Optional additional context from the ongoing conversation
75
77
  * @param sessionId Session to attribute token usage to (defaults to 'apoc')
78
+ * @param taskContext Optional Oracle task context (unused by Apoc directly — kept for ISubagent compatibility)
76
79
  */
77
- async execute(task, context, sessionId) {
80
+ async execute(task, context, sessionId, taskContext) {
78
81
  if (!this.agent) {
79
82
  await this.initialize();
80
83
  }
@@ -194,7 +197,7 @@ If refinement is triggered:
194
197
 
195
198
  2. Execute a second search cycle (Cycle 2).
196
199
  3. Repeat selection, navigation, extraction, verification.
197
- 4. Choose the stronger cycles evidence.
200
+ 4. Choose the stronger cycle's evidence.
198
201
  5. Do NOT perform more than 2 cycles.
199
202
 
200
203
  If Cycle 2 also fails:
@@ -239,10 +242,10 @@ LOW:
239
242
  OUTPUT FORMAT (STRICT)
240
243
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
241
244
 
242
- 1. Direct Answer
243
- 2. Evidence Summary
244
- 3. Sources (URLs)
245
- 4. Confidence Level (HIGH / MEDIUM / LOW)
245
+ 1. Direct Answer
246
+ 2. Evidence Summary
247
+ 3. Sources (URLs)
248
+ 4. Confidence Level (HIGH / MEDIUM / LOW)
246
249
  5. Completion Status (true / false)
247
250
 
248
251
  No conversational filler.
@@ -254,6 +257,7 @@ ${context ? `CONTEXT FROM ORACLE:\n${context}` : ""}
254
257
  const userMessage = new HumanMessage(task);
255
258
  const messages = [systemMessage, userMessage];
256
259
  try {
260
+ const inputCount = messages.length;
257
261
  const startMs = Date.now();
258
262
  const response = await this.agent.invoke({ messages });
259
263
  const durationMs = Date.now() - startMs;
@@ -262,45 +266,51 @@ ${context ? `CONTEXT FROM ORACLE:\n${context}` : ""}
262
266
  const content = typeof lastMessage.content === "string"
263
267
  ? lastMessage.content
264
268
  : JSON.stringify(lastMessage.content);
265
- // Aggregate token usage across all AI messages in this invocation
266
- const rawUsage = lastMessage.usage_metadata
267
- ?? lastMessage.response_metadata?.usage
268
- ?? lastMessage.response_metadata?.tokenUsage
269
- ?? lastMessage.usage;
270
- const inputTokens = rawUsage?.input_tokens ?? 0;
271
- const outputTokens = rawUsage?.output_tokens ?? 0;
269
+ const rawUsage = extractRawUsage(lastMessage);
272
270
  const stepCount = response.messages.filter((m) => m instanceof AIMessage).length;
273
271
  const targetSession = sessionId ?? Apoc.currentSessionId ?? "apoc";
274
- const history = new SQLiteChatMessageHistory({ sessionId: targetSession });
275
- try {
276
- const persisted = new AIMessage(content);
277
- if (rawUsage)
278
- persisted.usage_metadata = rawUsage;
279
- persisted.provider_metadata = { provider: apocConfig.provider, model: apocConfig.model };
280
- persisted.agent_metadata = { agent: 'apoc' };
281
- persisted.duration_ms = durationMs;
282
- await history.addMessage(persisted);
283
- }
284
- finally {
285
- history.close();
286
- }
272
+ await persistAgentMessage('apoc', content, apocConfig, targetSession, rawUsage, durationMs);
273
+ emitToolAuditEvents(response.messages.slice(inputCount), targetSession, 'apoc');
287
274
  this.display.log("Apoc task completed.", { source: "Apoc" });
288
- return {
289
- output: content,
290
- usage: {
291
- provider: apocConfig.provider,
292
- model: apocConfig.model,
293
- inputTokens,
294
- outputTokens,
295
- durationMs,
296
- stepCount,
297
- },
298
- };
275
+ return buildAgentResult(content, apocConfig, rawUsage, durationMs, stepCount);
299
276
  }
300
277
  catch (err) {
301
278
  throw new ProviderError(this.config.apoc?.provider || this.config.llm.provider, err, "Apoc task execution failed");
302
279
  }
303
280
  }
281
+ createDelegateTool() {
282
+ if (!Apoc._delegateTool) {
283
+ Apoc._delegateTool = buildDelegationTool({
284
+ name: "apoc_delegate",
285
+ description: `Delegate a devtools task to Apoc, the specialized development subagent.
286
+
287
+ This tool enqueues a background task and returns an acknowledgement with task id.
288
+ Do not expect final execution output in the same response.
289
+ Each task must contain a single atomic action with a clear expected result.
290
+
291
+ Use this tool when the user asks for ANY of the following:
292
+ - File operations: read, write, create, delete files or directories
293
+ - Shell commands: run scripts, execute commands, check output
294
+ - Git: status, log, diff, commit, push, pull, clone, branch
295
+ - Package management: npm install/update/audit, yarn, package.json inspection
296
+ - Process management: list processes, kill processes, check ports
297
+ - Network: ping hosts, curl URLs, DNS lookups
298
+ - System info: environment variables, OS info, disk space, memory
299
+ - Internet search: search DuckDuckGo and verify facts by reading at least 3 sources via browser_navigate before reporting results.
300
+ - Browser automation: navigate websites (JS/SPA), inspect DOM, click elements, fill forms. Apoc will ask for missing user input (e.g. credentials, form fields) before proceeding.
301
+
302
+ Provide a clear natural language task description. Optionally provide context
303
+ from the current conversation to help Apoc understand the broader goal.`,
304
+ agentKey: "apoc",
305
+ agentLabel: "Apoc",
306
+ auditAgent: "apoc",
307
+ isSync: () => ConfigManager.getInstance().get().apoc?.execution_mode === 'sync',
308
+ notifyText: '🧑‍🔬 Apoc is executing your request...',
309
+ executeSync: (task, context, sessionId) => Apoc.getInstance().execute(task, context, sessionId),
310
+ });
311
+ }
312
+ return Apoc._delegateTool;
313
+ }
304
314
  /** Reload with updated config (called when settings change) */
305
315
  async reload() {
306
316
  this.config = ConfigManager.getInstance().get();
@@ -65,7 +65,24 @@ export class AuditRepository {
65
65
  const rows = this.db.prepare(`
66
66
  SELECT ae.*,
67
67
  CASE
68
- WHEN ae.provider IS NOT NULL AND ae.model IS NOT NULL AND ae.input_tokens IS NOT NULL
68
+ -- Telephonist: prefer audio_cost_per_second when set
69
+ WHEN ae.event_type = 'telephonist'
70
+ AND mp.audio_cost_per_second IS NOT NULL
71
+ AND mp.audio_cost_per_second > 0
72
+ THEN
73
+ COALESCE(CAST(json_extract(ae.metadata, '$.audio_duration_seconds') AS REAL), 0)
74
+ * mp.audio_cost_per_second
75
+ -- Telephonist with token-based pricing (e.g. Gemini/OpenRouter with real tokens)
76
+ WHEN ae.event_type = 'telephonist'
77
+ AND ae.provider IS NOT NULL AND ae.model IS NOT NULL
78
+ AND ae.input_tokens IS NOT NULL AND ae.input_tokens > 0
79
+ THEN (
80
+ COALESCE(ae.input_tokens, 0) / 1000000.0 * COALESCE(mp.input_price_per_1m, 0) +
81
+ COALESCE(ae.output_tokens, 0) / 1000000.0 * COALESCE(mp.output_price_per_1m, 0)
82
+ )
83
+ -- All other events: token-based
84
+ WHEN ae.event_type != 'telephonist'
85
+ AND ae.provider IS NOT NULL AND ae.model IS NOT NULL AND ae.input_tokens IS NOT NULL
69
86
  THEN (
70
87
  COALESCE(ae.input_tokens, 0) / 1000000.0 * COALESCE(mp.input_price_per_1m, 0) +
71
88
  COALESCE(ae.output_tokens, 0) / 1000000.0 * COALESCE(mp.output_price_per_1m, 0)
@@ -84,11 +101,16 @@ export class AuditRepository {
84
101
  const events = this.getBySession(sessionId, { limit: 10_000 });
85
102
  const llmEvents = events.filter(e => e.event_type === 'llm_call');
86
103
  const toolEvents = events.filter(e => e.event_type === 'tool_call' || e.event_type === 'mcp_tool');
87
- const totalCostUsd = llmEvents.reduce((sum, e) => sum + (e.estimated_cost_usd ?? 0), 0);
104
+ const telephonistEvents = events.filter(e => e.event_type === 'telephonist');
105
+ const totalCostUsd = [...llmEvents, ...telephonistEvents].reduce((sum, e) => sum + (e.estimated_cost_usd ?? 0), 0);
88
106
  const totalDurationMs = events.reduce((sum, e) => sum + (e.duration_ms ?? 0), 0);
89
- // By agent
107
+ const totalAudioSeconds = telephonistEvents.reduce((sum, e) => {
108
+ const meta = e.metadata ? JSON.parse(e.metadata) : null;
109
+ return sum + (meta?.audio_duration_seconds ?? 0);
110
+ }, 0);
111
+ // By agent (llm + telephonist)
90
112
  const agentMap = new Map();
91
- for (const e of llmEvents) {
113
+ for (const e of [...llmEvents, ...telephonistEvents]) {
92
114
  const key = e.agent ?? 'unknown';
93
115
  const existing = agentMap.get(key) ?? { llmCalls: 0, inputTokens: 0, outputTokens: 0, estimatedCostUsd: 0 };
94
116
  agentMap.set(key, {
@@ -98,9 +120,9 @@ export class AuditRepository {
98
120
  estimatedCostUsd: existing.estimatedCostUsd + (e.estimated_cost_usd ?? 0),
99
121
  });
100
122
  }
101
- // By model
123
+ // By model (llm + telephonist)
102
124
  const modelMap = new Map();
103
- for (const e of llmEvents) {
125
+ for (const e of [...llmEvents, ...telephonistEvents]) {
104
126
  if (!e.model)
105
127
  continue;
106
128
  const key = `${e.provider}/${e.model}`;
@@ -116,6 +138,7 @@ export class AuditRepository {
116
138
  return {
117
139
  totalCostUsd,
118
140
  totalDurationMs,
141
+ totalAudioSeconds,
119
142
  llmCallCount: llmEvents.length,
120
143
  toolCallCount: toolEvents.length,
121
144
  byAgent: Array.from(agentMap.entries()).map(([agent, s]) => ({ agent, ...s })),
@@ -129,4 +152,168 @@ export class AuditRepository {
129
152
  })),
130
153
  };
131
154
  }
155
+ getGlobalSummary() {
156
+ // Reusable cost expression (telephonist + token-based)
157
+ const costExpr = `
158
+ CASE
159
+ WHEN ae.event_type = 'telephonist'
160
+ AND mp.audio_cost_per_second IS NOT NULL AND mp.audio_cost_per_second > 0
161
+ THEN COALESCE(CAST(json_extract(ae.metadata, '$.audio_duration_seconds') AS REAL), 0)
162
+ * mp.audio_cost_per_second
163
+ WHEN ae.event_type = 'telephonist'
164
+ AND ae.input_tokens IS NOT NULL AND ae.input_tokens > 0
165
+ THEN COALESCE(ae.input_tokens, 0) / 1000000.0 * COALESCE(mp.input_price_per_1m, 0)
166
+ + COALESCE(ae.output_tokens, 0) / 1000000.0 * COALESCE(mp.output_price_per_1m, 0)
167
+ WHEN ae.event_type != 'telephonist'
168
+ AND ae.provider IS NOT NULL AND ae.input_tokens IS NOT NULL
169
+ THEN COALESCE(ae.input_tokens, 0) / 1000000.0 * COALESCE(mp.input_price_per_1m, 0)
170
+ + COALESCE(ae.output_tokens, 0) / 1000000.0 * COALESCE(mp.output_price_per_1m, 0)
171
+ ELSE 0
172
+ END`;
173
+ // ── Sessions ─────────────────────────────────────────────────────────────
174
+ const sessionsRow = this.db.prepare(`
175
+ SELECT
176
+ COUNT(*) as total,
177
+ SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
178
+ SUM(CASE WHEN status = 'paused' THEN 1 ELSE 0 END) as paused,
179
+ SUM(CASE WHEN status = 'archived' THEN 1 ELSE 0 END) as archived,
180
+ SUM(CASE WHEN status = 'deleted' THEN 1 ELSE 0 END) as deleted
181
+ FROM sessions
182
+ `).get();
183
+ const withAuditRow = this.db.prepare(`SELECT COUNT(DISTINCT session_id) as n FROM audit_events`).get();
184
+ // ── Global totals (single JOIN pass) ─────────────────────────────────────
185
+ const totalsRow = this.db.prepare(`
186
+ SELECT
187
+ COUNT(*) as totalEventCount,
188
+ SUM(CASE WHEN ae.event_type = 'llm_call' THEN 1 ELSE 0 END) as llmCallCount,
189
+ SUM(CASE WHEN ae.event_type = 'tool_call' THEN 1 ELSE 0 END) as toolCallCount,
190
+ SUM(CASE WHEN ae.event_type = 'mcp_tool' THEN 1 ELSE 0 END) as mcpToolCount,
191
+ SUM(CASE WHEN ae.event_type = 'skill_executed' THEN 1 ELSE 0 END) as skillCount,
192
+ SUM(CASE WHEN ae.event_type = 'memory_recovery' THEN 1 ELSE 0 END) as memoryRecoveryCount,
193
+ SUM(CASE WHEN ae.event_type = 'chronos_job' THEN 1 ELSE 0 END) as chronosJobCount,
194
+ SUM(CASE WHEN ae.event_type = 'task_created' THEN 1 ELSE 0 END) as taskCreatedCount,
195
+ SUM(CASE WHEN ae.event_type = 'task_completed' THEN 1 ELSE 0 END) as taskCompletedCount,
196
+ SUM(CASE WHEN ae.event_type = 'telephonist' THEN 1 ELSE 0 END) as telephonistCount,
197
+ COALESCE(SUM(ae.input_tokens), 0) as totalInputTokens,
198
+ COALESCE(SUM(ae.output_tokens), 0) as totalOutputTokens,
199
+ COALESCE(SUM(ae.duration_ms), 0) as totalDurationMs,
200
+ COALESCE(SUM(
201
+ CASE WHEN ae.event_type = 'telephonist'
202
+ THEN COALESCE(CAST(json_extract(ae.metadata,'$.audio_duration_seconds') AS REAL),0)
203
+ ELSE 0 END
204
+ ), 0) as totalAudioSeconds,
205
+ COALESCE(SUM(${costExpr}), 0) as estimatedCostUsd
206
+ FROM audit_events ae
207
+ LEFT JOIN model_pricing mp ON mp.provider = ae.provider AND mp.model = ae.model
208
+ `).get();
209
+ // ── By agent ─────────────────────────────────────────────────────────────
210
+ const byAgentRows = this.db.prepare(`
211
+ SELECT
212
+ COALESCE(ae.agent, 'unknown') as agent,
213
+ SUM(CASE WHEN ae.event_type = 'llm_call' THEN 1 ELSE 0 END) as llmCalls,
214
+ SUM(CASE WHEN ae.event_type IN ('tool_call','mcp_tool') THEN 1 ELSE 0 END) as toolCalls,
215
+ COALESCE(SUM(ae.input_tokens), 0) as inputTokens,
216
+ COALESCE(SUM(ae.output_tokens), 0) as outputTokens,
217
+ COALESCE(SUM(ae.duration_ms), 0) as totalDurationMs,
218
+ COALESCE(SUM(${costExpr}), 0) as estimatedCostUsd
219
+ FROM audit_events ae
220
+ LEFT JOIN model_pricing mp ON mp.provider = ae.provider AND mp.model = ae.model
221
+ GROUP BY ae.agent
222
+ ORDER BY estimatedCostUsd DESC
223
+ `).all();
224
+ // ── By model ─────────────────────────────────────────────────────────────
225
+ const byModelRows = this.db.prepare(`
226
+ SELECT
227
+ ae.provider,
228
+ ae.model,
229
+ COUNT(*) as calls,
230
+ COALESCE(SUM(ae.input_tokens), 0) as inputTokens,
231
+ COALESCE(SUM(ae.output_tokens), 0) as outputTokens,
232
+ COALESCE(SUM(${costExpr}), 0) as estimatedCostUsd
233
+ FROM audit_events ae
234
+ LEFT JOIN model_pricing mp ON mp.provider = ae.provider AND mp.model = ae.model
235
+ WHERE ae.model IS NOT NULL
236
+ GROUP BY ae.provider, ae.model
237
+ ORDER BY estimatedCostUsd DESC
238
+ `).all();
239
+ // ── Top tools ─────────────────────────────────────────────────────────────
240
+ const topToolsRows = this.db.prepare(`
241
+ SELECT
242
+ ae.tool_name,
243
+ ae.agent,
244
+ ae.event_type,
245
+ COUNT(*) as count,
246
+ SUM(CASE WHEN ae.status = 'error' THEN 1 ELSE 0 END) as errorCount
247
+ FROM audit_events ae
248
+ WHERE ae.tool_name IS NOT NULL
249
+ AND ae.event_type IN ('tool_call','mcp_tool')
250
+ GROUP BY ae.tool_name, ae.agent, ae.event_type
251
+ ORDER BY count DESC
252
+ LIMIT 20
253
+ `).all();
254
+ // ── Recent sessions ──────────────────────────────────────────────────────
255
+ const recentRows = this.db.prepare(`
256
+ SELECT
257
+ ae.session_id,
258
+ s.title,
259
+ s.status,
260
+ s.started_at,
261
+ COUNT(ae.id) as event_count,
262
+ SUM(CASE WHEN ae.event_type = 'llm_call' THEN 1 ELSE 0 END) as llmCallCount,
263
+ COALESCE(SUM(ae.duration_ms), 0) as totalDurationMs,
264
+ COALESCE(SUM(${costExpr}), 0) as estimatedCostUsd
265
+ FROM audit_events ae
266
+ INNER JOIN sessions s ON ae.session_id = s.id
267
+ LEFT JOIN model_pricing mp ON mp.provider = ae.provider AND mp.model = ae.model
268
+ GROUP BY ae.session_id
269
+ ORDER BY MAX(ae.created_at) DESC
270
+ LIMIT 20
271
+ `).all();
272
+ // ── Daily activity (last 30 days) ─────────────────────────────────────────
273
+ const since = Date.now() - 30 * 24 * 60 * 60 * 1000;
274
+ const dailyRows = this.db.prepare(`
275
+ SELECT
276
+ date(ae.created_at / 1000, 'unixepoch') as date,
277
+ COUNT(*) as eventCount,
278
+ SUM(CASE WHEN ae.event_type = 'llm_call' THEN 1 ELSE 0 END) as llmCallCount,
279
+ COALESCE(SUM(${costExpr}), 0) as estimatedCostUsd
280
+ FROM audit_events ae
281
+ LEFT JOIN model_pricing mp ON mp.provider = ae.provider AND mp.model = ae.model
282
+ WHERE ae.created_at >= ?
283
+ GROUP BY date
284
+ ORDER BY date ASC
285
+ `).all(since);
286
+ return {
287
+ sessions: {
288
+ total: sessionsRow?.total ?? 0,
289
+ active: sessionsRow?.active ?? 0,
290
+ paused: sessionsRow?.paused ?? 0,
291
+ archived: sessionsRow?.archived ?? 0,
292
+ deleted: sessionsRow?.deleted ?? 0,
293
+ withAudit: withAuditRow?.n ?? 0,
294
+ },
295
+ totals: {
296
+ estimatedCostUsd: totalsRow?.estimatedCostUsd ?? 0,
297
+ totalDurationMs: totalsRow?.totalDurationMs ?? 0,
298
+ totalAudioSeconds: totalsRow?.totalAudioSeconds ?? 0,
299
+ totalInputTokens: totalsRow?.totalInputTokens ?? 0,
300
+ totalOutputTokens: totalsRow?.totalOutputTokens ?? 0,
301
+ totalEventCount: totalsRow?.totalEventCount ?? 0,
302
+ llmCallCount: totalsRow?.llmCallCount ?? 0,
303
+ toolCallCount: totalsRow?.toolCallCount ?? 0,
304
+ mcpToolCount: totalsRow?.mcpToolCount ?? 0,
305
+ skillCount: totalsRow?.skillCount ?? 0,
306
+ memoryRecoveryCount: totalsRow?.memoryRecoveryCount ?? 0,
307
+ chronosJobCount: totalsRow?.chronosJobCount ?? 0,
308
+ taskCreatedCount: totalsRow?.taskCreatedCount ?? 0,
309
+ taskCompletedCount: totalsRow?.taskCompletedCount ?? 0,
310
+ telephonistCount: totalsRow?.telephonistCount ?? 0,
311
+ },
312
+ byAgent: byAgentRows,
313
+ byModel: byModelRows,
314
+ topTools: topToolsRows,
315
+ recentSessions: recentRows,
316
+ dailyActivity: dailyRows,
317
+ };
318
+ }
132
319
  }