@zds-ai/cli 0.1.8 → 0.1.10

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 (73) hide show
  1. package/README.md +387 -34
  2. package/dist/agent/context-manager.d.ts +70 -0
  3. package/dist/agent/context-manager.js +138 -0
  4. package/dist/agent/context-manager.js.map +1 -0
  5. package/dist/agent/hook-manager.d.ts +194 -0
  6. package/dist/agent/hook-manager.js +676 -0
  7. package/dist/agent/hook-manager.js.map +1 -0
  8. package/dist/agent/llm-agent.d.ts +469 -100
  9. package/dist/agent/llm-agent.js +781 -1580
  10. package/dist/agent/llm-agent.js.map +1 -1
  11. package/dist/agent/message-processor.d.ts +103 -0
  12. package/dist/agent/message-processor.js +225 -0
  13. package/dist/agent/message-processor.js.map +1 -0
  14. package/dist/agent/prompt-variables.d.ts +103 -40
  15. package/dist/agent/prompt-variables.js +250 -113
  16. package/dist/agent/prompt-variables.js.map +1 -1
  17. package/dist/agent/session-manager.d.ts +75 -0
  18. package/dist/agent/session-manager.js +194 -0
  19. package/dist/agent/session-manager.js.map +1 -0
  20. package/dist/agent/tool-executor.d.ts +111 -0
  21. package/dist/agent/tool-executor.js +397 -0
  22. package/dist/agent/tool-executor.js.map +1 -0
  23. package/dist/bin/generate_image_sd.sh +19 -12
  24. package/dist/bin/joycaption.sh +37 -0
  25. package/dist/grok/client.d.ts +52 -0
  26. package/dist/grok/client.js +127 -19
  27. package/dist/grok/client.js.map +1 -1
  28. package/dist/grok/tools.js +42 -8
  29. package/dist/grok/tools.js.map +1 -1
  30. package/dist/hooks/use-input-handler.d.ts +1 -1
  31. package/dist/hooks/use-input-handler.js +100 -13
  32. package/dist/hooks/use-input-handler.js.map +1 -1
  33. package/dist/index.js +25 -3
  34. package/dist/index.js.map +1 -1
  35. package/dist/mcp/config.d.ts +1 -0
  36. package/dist/mcp/config.js +45 -7
  37. package/dist/mcp/config.js.map +1 -1
  38. package/dist/tools/character-tool.js +13 -1
  39. package/dist/tools/character-tool.js.map +1 -1
  40. package/dist/tools/image-tool.d.ts +11 -1
  41. package/dist/tools/image-tool.js +109 -2
  42. package/dist/tools/image-tool.js.map +1 -1
  43. package/dist/tools/introspect-tool.js +131 -30
  44. package/dist/tools/introspect-tool.js.map +1 -1
  45. package/dist/tools/morph-editor.d.ts +21 -9
  46. package/dist/tools/morph-editor.js +21 -9
  47. package/dist/tools/morph-editor.js.map +1 -1
  48. package/dist/ui/components/active-task-status.d.ts +1 -1
  49. package/dist/ui/components/api-key-input.d.ts +1 -1
  50. package/dist/ui/components/backend-status.d.ts +1 -1
  51. package/dist/ui/components/chat-history.d.ts +1 -1
  52. package/dist/ui/components/chat-interface.d.ts +1 -1
  53. package/dist/ui/components/chat-interface.js +1 -1
  54. package/dist/ui/components/chat-interface.js.map +1 -1
  55. package/dist/ui/components/context-status.d.ts +1 -1
  56. package/dist/ui/components/mood-status.d.ts +1 -1
  57. package/dist/ui/components/persona-status.d.ts +1 -1
  58. package/dist/utils/chat-history-manager.d.ts +12 -4
  59. package/dist/utils/chat-history-manager.js +26 -11
  60. package/dist/utils/chat-history-manager.js.map +1 -1
  61. package/dist/utils/hook-executor.d.ts +53 -2
  62. package/dist/utils/hook-executor.js +258 -36
  63. package/dist/utils/hook-executor.js.map +1 -1
  64. package/dist/utils/rephrase-handler.d.ts +1 -1
  65. package/dist/utils/settings-manager.d.ts +41 -11
  66. package/dist/utils/settings-manager.js +172 -40
  67. package/dist/utils/settings-manager.js.map +1 -1
  68. package/dist/utils/slash-commands.d.ts +3 -3
  69. package/dist/utils/slash-commands.js +11 -5
  70. package/dist/utils/slash-commands.js.map +1 -1
  71. package/dist/utils/startup-hook.js +9 -2
  72. package/dist/utils/startup-hook.js.map +1 -1
  73. package/package.json +10 -8
