morpheus-cli 0.8.9 → 0.9.1

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 (57) hide show
  1. package/dist/channels/discord.js +133 -6
  2. package/dist/channels/telegram.js +23 -17
  3. package/dist/http/api.js +2 -3
  4. package/dist/runtime/__tests__/keymaker.test.js +5 -2
  5. package/dist/runtime/apoc.js +7 -7
  6. package/dist/{devkit/registry.js → runtime/devkit-instrument.js} +5 -29
  7. package/dist/runtime/keymaker.js +6 -5
  8. package/dist/runtime/memory/sati/service.js +1 -1
  9. package/dist/runtime/memory/sqlite.js +31 -124
  10. package/dist/runtime/neo.js +1 -1
  11. package/dist/runtime/oracle.js +30 -52
  12. package/dist/runtime/smiths/delegator.js +2 -2
  13. package/dist/runtime/trinity.js +1 -1
  14. package/dist/ui/assets/{AuditDashboard-5sA8Sd8S.js → AuditDashboard-nVV9KKFp.js} +1 -1
  15. package/dist/ui/assets/Chat-ChsmnZzq.js +41 -0
  16. package/dist/ui/assets/{Chronos-BAjeLobF.js → Chronos-kgO7IkEj.js} +1 -1
  17. package/dist/ui/assets/{ConfirmationModal-fvgnOWTY.js → ConfirmationModal-D1BYPXJ4.js} +1 -1
  18. package/dist/ui/assets/{Dashboard-Ca5mSefz.js → Dashboard-DWB5NwQn.js} +1 -1
  19. package/dist/ui/assets/{DeleteConfirmationModal-A8EmnHoa.js → DeleteConfirmationModal-CgIMbyB7.js} +1 -1
  20. package/dist/ui/assets/{Logs-CYu7se7R.js → Logs-DGdRnEFi.js} +1 -1
  21. package/dist/ui/assets/{MCPManager-DsDA_ZVT.js → MCPManager-BDjWMRRX.js} +1 -1
  22. package/dist/ui/assets/{ModelPricing-DnSm_Nh-.js → ModelPricing-DAk1sS7D.js} +1 -1
  23. package/dist/ui/assets/{Notifications-CiljQzvM.js → Notifications-DMEq6EZR.js} +1 -1
  24. package/dist/ui/assets/{SatiMemories-rnO2b0LG.js → SatiMemories-BxicQE35.js} +1 -1
  25. package/dist/ui/assets/{SessionAudit-Dfvhge3Z.js → SessionAudit-CKJQf9LU.js} +1 -1
  26. package/dist/ui/assets/{Settings-OQlHAJoy.js → Settings-CulMd4Qr.js} +1 -1
  27. package/dist/ui/assets/{Skills-Crsybug0.js → Skills-DPoqYa8Y.js} +1 -1
  28. package/dist/ui/assets/{Smiths-wm90jRDT.js → Smiths-Clamjlph.js} +1 -1
  29. package/dist/ui/assets/{Tasks-C5FMu_Yu.js → Tasks-BfTkhB1J.js} +1 -1
  30. package/dist/ui/assets/{TrinityDatabases-BzYfecKI.js → TrinityDatabases-BmM1S9aQ.js} +1 -1
  31. package/dist/ui/assets/{UsageStats-CBo2vW2n.js → UsageStats-aAu2DFlb.js} +1 -1
  32. package/dist/ui/assets/{WebhookManager-0tDFkfHd.js → WebhookManager-DdnRSWl9.js} +1 -1
  33. package/dist/ui/assets/{audit-B-F8XPLi.js → audit-CqszEkOd.js} +1 -1
  34. package/dist/ui/assets/{chronos-BvMxfBQH.js → chronos-CPwFWid9.js} +1 -1
  35. package/dist/ui/assets/{config-DteVgNGR.js → config-D0DePxKu.js} +1 -1
  36. package/dist/ui/assets/{index-Cwqr-n0Y.js → index-BxVeRyTh.js} +2 -2
  37. package/dist/ui/assets/index-OLhpm8I7.css +1 -0
  38. package/dist/ui/assets/{mcp-DxzodOdH.js → mcp-Gjc3IZpO.js} +1 -1
  39. package/dist/ui/assets/{skills--hAyQnmG.js → skills-B5DnmnHW.js} +1 -1
  40. package/dist/ui/assets/{stats-Cibaisqd.js → stats-BAse7jj0.js} +1 -1
  41. package/dist/ui/index.html +2 -2
  42. package/dist/ui/sw.js +1 -1
  43. package/package.json +6 -4
  44. package/dist/devkit/adapters/shell.js +0 -80
  45. package/dist/devkit/index.js +0 -11
  46. package/dist/devkit/tools/browser.js +0 -825
  47. package/dist/devkit/tools/filesystem.js +0 -235
  48. package/dist/devkit/tools/git.js +0 -226
  49. package/dist/devkit/tools/network.js +0 -165
  50. package/dist/devkit/tools/packages.js +0 -73
  51. package/dist/devkit/tools/processes.js +0 -130
  52. package/dist/devkit/tools/shell.js +0 -106
  53. package/dist/devkit/tools/system.js +0 -132
  54. package/dist/devkit/types.js +0 -1
  55. package/dist/devkit/utils.js +0 -45
  56. package/dist/ui/assets/Chat-CjxeAQmd.js +0 -41
  57. package/dist/ui/assets/index-DcfyUdLI.css +0 -1
