orquesta-cli 0.2.44 → 0.2.46

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 (40) hide show
  1. package/dist/agents/planner/index.js +3 -1
  2. package/dist/cli.js +17 -16
  3. package/dist/constants.d.ts +1 -1
  4. package/dist/constants.js +1 -1
  5. package/dist/core/commands/clear.d.ts +3 -0
  6. package/dist/core/commands/clear.js +22 -0
  7. package/dist/core/commands/compact.d.ts +3 -0
  8. package/dist/core/commands/compact.js +45 -0
  9. package/dist/core/commands/help.d.ts +3 -0
  10. package/dist/core/commands/help.js +50 -0
  11. package/dist/core/commands/index.d.ts +3 -0
  12. package/dist/core/commands/index.js +11 -0
  13. package/dist/core/commands/memory.d.ts +3 -0
  14. package/dist/core/commands/memory.js +40 -0
  15. package/dist/core/commands/registry.d.ts +11 -0
  16. package/dist/core/commands/registry.js +25 -0
  17. package/dist/core/commands/types.d.ts +10 -0
  18. package/dist/core/commands/types.js +2 -0
  19. package/dist/core/event-bus.d.ts +20 -0
  20. package/dist/core/event-bus.js +35 -0
  21. package/dist/core/git-context.d.ts +11 -0
  22. package/dist/core/git-context.js +62 -0
  23. package/dist/core/ignore-filter.d.ts +4 -0
  24. package/dist/core/ignore-filter.js +50 -0
  25. package/dist/core/llm/llm-client.d.ts +1 -0
  26. package/dist/core/llm/llm-client.js +118 -40
  27. package/dist/core/memory.d.ts +7 -0
  28. package/dist/core/memory.js +55 -0
  29. package/dist/core/onboarding.d.ts +3 -0
  30. package/dist/core/onboarding.js +48 -0
  31. package/dist/core/slash-command-handler.js +8 -99
  32. package/dist/orchestration/plan-executor.js +78 -71
  33. package/dist/prompts/shared/tool-usage.js +0 -1
  34. package/dist/prompts/system/plan-execute.js +50 -57
  35. package/dist/tools/llm/simple/file-tools.js +12 -1
  36. package/dist/tools/llm/simple/final-response-tool.js +7 -11
  37. package/dist/tools/registry.js +63 -10
  38. package/dist/ui/components/PlanExecuteApp.d.ts +1 -0
  39. package/dist/ui/components/PlanExecuteApp.js +59 -22
  40. package/package.json +8 -4
