@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.
Files changed (273) hide show
  1. package/README.md +80 -0
  2. package/package.json +49 -0
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/benchmark/route.ts +113 -0
  6. package/pulse/app/api/benchmarks/route.ts +195 -0
  7. package/pulse/app/api/costs/route.ts +88 -0
  8. package/pulse/app/api/export/route.ts +77 -0
  9. package/pulse/app/api/history/route.ts +11 -0
  10. package/pulse/app/api/import/route.ts +31 -0
  11. package/pulse/app/api/memory/route.ts +50 -0
  12. package/pulse/app/api/plans/route.ts +9 -0
  13. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  14. package/pulse/app/api/projects/route.ts +121 -0
  15. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  16. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  17. package/pulse/app/api/sessions/route.ts +112 -0
  18. package/pulse/app/api/settings/route.ts +14 -0
  19. package/pulse/app/api/stats/route.ts +143 -0
  20. package/pulse/app/api/todos/route.ts +9 -0
  21. package/pulse/app/api/tools/route.ts +160 -0
  22. package/pulse/app/benchmarks/page.tsx +224 -0
  23. package/pulse/app/costs/page.tsx +179 -0
  24. package/pulse/app/export/page.tsx +465 -0
  25. package/pulse/app/favicon.ico +0 -0
  26. package/pulse/app/globals.css +263 -0
  27. package/pulse/app/help/page.tsx +143 -0
  28. package/pulse/app/history/page.tsx +157 -0
  29. package/pulse/app/layout.tsx +46 -0
  30. package/pulse/app/memory/page.tsx +365 -0
  31. package/pulse/app/overview-client.tsx +393 -0
  32. package/pulse/app/page.tsx +14 -0
  33. package/pulse/app/plans/page.tsx +308 -0
  34. package/pulse/app/projects/[slug]/page.tsx +390 -0
  35. package/pulse/app/projects/page.tsx +110 -0
  36. package/pulse/app/sessions/[id]/page.tsx +243 -0
  37. package/pulse/app/sessions/page.tsx +39 -0
  38. package/pulse/app/settings/page.tsx +188 -0
  39. package/pulse/app/todos/page.tsx +211 -0
  40. package/pulse/app/tools/page.tsx +249 -0
  41. package/pulse/cli.js +164 -0
  42. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  43. package/pulse/components/activity/streak-card.tsx +36 -0
  44. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  45. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  46. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  47. package/pulse/components/costs/model-token-table.tsx +60 -0
  48. package/pulse/components/global-search.tsx +193 -0
  49. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  50. package/pulse/components/layout/bottom-nav.tsx +53 -0
  51. package/pulse/components/layout/client-layout.tsx +31 -0
  52. package/pulse/components/layout/sidebar-context.tsx +50 -0
  53. package/pulse/components/layout/sidebar.tsx +183 -0
  54. package/pulse/components/layout/top-bar.tsx +121 -0
  55. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  56. package/pulse/components/overview/conversation-table.tsx +148 -0
  57. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  58. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  59. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  60. package/pulse/components/overview/stat-card.tsx +102 -0
  61. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  62. package/pulse/components/projects/project-card.tsx +175 -0
  63. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  64. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  65. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  66. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  67. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  68. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  69. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  70. package/pulse/components/sessions/session-badges.tsx +49 -0
  71. package/pulse/components/sessions/session-table.tsx +299 -0
  72. package/pulse/components/theme-provider.tsx +44 -0
  73. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  74. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  75. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  76. package/pulse/components/tools/version-history-table.tsx +32 -0
  77. package/pulse/components/ui/alert.tsx +66 -0
  78. package/pulse/components/ui/badge.tsx +48 -0
  79. package/pulse/components/ui/breadcrumb.tsx +109 -0
  80. package/pulse/components/ui/button.tsx +64 -0
  81. package/pulse/components/ui/calendar.tsx +220 -0
  82. package/pulse/components/ui/card.tsx +92 -0
  83. package/pulse/components/ui/command.tsx +158 -0
  84. package/pulse/components/ui/dialog.tsx +158 -0
  85. package/pulse/components/ui/input.tsx +21 -0
  86. package/pulse/components/ui/popover.tsx +89 -0
  87. package/pulse/components/ui/progress.tsx +31 -0
  88. package/pulse/components/ui/select.tsx +190 -0
  89. package/pulse/components/ui/separator.tsx +28 -0
  90. package/pulse/components/ui/sheet.tsx +143 -0
  91. package/pulse/components/ui/skeleton.tsx +13 -0
  92. package/pulse/components/ui/table.tsx +116 -0
  93. package/pulse/components/ui/tabs.tsx +91 -0
  94. package/pulse/components/ui/tooltip.tsx +57 -0
  95. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  96. package/pulse/components.json +23 -0
  97. package/pulse/eslint.config.mjs +18 -0
  98. package/pulse/lib/bahulam-paths.ts +23 -0
  99. package/pulse/lib/claude-reader.ts +592 -0
  100. package/pulse/lib/decode.ts +129 -0
  101. package/pulse/lib/pricing.ts +102 -0
  102. package/pulse/lib/replay-parser.ts +165 -0
  103. package/pulse/lib/tool-categories.ts +127 -0
  104. package/pulse/lib/utils.ts +6 -0
  105. package/pulse/next-env.d.ts +6 -0
  106. package/pulse/next.config.ts +16 -0
  107. package/pulse/package.json +45 -0
  108. package/pulse/postcss.config.mjs +7 -0
  109. package/pulse/public/activity.png +0 -0
  110. package/pulse/public/cc-lens.png +0 -0
  111. package/pulse/public/command-k.png +0 -0
  112. package/pulse/public/costs.png +0 -0
  113. package/pulse/public/dashboard-dark.png +0 -0
  114. package/pulse/public/dashboard-white.png +0 -0
  115. package/pulse/public/export.png +0 -0
  116. package/pulse/public/file.svg +1 -0
  117. package/pulse/public/globe.svg +1 -0
  118. package/pulse/public/next.svg +1 -0
  119. package/pulse/public/projects.png +0 -0
  120. package/pulse/public/session-chat.png +0 -0
  121. package/pulse/public/todos.png +0 -0
  122. package/pulse/public/tools.png +0 -0
  123. package/pulse/public/vercel.svg +1 -0
  124. package/pulse/public/window.svg +1 -0
  125. package/pulse/tsconfig.json +34 -0
  126. package/pulse/types/claude.ts +294 -0
  127. package/src/agents/loader.mjs +94 -0
  128. package/src/agents/multi_workflow_loader.mjs +330 -0
  129. package/src/agents/parser.mjs +205 -0
  130. package/src/agents/scaffold.mjs +222 -0
  131. package/src/agents/teams.mjs +123 -0
  132. package/src/agents/workflow_loader.mjs +122 -0
  133. package/src/agents/workflow_scaffold.mjs +249 -0
  134. package/src/auth/oauth.mjs +220 -0
  135. package/src/auth/tarang-auth.mjs +306 -0
  136. package/src/commands/agent.mjs +220 -0
  137. package/src/commands/workflow.mjs +581 -0
  138. package/src/config/cli-args.mjs +200 -0
  139. package/src/config/env.mjs +263 -0
  140. package/src/config/hook-runner.mjs +100 -0
  141. package/src/config/memory-loader.mjs +32 -0
  142. package/src/config/settings-loader.mjs +45 -0
  143. package/src/config/settings.mjs +132 -0
  144. package/src/context/ast-parser.mjs +298 -0
  145. package/src/context/bm25.mjs +85 -0
  146. package/src/context/retriever.mjs +308 -0
  147. package/src/context/skeleton.mjs +134 -0
  148. package/src/context/symbol-indexer.mjs +375 -0
  149. package/src/core/agent-history.mjs +111 -0
  150. package/src/core/agent-loop.mjs +486 -0
  151. package/src/core/approval-log.mjs +104 -0
  152. package/src/core/approval.mjs +476 -0
  153. package/src/core/attachments.mjs +380 -0
  154. package/src/core/backend-url.mjs +55 -0
  155. package/src/core/cache-control.mjs +92 -0
  156. package/src/core/cache.mjs +105 -0
  157. package/src/core/callback-client.mjs +180 -0
  158. package/src/core/checkpoints.mjs +142 -0
  159. package/src/core/compact-history.mjs +127 -0
  160. package/src/core/context-envelope.mjs +54 -0
  161. package/src/core/context-manager.mjs +198 -0
  162. package/src/core/error-guidance.mjs +311 -0
  163. package/src/core/file-diff.mjs +217 -0
  164. package/src/core/headless.mjs +448 -0
  165. package/src/core/hooks-manager.mjs +87 -0
  166. package/src/core/jsonl-writer.mjs +449 -0
  167. package/src/core/local-agent.mjs +537 -0
  168. package/src/core/local-store.mjs +836 -0
  169. package/src/core/mode-selector.mjs +51 -0
  170. package/src/core/output-filter.mjs +177 -0
  171. package/src/core/paths.mjs +190 -0
  172. package/src/core/policy-resolver.mjs +156 -0
  173. package/src/core/pricing.mjs +336 -0
  174. package/src/core/project-artifacts.mjs +39 -0
  175. package/src/core/project-context-loader.mjs +139 -0
  176. package/src/core/providers.mjs +219 -0
  177. package/src/core/rate-limit-display.mjs +121 -0
  178. package/src/core/rate-limiter.mjs +119 -0
  179. package/src/core/resume-mode.mjs +192 -0
  180. package/src/core/risk-tier.mjs +337 -0
  181. package/src/core/safety.mjs +203 -0
  182. package/src/core/scheduler.mjs +173 -0
  183. package/src/core/session-manager.mjs +360 -0
  184. package/src/core/session.mjs +143 -0
  185. package/src/core/settings-sync.mjs +85 -0
  186. package/src/core/stagnation.mjs +57 -0
  187. package/src/core/stream-client.mjs +829 -0
  188. package/src/core/streaming.mjs +182 -0
  189. package/src/core/system-prompt.mjs +140 -0
  190. package/src/core/tasks.mjs +196 -0
  191. package/src/core/tool-executor.mjs +1950 -0
  192. package/src/core/trust.mjs +158 -0
  193. package/src/core/work-scope.mjs +248 -0
  194. package/src/hooks/engine.mjs +162 -0
  195. package/src/index.mjs +426 -0
  196. package/src/mcp/client.mjs +253 -0
  197. package/src/mcp/transport-shttp.mjs +130 -0
  198. package/src/mcp/transport-sse.mjs +131 -0
  199. package/src/mcp/transport-ws.mjs +134 -0
  200. package/src/onboarding/preflight.mjs +360 -0
  201. package/src/permissions/checker.mjs +57 -0
  202. package/src/permissions/command-classifier.mjs +652 -0
  203. package/src/permissions/injection-check.mjs +60 -0
  204. package/src/permissions/path-check.mjs +102 -0
  205. package/src/permissions/prompt.mjs +73 -0
  206. package/src/permissions/sandbox.mjs +112 -0
  207. package/src/plugins/loader.mjs +138 -0
  208. package/src/skills/installer.mjs +188 -0
  209. package/src/skills/loader.mjs +252 -0
  210. package/src/skills/runner.mjs +55 -0
  211. package/src/state/orbit.mjs +263 -0
  212. package/src/state/verbosity.mjs +99 -0
  213. package/src/telemetry/index.mjs +96 -0
  214. package/src/terminal/agents.mjs +177 -0
  215. package/src/terminal/analytics.mjs +292 -0
  216. package/src/terminal/ansi.mjs +695 -0
  217. package/src/terminal/init.mjs +145 -0
  218. package/src/terminal/main.mjs +269 -0
  219. package/src/terminal/repl-explore.mjs +35 -0
  220. package/src/terminal/repl-format.mjs +257 -0
  221. package/src/terminal/repl-render.mjs +561 -0
  222. package/src/terminal/repl-resume.mjs +625 -0
  223. package/src/terminal/repl-state.mjs +103 -0
  224. package/src/terminal/repl-utils.mjs +34 -0
  225. package/src/terminal/repl.mjs +3832 -0
  226. package/src/terminal/skills.mjs +54 -0
  227. package/src/terminal/tool-display.mjs +240 -0
  228. package/src/tools/agent.mjs +137 -0
  229. package/src/tools/ask-user.mjs +61 -0
  230. package/src/tools/bash.mjs +231 -0
  231. package/src/tools/cron-create.mjs +120 -0
  232. package/src/tools/cron-delete.mjs +49 -0
  233. package/src/tools/cron-list.mjs +37 -0
  234. package/src/tools/edit.mjs +82 -0
  235. package/src/tools/enter-worktree.mjs +69 -0
  236. package/src/tools/exit-worktree.mjs +57 -0
  237. package/src/tools/glob.mjs +117 -0
  238. package/src/tools/grep.mjs +129 -0
  239. package/src/tools/lint.mjs +71 -0
  240. package/src/tools/ls.mjs +58 -0
  241. package/src/tools/lsp.mjs +115 -0
  242. package/src/tools/multi-edit.mjs +94 -0
  243. package/src/tools/notebook-edit.mjs +96 -0
  244. package/src/tools/project-overview.mjs +641 -0
  245. package/src/tools/read-mcp-resource.mjs +57 -0
  246. package/src/tools/read.mjs +138 -0
  247. package/src/tools/registry.mjs +116 -0
  248. package/src/tools/remote-trigger.mjs +84 -0
  249. package/src/tools/send-message.mjs +64 -0
  250. package/src/tools/skill.mjs +52 -0
  251. package/src/tools/test-runner.mjs +49 -0
  252. package/src/tools/todo-write.mjs +68 -0
  253. package/src/tools/tool-search.mjs +77 -0
  254. package/src/tools/web-fetch.mjs +65 -0
  255. package/src/tools/web-search.mjs +89 -0
  256. package/src/tools/write.mjs +55 -0
  257. package/src/ui/approval.mjs +263 -0
  258. package/src/ui/banner.mjs +235 -0
  259. package/src/ui/commands.mjs +537 -0
  260. package/src/ui/formatter.mjs +409 -0
  261. package/src/ui/icons.mjs +164 -0
  262. package/src/ui/input-dock.mjs +444 -0
  263. package/src/ui/markdown.mjs +278 -0
  264. package/src/ui/mission-report.mjs +296 -0
  265. package/src/ui/palette.mjs +189 -0
  266. package/src/ui/slash-commands.mjs +245 -0
  267. package/src/ui/spinner.mjs +116 -0
  268. package/src/ui/sub-agent.mjs +152 -0
  269. package/src/ui/term.mjs +159 -0
  270. package/src/ui/text-layout.mjs +127 -0
  271. package/src/ui/tool-card.mjs +463 -0
  272. package/src/ui/tool-details.mjs +312 -0
  273. package/src/ui/transcript-block.mjs +21 -0
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Multi-Provider — unified provider config and request/response transforms.
3
+ *
4
+ * Supports: Anthropic, OpenAI, Google, Bedrock (stub), Vertex (stub).
5
+ * Each provider defines endpoint, auth headers, and optional transforms.
6
+ */
7
+
8
+ const PROVIDERS = {
9
+ anthropic: {
10
+ name: 'Anthropic',
11
+ endpoint: 'https://api.anthropic.com/v1/messages',
12
+ envKey: 'ANTHROPIC_API_KEY',
13
+ authHeader(key) {
14
+ return {
15
+ 'x-api-key': key,
16
+ 'anthropic-version': '2023-06-01',
17
+ 'Content-Type': 'application/json',
18
+ };
19
+ },
20
+ models: ['claude-sonnet-4-6', 'claude-haiku-4-5', 'claude-opus-4-6'],
21
+ },
22
+
23
+ openai: {
24
+ name: 'OpenAI',
25
+ endpoint: 'https://api.openai.com/v1/chat/completions',
26
+ envKey: 'OPENAI_API_KEY',
27
+ authHeader(key) {
28
+ return {
29
+ 'Authorization': `Bearer ${key}`,
30
+ 'Content-Type': 'application/json',
31
+ };
32
+ },
33
+ models: ['gpt-4o', 'gpt-4o-mini', 'o1-preview', 'o1-mini', 'o3-mini'],
34
+ transformRequest(body) {
35
+ const messages = [];
36
+ if (body.system) {
37
+ messages.push({ role: 'system', content: body.system });
38
+ }
39
+ for (const msg of body.messages || []) {
40
+ if (typeof msg.content === 'string') {
41
+ messages.push({ role: msg.role, content: msg.content });
42
+ } else if (Array.isArray(msg.content)) {
43
+ for (const block of msg.content) {
44
+ if (block.type === 'tool_result') {
45
+ messages.push({
46
+ role: 'tool',
47
+ tool_call_id: block.tool_use_id,
48
+ content: block.content,
49
+ });
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ const tools = (body.tools || []).map(t => ({
56
+ type: 'function',
57
+ function: { name: t.name, description: t.description, parameters: t.input_schema },
58
+ }));
59
+
60
+ return {
61
+ model: body.model,
62
+ messages,
63
+ ...(tools.length > 0 && { tools }),
64
+ ...(body.max_tokens && { max_tokens: body.max_tokens }),
65
+ ...(body.stream && { stream: true }),
66
+ };
67
+ },
68
+ transformResponse(data) {
69
+ const choice = data.choices?.[0];
70
+ if (!choice) throw new Error('No choices in OpenAI response');
71
+
72
+ const content = [];
73
+ if (choice.message?.content) {
74
+ content.push({ type: 'text', text: choice.message.content });
75
+ }
76
+ if (choice.message?.tool_calls) {
77
+ for (const tc of choice.message.tool_calls) {
78
+ content.push({
79
+ type: 'tool_use',
80
+ id: tc.id,
81
+ name: tc.function.name,
82
+ input: JSON.parse(tc.function.arguments || '{}'),
83
+ });
84
+ }
85
+ }
86
+
87
+ return {
88
+ content,
89
+ stop_reason: choice.finish_reason === 'stop' ? 'end_turn' : choice.finish_reason,
90
+ usage: {
91
+ input_tokens: data.usage?.prompt_tokens || 0,
92
+ output_tokens: data.usage?.completion_tokens || 0,
93
+ },
94
+ };
95
+ },
96
+ },
97
+
98
+ google: {
99
+ name: 'Google',
100
+ endpoint: 'https://generativelanguage.googleapis.com/v1beta/models',
101
+ envKey: 'GOOGLE_API_KEY',
102
+ altEnvKey: 'GEMINI_API_KEY',
103
+ authHeader(key) {
104
+ return { 'Content-Type': 'application/json' };
105
+ },
106
+ models: ['gemini-2.0-flash', 'gemini-2.0-pro', 'gemini-1.5-flash'],
107
+ transformRequest(body) {
108
+ const contents = [];
109
+ for (const msg of body.messages || []) {
110
+ const role = msg.role === 'assistant' ? 'model' : 'user';
111
+ if (typeof msg.content === 'string') {
112
+ contents.push({ role, parts: [{ text: msg.content }] });
113
+ }
114
+ }
115
+
116
+ return {
117
+ contents,
118
+ ...(body.system && {
119
+ systemInstruction: { parts: [{ text: body.system }] },
120
+ }),
121
+ };
122
+ },
123
+ transformResponse(data) {
124
+ const candidate = data.candidates?.[0];
125
+ if (!candidate) throw new Error('No candidates in Google response');
126
+
127
+ const content = [];
128
+ for (const part of candidate.content?.parts || []) {
129
+ if (part.text) content.push({ type: 'text', text: part.text });
130
+ }
131
+
132
+ return {
133
+ content,
134
+ stop_reason: 'end_turn',
135
+ usage: {
136
+ input_tokens: data.usageMetadata?.promptTokenCount || 0,
137
+ output_tokens: data.usageMetadata?.candidatesTokenCount || 0,
138
+ },
139
+ };
140
+ },
141
+ },
142
+
143
+ bedrock: {
144
+ name: 'AWS Bedrock',
145
+ endpoint: null, // Dynamic based on region
146
+ envKey: 'AWS_ACCESS_KEY_ID',
147
+ models: ['anthropic.claude-3-sonnet', 'anthropic.claude-3-haiku'],
148
+ authHeader() {
149
+ // AWS SigV4 signing would go here
150
+ return { 'Content-Type': 'application/json' };
151
+ },
152
+ getEndpoint(model, region = 'us-east-1') {
153
+ return `https://bedrock-runtime.${region}.amazonaws.com/model/${model}/invoke`;
154
+ },
155
+ },
156
+
157
+ vertex: {
158
+ name: 'Google Vertex AI',
159
+ endpoint: null, // Dynamic based on project/region
160
+ envKey: 'GOOGLE_APPLICATION_CREDENTIALS',
161
+ models: ['claude-sonnet-4-6@anthropic'],
162
+ authHeader() {
163
+ // GCP bearer token would go here
164
+ return { 'Content-Type': 'application/json' };
165
+ },
166
+ getEndpoint(model, project, region = 'us-central1') {
167
+ return `https://${region}-aiplatform.googleapis.com/v1/projects/${project}/locations/${region}/publishers/anthropic/models/${model}:rawPredict`;
168
+ },
169
+ },
170
+ };
171
+
172
+ /**
173
+ * Get the provider configuration for a given model.
174
+ * @param {string} model - model name
175
+ * @returns {object} provider config
176
+ */
177
+ export function getProvider(model) {
178
+ if (model.startsWith('claude') || model.startsWith('anthropic')) return PROVIDERS.anthropic;
179
+ if (model.startsWith('gpt') || model.startsWith('o1') || model.startsWith('o3')) return PROVIDERS.openai;
180
+ if (model.startsWith('gemini')) return PROVIDERS.google;
181
+ return PROVIDERS.anthropic; // default
182
+ }
183
+
184
+ /**
185
+ * Get a provider by name.
186
+ * @param {string} name
187
+ * @returns {object|undefined}
188
+ */
189
+ export function getProviderByName(name) {
190
+ return PROVIDERS[name];
191
+ }
192
+
193
+ /**
194
+ * List all supported providers.
195
+ * @returns {Array<{ name: string, envKey: string, models: string[] }>}
196
+ */
197
+ export function listProviders() {
198
+ return Object.entries(PROVIDERS).map(([key, p]) => ({
199
+ id: key,
200
+ name: p.name,
201
+ envKey: p.envKey,
202
+ models: p.models || [],
203
+ hasEndpoint: !!p.endpoint,
204
+ }));
205
+ }
206
+
207
+ /**
208
+ * Check which providers have API keys configured.
209
+ * @returns {Array<{ id: string, name: string, configured: boolean }>}
210
+ */
211
+ export function checkProviderKeys() {
212
+ return Object.entries(PROVIDERS).map(([key, p]) => ({
213
+ id: key,
214
+ name: p.name,
215
+ configured: !!(process.env[p.envKey] || (p.altEnvKey && process.env[p.altEnvKey])),
216
+ }));
217
+ }
218
+
219
+ export { PROVIDERS };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * User-facing helpers for PRD-065 rolling message windows.
3
+ */
4
+
5
+ export function normalizeRateLimit(rateLimit) {
6
+ if (!rateLimit || typeof rateLimit !== 'object') return null;
7
+
8
+ const used = numberOrNull(rateLimit.msgs_used_in_window);
9
+ const limit = numberOrNull(rateLimit.msgs_per_window);
10
+ const configured = numberOrNull(rateLimit.configured_msgs_per_window);
11
+ const retryAfter = numberOrNull(rateLimit.retry_after ?? rateLimit.retry_after_seconds);
12
+
13
+ return {
14
+ ...rateLimit,
15
+ msgs_used_in_window: used,
16
+ msgs_per_window: limit,
17
+ configured_msgs_per_window: configured,
18
+ retry_after: retryAfter,
19
+ unlimited: Boolean(rateLimit.unlimited),
20
+ byok: Boolean(rateLimit.byok),
21
+ };
22
+ }
23
+
24
+ export function messagesRemaining(rateLimit) {
25
+ const rl = normalizeRateLimit(rateLimit);
26
+ if (!rl) return null;
27
+ if (rl.unlimited || rl.msgs_per_window === -1) return Infinity;
28
+ if (typeof rl.msgs_used_in_window !== 'number' || typeof rl.msgs_per_window !== 'number') return null;
29
+ return Math.max(0, rl.msgs_per_window - rl.msgs_used_in_window);
30
+ }
31
+
32
+ export function formatRetryAfter(seconds) {
33
+ const secs = Math.max(0, Math.ceil(Number(seconds) || 0));
34
+ if (secs <= 0) return 'soon';
35
+ const hours = Math.floor(secs / 3600);
36
+ const minutes = Math.ceil((secs % 3600) / 60);
37
+ if (hours > 0 && minutes > 0) return `${hours}h ${minutes}m`;
38
+ if (hours > 0) return `${hours}h`;
39
+ if (minutes > 0) return `${minutes}m`;
40
+ return `${secs}s`;
41
+ }
42
+
43
+ export function resetLabel(rateLimit, now = Date.now()) {
44
+ const rl = normalizeRateLimit(rateLimit);
45
+ if (!rl?.window_reset_at) return null;
46
+ const resetMs = Date.parse(rl.window_reset_at);
47
+ if (!Number.isFinite(resetMs)) return null;
48
+ return formatRetryAfter(Math.max(0, (resetMs - now) / 1000));
49
+ }
50
+
51
+ export function formatMessageWindow(rateLimit, { includeReset = true } = {}) {
52
+ const rl = normalizeRateLimit(rateLimit);
53
+ if (!rl) return null;
54
+
55
+ const tier = rl.tier ? String(rl.tier).toUpperCase() : null;
56
+ if (rl.byok || rl.unlimited || rl.msgs_per_window === -1) {
57
+ return [tier, 'unlimited messages'].filter(Boolean).join(' · ');
58
+ }
59
+
60
+ if (typeof rl.msgs_used_in_window !== 'number' || typeof rl.msgs_per_window !== 'number') {
61
+ return tier || null;
62
+ }
63
+
64
+ const remaining = Math.max(0, rl.msgs_per_window - rl.msgs_used_in_window);
65
+ const parts = [
66
+ tier,
67
+ `${remaining} / ${rl.msgs_per_window} messages this window`,
68
+ ].filter(Boolean);
69
+
70
+ const reset = includeReset ? resetLabel(rl) : null;
71
+ if (reset) parts.push(`resets in ${reset}`);
72
+ return parts.join(' · ');
73
+ }
74
+
75
+ export function lowWindowStatus(rateLimit) {
76
+ const rl = normalizeRateLimit(rateLimit);
77
+ if (!rl || rl.byok || rl.unlimited || rl.msgs_per_window === -1) return 'ok';
78
+ const remaining = messagesRemaining(rl);
79
+ if (typeof remaining !== 'number' || typeof rl.msgs_per_window !== 'number') return 'ok';
80
+ if (remaining <= 0) return 'exhausted';
81
+ if (remaining <= Math.max(5, Math.floor(rl.msgs_per_window * 0.2))) return 'low';
82
+ return 'ok';
83
+ }
84
+
85
+ export function rateLimitErrorMessage(payload, fallback = 'Message limit reached.') {
86
+ const detail = quotaErrorDetail(payload);
87
+ const retryAfter = detail?.retry_after ?? detail?.rate_limit?.retry_after ?? detail?.rate_limit?.retry_after_seconds;
88
+ const detailMessage = normalizeBillingBrandCopy(detail?.message);
89
+ if (detail?.code === 'credit_balance_exhausted') {
90
+ return detailMessage || 'Credit balance exhausted — add credits, upgrade your plan, or switch to BYOK in Settings.';
91
+ }
92
+ if (detail?.code === 'message_limit_reached') {
93
+ if (detailMessage) return detailMessage;
94
+ if (retryAfter != null) return `Message window exhausted — try again in ${formatRetryAfter(retryAfter)}, or upgrade your plan.`;
95
+ return 'Message window exhausted — wait for the window to reset or upgrade your plan.';
96
+ }
97
+ if (detailMessage) return detailMessage;
98
+ if (retryAfter != null) return `Message window exhausted — try again in ${formatRetryAfter(retryAfter)}, or upgrade your plan.`;
99
+ return fallback;
100
+ }
101
+
102
+ export function normalizeBillingBrandCopy(value) {
103
+ if (typeof value !== 'string') return value;
104
+ return value
105
+ .replace(/codekepler\.ai\/pricing/gi, 'bahulam.ai/pricing')
106
+ .replace(/Kepler credit charges/g, 'Bahulam credit charges')
107
+ .replace(/Kepler credits/g, 'Bahulam credits');
108
+ }
109
+
110
+ export function quotaErrorDetail(payload) {
111
+ if (!payload || typeof payload !== 'object') return {};
112
+ if (typeof payload.detail === 'string') return { message: payload.detail };
113
+ if (payload.detail && typeof payload.detail === 'object') return payload.detail;
114
+ return payload;
115
+ }
116
+
117
+ function numberOrNull(value) {
118
+ if (value == null) return null;
119
+ const n = Number(value);
120
+ return Number.isFinite(n) ? n : null;
121
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Rate Limiter — handle 429 and 529 API responses.
3
+ *
4
+ * Implements exponential backoff with jitter for rate-limited
5
+ * and overloaded API responses. Tracks retry state per-instance.
6
+ */
7
+
8
+ export class RateLimiter {
9
+ /**
10
+ * @param {object} [options]
11
+ * @param {number} [options.maxRetries] - max number of retries (default: 5)
12
+ * @param {number} [options.baseDelay] - base delay in ms (default: 1000)
13
+ * @param {number} [options.maxDelay] - max delay in ms (default: 60000)
14
+ */
15
+ constructor(options = {}) {
16
+ this.maxRetries = options.maxRetries ?? 5;
17
+ this.baseDelay = options.baseDelay ?? 1000;
18
+ this.maxDelay = options.maxDelay ?? 60000;
19
+ this.retryAfter = 0;
20
+ this.retryCount = 0;
21
+ this.lastRetryAt = null;
22
+ }
23
+
24
+ /**
25
+ * Handle an API response and determine whether to retry.
26
+ * @param {{ status: number, headers: { get: (name: string) => string|null } }} response
27
+ * @returns {Promise<'ok'|'retry'|'fail'>}
28
+ */
29
+ async handleResponse(response) {
30
+ if (response.status === 429) {
31
+ // Rate limited
32
+ if (this.retryCount >= this.maxRetries) return 'fail';
33
+
34
+ const retryAfter = parseInt(response.headers?.get?.('retry-after') || '10', 10);
35
+ const delayMs = Math.min(retryAfter * 1000, this.maxDelay);
36
+ this.retryAfter = Date.now() + delayMs;
37
+ this.retryCount++;
38
+ this.lastRetryAt = new Date().toISOString();
39
+
40
+ await this.wait(delayMs);
41
+ return 'retry';
42
+ }
43
+
44
+ if (response.status === 529) {
45
+ // API overloaded
46
+ if (this.retryCount >= this.maxRetries) return 'fail';
47
+
48
+ const delay = this.calculateBackoff();
49
+ this.retryAfter = Date.now() + delay;
50
+ this.retryCount++;
51
+ this.lastRetryAt = new Date().toISOString();
52
+
53
+ await this.wait(delay);
54
+ return 'retry';
55
+ }
56
+
57
+ // Success — reset retry count
58
+ this.retryCount = 0;
59
+ return 'ok';
60
+ }
61
+
62
+ /**
63
+ * Calculate exponential backoff with jitter.
64
+ * @returns {number} delay in milliseconds
65
+ */
66
+ calculateBackoff() {
67
+ const exponential = this.baseDelay * Math.pow(2, this.retryCount);
68
+ const jitter = Math.random() * this.baseDelay;
69
+ return Math.min(exponential + jitter, this.maxDelay);
70
+ }
71
+
72
+ /**
73
+ * Check if we should wait before making a request.
74
+ * @returns {boolean}
75
+ */
76
+ shouldWait() {
77
+ return Date.now() < this.retryAfter;
78
+ }
79
+
80
+ /**
81
+ * Get remaining wait time in ms.
82
+ * @returns {number}
83
+ */
84
+ remainingWait() {
85
+ return Math.max(0, this.retryAfter - Date.now());
86
+ }
87
+
88
+ /**
89
+ * Reset all retry state.
90
+ */
91
+ reset() {
92
+ this.retryAfter = 0;
93
+ this.retryCount = 0;
94
+ this.lastRetryAt = null;
95
+ }
96
+
97
+ /**
98
+ * Get current limiter status.
99
+ */
100
+ status() {
101
+ return {
102
+ retryCount: this.retryCount,
103
+ maxRetries: this.maxRetries,
104
+ retryAfter: this.retryAfter,
105
+ lastRetryAt: this.lastRetryAt,
106
+ isWaiting: this.shouldWait(),
107
+ remainingMs: this.remainingWait(),
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Wait for the specified duration.
113
+ * @param {number} ms
114
+ * @returns {Promise<void>}
115
+ */
116
+ wait(ms) {
117
+ return new Promise(resolve => setTimeout(resolve, ms));
118
+ }
119
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Resume Mode — decide how much of a transcript to send to the agent (PRD-068 §5.14.3).
3
+ *
4
+ * Rule of thumb: if the projected context (transcript + system prompt overhead)
5
+ * fits comfortably in the current model's window, resume with `full` silently.
6
+ * Only ask the user when we're above the highWatermark.
7
+ *
8
+ * Config lives in .bahulam/settings.json:
9
+ * {
10
+ * "resume": {
11
+ * "highWatermark": 0.50, // prompt above this (default 50%)
12
+ * "hardCap": 0.85 // refuse full above this (default 85%)
13
+ * }
14
+ * }
15
+ */
16
+
17
+ // System overhead — tools, memory, .bahulam/ context, kepler.md, skills index.
18
+ // Rough constant; individual runs vary. Tuned toward "assume ~4k of overhead".
19
+ const DEFAULT_SYSTEM_OVERHEAD_TOKENS = 4000;
20
+
21
+ // Model context windows we know about. Fallback used when unknown.
22
+ // Keep this table small — it's a hint, not a source of truth.
23
+ const MODEL_CONTEXT_WINDOWS = {
24
+ 'anthropic/claude-sonnet-4': 200000,
25
+ 'anthropic/claude-4-sonnet-20250522': 200000,
26
+ 'anthropic/claude-opus-4': 200000,
27
+ 'deepseek/deepseek-v4-flash': 128000,
28
+ 'deepseek/deepseek-v4-pro': 128000,
29
+ 'deepseek/deepseek-chat-v3-0324': 128000,
30
+ 'openai/gpt-5': 400000,
31
+ 'openai/gpt-5-mini': 400000,
32
+ 'google/gemini-2.5-pro': 1000000,
33
+ 'xiaomi/mimo-v2.5': 128000,
34
+ };
35
+ const DEFAULT_CONTEXT_WINDOW = 128000;
36
+
37
+ function contextWindowHintInfo(hint) {
38
+ const raw = typeof hint === 'number'
39
+ ? hint
40
+ : hint?.context_length ?? hint?.contextLength ?? hint?.tokens ?? hint?.windowSize;
41
+ const tokens = Number(raw);
42
+ if (Number.isFinite(tokens) && tokens > 0) {
43
+ return {
44
+ tokens: Math.trunc(tokens),
45
+ known: true,
46
+ source: hint?.source || 'backend_catalog',
47
+ };
48
+ }
49
+ return null;
50
+ }
51
+
52
+ export function modelContextWindowInfo(model, contextWindowHint = null) {
53
+ const hinted = contextWindowHintInfo(contextWindowHint);
54
+ if (hinted) return hinted;
55
+ if (model && MODEL_CONTEXT_WINDOWS[model]) {
56
+ return { tokens: MODEL_CONTEXT_WINDOWS[model], known: true, source: 'model_map' };
57
+ }
58
+ return { tokens: DEFAULT_CONTEXT_WINDOW, known: false, source: 'fallback' };
59
+ }
60
+
61
+ export function modelContextWindow(model, contextWindowHint = null) {
62
+ return modelContextWindowInfo(model, contextWindowHint).tokens;
63
+ }
64
+
65
+ /**
66
+ * Decide the resume mode for a session based on projected context usage.
67
+ *
68
+ * @param {object} args
69
+ * @param {number} args.transcriptTokens — projected transcript size when serialized
70
+ * @param {string} [args.model] — current agent model id
71
+ * @param {object|number} [args.contextWindow] — backend/catalog context-window hint
72
+ * @param {object} [args.settings] — .bahulam/settings.json contents (optional)
73
+ * @param {number} [args.systemOverhead] — override system overhead (defaults to 4k)
74
+ * @returns {{
75
+ * mode: 'full' | 'ask' | 'no-full-allowed',
76
+ * defaultChoice: 'full' | 'tail-20' | 'summary',
77
+ * projected: number, // total projected tokens
78
+ * windowSize: number, // model window
79
+ * usageRatio: number, // projected / windowSize
80
+ * highWatermark: number,
81
+ * hardCap: number,
82
+ * }}
83
+ *
84
+ * `mode = 'full'` — resume immediately in full mode; do not prompt
85
+ * `mode = 'ask'` — show the tri-choice overlay
86
+ * `mode = 'no-full-allowed'` — above hardCap; user must pick a tail mode or summary
87
+ */
88
+ export function decideResumeMode({
89
+ transcriptTokens,
90
+ model,
91
+ contextWindow = null,
92
+ settings,
93
+ systemOverhead = DEFAULT_SYSTEM_OVERHEAD_TOKENS,
94
+ } = {}) {
95
+ const cfg = settings?.resume || {};
96
+ const highWatermark = clampRatio(cfg.highWatermark, 0.50);
97
+ const hardCap = clampRatio(cfg.hardCap, 0.85);
98
+
99
+ const windowInfo = modelContextWindowInfo(model, contextWindow);
100
+ const windowSize = windowInfo.tokens;
101
+ const projected = Math.max(0, Number(transcriptTokens) || 0) + systemOverhead;
102
+ const usageRatio = windowSize > 0 ? projected / windowSize : 0;
103
+
104
+ let mode;
105
+ let defaultChoice;
106
+ if (usageRatio > hardCap) {
107
+ mode = 'no-full-allowed';
108
+ // Above hardCap — last 20 turns is usually the least-lossy fit; user picks.
109
+ defaultChoice = 'tail-20';
110
+ } else if (usageRatio > highWatermark) {
111
+ mode = 'ask';
112
+ defaultChoice = 'full';
113
+ } else {
114
+ mode = 'full';
115
+ defaultChoice = 'full';
116
+ }
117
+
118
+ return {
119
+ mode,
120
+ defaultChoice,
121
+ projected,
122
+ windowSize,
123
+ windowKnown: windowInfo.known,
124
+ windowSource: windowInfo.source,
125
+ usageRatio,
126
+ highWatermark,
127
+ hardCap,
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Estimate the context tokens for a candidate mode. Used by the tri-choice
133
+ * overlay to render "62k / 14k / 2k" per option before the user commits.
134
+ *
135
+ * @param {'full' | 'checkpoint-full' | 'summary' | 'tail-10' | 'tail-20' | 'recap+tail'} choice
136
+ * @param {number} fullTokens — projected transcript size in full mode
137
+ * @param {object} [opts]
138
+ * @returns {number} projected tokens for the chosen mode
139
+ */
140
+ export function projectedTokensForChoice(choice, fullTokens, opts = {}) {
141
+ const {
142
+ tailTurns = null,
143
+ // Rough per-turn estimate. Summary block is ~1-2k.
144
+ tailBaseTokens = 2000,
145
+ perTailTurnTokens = 500,
146
+ summaryBaseTokens = 2000,
147
+ resumeSummary = null,
148
+ } = opts;
149
+
150
+ switch (choice) {
151
+ case 'full':
152
+ return Math.max(0, Number(fullTokens) || 0);
153
+ case 'checkpoint-full': {
154
+ const full = Math.max(0, Number(fullTokens) || 0);
155
+ const fullMessages = Math.max(0, Number(resumeSummary?.fullMessageCount) || 0);
156
+ const covered = Math.max(0, Number(resumeSummary?.sourceMessageCount) || 0);
157
+ if (!fullMessages || covered <= 0) return full;
158
+ const remainingMessages = Math.max(0, fullMessages - covered);
159
+ if (remainingMessages === 0) return summaryBaseTokens;
160
+ return summaryBaseTokens + (perTailTurnTokens * remainingMessages);
161
+ }
162
+ case 'tail-10':
163
+ return tailBaseTokens + (10 * perTailTurnTokens);
164
+ case 'tail-20':
165
+ return tailBaseTokens + (20 * perTailTurnTokens);
166
+ case 'recap+tail':
167
+ return tailBaseTokens + ((Number(tailTurns) || 8) * perTailTurnTokens);
168
+ case 'summary':
169
+ return summaryBaseTokens;
170
+ default:
171
+ return Math.max(0, Number(fullTokens) || 0);
172
+ }
173
+ }
174
+
175
+ function clampRatio(value, fallback) {
176
+ const n = Number(value);
177
+ if (!Number.isFinite(n)) return fallback;
178
+ if (n <= 0) return fallback;
179
+ if (n > 1) return fallback;
180
+ return n;
181
+ }
182
+
183
+ /**
184
+ * Format tokens with a k/M suffix — "8k", "62k", "1.2M".
185
+ * Used by the picker + overlay so numbers fit in narrow columns.
186
+ */
187
+ export function formatTokens(n) {
188
+ const v = Math.round(Number(n) || 0);
189
+ if (v < 1000) return `${v}`;
190
+ if (v < 1_000_000) return `${Math.round(v / 1000)}k`;
191
+ return `${(v / 1_000_000).toFixed(1)}M`;
192
+ }