orquesta-cli 0.1.25 → 0.1.27

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.
@@ -1,6 +1,7 @@
1
1
  import { logger } from '../../utils/logger.js';
2
2
  import { buildPlanningSystemPrompt } from '../../prompts/agents/planning.js';
3
3
  import { toolRegistry } from '../../tools/registry.js';
4
+ import { getProjectContext } from '../../core/project-context.js';
4
5
  export class PlanningLLM {
5
6
  llmClient;
6
7
  askUserCallback = null;
@@ -18,7 +19,8 @@ export class PlanningLLM {
18
19
  async generateTODOList(userRequest, contextMessages) {
19
20
  const toolSummary = toolRegistry.getToolSummaryForPlanning();
20
21
  const optionalToolsInfo = toolRegistry.getEnabledOptionalToolsInfo();
21
- const systemPrompt = buildPlanningSystemPrompt(toolSummary, optionalToolsInfo);
22
+ const projectContext = getProjectContext();
23
+ const systemPrompt = buildPlanningSystemPrompt(toolSummary, optionalToolsInfo) + projectContext;
22
24
  const clarificationMessages = [];
23
25
  const messages = [
24
26
  {
@@ -38,7 +40,7 @@ export class PlanningLLM {
38
40
  });
39
41
  }
40
42
  const planningTools = toolRegistry.getLLMPlanningToolDefinitions();
41
- const MAX_RETRIES = 3;
43
+ const MAX_RETRIES = 2;
42
44
  const MAX_ASK_ITERATIONS = 5;
43
45
  let askIterations = 0;
44
46
  let lastError = null;
@@ -5,7 +5,7 @@ import { NetworkError, APIError, TimeoutError, ConnectionError, } from '../../er
5
5
  import { LLMError, TokenLimitError, RateLimitError, ContextLengthError, } from '../../errors/llm.js';
6
6
  import { logger, isLLMLogEnabled } from '../../utils/logger.js';
7
7
  import { usageTracker } from '../usage-tracker.js';
8
- import { getForcedTier, getBatutaSessionId } from '../routing-state.js';
8
+ import { getForcedTier, getBatutaSessionId, setLastBatutaRoute } from '../routing-state.js';
9
9
  function buildPerRequestHeaders() {
10
10
  const headers = {
11
11
  'X-Batuta-Session-ID': getBatutaSessionId(),
@@ -15,6 +15,22 @@ function buildPerRequestHeaders() {
15
15
  headers['X-Batuta-Force-Tier'] = tier;
16
16
  return headers;
17
17
  }
18
+ function captureBatutaHeaders(headers) {
19
+ const get = (k) => {
20
+ const lower = k.toLowerCase();
21
+ for (const key of Object.keys(headers)) {
22
+ if (key.toLowerCase() === lower)
23
+ return headers[key];
24
+ }
25
+ return undefined;
26
+ };
27
+ const routedFrom = get('X-Batuta-Routed-From');
28
+ const routedTo = get('X-Batuta-Routed-To');
29
+ const tier = get('X-Batuta-Tier');
30
+ if (routedFrom && routedTo && (tier === 'fast' || tier === 'balanced' || tier === 'premium')) {
31
+ setLastBatutaRoute({ tier, routedTo, routedFrom });
32
+ }
33
+ }
18
34
  export class LLMClient {
19
35
  axiosInstance;
20
36
  baseUrl;
@@ -125,6 +141,7 @@ export class LLMClient {
125
141
  });
126
142
  this.currentAbortController = null;
127
143
  const elapsed = logger.endTimer('llm-api-call');
144
+ captureBatutaHeaders(response.headers);
128
145
  logger.flow('API response received');
129
146
  if (!response.data.choices || !Array.isArray(response.data.choices)) {
130
147
  logger.error('Invalid response structure - missing choices array', response.data);
@@ -283,6 +300,7 @@ export class LLMClient {
283
300
  signal: this.currentAbortController.signal,
284
301
  headers: buildPerRequestHeaders(),
285
302
  });
303
+ captureBatutaHeaders(response.headers);
286
304
  logger.debug('Streaming response started', { status: response.status });
287
305
  const stream = response.data;
288
306
  let buffer = '';
@@ -0,0 +1,8 @@
1
+ export interface ModelPricing {
2
+ input: number;
3
+ output: number;
4
+ }
5
+ export declare const MODEL_PRICING: Record<string, ModelPricing>;
6
+ export declare function estimateCost(modelId: string, inputTokens: number, outputTokens: number): number | null;
7
+ export declare function formatCostUsd(usd: number): string;
8
+ //# sourceMappingURL=pricing.d.ts.map
@@ -0,0 +1,41 @@
1
+ export const MODEL_PRICING = {
2
+ 'claude-haiku-4-5-20251001': { input: 1, output: 5 },
3
+ 'claude-sonnet-4-6': { input: 3, output: 15 },
4
+ 'claude-sonnet-4-5-20250514': { input: 3, output: 15 },
5
+ 'claude-opus-4-6': { input: 15, output: 75 },
6
+ 'claude-opus-4-7': { input: 15, output: 75 },
7
+ 'gpt-4o': { input: 2.50, output: 10 },
8
+ 'gpt-4o-mini': { input: 0.15, output: 0.60 },
9
+ 'gpt-4-turbo': { input: 10, output: 30 },
10
+ 'o1': { input: 15, output: 60 },
11
+ 'o1-mini': { input: 3, output: 12 },
12
+ 'o3-mini': { input: 1.10, output: 4.40 },
13
+ 'deepseek-chat': { input: 0.27, output: 1.10 },
14
+ 'deepseek-reasoner': { input: 0.55, output: 2.19 },
15
+ 'gemini-2.0-flash': { input: 0.10, output: 0.40 },
16
+ 'gemini-2.0-pro': { input: 1.25, output: 5 },
17
+ 'gemini-1.5-pro': { input: 1.25, output: 5 },
18
+ 'gemini-1.5-flash': { input: 0.075, output: 0.30 },
19
+ 'llama-3.3-70b-versatile': { input: 0.59, output: 0.79 },
20
+ 'mistral-large-latest': { input: 2, output: 6 },
21
+ 'codestral-latest': { input: 0.30, output: 0.90 },
22
+ 'grok-4': { input: 5, output: 15 },
23
+ 'grok-3': { input: 5, output: 15 },
24
+ 'claude-cli': { input: 0, output: 0 },
25
+ };
26
+ export function estimateCost(modelId, inputTokens, outputTokens) {
27
+ const price = MODEL_PRICING[modelId];
28
+ if (!price)
29
+ return null;
30
+ return (inputTokens / 1_000_000) * price.input + (outputTokens / 1_000_000) * price.output;
31
+ }
32
+ export function formatCostUsd(usd) {
33
+ if (usd === 0)
34
+ return '$0.00';
35
+ if (usd < 0.01)
36
+ return `$${usd.toFixed(4)}`;
37
+ if (usd < 1)
38
+ return `$${usd.toFixed(3)}`;
39
+ return `$${usd.toFixed(2)}`;
40
+ }
41
+ //# sourceMappingURL=pricing.js.map
@@ -0,0 +1,3 @@
1
+ export declare function getProjectContext(cwd?: string): string;
2
+ export declare function invalidateProjectContext(): void;
3
+ //# sourceMappingURL=project-context.d.ts.map
@@ -0,0 +1,54 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { logger } from '../utils/logger.js';
4
+ const MAX_BYTES = 32_000;
5
+ const CANDIDATE_PATHS = [
6
+ 'CLAUDE.md',
7
+ '.claude/CLAUDE.md',
8
+ 'AGENT.md',
9
+ 'agent.md',
10
+ ];
11
+ let cachedContext;
12
+ function readFirstAvailable(cwd) {
13
+ for (const rel of CANDIDATE_PATHS) {
14
+ const abs = path.join(cwd, rel);
15
+ try {
16
+ const stat = fs.statSync(abs);
17
+ if (!stat.isFile())
18
+ continue;
19
+ let content = fs.readFileSync(abs, 'utf-8');
20
+ if (Buffer.byteLength(content, 'utf-8') > MAX_BYTES) {
21
+ content = content.slice(0, MAX_BYTES) + '\n\n[...truncated for context window]';
22
+ }
23
+ return { path: rel, content };
24
+ }
25
+ catch {
26
+ continue;
27
+ }
28
+ }
29
+ return null;
30
+ }
31
+ export function getProjectContext(cwd = process.cwd()) {
32
+ if (cachedContext !== undefined)
33
+ return cachedContext ?? '';
34
+ const hit = readFirstAvailable(cwd);
35
+ if (!hit) {
36
+ cachedContext = null;
37
+ logger.debug('No CLAUDE.md / AGENT.md found in cwd', { cwd });
38
+ return '';
39
+ }
40
+ logger.info(`Loaded project context from ${hit.path} (${hit.content.length} chars)`);
41
+ cachedContext = [
42
+ '',
43
+ '## PROJECT CONTEXT',
44
+ `(from ${hit.path} — treat as authoritative project conventions)`,
45
+ '',
46
+ hit.content,
47
+ '',
48
+ ].join('\n');
49
+ return cachedContext;
50
+ }
51
+ export function invalidateProjectContext() {
52
+ cachedContext = undefined;
53
+ }
54
+ //# sourceMappingURL=project-context.js.map
@@ -3,4 +3,11 @@ export declare function getForcedTier(): ForcedTier;
3
3
  export declare function setForcedTier(tier: ForcedTier): void;
4
4
  export declare function getBatutaSessionId(): string;
5
5
  export declare function resetBatutaSession(): void;
6
+ export interface LastBatutaRoute {
7
+ tier: 'fast' | 'balanced' | 'premium';
8
+ routedTo: string;
9
+ routedFrom: string;
10
+ }
11
+ export declare function getLastBatutaRoute(): LastBatutaRoute | null;
12
+ export declare function setLastBatutaRoute(route: LastBatutaRoute | null): void;
6
13
  //# sourceMappingURL=routing-state.d.ts.map
@@ -13,4 +13,11 @@ export function getBatutaSessionId() {
13
13
  export function resetBatutaSession() {
14
14
  sessionId = randomUUID();
15
15
  }
16
+ let lastBatutaRoute = null;
17
+ export function getLastBatutaRoute() {
18
+ return lastBatutaRoute;
19
+ }
20
+ export function setLastBatutaRoute(route) {
21
+ lastBatutaRoute = route;
22
+ }
16
23
  //# sourceMappingURL=routing-state.js.map
@@ -169,6 +169,19 @@ export async function executeSlashCommand(command, context) {
169
169
  updatedContext: { messages: updatedMessages },
170
170
  };
171
171
  }
172
+ if (trimmedCommand === '/cost') {
173
+ const costMessage = usageTracker.formatCostDisplay();
174
+ const updatedMessages = [
175
+ ...context.messages,
176
+ { role: 'assistant', content: costMessage },
177
+ ];
178
+ context.setMessages(updatedMessages);
179
+ return {
180
+ handled: true,
181
+ shouldContinue: false,
182
+ updatedContext: { messages: updatedMessages },
183
+ };
184
+ }
172
185
  if (trimmedCommand === '/usage') {
173
186
  const usageMessage = usageTracker.formatUsageDisplay();
174
187
  const updatedMessages = [
@@ -314,6 +327,7 @@ Available commands:
314
327
  /tool - Enable/disable optional tools (Browser, Background)
315
328
  /load - Load a saved session
316
329
  /usage - Show token usage statistics
330
+ /cost - Estimated USD spend this process (by model)
317
331
  /route - Pin Batuta Auto tier (fast/balanced/premium/auto)
318
332
  /sync - Bidirectional sync with Orquesta dashboard (pull & push LLM configs)
319
333
 
@@ -63,6 +63,7 @@ declare class UsageTrackerClass {
63
63
  resetSession(): void;
64
64
  formatSessionStatus(currentActivity?: string): string;
65
65
  formatUsageDisplay(): string;
66
+ formatCostDisplay(): string;
66
67
  clearData(): void;
67
68
  }
68
69
  export declare const usageTracker: UsageTrackerClass;
@@ -2,6 +2,7 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { logger } from '../utils/logger.js';
4
4
  import { contextTracker } from './compact/context-tracker.js';
5
+ import { estimateCost, formatCostUsd } from './pricing.js';
5
6
  const DATA_DIR = path.join(process.env['HOME'] || '.', '.local-cli');
6
7
  const USAGE_FILE = path.join(DATA_DIR, 'usage.json');
7
8
  class UsageTrackerClass {
@@ -232,6 +233,47 @@ class UsageTrackerClass {
232
233
  logger.exit('UsageTracker.formatUsageDisplay');
233
234
  return lines.join('\n');
234
235
  }
236
+ formatCostDisplay() {
237
+ const records = this.data.records;
238
+ if (records.length === 0) {
239
+ return '💸 Cost: no usage recorded yet this session.';
240
+ }
241
+ const byModel = new Map();
242
+ let totalCost = 0;
243
+ let hadUnknown = false;
244
+ for (const r of records) {
245
+ const cost = estimateCost(r.model, r.inputTokens, r.outputTokens);
246
+ const b = byModel.get(r.model) ?? { model: r.model, requests: 0, inputTokens: 0, outputTokens: 0, cost: 0 };
247
+ b.requests++;
248
+ b.inputTokens += r.inputTokens;
249
+ b.outputTokens += r.outputTokens;
250
+ if (cost === null) {
251
+ b.cost = null;
252
+ hadUnknown = true;
253
+ }
254
+ else if (b.cost !== null) {
255
+ b.cost += cost;
256
+ totalCost += cost;
257
+ }
258
+ byModel.set(r.model, b);
259
+ }
260
+ const lines = [];
261
+ lines.push('💸 Estimated Cost (this process)');
262
+ lines.push('');
263
+ lines.push(` Total: ${formatCostUsd(totalCost)}${hadUnknown ? ' (some models unknown — excluded)' : ''}`);
264
+ lines.push(` Requests: ${records.length}`);
265
+ lines.push('');
266
+ lines.push(' By model:');
267
+ const sorted = Array.from(byModel.values()).sort((a, b) => (b.cost ?? 0) - (a.cost ?? 0));
268
+ for (const b of sorted) {
269
+ const cost = b.cost === null ? '?' : formatCostUsd(b.cost);
270
+ const subsidy = b.model === 'claude-cli' ? ' (subscription)' : '';
271
+ lines.push(` ${b.model.padEnd(32)} ${b.requests}× in=${b.inputTokens.toLocaleString()} out=${b.outputTokens.toLocaleString()} ${cost}${subsidy}`);
272
+ }
273
+ lines.push('');
274
+ lines.push('Note: estimates use public list pricing. Batuta tier subscriptions absorb claude-cli routed costs (shown as $0.00).');
275
+ return lines.join('\n');
276
+ }
235
277
  clearData() {
236
278
  logger.flow('Clearing all usage data');
237
279
  this.data = {
@@ -8,6 +8,7 @@ import { setDocsSearchLLMClientGetter, clearDocsSearchLLMClientGetter, } from '.
8
8
  import { emitPlanCreated, emitTodoStart, emitTodoComplete, emitTodoFail, emitCompact, emitAssistantResponse, } from '../tools/llm/simple/file-tools.js';
9
9
  import { toolRegistry } from '../tools/registry.js';
10
10
  import { PLAN_EXECUTE_SYSTEM_PROMPT as PLAN_PROMPT } from '../prompts/system/plan-execute.js';
11
+ import { getProjectContext } from '../core/project-context.js';
11
12
  import { GIT_COMMIT_RULES } from '../prompts/shared/git-rules.js';
12
13
  import { logger } from '../utils/logger.js';
13
14
  import { getStreamLogger } from '../utils/json-stream-logger.js';
@@ -15,10 +16,9 @@ import { detectGitRepo } from '../utils/git-utils.js';
15
16
  import { formatErrorMessage, buildTodoContext, findActiveTodo, getTodoStats } from './utils.js';
16
17
  function buildSystemPrompt() {
17
18
  const isGitRepo = detectGitRepo();
18
- if (isGitRepo) {
19
- return `${PLAN_PROMPT}\n\n${GIT_COMMIT_RULES}`;
20
- }
21
- return PLAN_PROMPT;
19
+ const projectContext = getProjectContext();
20
+ const base = isGitRepo ? `${PLAN_PROMPT}\n\n${GIT_COMMIT_RULES}` : PLAN_PROMPT;
21
+ return base + projectContext;
22
22
  }
23
23
  export class PlanExecutor {
24
24
  currentLLMClient = null;
@@ -1,6 +1,16 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text } from 'ink';
3
3
  import { logger } from '../../utils/logger.js';
4
+ import { useBatutaRoute } from '../hooks/useBatutaRoute.js';
5
+ const MODEL_SHORT_LABEL = {
6
+ 'claude-cli-haiku': 'Haiku 4.5',
7
+ 'claude-cli-sonnet': 'Sonnet 4.6',
8
+ 'claude-cli-opus': 'Opus 4.7',
9
+ 'claude-haiku-4-5-20251001': 'Haiku 4.5',
10
+ 'claude-sonnet-4-6': 'Sonnet 4.6',
11
+ 'claude-opus-4-7': 'Opus 4.7',
12
+ 'claude-opus-4-6': 'Opus 4.6',
13
+ };
4
14
  function formatTokens(count) {
5
15
  if (count < 1000)
6
16
  return count.toString();
@@ -63,6 +73,11 @@ function formatElapsedTime(seconds) {
63
73
  return `${mins}m ${secs}s`;
64
74
  }
65
75
  export const StatusBar = ({ model, endpoint: _endpoint, projectName, organizationName: _organizationName, workingDirectory, status = 'idle', message: _message, messageCount = 0, sessionTokens = 0, contextUsage, contextRemainingPercent, todoCount, todoCompleted, healthStatus, currentActivity, sessionElapsedSeconds, batutaUsage, }) => {
76
+ const batutaRoute = useBatutaRoute();
77
+ const showBatutaSuffix = model === 'batuta-auto' && !!batutaRoute;
78
+ const batutaSuffix = showBatutaSuffix
79
+ ? ` → ${MODEL_SHORT_LABEL[batutaRoute.routedTo] || batutaRoute.routedTo}`
80
+ : '';
66
81
  void _endpoint;
67
82
  void _message;
68
83
  useEffect(() => {
@@ -109,7 +124,9 @@ export const StatusBar = ({ model, endpoint: _endpoint, projectName, organizatio
109
124
  formatTokens(sessionTokens),
110
125
  " tokens")),
111
126
  React.createElement(Text, { color: "gray" }, ")")),
112
- React.createElement(Box, null, model && React.createElement(Text, { color: "cyan" }, model.slice(0, 15)))));
127
+ React.createElement(Box, null, model && (React.createElement(React.Fragment, null,
128
+ React.createElement(Text, { color: "cyan" }, model.slice(0, 15)),
129
+ batutaSuffix && React.createElement(Text, { color: "magenta" }, batutaSuffix))))));
113
130
  }
114
131
  const getContextColor = (percent) => {
115
132
  if (percent > 50)
@@ -131,7 +148,8 @@ export const StatusBar = ({ model, endpoint: _endpoint, projectName, organizatio
131
148
  getStatusIndicator(),
132
149
  model && (React.createElement(React.Fragment, null,
133
150
  React.createElement(Text, { color: "gray" }, " | "),
134
- React.createElement(Text, { color: "cyan" }, model.slice(0, 15)))),
151
+ React.createElement(Text, { color: "cyan" }, model.slice(0, 15)),
152
+ batutaSuffix && React.createElement(Text, { color: "magenta" }, batutaSuffix))),
135
153
  projectName && (React.createElement(React.Fragment, null,
136
154
  React.createElement(Text, { color: "gray" }, " | "),
137
155
  React.createElement(Text, { color: "magenta" },
@@ -0,0 +1,3 @@
1
+ import { type LastBatutaRoute } from '../../core/routing-state.js';
2
+ export declare function useBatutaRoute(): LastBatutaRoute | null;
3
+ //# sourceMappingURL=useBatutaRoute.d.ts.map
@@ -0,0 +1,22 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { getLastBatutaRoute } from '../../core/routing-state.js';
3
+ export function useBatutaRoute() {
4
+ const [route, setRoute] = useState(getLastBatutaRoute());
5
+ useEffect(() => {
6
+ const interval = setInterval(() => {
7
+ const next = getLastBatutaRoute();
8
+ setRoute(prev => {
9
+ if (prev === next)
10
+ return prev;
11
+ if (!prev || !next)
12
+ return next;
13
+ if (prev.tier === next.tier && prev.routedTo === next.routedTo)
14
+ return prev;
15
+ return next;
16
+ });
17
+ }, 500);
18
+ return () => clearInterval(interval);
19
+ }, []);
20
+ return route;
21
+ }
22
+ //# sourceMappingURL=useBatutaRoute.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orquesta-cli",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Orquesta CLI - AI-powered coding assistant with team collaboration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",