@@ -108,6 +108,7 @@ export class LLMClient {
108
108
  modelName;
109
109
  currentAbortController = null;
110
110
  isInterrupted = false;
111
+ onStreamingContent = null;
111
112
  static DEFAULT_MAX_RETRIES = 3;
112
113
  constructor() {
113
114
  const endpoint = configManager.getCurrentEndpoint();
@@ -183,11 +184,11 @@ export class LLMClient {
183
184
  const modelId = options.model || this.model;
184
185
  const processedMessages = options.messages ?
185
186
  this.preprocessMessages(options.messages, modelId) : [];
186
- logger.vars({ name: 'modelId', value: modelId }, { name: 'originalMessages', value: options.messages?.length || 0 }, { name: 'processedMessages', value: processedMessages.length }, { name: 'temperature', value: options.temperature ?? 0.7 });
187
+ logger.vars({ name: 'modelId', value: modelId }, { name: 'originalMessages', value: options.messages?.length || 0 }, { name: 'processedMessages', value: processedMessages.length }, { name: 'temperature', value: options.temperature ?? 0 });
187
188
  const requestBody = {
188
189
  model: modelId,
189
190
  messages: processedMessages,
190
- temperature: options.temperature ?? 0.7,
191
+ temperature: options.temperature ?? 0,
191
192
  max_tokens: options.max_tokens,
192
193
  stream: false,
193
194
  ...(options.tools && {
@@ -210,13 +211,112 @@ export class LLMClient {
210
211
  }
211
212
  logger.startTimer('llm-api-call');
212
213
  this.currentAbortController = new AbortController();
213
- const response = await this.axiosInstance.post(url, requestBody, {
214
- signal: this.currentAbortController.signal,
215
- headers: buildPerRequestHeaders(),
216
- });
217
- this.currentAbortController = null;
214
+ let response;
215
+ if (this.onStreamingContent) {
216
+ const streamRequestBody = { ...requestBody, stream: true };
217
+ const streamResp = await this.axiosInstance.post(url, streamRequestBody, {
218
+ responseType: 'stream',
219
+ signal: this.currentAbortController.signal,
220
+ headers: buildPerRequestHeaders(),
221
+ });
222
+ captureBatutaHeaders(streamResp.headers);
223
+ const stream = streamResp.data;
224
+ let buffer = '';
225
+ let contentAccum = '';
226
+ let reasoningAccum = '';
227
+ let role = 'assistant';
228
+ let finishReason = null;
229
+ const toolCallsMap = new Map();
230
+ let responseId = '';
231
+ let responseModel = '';
232
+ for await (const chunk of stream) {
233
+ if (this.isInterrupted) {
234
+ throw new Error('INTERRUPTED');
235
+ }
236
+ buffer += chunk.toString();
237
+ const lines = buffer.split('\n');
238
+ buffer = lines.pop() || '';
239
+ for (const line of lines) {
240
+ const trimmed = line.trim();
241
+ if (!trimmed || trimmed === 'data: [DONE]')
242
+ continue;
243
+ if (!trimmed.startsWith('data: '))
244
+ continue;
245
+ try {
246
+ const data = JSON.parse(trimmed.slice(6));
247
+ if (data.id)
248
+ responseId = data.id;
249
+ if (data.model)
250
+ responseModel = data.model;
251
+ const choice = data.choices?.[0];
252
+ if (!choice)
253
+ continue;
254
+ if (choice.finish_reason)
255
+ finishReason = choice.finish_reason;
256
+ const delta = choice.delta;
257
+ if (!delta)
258
+ continue;
259
+ if (delta.role)
260
+ role = delta.role;
261
+ if (delta.content) {
262
+ contentAccum += delta.content;
263
+ this.onStreamingContent(delta.content);
264
+ }
265
+ if (delta.reasoning) {
266
+ reasoningAccum += delta.reasoning;
267
+ }
268
+ if (delta.tool_calls) {
269
+ for (const tc of delta.tool_calls) {
270
+ const idx = tc.index ?? 0;
271
+ if (!toolCallsMap.has(idx)) {
272
+ toolCallsMap.set(idx, { id: tc.id || '', type: 'function', function: { name: '', arguments: '' } });
273
+ }
274
+ const existing = toolCallsMap.get(idx);
275
+ if (tc.id)
276
+ existing.id = tc.id;
277
+ if (tc.function?.name)
278
+ existing.function.name += tc.function.name;
279
+ if (tc.function?.arguments)
280
+ existing.function.arguments += tc.function.arguments;
281
+ }
282
+ }
283
+ }
284
+ catch { }
285
+ }
286
+ }
287
+ this.currentAbortController = null;
288
+ const toolCalls = Array.from(toolCallsMap.values())
289
+ .filter(tc => tc.id && tc.function.name)
290
+ .map(tc => ({ id: tc.id, type: 'function', function: { name: tc.function.name, arguments: tc.function.arguments } }));
291
+ const reassembledMessage = {
292
+ role: role,
293
+ content: contentAccum,
294
+ ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
295
+ ...(reasoningAccum ? { reasoning: reasoningAccum } : {}),
296
+ };
297
+ response = {
298
+ data: {
299
+ id: responseId,
300
+ object: 'chat.completion',
301
+ created: Math.floor(Date.now() / 1000),
302
+ model: responseModel || modelId,
303
+ choices: [{ index: 0, message: reassembledMessage, finish_reason: finishReason }],
304
+ },
305
+ status: streamResp.status,
306
+ statusText: streamResp.statusText,
307
+ headers: streamResp.headers,
308
+ };
309
+ }
310
+ else {
311
+ const httpResp = await this.axiosInstance.post(url, requestBody, {
312
+ signal: this.currentAbortController.signal,
313
+ headers: buildPerRequestHeaders(),
314
+ });
315
+ this.currentAbortController = null;
316
+ response = { data: httpResp.data, status: httpResp.status, statusText: httpResp.statusText, headers: httpResp.headers };
317
+ captureBatutaHeaders(response.headers);
318
+ }
218
319
  const elapsed = logger.endTimer('llm-api-call');
219
- captureBatutaHeaders(response.headers);
220
320
  logger.flow('API response received');
221
321
  if (!response.data.choices || !Array.isArray(response.data.choices)) {
222
322
  logger.error('Invalid response structure - missing choices array', response.data);
@@ -354,7 +454,7 @@ export class LLMClient {
354
454
  const requestBody = {
355
455
  model: modelId,
356
456
  messages: processedMessages,
357
- temperature: options.temperature ?? 0.7,
457
+ temperature: options.temperature ?? 0,
358
458
  max_tokens: options.max_tokens,
359
459
  stream: true,
360
460
  ...(options.tools && {
@@ -495,9 +595,7 @@ export class LLMClient {
495
595
  const toolCallHistory = [];
496
596
  let iterations = 0;
497
597
  let contextLengthRecoveryAttempted = false;
498
- let noToolCallRetries = 0;
499
598
  let finalResponseFailures = 0;
500
- const MAX_NO_TOOL_CALL_RETRIES = 3;
501
599
  const MAX_FINAL_RESPONSE_FAILURES = 3;
502
600
  const recentToolSignatures = [];
503
601
  const recentNormalizedSignatures = [];
@@ -524,7 +622,7 @@ export class LLMClient {
524
622
  response = await this.chatCompletion({
525
623
  messages: workingMessages,
526
624
  tools,
527
- tool_choice: 'required',
625
+ tool_choice: 'auto',
528
626
  ...(roleModel ? { model: roleModel } : {}),
529
627
  });
530
628
  }
@@ -733,34 +831,14 @@ export class LLMClient {
733
831
  continue;
734
832
  }
735
833
  else {
736
- noToolCallRetries++;
737
- logger.flow(`No tool call - enforcing tool usage (attempt ${noToolCallRetries}/${MAX_NO_TOOL_CALL_RETRIES})`);
738
- if (noToolCallRetries > MAX_NO_TOOL_CALL_RETRIES) {
739
- logger.warn('Max no-tool-call retries exceeded - returning content as final response');
740
- const fallbackContent = assistantMessage.content || 'Task completed.';
741
- const { emitAssistantResponse } = await import('../../tools/llm/simple/file-tools.js');
742
- emitAssistantResponse(fallbackContent);
743
- return {
744
- message: { role: 'assistant', content: fallbackContent },
745
- toolCalls: toolCallHistory,
746
- allMessages: workingMessages,
747
- };
748
- }
749
- const hasMalformedToolCall = assistantMessage.content &&
750
- (/<tool_call>/i.test(assistantMessage.content) ||
751
- /<arg_key>/i.test(assistantMessage.content) ||
752
- /<arg_value>/i.test(assistantMessage.content) ||
753
- /<\/tool_call>/i.test(assistantMessage.content) ||
754
- /bash<arg_key>/i.test(assistantMessage.content));
755
- const retryMessage = hasMalformedToolCall
756
- ? 'Your previous response contained a malformed tool call (XML tags in content). You MUST use the proper tool_calls API format. Use final_response tool to deliver your message to the user.'
757
- : 'You must use tools for all actions. Use final_response tool to deliver your final message to the user after completing all tasks.';
758
- workingMessages.push({
759
- role: 'user',
760
- content: retryMessage,
761
- });
762
- logger.debug('Enforcing tool call - added retry message');
763
- continue;
834
+ const finalContent = assistantMessage.content || 'Task completed.';
835
+ const { emitAssistantResponse } = await import('../../tools/llm/simple/file-tools.js');
836
+ emitAssistantResponse(finalContent);
837
+ return {
838
+ message: { role: 'assistant', content: finalContent },
839
+ toolCalls: toolCallHistory,
840
+ allMessages: workingMessages,
841
+ };
764
842
  }
765
843
  }
766
844
  }
@@ -0,0 +1,7 @@
1
+ export declare function getUserMemory(): string;
2
+ export declare function getMemoryPrompt(): string;
3
+ export declare function addMemory(entry: string): void;
4
+ export declare function removeMemory(index: number): boolean;
5
+ export declare function clearMemory(): void;
6
+ export declare function listMemory(): string[];
7
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1,55 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { LOCAL_HOME_DIR } from '../constants.js';
4
+ const MEMORY_PATH = path.join(LOCAL_HOME_DIR, 'memory.md');
5
+ let cache = null;
6
+ export function getUserMemory() {
7
+ if (cache !== null)
8
+ return cache;
9
+ try {
10
+ cache = fs.readFileSync(MEMORY_PATH, 'utf-8').trim();
11
+ }
12
+ catch {
13
+ cache = '';
14
+ }
15
+ return cache;
16
+ }
17
+ export function getMemoryPrompt() {
18
+ const memory = getUserMemory();
19
+ if (!memory)
20
+ return '';
21
+ return `\n\n<user_memory>\nThe user has saved these persistent preferences and notes:\n${memory}\n</user_memory>\n`;
22
+ }
23
+ export function addMemory(entry) {
24
+ const current = getUserMemory();
25
+ const timestamp = new Date().toISOString().slice(0, 10);
26
+ const newContent = current ? `${current}\n- ${entry} (${timestamp})` : `- ${entry} (${timestamp})`;
27
+ fs.mkdirSync(path.dirname(MEMORY_PATH), { recursive: true });
28
+ fs.writeFileSync(MEMORY_PATH, newContent, 'utf-8');
29
+ cache = newContent;
30
+ }
31
+ export function removeMemory(index) {
32
+ const current = getUserMemory();
33
+ const lines = current.split('\n').filter(l => l.trim());
34
+ if (index < 1 || index > lines.length)
35
+ return false;
36
+ lines.splice(index - 1, 1);
37
+ const newContent = lines.join('\n');
38
+ fs.writeFileSync(MEMORY_PATH, newContent, 'utf-8');
39
+ cache = newContent;
40
+ return true;
41
+ }
42
+ export function clearMemory() {
43
+ try {
44
+ fs.unlinkSync(MEMORY_PATH);
45
+ }
46
+ catch { }
47
+ cache = '';
48
+ }
49
+ export function listMemory() {
50
+ const current = getUserMemory();
51
+ if (!current)
52
+ return [];
53
+ return current.split('\n').filter(l => l.trim());
54
+ }
55
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1,3 @@
1
+ export declare function shouldShowOnboarding(): boolean;
2
+ export declare function runOnboarding(): Promise<boolean>;
3
+ //# sourceMappingURL=onboarding.d.ts.map
@@ -0,0 +1,48 @@
1
+ import chalk from 'chalk';
2
+ import { configManager } from './config/config-manager.js';
3
+ import { scanProviders, toEndpointConfig } from './config/auto-detect.js';
4
+ import { CONFIG_FILE_PATH } from '../constants.js';
5
+ import * as fs from 'fs';
6
+ export function shouldShowOnboarding() {
7
+ if (!fs.existsSync(CONFIG_FILE_PATH))
8
+ return true;
9
+ return !configManager.hasEndpoints();
10
+ }
11
+ export async function runOnboarding() {
12
+ console.log();
13
+ console.log(chalk.cyan(' ╔══════════════════════════════════════════╗'));
14
+ console.log(chalk.cyan(' ║') + chalk.bold(' Welcome to Orquesta CLI! 🎵 ') + chalk.cyan('║'));
15
+ console.log(chalk.cyan(' ╚══════════════════════════════════════════╝'));
16
+ console.log();
17
+ console.log(chalk.dim(' Scanning for LLM providers...'));
18
+ console.log();
19
+ const result = await scanProviders();
20
+ if (result.detected.length === 0) {
21
+ console.log(chalk.yellow(' No LLM providers detected.'));
22
+ console.log();
23
+ console.log(chalk.dim(' To get started, do one of the following:'));
24
+ console.log(chalk.dim(' • Set an env var: OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.'));
25
+ console.log(chalk.dim(' • Start a local provider (Ollama on port 11434)'));
26
+ console.log(chalk.dim(' • Run: orquesta --add-provider <provider-id>'));
27
+ console.log();
28
+ return false;
29
+ }
30
+ let addedCount = 0;
31
+ for (const detected of result.detected) {
32
+ const endpoint = toEndpointConfig(detected);
33
+ await configManager.addEndpoint(endpoint);
34
+ addedCount++;
35
+ if (addedCount === 1 && endpoint.models.length > 0) {
36
+ await configManager.setCurrentEndpoint(endpoint.id);
37
+ await configManager.setCurrentModel(endpoint.models[0].id);
38
+ }
39
+ }
40
+ console.log(chalk.green(` ✓ Auto-configured ${addedCount} provider(s):`));
41
+ for (const d of result.detected) {
42
+ const modelCount = d.discoveredModels.length;
43
+ console.log(chalk.white(` • ${d.provider.name}`) + chalk.dim(` (${modelCount} model${modelCount !== 1 ? 's' : ''})`));
44
+ }
45
+ console.log();
46
+ return true;
47
+ }
48
+ //# sourceMappingURL=onboarding.js.map
@@ -1,3 +1,4 @@
1
+ import { commandRegistry } from './commands/index.js';
1
2
  import { sessionManager } from './session/session-manager.js';
2
3
  import { usageTracker } from './usage-tracker.js';
3
4
  import { logger } from '../utils/logger.js';
@@ -6,72 +7,24 @@ import { readHookConfig, writeHookFiles, disableHooks } from '../orquesta/hook-i
6
7
  import { checkForCliUpdate, runCliUpdate, setSkippedVersion } from '../utils/update-checker.js';
7
8
  import { createRequire } from 'module';
8
9
  import { configManager } from './config/config-manager.js';
9
- import { getForcedTier, setForcedTier, resetBatutaSession } from './routing-state.js';
10
+ import { getForcedTier, setForcedTier } from './routing-state.js';
10
11
  import { auditLog } from '../orchestration/audit-log.js';
11
12
  import { remotePhone } from '../orquesta/remote-phone.js';
12
13
  export async function executeSlashCommand(command, context) {
13
14
  const trimmedCommand = command.trim();
14
15
  logger.enter('executeSlashCommand', { command: trimmedCommand });
16
+ const commandName = trimmedCommand.split(/\s/)[0];
17
+ const registryResult = await commandRegistry.execute(commandName, context, trimmedCommand);
18
+ if (registryResult) {
19
+ logger.exit('executeSlashCommand', { handled: true, command: commandName, source: 'registry' });
20
+ return registryResult;
21
+ }
15
22
  if (trimmedCommand === '/exit' || trimmedCommand === '/quit') {
16
23
  logger.flow('Exit command received');
17
24
  context.exit();
18
25
  logger.exit('executeSlashCommand', { handled: true, command: 'exit' });
19
26
  return { handled: true, shouldContinue: false };
20
27
  }
21
- if (trimmedCommand === '/clear') {
22
- logger.flow('Clear command - resetting messages and todos');
23
- context.setMessages([]);
24
- context.setTodos([]);
25
- resetBatutaSession();
26
- logger.exit('executeSlashCommand', { handled: true, command: 'clear' });
27
- return {
28
- handled: true,
29
- shouldContinue: false,
30
- updatedContext: {
31
- messages: [],
32
- todos: [],
33
- },
34
- };
35
- }
36
- if (trimmedCommand === '/compact') {
37
- logger.flow('Compact command received');
38
- if (context.onCompact) {
39
- logger.flow('Executing compact callback');
40
- const result = await context.onCompact();
41
- logger.vars({ name: 'compactSuccess', value: result.success }, { name: 'originalCount', value: result.originalMessageCount }, { name: 'newCount', value: result.newMessageCount });
42
- const compactMessage = result.success
43
- ? `✅ Conversation compacted successfully. (${result.originalMessageCount} → ${result.newMessageCount} messages)`
44
- : `❌ Compact failed: ${result.error}`;
45
- const baseMessages = (result.success && result.compactedMessages)
46
- ? result.compactedMessages
47
- : context.messages;
48
- const updatedMessages = [
49
- ...baseMessages,
50
- { role: 'assistant', content: compactMessage },
51
- ];
52
- context.setMessages(updatedMessages);
53
- return {
54
- handled: true,
55
- shouldContinue: false,
56
- updatedContext: {
57
- messages: updatedMessages,
58
- },
59
- };
60
- }
61
- const fallbackMessage = '/compact is only available in interactive mode.';
62
- const updatedMessages = [
63
- ...context.messages,
64
- { role: 'assistant', content: fallbackMessage },
65
- ];
66
- context.setMessages(updatedMessages);
67
- return {
68
- handled: true,
69
- shouldContinue: false,
70
- updatedContext: {
71
- messages: updatedMessages,
72
- },
73
- };
74
- }
75
28
  if (trimmedCommand === '/settings') {
76
29
  if (context.onShowSettings) {
77
30
  context.onShowSettings();
@@ -626,50 +579,6 @@ ${executorLines}
626
579
  return reply(`❌ Could not open the remote phone channel: ${e.message}`);
627
580
  }
628
581
  }
629
- if (trimmedCommand === '/help') {
630
- const helpMessage = `
631
- Available commands:
632
- /exit, /quit - Exit the application
633
- /clear - Clear conversation and TODOs
634
- /compact - Compact conversation to free up context
635
- /settings - Open settings menu
636
- /model - Switch between LLM models
637
- /project - Switch between Orquesta projects
638
- /tool - Enable/disable optional tools (Browser, Background)
639
- /load - Load a saved session
640
- /usage - Show token usage statistics
641
- /cost - Estimated USD spend this process (by model)
642
- /route - Pin Batuta Auto tier (fast/balanced/premium/auto)
643
- /sync - Bidirectional sync with Orquesta dashboard (pull & push LLM configs)
644
- /login - Sign in to Orquesta via browser (opens getorquesta.com)
645
- /logout - Sign out of Orquesta (clears token, keeps local LLM configs)
646
- /whoami - Show current Orquesta connection
647
- /hook - Claude Code hook here: /hook status | enable | disable
648
- /remote-phone - Drive this session from your phone: on | off | status
649
- /update - Update orquesta-cli to the latest version
650
-
651
- Keyboard shortcuts:
652
- Ctrl+C - Exit
653
- Ctrl+T - Toggle TODO details
654
- ESC - Interrupt current execution
655
- @ - File browser
656
- / - Command autocomplete
657
-
658
- Note: All conversations are automatically saved.
659
- `;
660
- const updatedMessages = [
661
- ...context.messages,
662
- { role: 'assistant', content: helpMessage },
663
- ];
664
- context.setMessages(updatedMessages);
665
- return {
666
- handled: true,
667
- shouldContinue: false,
668
- updatedContext: {
669
- messages: updatedMessages,
670
- },
671
- };
672
- }
673
582
  if (trimmedCommand.startsWith('/load')) {
674
583
  logger.flow('Load command received');
675
584
  const parts = trimmedCommand.split(' ');