@@ -0,0 +1,676 @@
1
+ import { LLMClient } from "../grok/client.js";
2
+ import { getSettingsManager } from "../utils/settings-manager.js";
3
+ import { executeOperationHook, applyHookCommands } from "../utils/hook-executor.js";
4
+ import { getAllLLMTools } from "../grok/tools.js";
5
+ import { logApiError } from "../utils/error-logger.js";
6
+ import { Variable } from "./prompt-variables.js";
7
+ /**
8
+ * Manages hook execution for persona, mood, and task operations
9
+ *
10
+ * Handles:
11
+ * - Persona/mood/task hook execution with approval workflows
12
+ * - Backend and model switching with validation
13
+ * - Hook command processing and environment variable management
14
+ * - API testing for backend/model changes
15
+ * - System message generation for state changes
16
+ */
17
+ export class HookManager {
18
+ deps;
19
+ constructor(deps) {
20
+ this.deps = deps;
21
+ }
22
+ /**
23
+ * Set agent persona with optional hook execution
24
+ * Executes persona hook if configured and processes backend/model changes
25
+ *
26
+ * @param persona New persona name
27
+ * @param color Optional display color
28
+ * @returns Success status and error message if failed
29
+ */
30
+ async setPersona(persona, color) {
31
+ const settings = getSettingsManager();
32
+ const hookPath = settings.getPersonaHook();
33
+ const hookMandatory = settings.isPersonaHookMandatory();
34
+ if (!hookPath && hookMandatory) {
35
+ return { success: false, error: "Persona hook is mandatory but not configured" };
36
+ }
37
+ if (hookPath) {
38
+ const hookResult = await executeOperationHook(hookPath, "setPersona", {
39
+ persona_old: process.env.ZDS_AI_AGENT_PERSONA || "",
40
+ persona_new: persona,
41
+ persona_color: color || "white"
42
+ }, 30000, hookMandatory, this.deps.getCurrentTokenCount(), this.deps.getMaxContextSize());
43
+ if (!hookResult.approved) {
44
+ await this.processHookResult(hookResult);
45
+ return { success: false, error: hookResult.reason || "Hook rejected persona change" };
46
+ }
47
+ const result = await this.processHookResult(hookResult, 'ZDS_AI_AGENT_PERSONA');
48
+ if (!result.success) {
49
+ return { success: false, error: "Persona change rejected due to failed model/backend test" };
50
+ }
51
+ if (result.transformedValue) {
52
+ persona = result.transformedValue;
53
+ }
54
+ }
55
+ process.env.ZDS_AI_AGENT_PERSONA = persona;
56
+ this.deps.setPersona(persona, color || "white");
57
+ this.deps.emit('personaChange', { persona, color: color || "white" });
58
+ return { success: true };
59
+ }
60
+ /**
61
+ * Set agent mood with optional hook execution
62
+ * Executes mood hook if configured and adds system message to chat
63
+ *
64
+ * @param mood New mood name
65
+ * @param color Optional display color
66
+ * @returns Success status and error message if failed
67
+ */
68
+ async setMood(mood, color) {
69
+ const settings = getSettingsManager();
70
+ const hookPath = settings.getMoodHook();
71
+ const hookMandatory = settings.isMoodHookMandatory();
72
+ if (!hookPath && hookMandatory) {
73
+ return { success: false, error: "Mood hook is mandatory but not configured" };
74
+ }
75
+ if (hookPath) {
76
+ const hookResult = await executeOperationHook(hookPath, "setMood", {
77
+ mood_old: process.env.ZDS_AI_AGENT_MOOD || "",
78
+ mood_new: mood,
79
+ mood_color: color || "white"
80
+ }, 30000, hookMandatory, this.deps.getCurrentTokenCount(), this.deps.getMaxContextSize());
81
+ if (!hookResult.approved) {
82
+ await this.processHookResult(hookResult);
83
+ return { success: false, error: hookResult.reason || "Hook rejected mood change" };
84
+ }
85
+ const result = await this.processHookResult(hookResult, 'ZDS_AI_AGENT_MOOD');
86
+ if (!result.success) {
87
+ return { success: false, error: "Mood change rejected due to failed model/backend test" };
88
+ }
89
+ if (result.transformedValue) {
90
+ mood = result.transformedValue;
91
+ }
92
+ }
93
+ process.env.ZDS_AI_AGENT_MOOD = mood;
94
+ this.deps.setMood(mood, color || "white");
95
+ const oldMood = process.env.ZDS_AI_AGENT_MOOD_OLD || "";
96
+ const oldColor = process.env.ZDS_AI_AGENT_MOOD_COLOR_OLD || "white";
97
+ let systemContent;
98
+ if (oldMood) {
99
+ const oldColorStr = oldColor !== "white" ? ` (${oldColor})` : "";
100
+ const newColorStr = color && color !== "white" ? ` (${color})` : "";
101
+ systemContent = `Assistant changed the mood from "${oldMood}"${oldColorStr} to "${mood}"${newColorStr}`;
102
+ }
103
+ else {
104
+ const colorStr = color && color !== "white" ? ` (${color})` : "";
105
+ systemContent = `Assistant set the mood to "${mood}"${colorStr}`;
106
+ }
107
+ this.deps.chatHistory.push({
108
+ type: 'system',
109
+ content: systemContent,
110
+ timestamp: new Date()
111
+ });
112
+ this.deps.emit('moodChange', { mood, color: color || "white" });
113
+ return { success: true };
114
+ }
115
+ /**
116
+ * Start a new active task with approval hook
117
+ * Prevents starting if another task is already active
118
+ *
119
+ * @param activeTask Task name
120
+ * @param action Task action/status
121
+ * @param color Optional display color
122
+ * @returns Success status and error message if failed
123
+ */
124
+ async startActiveTask(activeTask, action, color) {
125
+ if (process.env.ZDS_AI_AGENT_ACTIVE_TASK) {
126
+ return {
127
+ success: false,
128
+ error: `Cannot start new task "${activeTask}". Active task "${process.env.ZDS_AI_AGENT_ACTIVE_TASK}" must be stopped first.`
129
+ };
130
+ }
131
+ const settings = getSettingsManager();
132
+ const hookPath = settings.getTaskApprovalHook();
133
+ if (hookPath) {
134
+ const hookResult = await executeOperationHook(hookPath, "startActiveTask", { activetask: activeTask, action, color: color || "white" }, 30000, false, this.deps.getCurrentTokenCount(), this.deps.getMaxContextSize());
135
+ await this.processHookResult(hookResult);
136
+ if (!hookResult.approved) {
137
+ return { success: false, error: hookResult.reason || "Hook rejected task start" };
138
+ }
139
+ }
140
+ process.env.ZDS_AI_AGENT_ACTIVE_TASK = activeTask;
141
+ process.env.ZDS_AI_AGENT_ACTIVE_TASK_ACTION = action;
142
+ this.deps.setActiveTask(activeTask, action, color || "white");
143
+ const colorStr = color && color !== "white" ? ` (${color})` : "";
144
+ this.deps.messages.push({
145
+ role: 'system',
146
+ content: `Assistant changed task status for "${activeTask}" to ${action}${colorStr}`
147
+ });
148
+ this.deps.emit('activeTaskChange', { activeTask, action, color: color || "white" });
149
+ return { success: true };
150
+ }
151
+ /**
152
+ * Transition active task status with approval hook
153
+ * Requires an active task to be running
154
+ *
155
+ * @param action New task action/status
156
+ * @param color Optional display color
157
+ * @returns Success status and error message if failed
158
+ */
159
+ async transitionActiveTaskStatus(action, color) {
160
+ if (!process.env.ZDS_AI_AGENT_ACTIVE_TASK) {
161
+ return { success: false, error: "Cannot transition task status. No active task is currently running." };
162
+ }
163
+ const settings = getSettingsManager();
164
+ const hookPath = settings.getTaskApprovalHook();
165
+ if (hookPath) {
166
+ const hookResult = await executeOperationHook(hookPath, "transitionActiveTaskStatus", { action, color: color || "white" }, 30000, false, this.deps.getCurrentTokenCount(), this.deps.getMaxContextSize());
167
+ await this.processHookResult(hookResult);
168
+ if (!hookResult.approved) {
169
+ return { success: false, error: hookResult.reason || "Hook rejected task status transition" };
170
+ }
171
+ }
172
+ const oldAction = process.env.ZDS_AI_AGENT_ACTIVE_TASK_ACTION || "";
173
+ process.env.ZDS_AI_AGENT_ACTIVE_TASK_ACTION = action;
174
+ const colorStr = color && color !== "white" ? ` (${color})` : "";
175
+ this.deps.messages.push({
176
+ role: 'system',
177
+ content: `Assistant changed task status for "${process.env.ZDS_AI_AGENT_ACTIVE_TASK}" from ${oldAction} to ${action}${colorStr}`
178
+ });
179
+ this.deps.emit('activeTaskChange', {
180
+ activeTask: process.env.ZDS_AI_AGENT_ACTIVE_TASK,
181
+ action,
182
+ color: color || "white"
183
+ });
184
+ return { success: true };
185
+ }
186
+ /**
187
+ * Stop active task with approval hook and minimum delay
188
+ * Enforces 3-second minimum delay for task completion
189
+ *
190
+ * @param reason Reason for stopping task
191
+ * @param documentationFile Documentation file path
192
+ * @param color Optional display color
193
+ * @returns Success status and error message if failed
194
+ */
195
+ async stopActiveTask(reason, documentationFile, color) {
196
+ if (!process.env.ZDS_AI_AGENT_ACTIVE_TASK) {
197
+ return { success: false, error: "Cannot stop task. No active task is currently running." };
198
+ }
199
+ const startTime = Date.now();
200
+ const settings = getSettingsManager();
201
+ const hookPath = settings.getTaskApprovalHook();
202
+ if (hookPath) {
203
+ const hookResult = await executeOperationHook(hookPath, "stopActiveTask", { reason, documentation_file: documentationFile, color: color || "white" }, 30000, false, this.deps.getCurrentTokenCount(), this.deps.getMaxContextSize());
204
+ await this.processHookResult(hookResult);
205
+ if (!hookResult.approved) {
206
+ return { success: false, error: hookResult.reason || "Hook rejected task stop" };
207
+ }
208
+ }
209
+ const elapsed = Date.now() - startTime;
210
+ const remainingDelay = Math.max(0, 3000 - elapsed);
211
+ if (remainingDelay > 0) {
212
+ await new Promise(resolve => setTimeout(resolve, remainingDelay));
213
+ }
214
+ const stoppedTask = process.env.ZDS_AI_AGENT_ACTIVE_TASK;
215
+ const stoppedAction = process.env.ZDS_AI_AGENT_ACTIVE_TASK_ACTION || "";
216
+ delete process.env.ZDS_AI_AGENT_ACTIVE_TASK;
217
+ delete process.env.ZDS_AI_AGENT_ACTIVE_TASK_ACTION;
218
+ const colorStr = color && color !== "white" ? ` (${color})` : "";
219
+ this.deps.messages.push({
220
+ role: 'system',
221
+ content: `Assistant stopped task "${stoppedTask}" (was ${stoppedAction}) with reason: ${reason}${colorStr}`
222
+ });
223
+ this.deps.emit('activeTaskChange', { activeTask: "", action: "", color: "white" });
224
+ return { success: true };
225
+ }
226
+ /**
227
+ * Process hook result commands and apply environment changes
228
+ * Handles backend/model changes, environment variables, and system messages
229
+ *
230
+ * @param hookResult Hook execution result with commands
231
+ * @param envKey Optional environment key to extract transformed value
232
+ * @returns Success status and transformed value if applicable
233
+ */
234
+ async processHookResult(hookResult, envKey) {
235
+ if (!hookResult.commands) {
236
+ return { success: true };
237
+ }
238
+ const results = applyHookCommands(hookResult.commands);
239
+ let transformedValue;
240
+ if (envKey && results.env[envKey]) {
241
+ transformedValue = results.env[envKey];
242
+ }
243
+ const success = await this.processHookCommands(results);
244
+ return { success, transformedValue };
245
+ }
246
+ /**
247
+ * Process hook commands with backend/model testing
248
+ * Tests API connectivity before applying changes
249
+ *
250
+ * @param commands Processed hook commands
251
+ * @returns Success status of command processing
252
+ */
253
+ async processHookCommands(commands) {
254
+ const { applyEnvVariables } = await import('../utils/hook-executor.js');
255
+ const hasBackendChange = commands.backend && commands.baseUrl && commands.apiKeyEnvVar;
256
+ const hasModelChange = commands.model;
257
+ // Apply immediate (non-conditional) commands right away
258
+ applyEnvVariables(commands.env);
259
+ const seenVars = new Set();
260
+ for (const { name, value } of commands.promptVars) {
261
+ if (seenVars.has(name)) {
262
+ Variable.append(name, value);
263
+ }
264
+ else {
265
+ Variable.set(name, value);
266
+ seenVars.add(name);
267
+ }
268
+ }
269
+ if (commands.system) {
270
+ this.deps.chatHistory.push({
271
+ type: "system",
272
+ content: commands.system,
273
+ timestamp: new Date(),
274
+ });
275
+ }
276
+ // Check for CONDITIONAL commands without any CONDITION - this is an error
277
+ if (commands.conditionalResults && !hasBackendChange && !hasModelChange) {
278
+ const errorMsg = "Hook error: CONDITIONAL commands present but no CONDITION BACKEND or CONDITION MODEL specified. Conditional commands ignored.";
279
+ console.warn(errorMsg);
280
+ this.deps.chatHistory.push({
281
+ type: "system",
282
+ content: errorMsg,
283
+ timestamp: new Date(),
284
+ });
285
+ // Don't return false - allow processing to continue, just skip the conditional commands
286
+ }
287
+ // If there's a backend/model change, test it and apply conditional commands on success
288
+ if (hasBackendChange) {
289
+ const testResult = await this.testBackendModelChange(commands.backend, commands.baseUrl, commands.apiKeyEnvVar, commands.model);
290
+ if (!testResult.success) {
291
+ const parts = [];
292
+ if (commands.backend)
293
+ parts.push(`backend to "${commands.backend}"`);
294
+ if (commands.model)
295
+ parts.push(`model to "${commands.model}"`);
296
+ const errorMsg = `Failed to change ${parts.join(' and ')}: ${testResult.error}`;
297
+ this.deps.chatHistory.push({
298
+ type: "system",
299
+ content: errorMsg,
300
+ timestamp: new Date(),
301
+ });
302
+ return false;
303
+ }
304
+ // Apply conditional commands after successful test
305
+ if (commands.conditionalResults) {
306
+ applyEnvVariables(commands.conditionalResults.env);
307
+ const seenVars = new Set();
308
+ for (const { name, value } of commands.conditionalResults.promptVars) {
309
+ if (seenVars.has(name)) {
310
+ Variable.append(name, value);
311
+ }
312
+ else {
313
+ Variable.set(name, value);
314
+ seenVars.add(name);
315
+ }
316
+ }
317
+ if (commands.conditionalResults.system) {
318
+ this.deps.chatHistory.push({
319
+ type: "system",
320
+ content: commands.conditionalResults.system,
321
+ timestamp: new Date(),
322
+ });
323
+ }
324
+ }
325
+ const parts = [];
326
+ if (commands.backend)
327
+ parts.push(`backend to "${commands.backend}"`);
328
+ if (commands.model)
329
+ parts.push(`model to "${commands.model}"`);
330
+ const successMsg = `Changed ${parts.join(' and ')}`;
331
+ this.deps.chatHistory.push({
332
+ type: "system",
333
+ content: successMsg,
334
+ timestamp: new Date(),
335
+ });
336
+ if (commands.backend) {
337
+ this.deps.emit('backendChange', { backend: commands.backend });
338
+ }
339
+ if (commands.model) {
340
+ this.deps.emit('modelChange', { model: commands.model });
341
+ }
342
+ }
343
+ else if (hasModelChange) {
344
+ const testResult = await this.testModel(commands.model);
345
+ if (!testResult.success) {
346
+ const errorMsg = `Failed to change model to "${commands.model}": ${testResult.error}`;
347
+ this.deps.chatHistory.push({
348
+ type: "system",
349
+ content: errorMsg,
350
+ timestamp: new Date(),
351
+ });
352
+ return false;
353
+ }
354
+ // Apply conditional commands after successful test
355
+ if (commands.conditionalResults) {
356
+ applyEnvVariables(commands.conditionalResults.env);
357
+ const seenVars = new Set();
358
+ for (const { name, value } of commands.conditionalResults.promptVars) {
359
+ if (seenVars.has(name)) {
360
+ Variable.append(name, value);
361
+ }
362
+ else {
363
+ Variable.set(name, value);
364
+ seenVars.add(name);
365
+ }
366
+ }
367
+ if (commands.conditionalResults.system) {
368
+ this.deps.chatHistory.push({
369
+ type: "system",
370
+ content: commands.conditionalResults.system,
371
+ timestamp: new Date(),
372
+ });
373
+ }
374
+ }
375
+ const successMsg = `Model changed to "${commands.model}"`;
376
+ this.deps.chatHistory.push({
377
+ type: "system",
378
+ content: successMsg,
379
+ timestamp: new Date(),
380
+ });
381
+ this.deps.emit('modelChange', { model: commands.model });
382
+ }
383
+ // If no backend/model change, conditional commands are ignored (there's no condition to satisfy)
384
+ // Execute CALL commands after all other processing (fire-and-forget)
385
+ // Execute immediate CALLs
386
+ if (commands.calls.length > 0) {
387
+ this.executeCalls(commands.calls).catch(error => {
388
+ console.error("Error executing immediate CALL commands:", error);
389
+ });
390
+ }
391
+ // Execute conditional CALLs only if backend/model test succeeded
392
+ if (commands.conditionalResults && commands.conditionalResults.calls.length > 0 && (hasBackendChange || hasModelChange)) {
393
+ this.executeCalls(commands.conditionalResults.calls).catch(error => {
394
+ console.error("Error executing conditional CALL commands:", error);
395
+ });
396
+ }
397
+ return true;
398
+ }
399
+ /**
400
+ * Render system message with current variable state
401
+ * Updates messages[0] with fresh system prompt from variables
402
+ */
403
+ renderSystemMessage() {
404
+ this.deps.messages[0] = {
405
+ role: "system",
406
+ content: Variable.renderFull('SYSTEM')
407
+ };
408
+ }
409
+ /**
410
+ * Test model change by making API call
411
+ * Validates model compatibility before switching
412
+ *
413
+ * @param newModel Model name to test
414
+ * @returns Success status and error message if failed
415
+ */
416
+ async testModel(newModel) {
417
+ const previousModel = this.deps.getCurrentModel();
418
+ const previousTokenCounter = this.deps.getTokenCounter();
419
+ // Render system message with current variable state before testing
420
+ this.renderSystemMessage();
421
+ const testMessages = this.stripInProgressToolCalls(this.deps.messages);
422
+ const supportsTools = this.deps.getLLMClient().getSupportsTools();
423
+ const tools = supportsTools ? await getAllLLMTools() : [];
424
+ const requestPayload = {
425
+ model: newModel,
426
+ messages: testMessages,
427
+ tools: supportsTools && tools.length > 0 ? tools : undefined,
428
+ temperature: this.deps.temperature,
429
+ max_tokens: 10
430
+ };
431
+ try {
432
+ this.deps.getLLMClient().setModel(newModel);
433
+ const { createTokenCounter } = await import("../utils/token-counter.js");
434
+ this.deps.setTokenCounter(createTokenCounter(newModel));
435
+ const response = await this.deps.getLLMClient().chat(testMessages, tools, newModel, undefined, this.deps.temperature, undefined, 10);
436
+ if (!response || !response.choices || response.choices.length === 0) {
437
+ throw new Error("Invalid response from API");
438
+ }
439
+ previousTokenCounter.dispose();
440
+ return { success: true };
441
+ }
442
+ catch (error) {
443
+ this.deps.getLLMClient().setModel(previousModel);
444
+ this.deps.getTokenCounter().dispose();
445
+ this.deps.setTokenCounter(previousTokenCounter);
446
+ const { message: logPaths } = await logApiError(requestPayload, error, { errorType: 'model-switch-test-failure', previousModel, newModel }, 'test-fail');
447
+ const errorMessage = error.message || "Unknown error during model test";
448
+ return {
449
+ success: false,
450
+ error: `Model test failed: ${errorMessage}\n${logPaths}`
451
+ };
452
+ }
453
+ }
454
+ /**
455
+ * Test backend and model change by making API call
456
+ * Validates backend connectivity and model compatibility
457
+ *
458
+ * @param backend Backend name
459
+ * @param baseUrl API base URL
460
+ * @param apiKeyEnvVar Environment variable for API key
461
+ * @param model Optional model name
462
+ * @returns Success status and error message if failed
463
+ */
464
+ async testBackendModelChange(backend, baseUrl, apiKeyEnvVar, model) {
465
+ const previousClient = this.deps.getLLMClient();
466
+ const previousTokenCounter = this.deps.getTokenCounter();
467
+ const previousApiKeyEnvVar = this.deps.apiKeyEnvVar;
468
+ const previousBackend = this.deps.getLLMClient().getBackendName();
469
+ const previousModel = this.deps.getCurrentModel();
470
+ let requestPayload;
471
+ let newModel;
472
+ let modelChanged = false;
473
+ try {
474
+ const apiKey = process.env[apiKeyEnvVar];
475
+ if (!apiKey) {
476
+ throw new Error(`API key not found in environment variable: ${apiKeyEnvVar}`);
477
+ }
478
+ newModel = model || this.deps.getCurrentModel();
479
+ modelChanged = newModel !== previousModel;
480
+ const newClient = new LLMClient(apiKey, newModel, baseUrl, backend);
481
+ this.deps.setLLMClient(newClient);
482
+ this.deps.setApiKeyEnvVar(apiKeyEnvVar);
483
+ // Update token counter only if model changed
484
+ if (modelChanged) {
485
+ const { createTokenCounter } = await import("../utils/token-counter.js");
486
+ this.deps.setTokenCounter(createTokenCounter(newModel));
487
+ }
488
+ const { loadMCPConfig } = await import("../mcp/config.js");
489
+ const { initializeMCPServers } = await import("../grok/tools.js");
490
+ try {
491
+ const config = loadMCPConfig();
492
+ if (config.servers.length > 0) {
493
+ await initializeMCPServers();
494
+ }
495
+ }
496
+ catch (mcpError) {
497
+ console.warn("MCP reinitialization failed:", mcpError);
498
+ }
499
+ // Render system message with current variable state before testing
500
+ this.renderSystemMessage();
501
+ const testMessages = this.stripInProgressToolCalls(this.deps.messages);
502
+ const supportsTools = this.deps.getLLMClient().getSupportsTools();
503
+ const tools = supportsTools ? await getAllLLMTools() : [];
504
+ requestPayload = {
505
+ backend,
506
+ baseUrl,
507
+ model: newModel,
508
+ messages: testMessages,
509
+ tools: supportsTools && tools.length > 0 ? tools : undefined,
510
+ temperature: this.deps.temperature,
511
+ max_tokens: 10
512
+ };
513
+ const response = await this.deps.getLLMClient().chat(testMessages, tools, newModel, undefined, this.deps.temperature, undefined, 10);
514
+ if (!response || !response.choices || response.choices.length === 0) {
515
+ throw new Error("Invalid response from API");
516
+ }
517
+ // Dispose old token counter if model changed
518
+ if (modelChanged) {
519
+ previousTokenCounter.dispose();
520
+ }
521
+ return { success: true };
522
+ }
523
+ catch (error) {
524
+ this.deps.setLLMClient(previousClient);
525
+ this.deps.setApiKeyEnvVar(previousApiKeyEnvVar);
526
+ // Restore token counter if we changed it
527
+ if (modelChanged) {
528
+ this.deps.getTokenCounter().dispose();
529
+ this.deps.setTokenCounter(previousTokenCounter);
530
+ }
531
+ let logPaths = '';
532
+ if (requestPayload) {
533
+ const result = await logApiError(requestPayload, error, {
534
+ errorType: 'backend-switch-test-failure',
535
+ previousBackend,
536
+ previousModel,
537
+ newBackend: backend,
538
+ newModel,
539
+ baseUrl,
540
+ apiKeyEnvVar
541
+ }, 'test-fail');
542
+ logPaths = result.message;
543
+ }
544
+ const errorMessage = error.message || "Unknown error during backend/model test";
545
+ return {
546
+ success: false,
547
+ error: logPaths ? `${errorMessage}\n${logPaths}` : errorMessage
548
+ };
549
+ }
550
+ }
551
+ /**
552
+ * Strip in-progress tool calls from messages for API testing
553
+ * Removes incomplete tool call sequences to avoid API errors
554
+ *
555
+ * @param messages Message array to clean
556
+ * @returns Cleaned message array without incomplete tool calls
557
+ */
558
+ stripInProgressToolCalls(messages) {
559
+ let lastAssistantIndex = -1;
560
+ for (let i = messages.length - 1; i >= 0; i--) {
561
+ if (messages[i].role === 'assistant') {
562
+ lastAssistantIndex = i;
563
+ break;
564
+ }
565
+ }
566
+ if (lastAssistantIndex === -1 || !messages[lastAssistantIndex].tool_calls) {
567
+ return messages;
568
+ }
569
+ const cleanedMessages = JSON.parse(JSON.stringify(messages));
570
+ const toolCallIds = new Set((cleanedMessages[lastAssistantIndex].tool_calls || []).map((tc) => tc.id));
571
+ delete cleanedMessages[lastAssistantIndex].tool_calls;
572
+ return cleanedMessages.filter((msg, idx) => {
573
+ if (idx <= lastAssistantIndex) {
574
+ return true;
575
+ }
576
+ if (msg.role === 'tool' && toolCallIds.has(msg.tool_call_id)) {
577
+ return false;
578
+ }
579
+ return true;
580
+ });
581
+ }
582
+ /**
583
+ * Execute CALL commands asynchronously with recursion depth and duplicate tracking
584
+ * Fire-and-forget execution that processes hooks from called tools
585
+ *
586
+ * @param calls Array of CALL command strings
587
+ * @param context Call context for tracking recursion and duplicates
588
+ */
589
+ async executeCalls(calls, context = { depth: 0, executedCalls: new Set() }) {
590
+ // Maximum recursion depth is 5
591
+ const MAX_DEPTH = 5;
592
+ if (context.depth >= MAX_DEPTH) {
593
+ console.warn(`CALL recursion depth limit (${MAX_DEPTH}) reached, skipping remaining calls`);
594
+ return;
595
+ }
596
+ // Check if executeToolByName is available
597
+ if (!this.deps.executeToolByName) {
598
+ console.warn("CALL commands require executeToolByName dependency, skipping calls");
599
+ return;
600
+ }
601
+ for (const callSpec of calls) {
602
+ // Parse "toolName arg1=val1 arg2=val2"
603
+ const parts = callSpec.trim().split(/\s+/);
604
+ if (parts.length === 0) {
605
+ continue;
606
+ }
607
+ const toolName = parts[0];
608
+ const parameters = {};
609
+ // Parse parameters
610
+ for (let i = 1; i < parts.length; i++) {
611
+ const match = parts[i].match(/^([^=]+)=(.*)$/);
612
+ if (match) {
613
+ const [, key, value] = match;
614
+ // Try to parse as JSON, fall back to string
615
+ try {
616
+ parameters[key] = JSON.parse(value);
617
+ }
618
+ catch {
619
+ parameters[key] = value;
620
+ }
621
+ }
622
+ }
623
+ // Create signature for duplicate detection
624
+ const signature = `${toolName}:${JSON.stringify(parameters)}`;
625
+ if (context.executedCalls.has(signature)) {
626
+ console.warn(`Skipping duplicate CALL: ${signature}`);
627
+ continue;
628
+ }
629
+ // Mark as executed
630
+ context.executedCalls.add(signature);
631
+ // Execute tool asynchronously (fire-and-forget)
632
+ this.executeCallAsync(toolName, parameters, context).catch(error => {
633
+ console.error(`Error executing CALL ${toolName}:`, error);
634
+ });
635
+ }
636
+ }
637
+ /**
638
+ * Execute a single CALL asynchronously with hook processing
639
+ * Runs tool hooks which may generate more CALL commands
640
+ *
641
+ * @param toolName Tool to execute
642
+ * @param parameters Tool parameters
643
+ * @param context Call context for tracking recursion
644
+ */
645
+ async executeCallAsync(toolName, parameters, context) {
646
+ if (!this.deps.executeToolByName) {
647
+ return;
648
+ }
649
+ try {
650
+ // Execute the tool
651
+ const result = await this.deps.executeToolByName(toolName, parameters);
652
+ // Process any hook commands that were generated during tool execution
653
+ if (result.hookCommands && result.hookCommands.length > 0) {
654
+ const hookResults = applyHookCommands(result.hookCommands);
655
+ // Extract CALL commands from hook results (both immediate and conditional)
656
+ const recursiveCalls = [...hookResults.calls];
657
+ // Add conditional calls if present (they would have been validated by the tool's hooks)
658
+ if (hookResults.conditionalResults && hookResults.conditionalResults.calls.length > 0) {
659
+ recursiveCalls.push(...hookResults.conditionalResults.calls);
660
+ }
661
+ // Recursively execute CALL commands with incremented depth
662
+ if (recursiveCalls.length > 0) {
663
+ const nestedContext = {
664
+ depth: context.depth + 1,
665
+ executedCalls: context.executedCalls, // Share the same set to prevent duplicates across entire chain
666
+ };
667
+ await this.executeCalls(recursiveCalls, nestedContext);
668
+ }
669
+ }
670
+ }
671
+ catch (error) {
672
+ console.error(`Error in executeCallAsync for ${toolName}:`, error);
673
+ }
674
+ }
675
+ }
676
+ //# sourceMappingURL=hook-manager.js.map