@bike4mind/cli 0.2.58 → 0.2.59-feat-cli-tavern-integration.21571

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  WebSocketToolExecutor,
23
23
  buildCoreSystemPrompt,
24
24
  buildSkillsPromptSection,
25
+ clearFeatureModuleTools,
25
26
  createAgentDelegateTool,
26
27
  createBackgroundAgentTools,
27
28
  createFindDefinitionTool,
@@ -41,12 +42,13 @@ import {
41
42
  mergeCommands,
42
43
  parseAgentConfig,
43
44
  processFileReferences,
45
+ registerFeatureModuleTools,
44
46
  searchCommands,
45
47
  searchFiles,
46
48
  setWebSocketToolExecutor,
47
49
  substituteArguments,
48
50
  warmFileCache
49
- } from "./chunk-WPK7K4BM.js";
51
+ } from "./chunk-BPUFVNEJ.js";
50
52
  import "./chunk-BDQBOLYG.js";
51
53
  import "./chunk-5TQE3IBF.js";
52
54
  import "./chunk-GQGOWACU.js";
@@ -60,15 +62,15 @@ import "./chunk-BPFEGDC7.js";
60
62
  import {
61
63
  ConfigStore,
62
64
  logger
63
- } from "./chunk-RB5GFKSM.js";
65
+ } from "./chunk-U52SND4Y.js";
64
66
  import {
65
67
  checkForUpdate,
66
68
  package_default
67
- } from "./chunk-XZLLBLTF.js";
69
+ } from "./chunk-PEG5MIUM.js";
68
70
  import {
69
71
  selectActiveBackgroundAgents,
70
72
  useCliStore
71
- } from "./chunk-G2LYCVZJ.js";
73
+ } from "./chunk-Y4WOJJM3.js";
72
74
  import {
73
75
  CREDIT_DEDUCT_TRANSACTION_TYPES,
74
76
  ChatModels,
@@ -1422,6 +1424,19 @@ function buildConfigItems(availableModels) {
1422
1424
  enableDynamicAgentCreation: value
1423
1425
  }
1424
1426
  })
1427
+ },
1428
+ {
1429
+ key: "featuresTavern",
1430
+ label: "Tavern",
1431
+ type: "boolean",
1432
+ getValue: (config) => config.features?.tavern ?? false,
1433
+ setValue: (config, value) => ({
1434
+ ...config,
1435
+ features: {
1436
+ ...config.features,
1437
+ tavern: value
1438
+ }
1439
+ })
1425
1440
  }
1426
1441
  );
1427
1442
  return items;