@@ -1,4 +1,4 @@
1
- import { Client, GatewayIntentBits, Partials, Events, ChannelType, REST, Routes, SlashCommandBuilder, } from 'discord.js';
1
+ import { Client, GatewayIntentBits, Partials, Events, ChannelType, REST, Routes, SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, } from 'discord.js';
2
2
  import chalk from 'chalk';
3
3
  import fs from 'fs-extra';
4
4
  import path from 'path';
@@ -26,6 +26,15 @@ const SLASH_COMMANDS = [
26
26
  .setName('newsession')
27
27
  .setDescription('Archive current session and start a new one')
28
28
  .setDMPermission(true),
29
+ new SlashCommandBuilder()
30
+ .setName('sessions')
31
+ .setDescription('List all sessions and switch between them')
32
+ .setDMPermission(true),
33
+ new SlashCommandBuilder()
34
+ .setName('session_switch')
35
+ .setDescription('Switch to a specific session')
36
+ .addStringOption(opt => opt.setName('id').setDescription('Session ID to switch to').setRequired(true))
37
+ .setDMPermission(true),
29
38
  new SlashCommandBuilder()
30
39
  .setName('chronos')
31
40
  .setDescription('Schedule a prompt for the Oracle')
@@ -103,6 +112,8 @@ export class DiscordAdapter {
103
112
  display = DisplayManager.getInstance();
104
113
  config = ConfigManager.getInstance();
105
114
  history = new SQLiteChatMessageHistory({ sessionId: '' });
115
+ /** Per-channel session tracking — which session this Discord adapter is currently using */
116
+ currentSessionId = null;
106
117
  telephonist = null;
107
118
  telephonistProvider = null;
108
119
  telephonistModel = null;
@@ -178,8 +189,8 @@ export class DiscordAdapter {
178
189
  return;
179
190
  this.display.log(`${message.author.tag}: ${text}`, { source: 'Discord' });
180
191
  try {
181
- const sessionId = await this.history.getCurrentSessionOrCreate();
182
- await this.oracle.setSessionId(sessionId);
192
+ const sessionId = this.currentSessionId ?? await this.history.getCurrentSessionOrCreate();
193
+ this.currentSessionId = sessionId;
183
194
  const response = await this.oracle.chat(text, undefined, false, {
184
195
  origin_channel: 'discord',
185
196
  session_id: sessionId,
@@ -292,8 +303,8 @@ export class DiscordAdapter {
292
303
  // Show transcription
293
304
  await channel.send(`🎤 "${text}"`);
294
305
  // Process with Oracle
295
- const sessionId = await this.history.getCurrentSessionOrCreate();
296
- await this.oracle.setSessionId(sessionId);
306
+ const sessionId = this.currentSessionId ?? await this.history.getCurrentSessionOrCreate();
307
+ this.currentSessionId = sessionId;
297
308
  const response = await this.oracle.chat(text, usage, true, {
298
309
  origin_channel: 'discord',
299
310
  session_id: sessionId,
@@ -355,6 +366,12 @@ export class DiscordAdapter {
355
366
  case 'newsession':
356
367
  await this.cmdNewSession(interaction);
357
368
  break;
369
+ case 'sessions':
370
+ await this.cmdSessions(interaction);
371
+ break;
372
+ case 'session_switch':
373
+ await this.cmdSessionSwitch(interaction);
374
+ break;
358
375
  case 'chronos':
359
376
  await this.cmdChronos(interaction);
360
377
  break;
@@ -407,6 +424,8 @@ export class DiscordAdapter {
407
424
  '`/status` — Check Morpheus status',
408
425
  '`/stats` — Token usage statistics',
409
426
  '`/newsession` — Start a new session',
427
+ '`/sessions` — List all sessions (switch, archive, delete)',
428
+ '`/session_switch id:` — Switch to a specific session',
410
429
  '',
411
430
  '**Chronos (Scheduler)**',
412
431
  '`/chronos prompt: time:` — Schedule a job for the Oracle',
@@ -474,14 +493,122 @@ export class DiscordAdapter {
474
493
  async cmdNewSession(interaction) {
475
494
  try {
476
495
  const history = new SQLiteChatMessageHistory({ sessionId: '' });
477
- await history.createNewSession();
496
+ const newSessionId = await history.createNewSession();
478
497
  history.close();
498
+ // Track the new session as the current one for this Discord channel
499
+ this.currentSessionId = newSessionId;
479
500
  await interaction.reply({ content: '✅ New session started.' });
480
501
  }
481
502
  catch (err) {
482
503
  await interaction.reply({ content: `Error: ${err.message}` });
483
504
  }
484
505
  }
506
+ async cmdSessions(interaction) {
507
+ try {
508
+ const history = new SQLiteChatMessageHistory({ sessionId: '' });
509
+ const sessions = (await history.listSessions()).filter((s) => !s.id.startsWith('chronos-job-') && !s.id.startsWith('sati-evaluation'));
510
+ history.close();
511
+ if (sessions.length === 0) {
512
+ await interaction.reply({ content: 'No sessions found.' });
513
+ return;
514
+ }
515
+ const lines = ['**Sessions:**\n'];
516
+ const rows = [];
517
+ for (const session of sessions) {
518
+ const title = session.title || 'Untitled Session';
519
+ const isCurrent = session.id === this.currentSessionId;
520
+ const icon = isCurrent ? '🟢' : '⚪';
521
+ const started = new Date(session.started_at).toLocaleString();
522
+ lines.push(`${icon} **${title}**`);
523
+ lines.push(` ID: \`${session.id}\``);
524
+ lines.push(` Started: ${started}\n`);
525
+ // Discord allows max 5 buttons per row, max 5 rows per message
526
+ if (rows.length < 5) {
527
+ const btns = [];
528
+ if (!isCurrent) {
529
+ btns.push(new ButtonBuilder()
530
+ .setCustomId(`session_switch_${session.id}`)
531
+ .setLabel('Switch')
532
+ .setStyle(ButtonStyle.Primary)
533
+ .setEmoji('➡️'));
534
+ }
535
+ btns.push(new ButtonBuilder()
536
+ .setCustomId(`session_archive_${session.id}`)
537
+ .setLabel('Archive')
538
+ .setStyle(ButtonStyle.Secondary)
539
+ .setEmoji('📂'));
540
+ btns.push(new ButtonBuilder()
541
+ .setCustomId(`session_delete_${session.id}`)
542
+ .setLabel('Delete')
543
+ .setStyle(ButtonStyle.Danger)
544
+ .setEmoji('🗑️'));
545
+ rows.push(new ActionRowBuilder().addComponents(...btns));
546
+ }
547
+ }
548
+ const content = lines.join('\n').slice(0, 2000);
549
+ const reply = await interaction.reply({ content, components: rows, fetchReply: true });
550
+ // Collect button interactions for 60 seconds
551
+ const collector = reply.createMessageComponentCollector({
552
+ componentType: ComponentType.Button,
553
+ time: 60_000,
554
+ });
555
+ collector.on('collect', async (btn) => {
556
+ try {
557
+ if (btn.customId.startsWith('session_switch_')) {
558
+ const sid = btn.customId.replace('session_switch_', '');
559
+ const h = new SQLiteChatMessageHistory({ sessionId: '' });
560
+ await h.switchSession(sid);
561
+ h.close();
562
+ this.currentSessionId = sid;
563
+ await btn.reply({ content: `✅ Switched to session \`${sid}\`.`, ephemeral: true });
564
+ }
565
+ else if (btn.customId.startsWith('session_archive_')) {
566
+ const sid = btn.customId.replace('session_archive_', '');
567
+ const h = new SQLiteChatMessageHistory({ sessionId: '' });
568
+ await h.archiveSession(sid);
569
+ h.close();
570
+ if (this.currentSessionId === sid)
571
+ this.currentSessionId = null;
572
+ await btn.reply({ content: `📂 Session \`${sid}\` archived.`, ephemeral: true });
573
+ }
574
+ else if (btn.customId.startsWith('session_delete_')) {
575
+ const sid = btn.customId.replace('session_delete_', '');
576
+ const h = new SQLiteChatMessageHistory({ sessionId: '' });
577
+ await h.deleteSession(sid);
578
+ h.close();
579
+ if (this.currentSessionId === sid)
580
+ this.currentSessionId = null;
581
+ await btn.reply({ content: `🗑️ Session \`${sid}\` deleted.`, ephemeral: true });
582
+ }
583
+ }
584
+ catch (err) {
585
+ await btn.reply({ content: `Error: ${err.message}`, ephemeral: true }).catch(() => { });
586
+ }
587
+ });
588
+ collector.on('end', async () => {
589
+ try {
590
+ await interaction.editReply({ components: [] });
591
+ }
592
+ catch { /* message may have been deleted */ }
593
+ });
594
+ }
595
+ catch (err) {
596
+ await interaction.reply({ content: `Error: ${err.message}` });
597
+ }
598
+ }
599
+ async cmdSessionSwitch(interaction) {
600
+ const sessionId = interaction.options.getString('id', true);
601
+ try {
602
+ const history = new SQLiteChatMessageHistory({ sessionId: '' });
603
+ await history.switchSession(sessionId);
604
+ history.close();
605
+ this.currentSessionId = sessionId;
606
+ await interaction.reply({ content: `✅ Switched to session \`${sessionId}\`.` });
607
+ }
608
+ catch (err) {
609
+ await interaction.reply({ content: `Error: ${err.message}` });
610
+ }
611
+ }
485
612
  async cmdChronos(interaction) {
486
613
  const prompt = interaction.options.getString('prompt', true);
487
614
  const timeExpr = interaction.options.getString('time', true);
@@ -144,6 +144,8 @@ export class TelegramAdapter {
144
144
  telephonistProvider = null;
145
145
  telephonistModel = null;
146
146
  history = new SQLiteChatMessageHistory({ sessionId: '' });
147
+ /** Per-channel session tracking — which session this Telegram adapter is currently using */
148
+ currentSessionId = null;
147
149
  RATE_LIMIT_MS = 3000; // minimum ms between requests per user
148
150
  rateLimiter = new Map(); // userId -> last request timestamp
149
151
  // Pending Chronos create confirmations (userId -> job data + expiry)
@@ -230,8 +232,8 @@ export class TelegramAdapter {
230
232
  try {
231
233
  // Send "typing" status
232
234
  await ctx.sendChatAction('typing');
233
- const sessionId = await this.history.getCurrentSessionOrCreate();
234
- await this.oracle.setSessionId(sessionId);
235
+ const sessionId = this.currentSessionId ?? await this.history.getCurrentSessionOrCreate();
236
+ this.currentSessionId = sessionId;
235
237
  // Process with Agent
236
238
  const response = await this.oracle.chat(text, undefined, false, {
237
239
  origin_channel: 'telegram',
@@ -317,9 +319,9 @@ export class TelegramAdapter {
317
319
  this.display.log(`Transcription success for @${user}: "${text}"`, { source: 'Telephonist', level: 'success' });
318
320
  // Audit: record telephonist execution
319
321
  try {
320
- const sessionId = await this.history.getCurrentSessionOrCreate();
322
+ const auditSessionId = this.currentSessionId ?? await this.history.getCurrentSessionOrCreate();
321
323
  AuditRepository.getInstance().insert({
322
- session_id: sessionId,
324
+ session_id: auditSessionId,
323
325
  event_type: 'telephonist',
324
326
  agent: 'telephonist',
325
327
  provider: config.audio.provider,
@@ -345,8 +347,8 @@ export class TelegramAdapter {
345
347
  // So I should treat 'text' as if it was a text message.
346
348
  await ctx.reply(`🎤 Transcription: "${text}"`);
347
349
  await ctx.sendChatAction('typing');
348
- const sessionId = await this.history.getCurrentSessionOrCreate();
349
- await this.oracle.setSessionId(sessionId);
350
+ const sessionId = this.currentSessionId ?? await this.history.getCurrentSessionOrCreate();
351
+ this.currentSessionId = sessionId;
350
352
  // Process with Agent
351
353
  const response = await this.oracle.chat(text, usage, true, {
352
354
  origin_channel: 'telegram',
@@ -380,9 +382,9 @@ export class TelegramAdapter {
380
382
  const detail = error?.cause?.message || error?.response?.data?.error?.message || error.message;
381
383
  this.display.log(`Audio processing error for @${user}: ${detail}`, { source: 'Telephonist', level: 'error' });
382
384
  try {
383
- const sessionId = await this.history.getCurrentSessionOrCreate();
385
+ const auditSessionId = this.currentSessionId ?? 'default';
384
386
  AuditRepository.getInstance().insert({
385
- session_id: sessionId,
387
+ session_id: auditSessionId,
386
388
  event_type: 'telephonist',
387
389
  agent: 'telephonist',
388
390
  provider: this.config.get().audio.provider,
@@ -428,12 +430,14 @@ export class TelegramAdapter {
428
430
  return;
429
431
  }
430
432
  try {
431
- // Obter a sessão atual antes de alternar
433
+ // Validate session exists and is usable
432
434
  const history = new SQLiteChatMessageHistory({ sessionId: "" });
433
- // Alternar para a nova sessão
434
435
  await history.switchSession(sessionId);
436
+ history.close();
437
+ // Track this session as the current one for this Telegram channel
438
+ this.currentSessionId = sessionId;
435
439
  await ctx.answerCbQuery();
436
- // Remover a mensagem anterior e enviar confirmação
440
+ // Remove the previous message and send confirmation
437
441
  if (ctx.updateType === 'callback_query') {
438
442
  ctx.deleteMessage().catch(() => { });
439
443
  }
@@ -1154,7 +1158,10 @@ export class TelegramAdapter {
1154
1158
  async handleApproveNewSessionCommand(ctx, user) {
1155
1159
  try {
1156
1160
  const history = new SQLiteChatMessageHistory({ sessionId: "" });
1157
- await history.createNewSession();
1161
+ const newSessionId = await history.createNewSession();
1162
+ history.close();
1163
+ // Track the new session as the current one for this Telegram channel
1164
+ this.currentSessionId = newSessionId;
1158
1165
  }
1159
1166
  catch (e) {
1160
1167
  await ctx.reply(`Error creating new session: ${e.message}`);
@@ -1167,21 +1174,20 @@ export class TelegramAdapter {
1167
1174
  // callback_data limit and they are not user-managed sessions.
1168
1175
  const sessions = (await history.listSessions()).filter((s) => !s.id.startsWith('chronos-job-') && !s.id.startsWith('sati-evaluation'));
1169
1176
  if (sessions.length === 0) {
1170
- await ctx.reply('No active or paused sessions found\\.', { parse_mode: 'MarkdownV2' });
1177
+ await ctx.reply('No sessions found\\.', { parse_mode: 'MarkdownV2' });
1171
1178
  return;
1172
1179
  }
1173
1180
  let response = '*Sessions:*\n\n';
1174
1181
  const keyboard = [];
1175
1182
  for (const session of sessions) {
1176
1183
  const title = session.title || 'Untitled Session';
1177
- const statusEmoji = session.status === 'active' ? '🟢' : '🟡';
1184
+ const isCurrent = session.id === this.currentSessionId;
1185
+ const statusEmoji = isCurrent ? '🟢' : '⚪';
1178
1186
  response += `${statusEmoji} *${escMdRaw(title)}*\n`;
1179
1187
  response += `\\- ID: \`${escMdRaw(session.id)}\`\n`;
1180
- response += `\\- Status: ${escMdRaw(session.status)}\n`;
1181
1188
  response += `\\- Started: ${escMdRaw(new Date(session.started_at).toLocaleString())}\n\n`;
1182
- // Adicionar botão inline para alternar para esta sessão
1183
1189
  const sessionButtons = [];
1184
- if (session.status !== 'active') {
1190
+ if (!isCurrent) {
1185
1191
  sessionButtons.push({
1186
1192
  text: `➡️ Switch`,
1187
1193
  callback_data: `switch_session_${session.id}`
package/dist/http/api.js CHANGED
@@ -61,8 +61,7 @@ export function createApiRouter(oracle, chronosWorker) {
61
61
  });
62
62
  router.post('/sessions', async (req, res) => {
63
63
  try {
64
- await history.createNewSession();
65
- const newSessionId = await history.getCurrentSessionOrCreate(); // Should be the new one
64
+ const newSessionId = await history.createNewSession();
66
65
  res.json({ success: true, id: newSessionId, message: 'New session started' });
67
66
  }
68
67
  catch (err) {
@@ -248,7 +247,7 @@ export function createApiRouter(oracle, chronosWorker) {
248
247
  }
249
248
  try {
250
249
  const { message, sessionId } = parsed.data;
251
- await oracle.setSessionId(sessionId);
250
+ // Session is passed via taskContext — no need to mutate global Oracle state.
252
251
  const response = await oracle.chat(message, undefined, false, {
253
252
  origin_channel: 'ui',
254
253
  session_id: sessionId,
@@ -32,12 +32,15 @@ vi.mock('../display.js', () => ({
32
32
  })),
33
33
  },
34
34
  }));
35
- vi.mock('../../devkit/index.js', () => ({
35
+ vi.mock('morpheus-devkit', () => ({
36
36
  buildDevKit: vi.fn(() => [
37
37
  { name: 'fs_read', description: 'Read file' },
38
38
  { name: 'shell_exec', description: 'Execute shell command' },
39
39
  ]),
40
40
  }));
41
+ vi.mock('../devkit-instrument.js', () => ({
42
+ instrumentDevKitTools: vi.fn((tools) => tools),
43
+ }));
41
44
  vi.mock('../tools/factory.js', () => ({
42
45
  Construtor: {
43
46
  create: vi.fn(() => Promise.resolve([
@@ -97,7 +100,7 @@ describe('Keymaker', () => {
97
100
  describe('initialize()', () => {
98
101
  it('should initialize agent with all tools', async () => {
99
102
  const { ProviderFactory } = await import('../providers/factory.js');
100
- const { buildDevKit } = await import('../../devkit/index.js');
103
+ const { buildDevKit } = await import('morpheus-devkit');
101
104
  const { Construtor } = await import('../tools/factory.js');
102
105
  const keymaker = new Keymaker('test-skill', '# Instructions');
103
106
  await keymaker.initialize();
@@ -3,7 +3,8 @@ import { ConfigManager } from "../config/manager.js";
3
3
  import { ProviderFactory } from "./providers/factory.js";
4
4
  import { ProviderError } from "./errors.js";
5
5
  import { DisplayManager } from "./display.js";
6
- import { buildDevKit } from "../devkit/index.js";
6
+ import { buildDevKit } from "morpheus-devkit";
7
+ import { instrumentDevKitTools } from "./devkit-instrument.js";
7
8
  import { extractRawUsage, persistAgentMessage, buildAgentResult, emitToolAuditEvents } from "./subagent-utils.js";
8
9
  import { buildDelegationTool } from "./tools/delegation-utils.js";
9
10
  /**
@@ -47,9 +48,9 @@ export class Apoc {
47
48
  const devkit = ConfigManager.getInstance().getDevKitConfig();
48
49
  const timeout_ms = devkit.timeout_ms || this.config.apoc?.timeout_ms || 30_000;
49
50
  const personality = this.config.apoc?.personality || 'pragmatic_dev';
50
- // Import all devkit tool factories (side-effect registration)
51
- await import("../devkit/index.js");
52
- const tools = buildDevKit({
51
+ // Import morpheus-devkit to trigger side-effect tool registration
52
+ await import("morpheus-devkit");
53
+ const rawTools = buildDevKit({
53
54
  working_dir: devkit.sandbox_dir || process.cwd(),
54
55
  allowed_commands: devkit.allowed_shell_commands || [],
55
56
  timeout_ms,
@@ -59,9 +60,8 @@ export class Apoc {
59
60
  enable_shell: devkit.enable_shell,
60
61
  enable_git: devkit.enable_git,
61
62
  enable_network: devkit.enable_network,
62
- getSessionId: () => Apoc.currentSessionId,
63
- getAgent: () => 'apoc',
64
63
  });
64
+ const tools = instrumentDevKitTools(rawTools, () => Apoc.currentSessionId, () => 'apoc');
65
65
  this.display.log(`Apoc initialized with ${tools.length} DevKit tools (sandbox_dir: ${devkit.sandbox_dir}, personality: ${personality})`, { source: "Apoc" });
66
66
  try {
67
67
  this.agent = await ProviderFactory.createBare(apocConfig, tools);
@@ -259,7 +259,7 @@ ${context ? `CONTEXT FROM ORACLE:\n${context}` : ""}
259
259
  try {
260
260
  const inputCount = messages.length;
261
261
  const startMs = Date.now();
262
- const response = await this.agent.invoke({ messages }, { recursionLimit: 100 });
262
+ const response = await this.agent.invoke({ messages }, { recursionLimit: 50 });
263
263
  const durationMs = Date.now() - startMs;
264
264
  const apocConfig = this.config.apoc || this.config.llm;
265
265
  const lastMessage = response.messages[response.messages.length - 1];
@@ -1,15 +1,4 @@
1
- import { AuditRepository } from '../runtime/audit/repository.js';
2
- const factories = [];
3
- export function registerToolFactory(factory, category = 'system') {
4
- factories.push({ category, factory });
5
- }
6
- /** Categories that can be toggled off via DevKit config */
7
- const TOGGLEABLE_CATEGORIES = {
8
- filesystem: 'enable_filesystem',
9
- shell: 'enable_shell',
10
- git: 'enable_git',
11
- network: 'enable_network',
12
- };
1
+ import { AuditRepository } from './audit/repository.js';
13
2
  /**
14
3
  * Wraps a StructuredTool to record audit events on each invocation.
15
4
  * The `getSessionId` getter is called at invocation time so it reflects
@@ -51,22 +40,9 @@ function instrumentTool(tool, getSessionId, getAgent) {
51
40
  return tool;
52
41
  }
53
42
  /**
54
- * Builds the full DevKit tool set for a given context.
55
- * Each factory receives the context (working_dir, allowed_commands, etc.)
56
- * and returns tools with the context captured in closure.
57
- * Disabled categories are filtered out based on context flags.
58
- * All tools are wrapped with audit instrumentation.
43
+ * Wraps all DevKit tools with audit instrumentation.
44
+ * Call this after buildDevKit() to add Morpheus audit tracking.
59
45
  */
60
- export function buildDevKit(ctx) {
61
- const getSessionId = ctx.getSessionId ?? (() => undefined);
62
- const getAgent = ctx.getAgent ?? (() => 'apoc');
63
- return factories
64
- .filter(({ category }) => {
65
- const ctxKey = TOGGLEABLE_CATEGORIES[category];
66
- if (!ctxKey)
67
- return true; // non-toggleable categories always load
68
- return ctx[ctxKey] !== false;
69
- })
70
- .flatMap(({ factory }) => factory(ctx))
71
- .map(tool => instrumentTool(tool, getSessionId, getAgent));
46
+ export function instrumentDevKitTools(tools, getSessionId, getAgent) {
47
+ return tools.map(tool => instrumentTool(tool, getSessionId, getAgent));
72
48
  }
@@ -3,7 +3,8 @@ import { ConfigManager } from "../config/manager.js";
3
3
  import { ProviderFactory } from "./providers/factory.js";
4
4
  import { ProviderError } from "./errors.js";
5
5
  import { DisplayManager } from "./display.js";
6
- import { buildDevKit } from "../devkit/index.js";
6
+ import { buildDevKit } from "morpheus-devkit";
7
+ import { instrumentDevKitTools } from "./devkit-instrument.js";
7
8
  import { Construtor } from "./tools/factory.js";
8
9
  import { morpheusTools } from "./tools/index.js";
9
10
  import { SkillRegistry } from "./skills/registry.js";
@@ -36,8 +37,8 @@ export class Keymaker {
36
37
  // Build DevKit tools (filesystem, shell, git, browser, network, etc.)
37
38
  const devkit = ConfigManager.getInstance().getDevKitConfig();
38
39
  const timeout_ms = devkit.timeout_ms || 30_000;
39
- await import("../devkit/index.js");
40
- const devKitTools = buildDevKit({
40
+ await import("morpheus-devkit");
41
+ const devKitTools = instrumentDevKitTools(buildDevKit({
41
42
  working_dir: devkit.sandbox_dir || process.cwd(),
42
43
  allowed_commands: devkit.allowed_shell_commands || [],
43
44
  timeout_ms,
@@ -47,7 +48,7 @@ export class Keymaker {
47
48
  enable_shell: devkit.enable_shell,
48
49
  enable_git: devkit.enable_git,
49
50
  enable_network: devkit.enable_network,
50
- });
51
+ }), () => undefined, () => 'keymaker');
51
52
  // Load MCP tools from configured servers
52
53
  const mcpTools = await Construtor.create();
53
54
  // Combine all tools
@@ -116,7 +117,7 @@ CRITICAL — NEVER FABRICATE DATA:
116
117
  origin_user_id: taskContext?.origin_user_id,
117
118
  };
118
119
  const startMs = Date.now();
119
- const response = await TaskRequestContext.run(invokeContext, () => this.agent.invoke({ messages }, { recursionLimit: 100 }));
120
+ const response = await TaskRequestContext.run(invokeContext, () => this.agent.invoke({ messages }, { recursionLimit: 50 }));
120
121
  const durationMs = Date.now() - startMs;
121
122
  const lastMessage = response.messages[response.messages.length - 1];
122
123
  const content = typeof lastMessage.content === "string"
@@ -97,7 +97,7 @@ export class SatiService {
97
97
  console.warn('[SatiService] Failed to persist input log:', e);
98
98
  }
99
99
  const satiStartMs = Date.now();
100
- const response = await agent.invoke({ messages }, { recursionLimit: 100 });
100
+ const response = await agent.invoke({ messages }, { recursionLimit: 50 });
101
101
  const satiDurationMs = Date.now() - satiStartMs;
102
102
  const lastMessage = response.messages[response.messages.length - 1];
103
103
  let content = lastMessage.content.toString();