@bahulam/code 2.6.0
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/README.md +80 -0
- package/package.json +49 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +50 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +143 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +164 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +53 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +183 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +222 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +306 -0
- package/src/commands/agent.mjs +220 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +200 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +308 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +476 -0
- package/src/core/attachments.mjs +380 -0
- package/src/core/backend-url.mjs +55 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +311 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +448 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +537 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +337 -0
- package/src/core/safety.mjs +203 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +829 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +1950 -0
- package/src/core/trust.mjs +158 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +426 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/onboarding/preflight.mjs +360 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +652 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +269 -0
- package/src/terminal/repl-explore.mjs +35 -0
- package/src/terminal/repl-format.mjs +257 -0
- package/src/terminal/repl-render.mjs +561 -0
- package/src/terminal/repl-resume.mjs +625 -0
- package/src/terminal/repl-state.mjs +103 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +3832 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +240 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/project-overview.mjs +641 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/approval.mjs +263 -0
- package/src/ui/banner.mjs +235 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/input-dock.mjs +444 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/slash-commands.mjs +245 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +463 -0
- package/src/ui/tool-details.mjs +312 -0
- package/src/ui/transcript-block.mjs +21 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Pricing — per-model cost calculation.
|
|
3
|
+
*
|
|
4
|
+
* Two sources of truth:
|
|
5
|
+
* 1. OpenRouter /api/v1/models endpoint (live pricing, fetched at startup)
|
|
6
|
+
* 2. Static fallback table (for non-OpenRouter providers: Anthropic, OpenAI, Google)
|
|
7
|
+
*
|
|
8
|
+
* All prices in USD per million tokens (MTok).
|
|
9
|
+
*
|
|
10
|
+
* PRD-028: Phase 1 — CLI-side pricing with live OpenRouter fetch.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ── Static Fallback Table (non-OpenRouter providers) ─────────
|
|
14
|
+
|
|
15
|
+
const STATIC_PRICING = {
|
|
16
|
+
// ── Anthropic (direct API, not via OpenRouter) ──
|
|
17
|
+
'claude-opus-4': { input: 15, output: 75, cache_read: 1.50, cache_write: 18.75 },
|
|
18
|
+
'claude-sonnet-4': { input: 3, output: 15, cache_read: 0.30, cache_write: 3.75 },
|
|
19
|
+
'claude-haiku-3-5': { input: 0.80, output: 4, cache_read: 0.08, cache_write: 1.0 },
|
|
20
|
+
|
|
21
|
+
// ── OpenAI (direct API) ──
|
|
22
|
+
'gpt-4o': { input: 2.50, output: 10 },
|
|
23
|
+
'gpt-4.1': { input: 2, output: 8 },
|
|
24
|
+
'gpt-4.1-mini': { input: 0.40, output: 1.60 },
|
|
25
|
+
'gpt-4.1-nano': { input: 0.10, output: 0.40 },
|
|
26
|
+
'o3': { input: 2, output: 8 },
|
|
27
|
+
'o3-mini': { input: 1.10, output: 4.40 },
|
|
28
|
+
'o4-mini': { input: 1.10, output: 4.40 },
|
|
29
|
+
|
|
30
|
+
// ── Google (direct API) ──
|
|
31
|
+
'gemini-2.5-pro': { input: 1.25, output: 10 },
|
|
32
|
+
'gemini-2.5-flash': { input: 0.30, output: 2.50 },
|
|
33
|
+
'gemini-2.0-flash': { input: 0.10, output: 0.40 },
|
|
34
|
+
|
|
35
|
+
// ── Bedrock (Anthropic models via AWS) ──
|
|
36
|
+
'us.anthropic.claude-sonnet-4-6-20260401-v1:0': { input: 3, output: 15, cache_read: 0.30, cache_write: 3.75 },
|
|
37
|
+
'us.anthropic.claude-haiku-4-5-20251001-v1:0': { input: 0.80, output: 4, cache_read: 0.08, cache_write: 1.0 },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const DEFAULT_PRICING = { input: 3, output: 15 };
|
|
41
|
+
|
|
42
|
+
// ── Live OpenRouter Pricing Cache ────────────────────────────
|
|
43
|
+
|
|
44
|
+
let _openRouterPricing = null; // Map<model_id, { input, output }>
|
|
45
|
+
let _fetchPromise = null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Fetch live pricing from OpenRouter's /api/v1/models endpoint.
|
|
49
|
+
* Called once at startup, cached for the session.
|
|
50
|
+
* Non-blocking — returns immediately, populates cache in background.
|
|
51
|
+
*/
|
|
52
|
+
export async function fetchOpenRouterPricing() {
|
|
53
|
+
if (_openRouterPricing) return _openRouterPricing;
|
|
54
|
+
if (_fetchPromise) return _fetchPromise;
|
|
55
|
+
|
|
56
|
+
_fetchPromise = (async () => {
|
|
57
|
+
try {
|
|
58
|
+
const controller = new AbortController();
|
|
59
|
+
const timeout = setTimeout(() => controller.abort(), 8000);
|
|
60
|
+
|
|
61
|
+
const res = await fetch('https://openrouter.ai/api/v1/models', {
|
|
62
|
+
signal: controller.signal,
|
|
63
|
+
headers: { 'Accept': 'application/json' },
|
|
64
|
+
});
|
|
65
|
+
clearTimeout(timeout);
|
|
66
|
+
|
|
67
|
+
if (!res.ok) return null;
|
|
68
|
+
|
|
69
|
+
const data = await res.json();
|
|
70
|
+
const models = data.data || [];
|
|
71
|
+
const pricing = new Map();
|
|
72
|
+
|
|
73
|
+
for (const m of models) {
|
|
74
|
+
const id = m.id;
|
|
75
|
+
const p = m.pricing;
|
|
76
|
+
if (!id || !p) continue;
|
|
77
|
+
|
|
78
|
+
// OpenRouter returns per-token prices as strings
|
|
79
|
+
const input = parseFloat(p.prompt || '0') * 1_000_000; // convert to per-MTok
|
|
80
|
+
const output = parseFloat(p.completion || '0') * 1_000_000;
|
|
81
|
+
|
|
82
|
+
pricing.set(id, { input, output });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_openRouterPricing = pricing;
|
|
86
|
+
return pricing;
|
|
87
|
+
} catch {
|
|
88
|
+
// Network error, timeout, or parse error — fall back to static
|
|
89
|
+
return null;
|
|
90
|
+
} finally {
|
|
91
|
+
_fetchPromise = null;
|
|
92
|
+
}
|
|
93
|
+
})();
|
|
94
|
+
|
|
95
|
+
return _fetchPromise;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Start fetching OpenRouter pricing in the background.
|
|
100
|
+
* Call this at CLI startup — non-blocking.
|
|
101
|
+
*/
|
|
102
|
+
export function prefetchPricing() {
|
|
103
|
+
fetchOpenRouterPricing().catch(() => {});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Model ID Normalization ───────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Normalize a model ID for pricing lookup.
|
|
110
|
+
* Strips version suffixes, date stamps, provider prefixes (openrouter/).
|
|
111
|
+
*/
|
|
112
|
+
function normalizeModelId(model) {
|
|
113
|
+
if (!model) return '';
|
|
114
|
+
let id = model.trim().toLowerCase();
|
|
115
|
+
|
|
116
|
+
// Strip 'openrouter/' prefix
|
|
117
|
+
id = id.replace(/^openrouter\//, '');
|
|
118
|
+
|
|
119
|
+
// Strip date suffixes like -20250514 or -preview-05-06
|
|
120
|
+
id = id.replace(/-\d{8}$/, '');
|
|
121
|
+
id = id.replace(/-preview[-\d]*$/, '');
|
|
122
|
+
|
|
123
|
+
return id;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Look up pricing for a model ID.
|
|
128
|
+
*
|
|
129
|
+
* Search order:
|
|
130
|
+
* 1. OpenRouter live pricing (exact match)
|
|
131
|
+
* 2. Static table (exact → normalized → provider-stripped)
|
|
132
|
+
* 3. Default fallback ($3/$15 Sonnet)
|
|
133
|
+
*
|
|
134
|
+
* @param {string} model
|
|
135
|
+
* @returns {{ input: number, output: number, cache_read?: number, cache_write?: number }}
|
|
136
|
+
*/
|
|
137
|
+
export function lookupPricing(model) {
|
|
138
|
+
if (!model) return DEFAULT_PRICING;
|
|
139
|
+
|
|
140
|
+
// 1. OpenRouter live cache (exact match — OpenRouter IDs are canonical)
|
|
141
|
+
if (_openRouterPricing) {
|
|
142
|
+
const orPrice = _openRouterPricing.get(model);
|
|
143
|
+
if (orPrice) return orPrice;
|
|
144
|
+
|
|
145
|
+
// Try normalized
|
|
146
|
+
const normalized = normalizeModelId(model);
|
|
147
|
+
const orNorm = _openRouterPricing.get(normalized);
|
|
148
|
+
if (orNorm) return orNorm;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 2. Static table — exact match
|
|
152
|
+
if (STATIC_PRICING[model]) return STATIC_PRICING[model];
|
|
153
|
+
|
|
154
|
+
// Normalized match
|
|
155
|
+
const normalized = normalizeModelId(model);
|
|
156
|
+
if (STATIC_PRICING[normalized]) return STATIC_PRICING[normalized];
|
|
157
|
+
|
|
158
|
+
// Try without provider prefix (e.g. 'google/gemini-2.5-pro' → 'gemini-2.5-pro')
|
|
159
|
+
const withoutProvider = normalized.replace(/^[^/]+\//, '');
|
|
160
|
+
if (STATIC_PRICING[withoutProvider]) return STATIC_PRICING[withoutProvider];
|
|
161
|
+
|
|
162
|
+
// Try with common provider prefixes
|
|
163
|
+
for (const prefix of ['deepseek/', 'xiaomi/', 'google/', 'meta/', 'qwen/', 'moonshotai/']) {
|
|
164
|
+
if (STATIC_PRICING[prefix + withoutProvider]) return STATIC_PRICING[prefix + withoutProvider];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 3. OpenRouter fuzzy match (without provider prefix)
|
|
168
|
+
if (_openRouterPricing) {
|
|
169
|
+
for (const [id, price] of _openRouterPricing) {
|
|
170
|
+
if (id.endsWith('/' + withoutProvider)) return price;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return DEFAULT_PRICING;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Cost Calculation ─────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Calculate cost from a usage object.
|
|
181
|
+
*
|
|
182
|
+
* Supports two formats:
|
|
183
|
+
* 1. New format (with per-model breakdown):
|
|
184
|
+
* { models: [{ model, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens }] }
|
|
185
|
+
*
|
|
186
|
+
* 2. Legacy format (flat tokens, no model):
|
|
187
|
+
* { input_tokens, output_tokens }
|
|
188
|
+
*
|
|
189
|
+
* @param {Object} usage - Usage object from backend complete event
|
|
190
|
+
* @returns {{ total: number, breakdown: Array, accurate: boolean }}
|
|
191
|
+
*/
|
|
192
|
+
export function calculateCost(usage) {
|
|
193
|
+
if (!usage) return { total: 0, breakdown: [], accurate: false };
|
|
194
|
+
|
|
195
|
+
// Best case: backend computed cost from live OpenRouter pricing
|
|
196
|
+
if (typeof usage.total_cost === 'number' && usage.models && usage.models.length > 0) {
|
|
197
|
+
const breakdown = usage.models.map(m => ({
|
|
198
|
+
model: m.model,
|
|
199
|
+
role: m.role || 'unknown',
|
|
200
|
+
input_tokens: m.input_tokens || 0,
|
|
201
|
+
output_tokens: m.output_tokens || 0,
|
|
202
|
+
cache_read_tokens: m.cache_read_tokens || 0,
|
|
203
|
+
cache_creation_tokens: m.cache_creation_tokens || 0,
|
|
204
|
+
cost: m.cost || 0,
|
|
205
|
+
free: (m.cost || 0) === 0 && (m.input_tokens || 0) > 0,
|
|
206
|
+
pricingSource: 'backend',
|
|
207
|
+
}));
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
total: usage.total_cost,
|
|
211
|
+
breakdown,
|
|
212
|
+
accurate: true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Fallback: per-model breakdown without backend cost — CLI computes
|
|
217
|
+
if (usage.models && usage.models.length > 0) {
|
|
218
|
+
const breakdown = usage.models.map(m => {
|
|
219
|
+
const pricing = lookupPricing(m.model);
|
|
220
|
+
const inputCost = (m.input_tokens || 0) * pricing.input / 1_000_000;
|
|
221
|
+
const outputCost = (m.output_tokens || 0) * pricing.output / 1_000_000;
|
|
222
|
+
const cacheReadCost = (m.cache_read_tokens || 0) * (pricing.cache_read || pricing.input) / 1_000_000;
|
|
223
|
+
const cacheWriteCost = (m.cache_creation_tokens || 0) * (pricing.cache_write || pricing.input) / 1_000_000;
|
|
224
|
+
const cost = inputCost + outputCost + cacheReadCost + cacheWriteCost;
|
|
225
|
+
|
|
226
|
+
const usedLivePricing = _openRouterPricing?.has(m.model) || false;
|
|
227
|
+
const usedStaticPricing = STATIC_PRICING[m.model] || STATIC_PRICING[normalizeModelId(m.model)];
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
model: m.model,
|
|
231
|
+
role: m.role || 'unknown',
|
|
232
|
+
input_tokens: m.input_tokens || 0,
|
|
233
|
+
output_tokens: m.output_tokens || 0,
|
|
234
|
+
cache_read_tokens: m.cache_read_tokens || 0,
|
|
235
|
+
cache_creation_tokens: m.cache_creation_tokens || 0,
|
|
236
|
+
cost,
|
|
237
|
+
free: pricing.input === 0 && pricing.output === 0,
|
|
238
|
+
pricingSource: usedLivePricing ? 'openrouter' : usedStaticPricing ? 'static' : 'default',
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const allAccurate = breakdown.every(b => b.pricingSource !== 'default');
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
total: breakdown.reduce((sum, b) => sum + b.cost, 0),
|
|
246
|
+
breakdown,
|
|
247
|
+
accurate: allAccurate,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Legacy format: flat tokens, unknown model — use default pricing
|
|
252
|
+
const inputTokens = usage.total_input_tokens || usage.input_tokens || 0;
|
|
253
|
+
const outputTokens = usage.total_output_tokens || usage.output_tokens || 0;
|
|
254
|
+
const total = (inputTokens * DEFAULT_PRICING.input + outputTokens * DEFAULT_PRICING.output) / 1_000_000;
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
total,
|
|
258
|
+
breakdown: [{
|
|
259
|
+
model: 'unknown',
|
|
260
|
+
role: 'unknown',
|
|
261
|
+
input_tokens: inputTokens,
|
|
262
|
+
output_tokens: outputTokens,
|
|
263
|
+
cost: total,
|
|
264
|
+
free: false,
|
|
265
|
+
pricingSource: 'default',
|
|
266
|
+
}],
|
|
267
|
+
accurate: false,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Formatting ───────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Format a cost value as a dollar string (diagnostic/telemetry only).
|
|
275
|
+
* @param {number} cost
|
|
276
|
+
* @returns {string}
|
|
277
|
+
*/
|
|
278
|
+
export function formatCostValue(cost) {
|
|
279
|
+
if (cost === 0) return '$0.00';
|
|
280
|
+
if (cost < 0.01) return `$${cost.toFixed(4)}`;
|
|
281
|
+
return `$${cost.toFixed(2)}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Convert provider cost to Kepler credits.
|
|
286
|
+
* 100 credits = $1 of retail model usage, 2x multiplier.
|
|
287
|
+
* @param {number} costUsd
|
|
288
|
+
* @returns {number}
|
|
289
|
+
*/
|
|
290
|
+
export function costToCredits(costUsd) {
|
|
291
|
+
if (costUsd <= 0) return 0;
|
|
292
|
+
return Math.max(1, Math.ceil(costUsd * 2.0 * 100));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Format credits for display (e.g. "5 cr", "1.2k cr").
|
|
297
|
+
* @param {number} credits
|
|
298
|
+
* @returns {string}
|
|
299
|
+
*/
|
|
300
|
+
export function formatCredits(credits) {
|
|
301
|
+
if (credits === 0) return '0 cr';
|
|
302
|
+
if (credits >= 1000) return `${(credits / 1000).toFixed(1)}k cr`;
|
|
303
|
+
return `${credits} cr`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Format a token count for display (e.g. 42100 → '42.1k').
|
|
308
|
+
* @param {number} tokens
|
|
309
|
+
* @returns {string}
|
|
310
|
+
*/
|
|
311
|
+
export function formatTokens(tokens) {
|
|
312
|
+
if (tokens === 0) return '0';
|
|
313
|
+
if (tokens < 1000) return String(tokens);
|
|
314
|
+
return `${(tokens / 1000).toFixed(1)}k`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Check if a model is free-tier.
|
|
319
|
+
* @param {string} model
|
|
320
|
+
* @returns {boolean}
|
|
321
|
+
*/
|
|
322
|
+
export function isFreeModel(model) {
|
|
323
|
+
const pricing = lookupPricing(model);
|
|
324
|
+
return pricing.input === 0 && pricing.output === 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get pricing status (for /status display).
|
|
329
|
+
* @returns {{ source: string, modelCount: number }}
|
|
330
|
+
*/
|
|
331
|
+
export function getPricingStatus() {
|
|
332
|
+
if (_openRouterPricing) {
|
|
333
|
+
return { source: 'openrouter-live', modelCount: _openRouterPricing.size };
|
|
334
|
+
}
|
|
335
|
+
return { source: 'static-fallback', modelCount: Object.keys(STATIC_PRICING).length };
|
|
336
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { projectConfigDir } from './paths.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Persist generated session artifacts only beneath roots registered by the CLI.
|
|
7
|
+
*/
|
|
8
|
+
export function persistProjectArtifacts(data, resources, log = () => {}) {
|
|
9
|
+
const projectIds = new Set(
|
|
10
|
+
Array.isArray(data?.project_ids) ? data.project_ids.filter(Boolean) : []
|
|
11
|
+
);
|
|
12
|
+
const registered = Array.isArray(resources) ? resources : [];
|
|
13
|
+
const targets = projectIds.size > 0
|
|
14
|
+
? registered.filter(resource => projectIds.has(resource.project_id))
|
|
15
|
+
: registered.length === 1 ? registered : [];
|
|
16
|
+
const artifacts = [
|
|
17
|
+
['goal', 'goal.md'],
|
|
18
|
+
['plan', 'plan.md'],
|
|
19
|
+
].filter(([field]) => typeof data?.[field] === 'string' && data[field].trim());
|
|
20
|
+
const written = [];
|
|
21
|
+
|
|
22
|
+
for (const resource of targets) {
|
|
23
|
+
for (const [field, filename] of artifacts) {
|
|
24
|
+
try {
|
|
25
|
+
// Resolver honors legacy .kepler/ when it's the only dir that exists.
|
|
26
|
+
const keplerDir = projectConfigDir(resource.root);
|
|
27
|
+
fs.mkdirSync(keplerDir, { recursive: true });
|
|
28
|
+
const artifactPath = path.join(keplerDir, filename);
|
|
29
|
+
fs.writeFileSync(artifactPath, data[field], 'utf-8');
|
|
30
|
+
resource[field] = data[field];
|
|
31
|
+
written.push(artifactPath);
|
|
32
|
+
log(`${field} saved -> ${artifactPath}`);
|
|
33
|
+
} catch {
|
|
34
|
+
// Persistence is helpful but must not stop the agent.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return written;
|
|
39
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { loadKeplerMemory } from '../config/memory-loader.mjs';
|
|
5
|
+
import { bahulamHome, projectConfigDir } from './paths.mjs';
|
|
6
|
+
|
|
7
|
+
function sha(content) {
|
|
8
|
+
return crypto.createHash('sha256').update(content || '').digest('hex').slice(0, 12);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function readFile(filePath, label, maxChars = 12000) {
|
|
12
|
+
try {
|
|
13
|
+
if (!fs.existsSync(filePath)) return null;
|
|
14
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
15
|
+
return {
|
|
16
|
+
label,
|
|
17
|
+
path: filePath,
|
|
18
|
+
hash: sha(content),
|
|
19
|
+
bytes: Buffer.byteLength(content),
|
|
20
|
+
content: content.length > maxChars
|
|
21
|
+
? content.slice(0, maxChars) + '\n\n[...truncated...]'
|
|
22
|
+
: content,
|
|
23
|
+
truncated: content.length > maxChars,
|
|
24
|
+
};
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return { label, path: filePath, error: err.message, content: '' };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readTasks(keplerDir) {
|
|
31
|
+
const tasksDir = path.join(keplerDir, 'tasks');
|
|
32
|
+
const files = [];
|
|
33
|
+
try {
|
|
34
|
+
if (!fs.existsSync(tasksDir)) return files;
|
|
35
|
+
for (const name of fs.readdirSync(tasksDir).sort()) {
|
|
36
|
+
if (!name.endsWith('.md')) continue;
|
|
37
|
+
const file = readFile(path.join(tasksDir, name), `tasks/${name}`, 6000);
|
|
38
|
+
if (file) files.push(file);
|
|
39
|
+
}
|
|
40
|
+
} catch { /* best effort */ }
|
|
41
|
+
return files;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function scanSkills(dir, scope) {
|
|
45
|
+
const skillsDir = path.join(dir, 'skills');
|
|
46
|
+
const out = [];
|
|
47
|
+
try {
|
|
48
|
+
if (!fs.existsSync(skillsDir)) return out;
|
|
49
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
50
|
+
let filePath = null;
|
|
51
|
+
let name = entry.name;
|
|
52
|
+
if (entry.isDirectory()) filePath = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
53
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
54
|
+
filePath = path.join(skillsDir, entry.name);
|
|
55
|
+
name = entry.name.replace(/\.md$/, '');
|
|
56
|
+
}
|
|
57
|
+
if (!filePath || !fs.existsSync(filePath)) continue;
|
|
58
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
59
|
+
const desc = raw.match(/^description:\s*(.+)$/mi)?.[1]?.trim()
|
|
60
|
+
|| raw.match(/^#\s+.+\n+([^\n]+)/)?.[1]?.trim()
|
|
61
|
+
|| '';
|
|
62
|
+
out.push({ name, description: desc, scope, source_id: `${scope}:${name}`, path: filePath });
|
|
63
|
+
}
|
|
64
|
+
} catch { /* best effort */ }
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function loadProjectContext({ cwd = process.cwd(), previous = null } = {}) {
|
|
69
|
+
const keplerDir = projectConfigDir(cwd);
|
|
70
|
+
const files = [];
|
|
71
|
+
for (const file of loadKeplerMemory({ cwd })) {
|
|
72
|
+
const label = file.path.endsWith(path.join('.bahulam', 'KEPLER.md'))
|
|
73
|
+
? 'KEPLER.md'
|
|
74
|
+
: path.basename(file.path);
|
|
75
|
+
files.push({
|
|
76
|
+
label,
|
|
77
|
+
source: file.source,
|
|
78
|
+
path: file.path,
|
|
79
|
+
hash: sha(file.content),
|
|
80
|
+
bytes: Buffer.byteLength(file.content),
|
|
81
|
+
content: file.content,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const name of ['config.json', 'project.md', 'style.md', 'goal.md', 'plan.md', 'hitl.md']) {
|
|
86
|
+
const file = readFile(path.join(keplerDir, name), name, name.endsWith('.json') ? 4000 : 12000);
|
|
87
|
+
if (file) files.push(file);
|
|
88
|
+
}
|
|
89
|
+
files.push(...readTasks(keplerDir));
|
|
90
|
+
|
|
91
|
+
const previousHashes = new Map((previous?.files || []).map(f => [f.path, f.hash]));
|
|
92
|
+
const changed = files.filter(f => f.hash && previousHashes.get(f.path) && previousHashes.get(f.path) !== f.hash);
|
|
93
|
+
const loaded = files.filter(f => !f.error).map(f => ({
|
|
94
|
+
label: f.label,
|
|
95
|
+
path: f.path,
|
|
96
|
+
hash: f.hash,
|
|
97
|
+
bytes: f.bytes,
|
|
98
|
+
source: f.source || 'project',
|
|
99
|
+
included: true,
|
|
100
|
+
changed: changed.some(c => c.path === f.path),
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
const skills = [
|
|
104
|
+
...scanSkills(bahulamHome(), 'global'),
|
|
105
|
+
...scanSkills(keplerDir, 'project'),
|
|
106
|
+
];
|
|
107
|
+
const byName = new Map();
|
|
108
|
+
for (const skill of skills) byName.set(skill.name, skill);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
root: cwd,
|
|
112
|
+
kepler_dir: keplerDir,
|
|
113
|
+
files,
|
|
114
|
+
loaded,
|
|
115
|
+
changed,
|
|
116
|
+
available_skills: [...byName.values()],
|
|
117
|
+
task_state: files
|
|
118
|
+
.filter(f => f.label.startsWith('tasks/'))
|
|
119
|
+
.map(f => ({ name: f.label.replace('tasks/', ''), hash: f.hash, content: f.content })),
|
|
120
|
+
loaded_at: new Date().toISOString(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function contextToPromptBlock(context) {
|
|
125
|
+
const parts = [];
|
|
126
|
+
if (context?.root) {
|
|
127
|
+
parts.push([
|
|
128
|
+
'--- task workflow ---',
|
|
129
|
+
'Keep .bahulam/tasks/active.md current for the work in progress.',
|
|
130
|
+
'Use .bahulam/tasks/backlog.md for deferred work, .bahulam/tasks/blocked.md for items waiting on input, and .bahulam/tasks/done.md for completed work.',
|
|
131
|
+
'When the task state changes materially, update the appropriate task markdown file before the turn finishes.',
|
|
132
|
+
].join('\n'));
|
|
133
|
+
}
|
|
134
|
+
for (const file of context?.files || []) {
|
|
135
|
+
if (!file.content || file.label === 'config.json') continue;
|
|
136
|
+
parts.push(`--- ${file.label} (${file.hash}) ---\n${file.content}`);
|
|
137
|
+
}
|
|
138
|
+
return parts.join('\n\n');
|
|
139
|
+
}
|