@@ -1433,8 +1448,15 @@ function ConfigEditor({ config, availableModels, onSave, onClose }) {
1433
1448
  const [isSaving, setIsSaving] = useState7(false);
1434
1449
  const configItems = useMemo3(() => buildConfigItems(availableModels), [availableModels]);
1435
1450
  const hasChanges = useMemo3(() => {
1436
- return JSON.stringify(config.preferences) !== JSON.stringify(editedConfig.preferences) || config.defaultModel !== editedConfig.defaultModel;
1437
- }, [config.preferences, editedConfig.preferences, config.defaultModel, editedConfig.defaultModel]);
1451
+ return JSON.stringify(config.preferences) !== JSON.stringify(editedConfig.preferences) || config.defaultModel !== editedConfig.defaultModel || JSON.stringify(config.features) !== JSON.stringify(editedConfig.features);
1452
+ }, [
1453
+ config.preferences,
1454
+ editedConfig.preferences,
1455
+ config.defaultModel,
1456
+ editedConfig.defaultModel,
1457
+ config.features,
1458
+ editedConfig.features
1459
+ ]);
1438
1460
  const handleSaveAndClose = async () => {
1439
1461
  if (hasChanges) {
1440
1462
  try {
@@ -2982,6 +3004,942 @@ Unlike agent_delegate (which uses pre-defined agents), this tool lets you compos
2982
3004
  };
2983
3005
  }
2984
3006
 
3007
+ // src/features/FeatureModuleRegistry.ts
3008
+ var FeatureModuleRegistry = class {
3009
+ constructor() {
3010
+ this.modules = [];
3011
+ }
3012
+ /** Register a feature module */
3013
+ register(module) {
3014
+ if (this.modules.some((m) => m.name === module.name)) {
3015
+ throw new Error(`Feature module '${module.name}' is already registered`);
3016
+ }
3017
+ this.modules.push(module);
3018
+ }
3019
+ /** Collect all tools from all registered modules */
3020
+ getAllTools() {
3021
+ return this.modules.flatMap((m) => m.getTools());
3022
+ }
3023
+ /** Get all tool names from all registered modules */
3024
+ getAllToolNames() {
3025
+ return this.getAllTools().map((t) => t.toolSchema.name);
3026
+ }
3027
+ /** Build combined system prompt section from all modules */
3028
+ getSystemPromptSections() {
3029
+ const sections = this.modules.map((m) => m.getSystemPromptSection()).filter((s) => s.length > 0);
3030
+ return sections.length > 0 ? "\n\n" + sections.join("\n\n") : "";
3031
+ }
3032
+ /** Register all WS handlers from all modules */
3033
+ registerAllWsHandlers(wsManager) {
3034
+ for (const module of this.modules) {
3035
+ module.registerWsHandlers?.(wsManager);
3036
+ }
3037
+ }
3038
+ /** Collect all slash commands from all registered modules */
3039
+ getAllCommands() {
3040
+ return this.modules.flatMap((m) => m.getCommands?.() ?? []);
3041
+ }
3042
+ /** Try to execute a slash command. Returns true if handled. */
3043
+ executeCommand(name, args) {
3044
+ for (const module of this.modules) {
3045
+ const commands = module.getCommands?.() ?? [];
3046
+ const command = commands.find((c) => c.name === name);
3047
+ if (command) {
3048
+ command.execute(args);
3049
+ return true;
3050
+ }
3051
+ }
3052
+ return false;
3053
+ }
3054
+ /** Cleanup all modules */
3055
+ disposeAll() {
3056
+ for (const module of this.modules) {
3057
+ module.dispose?.();
3058
+ }
3059
+ }
3060
+ /** Get names of all registered modules */
3061
+ getModuleNames() {
3062
+ return this.modules.map((m) => m.name);
3063
+ }
3064
+ /** Check if any modules are registered */
3065
+ get hasModules() {
3066
+ return this.modules.length > 0;
3067
+ }
3068
+ };
3069
+
3070
+ // src/features/tavern/TavernService.ts
3071
+ var TavernService = class {
3072
+ constructor(apiClient) {
3073
+ this.apiClient = apiClient;
3074
+ }
3075
+ async listAgents() {
3076
+ return this.apiClient.get("/api/agents?limit=100");
3077
+ }
3078
+ async createAgent(request) {
3079
+ return this.apiClient.post("/api/agents", request);
3080
+ }
3081
+ async updateAgent(agentId, request) {
3082
+ return this.apiClient.put(`/api/agents/${encodeURIComponent(agentId)}`, request);
3083
+ }
3084
+ async deleteAgent(agentId) {
3085
+ await this.apiClient.delete(`/api/agents/${encodeURIComponent(agentId)}`);
3086
+ }
3087
+ async mentionAgent(agentName, message, config) {
3088
+ return this.apiClient.post("/api/tavern/mention", {
3089
+ agentName,
3090
+ message,
3091
+ config
3092
+ });
3093
+ }
3094
+ async listQuests() {
3095
+ return this.apiClient.get("/api/tavern/quests");
3096
+ }
3097
+ async postQuest(request) {
3098
+ return this.apiClient.post("/api/tavern/quests", request);
3099
+ }
3100
+ async deleteQuest(questId) {
3101
+ await this.apiClient.delete(`/api/tavern/quests`, { data: { questId } });
3102
+ }
3103
+ async getAgentNotebook(agentId, limit = 50) {
3104
+ return this.apiClient.get(
3105
+ `/api/tavern/agent-notebook?agentId=${encodeURIComponent(agentId)}&limit=${limit}`
3106
+ );
3107
+ }
3108
+ async listGates() {
3109
+ return this.apiClient.get("/api/tavern/gates");
3110
+ }
3111
+ async resolveGate(gateId, resolution) {
3112
+ return this.apiClient.post("/api/tavern/gate-resolve", {
3113
+ gateId,
3114
+ resolution
3115
+ });
3116
+ }
3117
+ async toggleHeartbeats(enabled) {
3118
+ return this.apiClient.post("/api/tavern/toggle-heartbeats", { enabled });
3119
+ }
3120
+ async triggerHeartbeat(config) {
3121
+ return this.apiClient.post("/api/tavern/trigger-heartbeat", { config });
3122
+ }
3123
+ async abortHeartbeats() {
3124
+ await this.apiClient.post("/api/tavern/abort-heartbeats", { abort: true });
3125
+ }
3126
+ };
3127
+
3128
+ // src/features/tavern/types.ts
3129
+ import { z } from "zod";
3130
+ var HeartbeatLogEntrySchema = z.object({
3131
+ id: z.string(),
3132
+ agentId: z.string(),
3133
+ agentName: z.string(),
3134
+ action: z.enum([
3135
+ "idle",
3136
+ "speech",
3137
+ "thought",
3138
+ "memory",
3139
+ "move",
3140
+ "reply",
3141
+ "post_quest",
3142
+ "claim_quest",
3143
+ "complete_quest",
3144
+ "tool_use",
3145
+ "email",
3146
+ "move_decoration",
3147
+ "gate_paused",
3148
+ "gate_timed",
3149
+ "gate_proceed",
3150
+ "yolo_override",
3151
+ "intent",
3152
+ "report",
3153
+ "credits"
3154
+ ]),
3155
+ text: z.string().optional(),
3156
+ toolOutput: z.string().optional(),
3157
+ targetAgentName: z.string().optional(),
3158
+ threadId: z.string().optional(),
3159
+ timestamp: z.string(),
3160
+ burstId: z.string().optional(),
3161
+ stepIndex: z.number().optional(),
3162
+ totalSteps: z.number().optional(),
3163
+ confidence: z.number().optional(),
3164
+ confidenceSource: z.string().optional(),
3165
+ creditsUsed: z.number().optional()
3166
+ });
3167
+ var CreateAgentRequestSchema = z.object({
3168
+ name: z.string(),
3169
+ description: z.string().optional(),
3170
+ systemPrompt: z.string().optional(),
3171
+ personality: z.object({
3172
+ majorMotivation: z.string().optional(),
3173
+ minorMotivation: z.string().optional(),
3174
+ flaw: z.string().optional(),
3175
+ quirk: z.string().optional(),
3176
+ description: z.string().optional(),
3177
+ personalMission: z.string().optional(),
3178
+ activeProject: z.string().optional(),
3179
+ communicationPattern: z.string().optional(),
3180
+ humorStyle: z.string().optional(),
3181
+ backstoryElement: z.string().optional(),
3182
+ energyLevel: z.string().optional(),
3183
+ coreValues: z.string().optional()
3184
+ }).optional()
3185
+ });
3186
+ var CreateAgentResponseSchema = z.object({
3187
+ _id: z.string(),
3188
+ name: z.string(),
3189
+ description: z.string().optional()
3190
+ });
3191
+ var UpdateAgentRequestSchema = CreateAgentRequestSchema.partial().extend({
3192
+ heartbeatConfig: z.object({
3193
+ enabled: z.boolean().optional(),
3194
+ intervalMinutes: z.number().optional()
3195
+ }).optional()
3196
+ });
3197
+ var AgentSummarySchema = z.object({
3198
+ _id: z.string(),
3199
+ name: z.string(),
3200
+ description: z.string().optional(),
3201
+ heartbeatConfig: z.object({
3202
+ enabled: z.boolean().optional()
3203
+ }).optional()
3204
+ });
3205
+ var AgentListResponseSchema = z.object({
3206
+ data: z.array(AgentSummarySchema),
3207
+ total: z.number().optional()
3208
+ });
3209
+ var MentionRequestSchema = z.object({
3210
+ agentName: z.string().optional(),
3211
+ message: z.string(),
3212
+ config: z.record(z.string(), z.unknown()).optional()
3213
+ });
3214
+ var DirectedMentionResponseSchema = z.object({
3215
+ success: z.boolean(),
3216
+ agentName: z.string(),
3217
+ agentId: z.string(),
3218
+ warning: z.string().optional()
3219
+ });
3220
+ var AmbientMentionResponseSchema = z.object({
3221
+ success: z.boolean(),
3222
+ mode: z.literal("ambient"),
3223
+ agentCount: z.number(),
3224
+ results: z.array(
3225
+ z.object({
3226
+ agentName: z.string(),
3227
+ status: z.string()
3228
+ })
3229
+ )
3230
+ });
3231
+ var MentionResponseSchema = z.union([DirectedMentionResponseSchema, AmbientMentionResponseSchema]);
3232
+ var TavernQuestSchema = z.object({
3233
+ _id: z.string(),
3234
+ title: z.string(),
3235
+ description: z.string().optional(),
3236
+ status: z.string(),
3237
+ postedBy: z.string(),
3238
+ claimedBy: z.string().optional(),
3239
+ difficulty: z.string().optional(),
3240
+ createdAt: z.string().optional(),
3241
+ updatedAt: z.string().optional()
3242
+ });
3243
+ var QuestListResponseSchema = z.object({
3244
+ quests: z.array(TavernQuestSchema)
3245
+ });
3246
+ var CreateQuestRequestSchema = z.object({
3247
+ title: z.string(),
3248
+ description: z.string().optional(),
3249
+ agentId: z.string(),
3250
+ agentName: z.string(),
3251
+ difficulty: z.enum(["easy", "medium", "hard", "epic"]).optional()
3252
+ });
3253
+ var QuestResponseSchema = z.object({
3254
+ quest: TavernQuestSchema
3255
+ });
3256
+ var NotebookEntrySchema = z.object({
3257
+ _id: z.string(),
3258
+ title: z.string().optional(),
3259
+ content: z.string().optional(),
3260
+ createdAt: z.string().optional()
3261
+ });
3262
+ var NotebookResponseSchema = z.object({
3263
+ sessionId: z.string().nullable(),
3264
+ entries: z.array(NotebookEntrySchema)
3265
+ });
3266
+ var TimedGateSchema = z.object({
3267
+ gateId: z.string(),
3268
+ agentId: z.string(),
3269
+ agentName: z.string(),
3270
+ userId: z.string(),
3271
+ confidence: z.number(),
3272
+ reason: z.string(),
3273
+ createdAt: z.number(),
3274
+ expiresAt: z.number(),
3275
+ delayMs: z.number(),
3276
+ status: z.enum(["pending", "approved", "rejected", "expired", "auto_proceeded"]),
3277
+ resolvedBy: z.enum(["human", "timer"]).optional(),
3278
+ resolvedAt: z.number().optional(),
3279
+ burstId: z.string(),
3280
+ iteration: z.number()
3281
+ });
3282
+ var GateListResponseSchema = z.object({
3283
+ gates: z.array(TimedGateSchema)
3284
+ });
3285
+ var GateResolveRequestSchema = z.object({
3286
+ gateId: z.string(),
3287
+ resolution: z.enum(["approve", "reject"])
3288
+ });
3289
+ var GateResolveResponseSchema = z.object({
3290
+ success: z.boolean(),
3291
+ gate: TimedGateSchema
3292
+ });
3293
+ var HeartbeatToggleResponseSchema = z.object({
3294
+ success: z.boolean(),
3295
+ enabled: z.boolean(),
3296
+ agentCount: z.number()
3297
+ });
3298
+ var TriggerHeartbeatResponseSchema = z.object({
3299
+ triggered: z.number(),
3300
+ results: z.array(
3301
+ z.object({
3302
+ agentId: z.string(),
3303
+ agentName: z.string(),
3304
+ status: z.string(),
3305
+ error: z.string().optional()
3306
+ })
3307
+ )
3308
+ });
3309
+
3310
+ // src/features/tavern/TavernActivityStream.ts
3311
+ var TavernActivityStream = class {
3312
+ constructor(onLogEntry) {
3313
+ this.onLogEntry = onLogEntry;
3314
+ this.wsManager = null;
3315
+ }
3316
+ /** Register the WS handler for tavern_heartbeat_log events */
3317
+ registerHandlers(wsManager) {
3318
+ this.dispose();
3319
+ this.wsManager = wsManager;
3320
+ wsManager.onAction("tavern_heartbeat_log", (message) => {
3321
+ const parsed = message;
3322
+ if (!parsed.entry) return;
3323
+ const result = HeartbeatLogEntrySchema.safeParse(parsed.entry);
3324
+ if (result.success) {
3325
+ this.onLogEntry(result.data);
3326
+ }
3327
+ });
3328
+ }
3329
+ /** Unsubscribe from WS events */
3330
+ dispose() {
3331
+ if (this.wsManager) {
3332
+ this.wsManager.offAction("tavern_heartbeat_log");
3333
+ this.wsManager = null;
3334
+ }
3335
+ }
3336
+ };
3337
+
3338
+ // src/features/tavern/tavernTools.ts
3339
+ function createTavernTools(service) {
3340
+ return [
3341
+ createListAgentsTool(service),
3342
+ createCreateAgentTool(service),
3343
+ createEditAgentTool(service),
3344
+ createDeleteAgentTool(service),
3345
+ createMentionTool(service),
3346
+ createListQuestsTool(service),
3347
+ createPostQuestTool(service),
3348
+ createDeleteQuestTool(service),
3349
+ createReadNotebookTool(service),
3350
+ createListGatesTool(service),
3351
+ createResolveGateTool(service),
3352
+ createToggleHeartbeatsTool(service),
3353
+ createTriggerHeartbeatTool(service),
3354
+ createAbortHeartbeatsTool(service),
3355
+ createStatusTool(service)
3356
+ ];
3357
+ }
3358
+ function createListAgentsTool(service) {
3359
+ return {
3360
+ toolSchema: {
3361
+ name: "tavern_list_agents",
3362
+ description: "List all Tavern agents with their IDs, names, descriptions, and heartbeat status. Use this FIRST to discover agent IDs before using tools that require an agent_id (like tavern_read_notebook or tavern_post_quest).",
3363
+ parameters: {
3364
+ type: "object",
3365
+ properties: {}
3366
+ }
3367
+ },
3368
+ toolFn: async () => {
3369
+ const result = await service.listAgents();
3370
+ return JSON.stringify(result);
3371
+ }
3372
+ };
3373
+ }
3374
+ function createCreateAgentTool(service) {
3375
+ return {
3376
+ toolSchema: {
3377
+ name: "tavern_create_agent",
3378
+ description: "Create a new Tavern agent with a personality. The agent will be created without heartbeats enabled \u2014 use tavern_toggle_heartbeats after creation to activate autonomous behavior. Personality fields shape how the agent thinks and acts during heartbeats.",
3379
+ parameters: {
3380
+ type: "object",
3381
+ properties: {
3382
+ name: {
3383
+ type: "string",
3384
+ description: 'The agent name (e.g. "Spock", "Luna", "Chef Gordon")'
3385
+ },
3386
+ description: {
3387
+ type: "string",
3388
+ description: "A short description of the agent"
3389
+ },
3390
+ system_prompt: {
3391
+ type: "string",
3392
+ description: "System prompt that defines the agent's core behavior and knowledge"
3393
+ },
3394
+ major_motivation: {
3395
+ type: "string",
3396
+ description: 'What primarily drives this agent (e.g. "Exploring the unknown")'
3397
+ },
3398
+ minor_motivation: {
3399
+ type: "string",
3400
+ description: 'Secondary drive (e.g. "Collecting rare artifacts")'
3401
+ },
3402
+ flaw: {
3403
+ type: "string",
3404
+ description: 'A character flaw that creates interesting behavior (e.g. "Overthinks simple problems")'
3405
+ },
3406
+ quirk: {
3407
+ type: "string",
3408
+ description: 'A distinctive behavioral quirk (e.g. "Speaks in nautical metaphors")'
3409
+ },
3410
+ personality_description: {
3411
+ type: "string",
3412
+ description: "Overall personality summary"
3413
+ },
3414
+ personal_mission: {
3415
+ type: "string",
3416
+ description: `The agent's purpose in the tavern (e.g. "Map every corner of the digital realm")`
3417
+ },
3418
+ active_project: {
3419
+ type: "string",
3420
+ description: "What the agent is currently working on"
3421
+ },
3422
+ communication_pattern: {
3423
+ type: "string",
3424
+ description: 'How the agent communicates (e.g. "Formal and precise", "Casual and witty")'
3425
+ },
3426
+ humor_style: {
3427
+ type: "string",
3428
+ description: `The agent's sense of humor (e.g. "Dry wit", "Puns and wordplay")`
3429
+ },
3430
+ backstory_element: {
3431
+ type: "string",
3432
+ description: "A backstory detail that influences behavior"
3433
+ },
3434
+ energy_level: {
3435
+ type: "string",
3436
+ description: 'Default energy level (e.g. "High energy morning person", "Calm and measured")'
3437
+ },
3438
+ core_values: {
3439
+ type: "string",
3440
+ description: 'What the agent values most (e.g. "Truth and transparency")'
3441
+ }
3442
+ },
3443
+ required: ["name"]
3444
+ }
3445
+ },
3446
+ toolFn: async (params) => {
3447
+ const raw = params;
3448
+ const request = CreateAgentRequestSchema.parse({
3449
+ name: raw.name,
3450
+ description: raw.description,
3451
+ systemPrompt: raw.system_prompt,
3452
+ personality: {
3453
+ majorMotivation: raw.major_motivation,
3454
+ minorMotivation: raw.minor_motivation,
3455
+ flaw: raw.flaw,
3456
+ quirk: raw.quirk,
3457
+ description: raw.personality_description,
3458
+ personalMission: raw.personal_mission,
3459
+ activeProject: raw.active_project,
3460
+ communicationPattern: raw.communication_pattern,
3461
+ humorStyle: raw.humor_style,
3462
+ backstoryElement: raw.backstory_element,
3463
+ energyLevel: raw.energy_level,
3464
+ coreValues: raw.core_values
3465
+ }
3466
+ });
3467
+ const result = await service.createAgent(request);
3468
+ return JSON.stringify(result);
3469
+ }
3470
+ };
3471
+ }
3472
+ function createEditAgentTool(service) {
3473
+ return {
3474
+ toolSchema: {
3475
+ name: "tavern_edit_agent",
3476
+ description: "Update an existing Tavern agent. Can change personality, system prompt, heartbeat config, or any other field. Use this to enable/disable heartbeats for a SINGLE agent (set heartbeat_enabled), or to update personality traits. Only provide fields you want to change \u2014 unspecified fields remain unchanged.",
3477
+ parameters: {
3478
+ type: "object",
3479
+ properties: {
3480
+ agent_id: {
3481
+ type: "string",
3482
+ description: "The MongoDB ObjectId of the agent (get from tavern_list_agents)"
3483
+ },
3484
+ name: { type: "string", description: "New agent name" },
3485
+ description: { type: "string", description: "New description" },
3486
+ system_prompt: { type: "string", description: "New system prompt" },
3487
+ heartbeat_enabled: {
3488
+ type: "boolean",
3489
+ description: "Enable or disable heartbeats for THIS specific agent"
3490
+ },
3491
+ heartbeat_interval_minutes: {
3492
+ type: "number",
3493
+ description: "Heartbeat interval in minutes (default: 3)"
3494
+ },
3495
+ major_motivation: { type: "string", description: "What primarily drives this agent" },
3496
+ minor_motivation: { type: "string", description: "Secondary drive" },
3497
+ flaw: { type: "string", description: "Character flaw" },
3498
+ quirk: { type: "string", description: "Distinctive quirk" },
3499
+ personal_mission: { type: "string", description: "Agent's purpose in the tavern" },
3500
+ active_project: { type: "string", description: "What the agent is currently working on" },
3501
+ communication_pattern: { type: "string", description: "How the agent communicates" },
3502
+ humor_style: { type: "string", description: "Agent's sense of humor" }
3503
+ },
3504
+ required: ["agent_id"]
3505
+ }
3506
+ },
3507
+ toolFn: async (params) => {
3508
+ const raw = params;
3509
+ const agentId = raw.agent_id;
3510
+ const payload = {};
3511
+ if (raw.name !== void 0) payload.name = raw.name;
3512
+ if (raw.description !== void 0) payload.description = raw.description;
3513
+ if (raw.system_prompt !== void 0) payload.systemPrompt = raw.system_prompt;
3514
+ if (raw.heartbeat_enabled !== void 0 || raw.heartbeat_interval_minutes !== void 0) {
3515
+ const hb = {};
3516
+ if (raw.heartbeat_enabled !== void 0) hb.enabled = raw.heartbeat_enabled;
3517
+ if (raw.heartbeat_interval_minutes !== void 0) hb.intervalMinutes = raw.heartbeat_interval_minutes;
3518
+ payload.heartbeatConfig = hb;
3519
+ }
3520
+ const personality = {};
3521
+ if (raw.major_motivation !== void 0) personality.majorMotivation = raw.major_motivation;
3522
+ if (raw.minor_motivation !== void 0) personality.minorMotivation = raw.minor_motivation;
3523
+ if (raw.flaw !== void 0) personality.flaw = raw.flaw;
3524
+ if (raw.quirk !== void 0) personality.quirk = raw.quirk;
3525
+ if (raw.personal_mission !== void 0) personality.personalMission = raw.personal_mission;
3526
+ if (raw.active_project !== void 0) personality.activeProject = raw.active_project;
3527
+ if (raw.communication_pattern !== void 0) personality.communicationPattern = raw.communication_pattern;
3528
+ if (raw.humor_style !== void 0) personality.humorStyle = raw.humor_style;
3529
+ if (Object.keys(personality).length > 0) payload.personality = personality;
3530
+ const request = UpdateAgentRequestSchema.parse(payload);
3531
+ const result = await service.updateAgent(agentId, request);
3532
+ return JSON.stringify(result);
3533
+ }
3534
+ };
3535
+ }
3536
+ function createDeleteAgentTool(service) {
3537
+ return {
3538
+ toolSchema: {
3539
+ name: "tavern_delete_agent",
3540
+ description: "Permanently delete a Tavern agent. This is irreversible. IMPORTANT: agent_id must be a MongoDB ObjectId from tavern_list_agents.",
3541
+ parameters: {
3542
+ type: "object",
3543
+ properties: {
3544
+ agent_id: {
3545
+ type: "string",
3546
+ description: "The MongoDB ObjectId of the agent to delete"
3547
+ }
3548
+ },
3549
+ required: ["agent_id"]
3550
+ }
3551
+ },
3552
+ toolFn: async (params) => {
3553
+ const { agent_id } = params;
3554
+ await service.deleteAgent(agent_id);
3555
+ return JSON.stringify({ success: true, message: `Agent ${agent_id} deleted` });
3556
+ }
3557
+ };
3558
+ }
3559
+ function createMentionTool(service) {
3560
+ return {
3561
+ toolSchema: {
3562
+ name: "tavern_mention",
3563
+ description: "Send a message to a specific Tavern agent by name, or broadcast an ambient message to all agents if no agent_name is provided. Use this to talk to agents, ask them questions, give them instructions, or announce something to the whole tavern.",
3564
+ parameters: {
3565
+ type: "object",
3566
+ properties: {
3567
+ agent_name: {
3568
+ type: "string",
3569
+ description: "Name of the agent to mention (omit for ambient broadcast to all agents)"
3570
+ },
3571
+ message: {
3572
+ type: "string",
3573
+ description: "The message to send to the agent(s)"
3574
+ }
3575
+ },
3576
+ required: ["message"]
3577
+ }
3578
+ },
3579
+ toolFn: async (params) => {
3580
+ const { agent_name, message } = params;
3581
+ const result = await service.mentionAgent(agent_name, message);
3582
+ return JSON.stringify(result);
3583
+ }
3584
+ };
3585
+ }
3586
+ function createListQuestsTool(service) {
3587
+ return {
3588
+ toolSchema: {
3589
+ name: "tavern_list_quests",
3590
+ description: "List all quests on the Tavern quest board. Shows quest title, status, who posted it, and who claimed it.",
3591
+ parameters: {
3592
+ type: "object",
3593
+ properties: {}
3594
+ }
3595
+ },
3596
+ toolFn: async () => {
3597
+ const result = await service.listQuests();
3598
+ return JSON.stringify(result);
3599
+ }
3600
+ };
3601
+ }
3602
+ function createPostQuestTool(service) {
3603
+ return {
3604
+ toolSchema: {
3605
+ name: "tavern_post_quest",
3606
+ description: "Post a new quest to the Tavern quest board for agents to discover and claim.",
3607
+ parameters: {
3608
+ type: "object",
3609
+ properties: {
3610
+ title: {
3611
+ type: "string",
3612
+ description: "Quest title"
3613
+ },
3614
+ description: {
3615
+ type: "string",
3616
+ description: "Detailed quest description"
3617
+ },
3618
+ agent_id: {
3619
+ type: "string",
3620
+ description: "ID of the agent posting the quest"
3621
+ },
3622
+ agent_name: {
3623
+ type: "string",
3624
+ description: "Name of the agent posting the quest"
3625
+ },
3626
+ difficulty: {
3627
+ type: "string",
3628
+ description: "Quest difficulty level",
3629
+ enum: ["easy", "medium", "hard", "epic"]
3630
+ }
3631
+ },
3632
+ required: ["title", "agent_id", "agent_name"]
3633
+ }
3634
+ },
3635
+ toolFn: async (params) => {
3636
+ const raw = params;
3637
+ const request = CreateQuestRequestSchema.parse({
3638
+ title: raw.title,
3639
+ description: raw.description,
3640
+ agentId: raw.agent_id,
3641
+ agentName: raw.agent_name,
3642
+ difficulty: raw.difficulty
3643
+ });
3644
+ const result = await service.postQuest(request);
3645
+ return JSON.stringify(result);
3646
+ }
3647
+ };
3648
+ }
3649
+ function createDeleteQuestTool(service) {
3650
+ return {
3651
+ toolSchema: {
3652
+ name: "tavern_delete_quest",
3653
+ description: "Remove a quest from the Tavern quest board by its ID.",
3654
+ parameters: {
3655
+ type: "object",
3656
+ properties: {
3657
+ quest_id: {
3658
+ type: "string",
3659
+ description: "The ID of the quest to delete"
3660
+ }
3661
+ },
3662
+ required: ["quest_id"]
3663
+ }
3664
+ },
3665
+ toolFn: async (params) => {
3666
+ const { quest_id } = params;
3667
+ await service.deleteQuest(quest_id);
3668
+ return JSON.stringify({ success: true });
3669
+ }
3670
+ };
3671
+ }
3672
+ function createReadNotebookTool(service) {
3673
+ return {
3674
+ toolSchema: {
3675
+ name: "tavern_read_notebook",
3676
+ description: `Read a Tavern agent's activity notebook/history. Shows their recent actions, thoughts, conversations, and quest progress. IMPORTANT: agent_id must be a MongoDB ObjectId (e.g. "6540b58d1f703ade3ea1e82b"), NOT the agent name. Use tavern_list_agents first to get the correct ID.`,
3677
+ parameters: {
3678
+ type: "object",
3679
+ properties: {
3680
+ agent_id: {
3681
+ type: "string",
3682
+ description: "The MongoDB ObjectId of the agent (get this from tavern_list_agents, NOT the agent name)"
3683
+ },
3684
+ limit: {
3685
+ type: "number",
3686
+ description: "Maximum number of entries to return (default: 50)"
3687
+ }
3688
+ },
3689
+ required: ["agent_id"]
3690
+ }
3691
+ },
3692
+ toolFn: async (params) => {
3693
+ const { agent_id, limit } = params;
3694
+ const result = await service.getAgentNotebook(agent_id, limit);
3695
+ return JSON.stringify(result);
3696
+ }
3697
+ };
3698
+ }
3699
+ function createListGatesTool(service) {
3700
+ return {
3701
+ toolSchema: {
3702
+ name: "tavern_list_gates",
3703
+ description: "List all pending confidence gates in the Tavern. Gates are pause points where agents need human approval to proceed with low-confidence actions.",
3704
+ parameters: {
3705
+ type: "object",
3706
+ properties: {}
3707
+ }
3708
+ },
3709
+ toolFn: async () => {
3710
+ const result = await service.listGates();
3711
+ return JSON.stringify(result);
3712
+ }
3713
+ };
3714
+ }
3715
+ function createResolveGateTool(service) {
3716
+ return {
3717
+ toolSchema: {
3718
+ name: "tavern_resolve_gate",
3719
+ description: "Approve or reject a pending confidence gate. This lets an agent proceed or stops its current action.",
3720
+ parameters: {
3721
+ type: "object",
3722
+ properties: {
3723
+ gate_id: {
3724
+ type: "string",
3725
+ description: "The ID of the gate to resolve"
3726
+ },
3727
+ resolution: {
3728
+ type: "string",
3729
+ description: "Whether to approve or reject the gate",
3730
+ enum: ["approve", "reject"]
3731
+ }
3732
+ },
3733
+ required: ["gate_id", "resolution"]
3734
+ }
3735
+ },
3736
+ toolFn: async (params) => {
3737
+ const { gate_id, resolution } = params;
3738
+ const result = await service.resolveGate(gate_id, resolution);
3739
+ return JSON.stringify(result);
3740
+ }
3741
+ };
3742
+ }
3743
+ function createToggleHeartbeatsTool(service) {
3744
+ return {
3745
+ toolSchema: {
3746
+ name: "tavern_toggle_heartbeats",
3747
+ description: "Enable or disable background heartbeats for all Tavern agents. When enabled, agents autonomously think, act, and interact on a schedule.",
3748
+ parameters: {
3749
+ type: "object",
3750
+ properties: {
3751
+ enabled: {
3752
+ type: "boolean",
3753
+ description: "true to enable heartbeats, false to disable"
3754
+ }
3755
+ },
3756
+ required: ["enabled"]
3757
+ }
3758
+ },
3759
+ toolFn: async (params) => {
3760
+ const { enabled } = params;
3761
+ const result = await service.toggleHeartbeats(enabled);
3762
+ return JSON.stringify(result);
3763
+ }
3764
+ };
3765
+ }
3766
+ function createTriggerHeartbeatTool(service) {
3767
+ return {
3768
+ toolSchema: {
3769
+ name: "tavern_trigger_heartbeat",
3770
+ description: "Manually trigger a heartbeat cycle for all Tavern agents. Useful for testing or forcing immediate agent activity.",
3771
+ parameters: {
3772
+ type: "object",
3773
+ properties: {
3774
+ config: {
3775
+ type: "object",
3776
+ description: 'Optional configuration overrides for the heartbeat. Known keys: preferredModelId (string \u2014 model ID to use), costMode ("low" | "normal" | "high")',
3777
+ additionalProperties: true
3778
+ }
3779
+ }
3780
+ }
3781
+ },
3782
+ toolFn: async (params) => {
3783
+ const { config } = params ?? {};
3784
+ const result = await service.triggerHeartbeat(config);
3785
+ return JSON.stringify(result);
3786
+ }
3787
+ };
3788
+ }
3789
+ function createAbortHeartbeatsTool(service) {
3790
+ return {
3791
+ toolSchema: {
3792
+ name: "tavern_abort_heartbeats",
3793
+ description: "Emergency stop \u2014 abort all in-flight Tavern agent heartbeats immediately.",
3794
+ parameters: {
3795
+ type: "object",
3796
+ properties: {}
3797
+ }
3798
+ },
3799
+ toolFn: async () => {
3800
+ await service.abortHeartbeats();
3801
+ return JSON.stringify({ success: true, message: "All in-flight heartbeats aborted" });
3802
+ }
3803
+ };
3804
+ }
3805
+ function createStatusTool(service) {
3806
+ return {
3807
+ toolSchema: {
3808
+ name: "tavern_status",
3809
+ description: "Get a quick overview of the Tavern: agent count, heartbeat status, active quests, and pending gates. Use this for situational awareness before taking action.",
3810
+ parameters: {
3811
+ type: "object",
3812
+ properties: {}
3813
+ }
3814
+ },
3815
+ toolFn: async () => {
3816
+ const [agents, quests, gates] = await Promise.all([
3817
+ service.listAgents(),
3818
+ service.listQuests(),
3819
+ service.listGates()
3820
+ ]);
3821
+ const agentList = agents.data ?? [];
3822
+ const heartbeatEnabled = agentList.filter((a) => a.heartbeatConfig?.enabled);
3823
+ return JSON.stringify({
3824
+ agents: {
3825
+ total: agentList.length,
3826
+ withHeartbeats: heartbeatEnabled.length,
3827
+ names: agentList.map((a) => ({ name: a.name, heartbeat: a.heartbeatConfig?.enabled ?? false }))
3828
+ },
3829
+ quests: {
3830
+ total: quests.quests.length,
3831
+ byStatus: quests.quests.reduce(
3832
+ (acc, q) => {
3833
+ acc[q.status] = (acc[q.status] ?? 0) + 1;
3834
+ return acc;
3835
+ },
3836
+ {}
3837
+ )
3838
+ },
3839
+ gates: {
3840
+ pending: gates.gates.length
3841
+ }
3842
+ });
3843
+ }
3844
+ };
3845
+ }
3846
+
3847
+ // src/features/tavern/TavernModule.ts
3848
+ var ACTION_ICONS = {
3849
+ speech: "\u{1F4AC}",
3850
+ thought: "\u{1F4AD}",
3851
+ memory: "\u{1F9E0}",
3852
+ move: "\u{1F6B6}",
3853
+ reply: "\u{1F4E9}",
3854
+ post_quest: "\u{1F4DC}",
3855
+ claim_quest: "\u2694\uFE0F",
3856
+ complete_quest: "\u2705",
3857
+ tool_use: "\u{1F527}",
3858
+ email: "\u{1F4E7}",
3859
+ gate_paused: "\u23F8\uFE0F",
3860
+ gate_timed: "\u23F3",
3861
+ gate_proceed: "\u25B6\uFE0F",
3862
+ idle: "\u{1F4A4}",
3863
+ intent: "\u{1F3AF}",
3864
+ report: "\u{1F4CB}",
3865
+ credits: "\u{1FA99}",
3866
+ move_decoration: "\u{1F3A8}",
3867
+ yolo_override: "\u26A1"
3868
+ };
3869
+ var TavernModule = class {
3870
+ constructor(apiClient, onLogEntry, getActivityLog) {
3871
+ this.name = "tavern";
3872
+ this.description = "Interact with autonomous AI agents in the B4M Tavern";
3873
+ this.service = new TavernService(apiClient);
3874
+ this.activityStream = new TavernActivityStream(onLogEntry);
3875
+ this.getActivityLog = getActivityLog;
3876
+ }
3877
+ getTools() {
3878
+ return createTavernTools(this.service);
3879
+ }
3880
+ getSystemPromptSection() {
3881
+ return `## Tavern Integration
3882
+ You can interact with autonomous AI agents in the B4M Tavern using tavern_* tools.
3883
+
3884
+ IMPORTANT: Many tools require agent IDs (MongoDB ObjectIds like "6540b58d1f703ade3ea1e82b"). Always use tavern_list_agents FIRST to discover agent names and their IDs before using tools that need an agent_id.
3885
+
3886
+ Available actions:
3887
+ - **tavern_list_agents**: List all agents with their IDs, names, and status \u2014 USE THIS FIRST
3888
+ - **tavern_create_agent**: Create a new agent with a personality (heartbeats disabled by default)
3889
+ - **tavern_edit_agent**: Update an agent's personality, system prompt, or heartbeat config (per-agent toggle)
3890
+ - **tavern_delete_agent**: Permanently delete an agent
3891
+ - **tavern_mention**: Talk to a specific agent by name or broadcast to all agents
3892
+ - **tavern_list_quests**: View the quest board
3893
+ - **tavern_post_quest**: Post a new quest for agents to claim
3894
+ - **tavern_delete_quest**: Remove a quest from the board
3895
+ - **tavern_read_notebook**: Read an agent's activity history (requires agent ID from tavern_list_agents)
3896
+ - **tavern_list_gates**: See pending confidence gates awaiting human approval
3897
+ - **tavern_resolve_gate**: Approve or reject a confidence gate
3898
+ - **tavern_toggle_heartbeats**: Enable/disable agent background heartbeats
3899
+ - **tavern_trigger_heartbeat**: Manually trigger a heartbeat cycle
3900
+ - **tavern_abort_heartbeats**: Emergency stop all in-flight heartbeats
3901
+ - **tavern_status**: Quick overview of agents, quests, and gates \u2014 good for situational awareness
3902
+
3903
+ When the user mentions talking to agents, checking the quest board, or managing the tavern, use these tools.
3904
+ Agents have personalities, moods, quests, and memories \u2014 they are autonomous entities, not chatbots.`;
3905
+ }
3906
+ getCommands() {
3907
+ return [
3908
+ {
3909
+ name: "tavern",
3910
+ description: "Show recent Tavern agent activity stream",
3911
+ execute: () => {
3912
+ const activityLog = this.getActivityLog();
3913
+ if (activityLog.length === 0) {
3914
+ console.log("\nTavern Activity: No activity yet.");
3915
+ console.log(" Agents broadcast activity during heartbeats.");
3916
+ console.log(' Try: "trigger a heartbeat cycle" to generate activity.\n');
3917
+ return;
3918
+ }
3919
+ const recentEntries = activityLog.slice(-20);
3920
+ console.log(`
3921
+ Tavern Activity (last ${recentEntries.length} of ${activityLog.length} entries):
3922
+ `);
3923
+ for (const entry of recentEntries) {
3924
+ const time = new Date(entry.timestamp).toLocaleTimeString();
3925
+ const icon = ACTION_ICONS[entry.action] ?? "\xB7";
3926
+ const target = entry.targetAgentName ? ` \u2192 ${entry.targetAgentName}` : "";
3927
+ const text = entry.text ? `: ${entry.text.slice(0, 120)}${entry.text.length > 120 ? "..." : ""}` : "";
3928
+ console.log(` ${time} ${icon} ${entry.agentName}${target} [${entry.action}]${text}`);
3929
+ }
3930
+ console.log("");
3931
+ }
3932
+ }
3933
+ ];
3934
+ }
3935
+ registerWsHandlers(wsManager) {
3936
+ this.activityStream.registerHandlers(wsManager);
3937
+ }
3938
+ dispose() {
3939
+ this.activityStream.dispose();
3940
+ }
3941
+ };
3942
+
2985
3943
  // src/index.tsx
2986
3944
  process.removeAllListeners("warning");
2987
3945
  process.on("warning", (warning) => {
@@ -3030,7 +3988,8 @@ function CliApp() {
3030
3988
  sandboxOrchestrator: null,
3031
3989
  wsManager: null,
3032
3990
  checkpointStore: null,
3033
- additionalDirectories: []
3991
+ additionalDirectories: [],
3992
+ featureRegistry: null
3034
3993
  });
3035
3994
  const [isInitialized, setIsInitialized] = useState12(false);
3036
3995
  const [initError, setInitError] = useState12(null);
@@ -3060,6 +4019,9 @@ function CliApp() {
3060
4019
  })
3061
4020
  );
3062
4021
  }
4022
+ if (state.featureRegistry) {
4023
+ state.featureRegistry.disposeAll();
4024
+ }
3063
4025
  if (state.wsManager) {
3064
4026
  state.wsManager.disconnect();
3065
4027
  setWebSocketToolExecutor(null);
@@ -3082,7 +4044,15 @@ function CliApp() {
3082
4044
  setTimeout(() => {
3083
4045
  process.exit(0);
3084
4046
  }, 100);
3085
- }, [state.session, state.sessionStore, state.mcpManager, state.agent, state.imageStore, state.wsManager]);
4047
+ }, [
4048
+ state.session,
4049
+ state.sessionStore,
4050
+ state.mcpManager,
4051
+ state.agent,
4052
+ state.imageStore,
4053
+ state.wsManager,
4054
+ state.featureRegistry
4055
+ ]);
3086
4056
  useInput10((input, key) => {
3087
4057
  if (key.escape) {
3088
4058
  const store = useCliStore.getState();
@@ -3549,6 +4519,23 @@ Pull a model: ollama pull qwen3.5`
3549
4519
  }) : null;
3550
4520
  const findDefinitionTool = createFindDefinitionTool();
3551
4521
  const getFileStructureTool = createGetFileStructureTool();
4522
+ const featureRegistry = new FeatureModuleRegistry();
4523
+ if (config.features?.tavern) {
4524
+ featureRegistry.register(
4525
+ new TavernModule(
4526
+ apiClient,
4527
+ (entry) => useCliStore.getState().addTavernLogEntry(entry),
4528
+ () => useCliStore.getState().tavernActivityLog
4529
+ )
4530
+ );
4531
+ }
4532
+ const featureModuleToolNames = featureRegistry.getAllToolNames();
4533
+ if (featureModuleToolNames.length > 0) {
4534
+ registerFeatureModuleTools(featureModuleToolNames);
4535
+ }
4536
+ if (wsManager && featureRegistry.hasModules) {
4537
+ featureRegistry.registerAllWsHandlers(wsManager);
4538
+ }
3552
4539
  const cliTools = [
3553
4540
  agentDelegateTool,
3554
4541
  ...backgroundTools,
@@ -3562,7 +4549,8 @@ Pull a model: ollama pull qwen3.5`
3562
4549
  if (dynamicAgentTool) {
3563
4550
  cliTools.push(dynamicAgentTool);
3564
4551
  }
3565
- const allTools = [...b4mTools, ...mcpTools, ...cliTools];
4552
+ const featureTools = featureRegistry.getAllTools();
4553
+ const allTools = [...b4mTools, ...mcpTools, ...cliTools, ...featureTools];
3566
4554
  startupLog.push(`\u{1F4C2} Working directory: ${process.cwd()}`);
3567
4555
  if (additionalDirectories.length > 0) {
3568
4556
  startupLog.push(`\u{1F4C1} Additional directories: ${additionalDirectories.length}`);
@@ -3578,8 +4566,12 @@ Pull a model: ollama pull qwen3.5`
3578
4566
  if (dynamicAgentTool) {
3579
4567
  startupLog.push(`\u{1F9EA} Dynamic agent creation enabled (experimental)`);
3580
4568
  }
4569
+ if (featureRegistry.hasModules) {
4570
+ const moduleNames = featureRegistry.getModuleNames().join(", ");
4571
+ startupLog.push(`\u{1F3F0} Feature modules: ${moduleNames} (${featureTools.length} tools)`);
4572
+ }
3581
4573
  logger.debug(
3582
- `Total tools available to agent: ${allTools.length} (${b4mTools.length} B4M + ${mcpTools.length} MCP + ${cliTools.length} CLI)`
4574
+ `Total tools available to agent: ${allTools.length} (${b4mTools.length} B4M + ${mcpTools.length} MCP + ${cliTools.length} CLI + ${featureTools.length} feature)`
3583
4575
  );
3584
4576
  if (contextResult.globalContext) {
3585
4577
  startupLog.push(`\u{1F4C4} Global context: ${contextResult.globalContext.filename}`);
@@ -3590,13 +4582,15 @@ Pull a model: ollama pull qwen3.5`
3590
4582
  for (const error of contextResult.errors) {
3591
4583
  startupLog.push(`\u26A0\uFE0F Context file error: ${error}`);
3592
4584
  }
4585
+ const featureModulePrompts = featureRegistry.getSystemPromptSections();
3593
4586
  const cliSystemPrompt = buildCoreSystemPrompt({
3594
4587
  contextContent: contextResult.mergedContent,
3595
4588
  agentStore,
3596
4589
  customCommands: state.customCommandStore.getAllCommands(),
3597
4590
  enableSkillTool,
3598
4591
  enableDynamicAgentCreation: config.preferences.enableDynamicAgentCreation === true,
3599
- additionalDirectories
4592
+ additionalDirectories,
4593
+ featureModulePrompts: featureModulePrompts || void 0
3600
4594
  });
3601
4595
  const maxIterations = config.preferences.maxIterations === null ? 999999 : config.preferences.maxIterations;
3602
4596
  const agent = new ReActAgent({
@@ -3664,8 +4658,10 @@ Pull a model: ollama pull qwen3.5`
3664
4658
  // WebSocket connection manager (null if using SSE fallback)
3665
4659
  checkpointStore,
3666
4660
  // File change checkpointing for undo/restore
3667
- additionalDirectories
4661
+ additionalDirectories,
3668
4662
  // Store additional directories for file access
4663
+ featureRegistry
4664
+ // Feature module registry for opt-in modules
3669
4665
  }));
3670
4666
  setStoreSession(newSession);
3671
4667
  const bannerLines = [
@@ -3942,7 +4938,8 @@ Pull a model: ollama pull qwen3.5`
3942
4938
  agentStore: state.agentStore || void 0,
3943
4939
  customCommands: state.customCommandStore.getAllCommands(),
3944
4940
  enableSkillTool: config?.preferences.enableSkillTool !== false,
3945
- additionalDirectories: state.additionalDirectories
4941
+ additionalDirectories: state.additionalDirectories,
4942
+ featureModulePrompts: state.featureRegistry?.getSystemPromptSections() || void 0
3946
4943
  });
3947
4944
  const contextUsage = tokenCounter2.countSessionTokens(activeSession, systemPrompt);
3948
4945
  if (contextUsage.totalTokens >= threshold) {
@@ -4997,7 +5994,8 @@ No usage data available for the last ${USAGE_DAYS} days.`);
4997
5994
  agentStore: state.agentStore || void 0,
4998
5995
  customCommands: commands,
4999
5996
  enableSkillTool: state.config?.preferences.enableSkillTool !== false,
5000
- additionalDirectories: state.additionalDirectories
5997
+ additionalDirectories: state.additionalDirectories,
5998
+ featureModulePrompts: state.featureRegistry?.getSystemPromptSections() || void 0
5001
5999
  });
5002
6000
  const usage = tokenCounter2.countSessionTokens(state.session, systemPrompt);
5003
6001
  const totalWithTools = usage.totalTokens + mcpToolsTokens;
@@ -5545,15 +6543,70 @@ Allowed domains (${domains.length}):`);
5545
6543
  console.log("Violation log cleared.");
5546
6544
  break;
5547
6545
  }
5548
- default:
6546
+ default: {
6547
+ if (state.featureRegistry?.executeCommand(command, args)) {
6548
+ break;
6549
+ }
5549
6550
  console.log(`Unknown command: /${command}`);
5550
6551
  console.log("Type /help for available commands");
6552
+ }
5551
6553
  }
5552
6554
  };
5553
6555
  const handleSaveConfig = async (updatedConfig) => {
5554
6556
  await state.configStore.save(updatedConfig);
5555
6557
  const modelChanged = state.config?.defaultModel !== updatedConfig.defaultModel;
6558
+ const featuresChanged = JSON.stringify(state.config?.features ?? {}) !== JSON.stringify(updatedConfig.features ?? {});
6559
+ let newFeatureRegistry = state.featureRegistry;
6560
+ if (featuresChanged && state.agent) {
6561
+ state.featureRegistry?.disposeAll();
6562
+ clearFeatureModuleTools();
6563
+ newFeatureRegistry = new FeatureModuleRegistry();
6564
+ const apiClient = new ApiClient(getApiUrl(updatedConfig.apiConfig), state.configStore);
6565
+ if (updatedConfig.features?.tavern) {
6566
+ newFeatureRegistry.register(
6567
+ new TavernModule(
6568
+ apiClient,
6569
+ (entry) => useCliStore.getState().addTavernLogEntry(entry),
6570
+ () => useCliStore.getState().tavernActivityLog
6571
+ )
6572
+ );
6573
+ }
6574
+ const newToolNames = newFeatureRegistry.getAllToolNames();
6575
+ if (newToolNames.length > 0) {
6576
+ registerFeatureModuleTools(newToolNames);
6577
+ }
6578
+ if (state.wsManager && newFeatureRegistry.hasModules) {
6579
+ newFeatureRegistry.registerAllWsHandlers(state.wsManager);
6580
+ }
6581
+ const agentContext = state.agent.context;
6582
+ const oldFeatureToolNames = new Set(state.featureRegistry?.getAllToolNames() ?? []);
6583
+ const baseTools = agentContext.tools.filter((t) => !oldFeatureToolNames.has(t.toolSchema.name));
6584
+ const newFeatureTools = newFeatureRegistry.getAllTools();
6585
+ agentContext.tools = [...baseTools, ...newFeatureTools];
6586
+ const newFeaturePrompts = newFeatureRegistry.getSystemPromptSections();
6587
+ agentContext.systemPrompt = buildCoreSystemPrompt({
6588
+ contextContent: state.contextContent,
6589
+ agentStore: state.agentStore || void 0,
6590
+ customCommands: state.customCommandStore.getAllCommands(),
6591
+ enableSkillTool: updatedConfig.preferences.enableSkillTool !== false,
6592
+ enableDynamicAgentCreation: updatedConfig.preferences.enableDynamicAgentCreation === true,
6593
+ additionalDirectories: state.additionalDirectories,
6594
+ featureModulePrompts: newFeaturePrompts || void 0
6595
+ });
6596
+ const moduleNames = newFeatureRegistry.getModuleNames();
6597
+ if (moduleNames.length > 0) {
6598
+ console.error(`
6599
+ \x1B[36m\u{1F3F0} Feature modules hot-reloaded: ${moduleNames.join(", ")}\x1B[0m`);
6600
+ } else {
6601
+ console.error(`
6602
+ \x1B[36m\u{1F3F0} Feature modules disabled\x1B[0m`);
6603
+ }
6604
+ }
5556
6605
  setState((prev) => {
6606
+ const updates = { config: updatedConfig };
6607
+ if (featuresChanged) {
6608
+ updates.featureRegistry = newFeatureRegistry;
6609
+ }
5557
6610
  if (modelChanged && prev.session) {
5558
6611
  const updatedSession = {
5559
6612
  ...prev.session,
@@ -5564,9 +6617,9 @@ Allowed domains (${domains.length}):`);
5564
6617
  if (prev.agent) {
5565
6618
  prev.agent.context.model = updatedConfig.defaultModel;
5566
6619
  }
5567
- return { ...prev, config: updatedConfig, session: updatedSession };
6620
+ return { ...prev, ...updates, session: updatedSession };
5568
6621
  }
5569
- return { ...prev, config: updatedConfig };
6622
+ return { ...prev, ...updates };
5570
6623
  });
5571
6624
  if (modelChanged && state.agent) {
5572
6625
  const backend = state.agent.context.llm;
@@ -5662,7 +6715,11 @@ Allowed domains (${domains.length}):`);
5662
6715
  if (!isInitialized) {
5663
6716
  return /* @__PURE__ */ React22.createElement(Box21, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React22.createElement(Text21, null, "\u{1F680} Initializing..."));
5664
6717
  }
5665
- const allCommands = mergeCommands(state.customCommandStore.getAllCommands());
6718
+ const featureCommandDefs = (state.featureRegistry?.getAllCommands() ?? []).map((cmd) => ({
6719
+ name: cmd.name,
6720
+ description: cmd.description
6721
+ }));
6722
+ const allCommands = mergeCommands(state.customCommandStore.getAllCommands(), featureCommandDefs);
5666
6723
  return /* @__PURE__ */ React22.createElement(
5667
6724
  App,
5668
6725
  {