@bahulam/code 0.1.5 → 0.1.7

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 (179) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +6 -6
  3. package/README.md +10 -17
  4. package/package.json +9 -9
  5. package/src/agents/loader.mjs +1 -1
  6. package/src/agents/workflow_loader.mjs +2 -2
  7. package/src/auth/tarang-auth.mjs +3 -2
  8. package/src/config/model-catalog-default.json +4 -3
  9. package/src/config/model-catalog.mjs +2 -0
  10. package/src/context/prose-chunker.mjs +162 -5
  11. package/src/core/attachments.mjs +65 -3
  12. package/src/core/bundled-runtime.mjs +1 -1
  13. package/src/core/headless.mjs +14 -0
  14. package/src/core/jsonl-writer.mjs +3 -3
  15. package/src/core/lint-resolver.mjs +472 -0
  16. package/src/core/local-agent.mjs +15 -4
  17. package/src/core/local-store.mjs +4 -0
  18. package/src/core/output-filter.mjs +21 -25
  19. package/src/core/pricing.mjs +10 -3
  20. package/src/core/stream-client.mjs +112 -16
  21. package/src/core/tool-executor.mjs +123 -31
  22. package/src/daemon/http-dashboard.mjs +400 -0
  23. package/src/daemon/session-core.mjs +1 -0
  24. package/src/local-service/agent-relay.mjs +868 -0
  25. package/src/local-service/approval-bridge.mjs +222 -0
  26. package/src/local-service/browser.mjs +24 -0
  27. package/src/local-service/command.mjs +155 -0
  28. package/src/local-service/file-access.mjs +393 -0
  29. package/src/local-service/machine.mjs +86 -0
  30. package/src/local-service/paths.mjs +29 -0
  31. package/src/local-service/preview-converters.mjs +260 -0
  32. package/src/local-service/server.mjs +2644 -0
  33. package/src/local-service/session-store.mjs +221 -0
  34. package/src/onboarding/preflight.mjs +1 -1
  35. package/src/terminal/analytics.mjs +2 -6
  36. package/src/terminal/ansi.mjs +11 -3
  37. package/src/terminal/main.mjs +37 -11
  38. package/src/terminal/model-catalog-display.mjs +111 -0
  39. package/src/terminal/repl-format.mjs +4 -1
  40. package/src/terminal/repl-model-form.mjs +81 -14
  41. package/src/terminal/repl-render.mjs +35 -5
  42. package/src/terminal/repl-state.mjs +12 -0
  43. package/src/terminal/repl.mjs +255 -55
  44. package/src/terminal/tool-display.mjs +32 -0
  45. package/src/terminal/watch-state.mjs +118 -0
  46. package/src/tools/analyze-image.mjs +158 -0
  47. package/src/tools/bash.mjs +11 -9
  48. package/src/tools/generate-image.mjs +411 -0
  49. package/src/tools/lint.mjs +24 -43
  50. package/src/tools/project-overview.mjs +5 -2
  51. package/src/tools/registry.mjs +4 -0
  52. package/src/ui/icons.mjs +2 -0
  53. package/src/ui/palette.mjs +1 -1
  54. package/src/ui/slash-commands.mjs +4 -6
  55. package/src/ui/tool-card.mjs +7 -3
  56. package/pulse/app/activity/page.tsx +0 -190
  57. package/pulse/app/api/activity/route.ts +0 -138
  58. package/pulse/app/api/benchmark/route.ts +0 -113
  59. package/pulse/app/api/benchmarks/route.ts +0 -195
  60. package/pulse/app/api/costs/route.ts +0 -88
  61. package/pulse/app/api/export/route.ts +0 -77
  62. package/pulse/app/api/history/route.ts +0 -11
  63. package/pulse/app/api/import/route.ts +0 -31
  64. package/pulse/app/api/memory/route.ts +0 -50
  65. package/pulse/app/api/plans/route.ts +0 -9
  66. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  67. package/pulse/app/api/projects/route.ts +0 -121
  68. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  69. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  70. package/pulse/app/api/sessions/route.ts +0 -112
  71. package/pulse/app/api/settings/route.ts +0 -14
  72. package/pulse/app/api/stats/route.ts +0 -143
  73. package/pulse/app/api/todos/route.ts +0 -9
  74. package/pulse/app/api/tools/route.ts +0 -160
  75. package/pulse/app/benchmarks/page.tsx +0 -224
  76. package/pulse/app/costs/page.tsx +0 -179
  77. package/pulse/app/export/page.tsx +0 -465
  78. package/pulse/app/favicon.ico +0 -0
  79. package/pulse/app/globals.css +0 -263
  80. package/pulse/app/help/page.tsx +0 -143
  81. package/pulse/app/history/page.tsx +0 -157
  82. package/pulse/app/layout.tsx +0 -46
  83. package/pulse/app/memory/page.tsx +0 -365
  84. package/pulse/app/overview-client.tsx +0 -393
  85. package/pulse/app/page.tsx +0 -14
  86. package/pulse/app/plans/page.tsx +0 -308
  87. package/pulse/app/projects/[slug]/page.tsx +0 -390
  88. package/pulse/app/projects/page.tsx +0 -110
  89. package/pulse/app/sessions/[id]/page.tsx +0 -243
  90. package/pulse/app/sessions/page.tsx +0 -39
  91. package/pulse/app/settings/page.tsx +0 -188
  92. package/pulse/app/todos/page.tsx +0 -211
  93. package/pulse/app/tools/page.tsx +0 -249
  94. package/pulse/cli.js +0 -164
  95. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  96. package/pulse/components/activity/streak-card.tsx +0 -36
  97. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  98. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  99. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  100. package/pulse/components/costs/model-token-table.tsx +0 -60
  101. package/pulse/components/global-search.tsx +0 -193
  102. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  103. package/pulse/components/layout/bottom-nav.tsx +0 -53
  104. package/pulse/components/layout/client-layout.tsx +0 -31
  105. package/pulse/components/layout/sidebar-context.tsx +0 -50
  106. package/pulse/components/layout/sidebar.tsx +0 -183
  107. package/pulse/components/layout/top-bar.tsx +0 -121
  108. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  109. package/pulse/components/overview/conversation-table.tsx +0 -148
  110. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  111. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  112. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  113. package/pulse/components/overview/stat-card.tsx +0 -102
  114. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  115. package/pulse/components/projects/project-card.tsx +0 -175
  116. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  117. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  118. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  119. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  120. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  121. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  122. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  123. package/pulse/components/sessions/session-badges.tsx +0 -49
  124. package/pulse/components/sessions/session-table.tsx +0 -299
  125. package/pulse/components/theme-provider.tsx +0 -44
  126. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  127. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  128. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  129. package/pulse/components/tools/version-history-table.tsx +0 -32
  130. package/pulse/components/ui/alert.tsx +0 -66
  131. package/pulse/components/ui/badge.tsx +0 -48
  132. package/pulse/components/ui/breadcrumb.tsx +0 -109
  133. package/pulse/components/ui/button.tsx +0 -64
  134. package/pulse/components/ui/calendar.tsx +0 -220
  135. package/pulse/components/ui/card.tsx +0 -92
  136. package/pulse/components/ui/command.tsx +0 -158
  137. package/pulse/components/ui/dialog.tsx +0 -158
  138. package/pulse/components/ui/input.tsx +0 -21
  139. package/pulse/components/ui/popover.tsx +0 -89
  140. package/pulse/components/ui/progress.tsx +0 -31
  141. package/pulse/components/ui/select.tsx +0 -190
  142. package/pulse/components/ui/separator.tsx +0 -28
  143. package/pulse/components/ui/sheet.tsx +0 -143
  144. package/pulse/components/ui/skeleton.tsx +0 -13
  145. package/pulse/components/ui/table.tsx +0 -116
  146. package/pulse/components/ui/tabs.tsx +0 -91
  147. package/pulse/components/ui/tooltip.tsx +0 -57
  148. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  149. package/pulse/components.json +0 -23
  150. package/pulse/eslint.config.mjs +0 -18
  151. package/pulse/lib/bahulam-paths.ts +0 -23
  152. package/pulse/lib/claude-reader.ts +0 -592
  153. package/pulse/lib/decode.ts +0 -129
  154. package/pulse/lib/pricing.ts +0 -102
  155. package/pulse/lib/replay-parser.ts +0 -165
  156. package/pulse/lib/tool-categories.ts +0 -140
  157. package/pulse/lib/utils.ts +0 -6
  158. package/pulse/next-env.d.ts +0 -6
  159. package/pulse/next.config.ts +0 -16
  160. package/pulse/package.json +0 -45
  161. package/pulse/postcss.config.mjs +0 -7
  162. package/pulse/public/activity.png +0 -0
  163. package/pulse/public/cc-lens.png +0 -0
  164. package/pulse/public/command-k.png +0 -0
  165. package/pulse/public/costs.png +0 -0
  166. package/pulse/public/dashboard-dark.png +0 -0
  167. package/pulse/public/dashboard-white.png +0 -0
  168. package/pulse/public/export.png +0 -0
  169. package/pulse/public/file.svg +0 -1
  170. package/pulse/public/globe.svg +0 -1
  171. package/pulse/public/next.svg +0 -1
  172. package/pulse/public/projects.png +0 -0
  173. package/pulse/public/session-chat.png +0 -0
  174. package/pulse/public/todos.png +0 -0
  175. package/pulse/public/tools.png +0 -0
  176. package/pulse/public/vercel.svg +0 -1
  177. package/pulse/public/window.svg +0 -1
  178. package/pulse/tsconfig.json +0 -34
  179. package/pulse/types/claude.ts +0 -294
package/LICENSE CHANGED
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright 2026 axplusb.tech
189
+ Copyright 2026 Bahulam
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
package/NOTICE CHANGED
@@ -1,8 +1,8 @@
1
1
  Bahulam Code
2
- Copyright 2026 axplusb.tech
2
+ Copyright 2026 Bahulam
3
3
 
4
- This product includes software developed by axplusb.tech
5
- (https://axplusb.tech).
4
+ This product includes software developed by Bahulam
5
+ (https://bahulam.ai).
6
6
 
7
7
  ------------------------------------------------------------------------
8
8
  Upstream and third-party attribution
@@ -11,12 +11,12 @@ Upstream and third-party attribution
11
11
  The CLI runtime derives, in part, from the open-source open-claude-code
12
12
  project. Original work bears its own copyright and license terms; those
13
13
  files remain governed by their upstream licenses, and any modifications
14
- to them by axplusb.tech are additionally licensed under Apache License,
14
+ to them by Bahulam are additionally licensed under Apache License,
15
15
  Version 2.0 as declared in the LICENSE file at the root of this project.
16
16
 
17
17
  The Bahulam runtime bundle (@bahulam/runtime-<platform>-<arch>) ships
18
18
  with a PyArmor-obfuscated Python payload that is proprietary to
19
- axplusb.tech and is NOT covered by the Apache License. It is distributed
19
+ Bahulam and is NOT covered by the Apache License. It is distributed
20
20
  under a separate end-user license embedded in the runtime as a signed
21
21
  license JWT. Use of the runtime requires accepting that license.
22
22
 
@@ -30,7 +30,7 @@ Trademarks
30
30
  ------------------------------------------------------------------------
31
31
 
32
32
  "Bahulam", the Bahulam wordmark, the ∞ mark, and "Orca" are trademarks
33
- of axplusb.tech. The Apache License, Section 6, does not grant permission
33
+ of Bahulam. The Apache License, Section 6, does not grant permission
34
34
  to use these names, marks, or logos, except as required to describe the
35
35
  origin of the software or to reproduce the content of this NOTICE file.
36
36
 
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @bahulamai/code
1
+ # @bahulam/code
2
2
 
3
3
  **Bahulam Code** is Bahulam's AI coding agent for terminal-first software work.
4
4
  It inspects a repo, plans changes, runs tools, asks for human approval, resumes
@@ -19,13 +19,13 @@ ship with are design decisions — ones we chose not to make.
19
19
  ## Install
20
20
 
21
21
  ```bash
22
- npm install -g @bahulamai/code@latest
22
+ npm install -g @bahulam/code@latest
23
23
  ```
24
24
 
25
25
  Run without global install:
26
26
 
27
27
  ```bash
28
- npx @bahulamai/code@latest
28
+ npx @bahulam/code@latest
29
29
  ```
30
30
 
31
31
  ## Start the CLI
@@ -59,19 +59,13 @@ bahulam-code --help Full command reference
59
59
  Inside the REPL, type `/help` for slash commands (models, cache, approvals,
60
60
  skills, workflows, session tools).
61
61
 
62
- ## 2.6.1 Highlights
62
+ ## 0.1.7 Highlights
63
63
 
64
- - Published under both `@bahulam/code` and `@bahulamai/code` scopes.
65
- - Full removal of legacy `b0` references from CLI help, error messages,
66
- analytics headers, internal comments, and Pulse dashboard.
67
-
68
- ## 2.6.0 Highlights
69
-
70
- - Rebrand: `@bahulamai/b0` → `@bahulamai/code`; binary `b0` → `bahulam-code`.
71
- - Redesigned fixed input dock — labeled "Bahulam Code" identity, colored
72
- border, horizontal and vertical margins, meta row showing project, git
73
- branch, turn count, and token usage.
74
- - Extended session-context meta line under the input dock.
64
+ - Published as `@bahulam/code`.
65
+ - Local browser workspaces through `bahulam workspace open`.
66
+ - Image analysis and image generation tools for agent workflows.
67
+ - Shared project-aware lint resolver for TS/TSX, JS/JSX, MDX, Python, Go, and Rust.
68
+ - Full shell output is preserved for the agent while terminal cards stay compact.
75
69
 
76
70
  ## Development
77
71
 
@@ -85,9 +79,8 @@ See [RELEASE.md](./RELEASE.md) for the merge, PR, and npm publish checklist.
85
79
  ## Links
86
80
 
87
81
  - Website: https://bahulam.ai
88
- - Company: https://axplusb.tech
89
82
  - Repository: https://github.com/raviakasapu/codekepler-npm
90
83
 
91
84
  ## License
92
85
 
93
- MIT
86
+ Apache-2.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bahulam/code",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Bahulam Code \u2014 abundance, in your terminal. CLI-first, reliability-first, sub-agents, 65.6% SWE-bench Verified.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,14 +10,13 @@
10
10
  },
11
11
  "files": [
12
12
  "src/",
13
- "pulse/",
14
13
  "README.md",
15
14
  "LICENSE",
16
15
  "NOTICE"
17
16
  ],
18
17
  "scripts": {
19
18
  "start": "node src/terminal/main.mjs",
20
- "test": "BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-backend-url.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-agent-history.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-sse-client.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-tool-executor.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-project-artifacts.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-work-scope.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-skills.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-callback.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-rate-limit-display.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-preflight.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-formatter.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-terminal-rendering.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-render-queue.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-input-dock.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-live-steering-client.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-slash-commands.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-approval.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-approval-log.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-kepler-contract.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-session-manager.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-safety.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-jsonl-writer.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-analytics.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-stagnation.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-attachments.mjs",
19
+ "test": "BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-backend-url.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-agent-history.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-sse-client.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-tool-executor.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-generate-image.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-analyze-image.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-project-artifacts.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-work-scope.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-lint-resolver.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-local-service.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-skills.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-callback.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-rate-limit-display.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-preflight.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-formatter.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-terminal-rendering.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-render-queue.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-input-dock.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-live-steering-client.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-slash-commands.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-approval.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-approval-log.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-kepler-contract.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-session-manager.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-safety.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-jsonl-writer.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-analytics.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-stagnation.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-attachments.mjs",
21
20
  "sync-catalog": "node scripts/sync-catalog.mjs",
22
21
  "prepublishOnly": "node scripts/sync-catalog.mjs"
23
22
  },
@@ -38,20 +37,21 @@
38
37
  "deepseek",
39
38
  "claude",
40
39
  "gpt",
41
- "gemini",
42
- "axplusb"
40
+ "gemini"
43
41
  ],
44
42
  "license": "Apache-2.0",
45
- "author": "axplusb.tech <hello@axplusb.tech>",
43
+ "author": "Bahulam <hello@bahulam.ai>",
46
44
  "homepage": "https://bahulam.ai/code",
47
45
  "repository": {
48
46
  "type": "git",
49
47
  "url": "git+https://github.com/raviakasapu/codekepler-npm.git"
50
48
  },
51
49
  "dependencies": {
50
+ "mermaid": "^11.17.2",
51
+ "monaco-editor": "^0.56.0",
52
52
  "pdf-parse": "^1.1.1",
53
53
  "tree-sitter-wasms": "^0.1.13",
54
- "web-tree-sitter": "^0.26.9"
55
- },
56
- "optionalDependencies": {}
54
+ "web-tree-sitter": "^0.26.9",
55
+ "xlsx": "^0.18.5"
56
+ }
57
57
  }
@@ -2,7 +2,7 @@
2
2
  * Agent Loader — loads custom agent definitions from .bahulam/agents/
3
3
  *
4
4
  * Supports two formats:
5
- * - YAML: .bahulam/agents/*.yaml / *.yml (native Kepler SubAgent config)
5
+ * - YAML: .bahulam/agents/*.yaml / *.yml (native Bahulam SubAgent config)
6
6
  * - JSON: .bahulam/agents/*.json
7
7
  * - Markdown with YAML frontmatter: .bahulam/agents/*.md
8
8
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Workflow Loader — loads single-agent workflow YAML files from .bahulam/workflows/
3
3
  *
4
- * Workflow YAML files follow the same structure as Kepler SubAgent YAML:
4
+ * Workflow YAML files follow the same structure as Bahulam SubAgent YAML:
5
5
  * metadata: { id, name, description, icon, tags }
6
6
  * agent: { system_prompt, model, max_iterations }
7
7
  * tools: { include: [...], exclude: [...] }
@@ -119,4 +119,4 @@ export function workflowToTemplatePayload(agent, sourcePath) {
119
119
  params,
120
120
  channel: raw.channel || 'server',
121
121
  };
122
- }
122
+ }
@@ -206,9 +206,10 @@ export class TarangAuth {
206
206
  process.stderr.write(` Gateway: ${DIM}${creds.gatewayType}${RESET}\n`);
207
207
 
208
208
  const models = creds.models || {};
209
- if (models.orchestrator || models.reasoning || models.local) {
209
+ const planningModel = models.planning || models.orchestrator;
210
+ if (planningModel || models.reasoning || models.local) {
210
211
  process.stderr.write(`\n${BOLD} Models${RESET}\n`);
211
- if (models.orchestrator) process.stderr.write(` Orchestrator: ${DIM}${models.orchestrator}${RESET}\n`);
212
+ if (planningModel) process.stderr.write(` Planning: ${DIM}${planningModel}${RESET}\n`);
212
213
  if (models.reasoning) process.stderr.write(` Coding: ${DIM}${models.reasoning}${RESET}\n`);
213
214
  if (models.local) process.stderr.write(` Local: ${DIM}${models.local}${RESET}\n`);
214
215
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "schema": "bahulam.models_catalog.snapshot.v1",
2
+ "schema": "bahulam.models_catalog.snapshot.v2",
3
3
  "source": "delivered_static_catalog",
4
4
  "generated_at": "2026-08-23T20:06:08.903480Z",
5
5
  "deactivate_missing": false,
@@ -10,7 +10,7 @@
10
10
  {"value": "us.anthropic.claude-opus-4-6-v1", "label": "Claude Opus 4.6 (Bedrock)", "provider": "anthropic", "inputCost": 15, "outputCost": 75, "context": 200000, "maxOutput": 32000, "supportsTools": true, "supportsReasoning": true, "harnessValidated": false},
11
11
  {"value": "us.anthropic.claude-sonnet-4-6", "label": "Claude Sonnet 4.6 (Bedrock)", "provider": "anthropic", "inputCost": 3, "outputCost": 15, "context": 200000, "maxOutput": 64000, "supportsTools": true, "supportsReasoning": true, "harnessValidated": false},
12
12
  {"value": "us.anthropic.claude-haiku-4-5-20251001-v1:0", "label": "Claude Haiku 4.5 (Bedrock)", "provider": "anthropic", "inputCost": 0.8, "outputCost": 4, "context": 200000, "maxOutput": 8192, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false},
13
- {"value": "google/gemini-2.5-pro", "label": "Gemini 2.5 Pro", "provider": "google", "inputCost": 1.25, "outputCost": 10, "context": 1048576, "maxOutput": 65536, "supportsTools": true, "supportsReasoning": true, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
13
+ {"value": "google/gemini-2.5-pro", "label": "Gemini 2.5 Pro", "provider": "google", "category": "image", "inputCost": 1.25, "outputCost": 10, "context": 1048576, "maxOutput": 65536, "supportsTools": true, "supportsReasoning": true, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
14
14
  {"value": "google/gemini-2.5-flash", "label": "Gemini 2.5 Flash", "provider": "google", "inputCost": 0.15, "outputCost": 0.6, "context": 1048576, "maxOutput": 65535, "supportsTools": true, "supportsReasoning": true, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
15
15
  {"value": "google/gemini-2.5-flash-lite", "label": "Gemini 2.5 Flash Lite", "provider": "google", "inputCost": 0.075, "outputCost": 0.3, "context": 1048576, "maxOutput": 65535, "supportsTools": true, "supportsReasoning": false, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
16
16
  {"value": "openai/gpt-4.1", "label": "GPT-4.1", "provider": "openai", "inputCost": 2, "outputCost": 8, "context": 1047576, "maxOutput": 32768, "supportsTools": true, "supportsReasoning": false, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
@@ -19,6 +19,7 @@
19
19
  {"value": "openai/codex-mini", "label": "Codex Mini", "provider": "openai", "inputCost": 1.5, "outputCost": 6, "context": 200000, "maxOutput": 100000, "supportsTools": true, "supportsReasoning": true, "harnessValidated": false},
20
20
  {"value": "deepseek/deepseek-v4-flash", "label": "DeepSeek V4 Flash", "provider": "deepseek", "inputCost": 0.098, "outputCost": 0.197, "context": 1048576, "maxOutput": 384000, "supportsTools": true, "supportsReasoning": true, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
21
21
  {"value": "deepseek/deepseek-v4-flash-0731", "label": "DeepSeek V4 Flash (0731)", "provider": "deepseek", "inputCost": 0.098, "outputCost": 0.197, "context": 1310720, "maxOutput": 384000, "supportsTools": true, "supportsReasoning": true, "harnessValidated": true, "cacheProfile": "{\"type\": \"prefix_hash\", \"provider_hint\": \"DeepSeek\"}", "platformAccessTier": ["pro", "tier_49", "tier_99"]},
22
+ {"value": "deepseek/deepseek-v4-flash-vision-exp", "label": "DeepSeek V4 Flash Vision Exp", "provider": "deepseek", "category": "image", "inputCost": 0.22, "outputCost": 0.66, "context": 1048576, "maxOutput": 384000, "supportsTools": true, "supportsReasoning": true, "harnessValidated": true, "platformAccessTier": ["free", "pro", "tier_49", "tier_99"]},
22
23
  {"value": "deepseek/deepseek-v4-pro", "label": "DeepSeek V4 Pro", "provider": "deepseek", "inputCost": 0.435, "outputCost": 0.87, "context": 1048576, "maxOutput": 384000, "supportsTools": true, "supportsReasoning": true, "harnessValidated": true, "cacheProfile": "{\"type\": \"prefix_hash\", \"provider_hint\": \"DeepSeek\"}", "platformAccessTier": ["pro", "tier_49", "tier_99"]},
23
24
  {"value": "deepseek/deepseek-chat-v3-0324", "label": "DeepSeek V3", "provider": "deepseek", "inputCost": 0.27, "outputCost": 1.1, "context": 163840, "maxOutput": 163840, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false},
24
25
  {"value": "deepseek/deepseek-r1-0528", "label": "DeepSeek R1 0528", "provider": "deepseek", "inputCost": 0.8, "outputCost": 2.4, "context": 163840, "maxOutput": 32768, "supportsTools": true, "supportsReasoning": true, "harnessValidated": false},
@@ -93,7 +94,7 @@
93
94
  {"value": "google/gemini-2.5-pro-preview", "label": "Google: Gemini 2.5 Pro Preview 06-05", "provider": "google", "inputCost": 1.25, "outputCost": 10.0, "context": 1048576, "maxOutput": 65536, "supportsTools": true, "supportsReasoning": true, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
94
95
  {"value": "google/gemini-2.5-pro-preview-05-06", "label": "Google: Gemini 2.5 Pro Preview 05-06", "provider": "google", "inputCost": 1.25, "outputCost": 10.0, "context": 1048576, "maxOutput": 65535, "supportsTools": true, "supportsReasoning": true, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
95
96
  {"value": "google/gemini-3-flash-preview", "label": "Google: Gemini 3 Flash Preview", "provider": "google", "inputCost": 0.5, "outputCost": 3.0, "context": 1048576, "maxOutput": 65536, "supportsTools": true, "supportsReasoning": true, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
96
- {"value": "google/gemini-3-pro-image", "label": "Google: Nano Banana Pro (Gemini 3 Pro Image)", "provider": "google", "inputCost": 2.0, "outputCost": 12.0, "context": 131072, "maxOutput": 32768, "supportsTools": true, "supportsReasoning": true, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
97
+ {"value": "google/gemini-3-pro-image", "label": "Google: Nano Banana Pro (Gemini 3 Pro Image)", "provider": "google", "category": "image", "inputCost": 2.0, "outputCost": 12.0, "context": 131072, "maxOutput": 32768, "supportsTools": true, "supportsReasoning": true, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
97
98
  {"value": "google/gemini-3.1-flash-lite", "label": "Google: Gemini 3.1 Flash Lite", "provider": "google", "inputCost": 0.25, "outputCost": 1.5, "context": 1048576, "maxOutput": 65536, "supportsTools": true, "supportsReasoning": true, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
98
99
  {"value": "google/gemini-3.1-flash-lite-preview", "label": "Google: Gemini 3.1 Flash Lite Preview", "provider": "google", "inputCost": 0.25, "outputCost": 1.5, "context": 1048576, "maxOutput": 65536, "supportsTools": true, "supportsReasoning": true, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
99
100
  {"value": "google/gemini-3.1-pro-preview", "label": "Google: Gemini 3.1 Pro Preview", "provider": "google", "inputCost": 2.0, "outputCost": 12.0, "context": 1048576, "maxOutput": 65536, "supportsTools": true, "supportsReasoning": true, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
@@ -31,10 +31,12 @@ let _cache = null;
31
31
  function normalizeSnapshotRow(row) {
32
32
  const id = row.value || row.id;
33
33
  if (!id) return null;
34
+ const category = row.category || row.modelCategory || row.model_category || 'text';
34
35
  return {
35
36
  id,
36
37
  provider: row.provider || (id.includes('/') ? id.split('/', 1)[0] : 'unknown'),
37
38
  label: row.label || id,
39
+ category,
38
40
  input_cost_usd_per_m: row.inputCost ?? row.input_cost_usd_per_m ?? null,
39
41
  output_cost_usd_per_m: row.outputCost ?? row.output_cost_usd_per_m ?? null,
40
42
  context_length: row.context ?? row.context_length ?? null,
@@ -62,7 +62,8 @@ export const TEXT_MIMES = new Set([
62
62
  'text/tab-separated-values',
63
63
  ]);
64
64
  export const PDF_MIMES = new Set(['application/pdf']);
65
- export const DOCUMENT_MIMES = new Set([...TEXT_MIMES, ...PDF_MIMES]);
65
+ export const NOTEBOOK_MIMES = new Set(['application/x-ipynb+json']);
66
+ export const DOCUMENT_MIMES = new Set([...TEXT_MIMES, ...PDF_MIMES, ...NOTEBOOK_MIMES]);
66
67
 
67
68
  // Wider set the CLI already supports at the `read_attachment` tool layer.
68
69
  // Chunking works fine on any UTF-8 text — retrieval quality on structured
@@ -72,17 +73,26 @@ export const TEXT_LIKE_MIMES = new Set([
72
73
  'application/json',
73
74
  'application/x-yaml',
74
75
  'application/toml',
76
+ 'application/xml',
77
+ 'application/sql',
75
78
  'text/yaml',
76
79
  'text/html',
80
+ 'text/xml',
77
81
  'text/x-log',
78
82
  'text/x-rst',
79
83
  'text/x-restructuredtext',
84
+ 'text/x-sql',
85
+ 'text/x-sh',
86
+ 'text/x-ini',
87
+ 'text/x-env',
88
+ 'text/x-dockerfile',
80
89
  ]);
81
90
 
82
91
  // Extension → mime map. Kept small and explicit — Python's
83
92
  // mimetypes.guess_type varies by OS registry; this table is stable.
84
93
  const EXT_TO_MIME = new Map([
85
94
  ['.txt', 'text/plain'],
95
+ ['.text', 'text/plain'],
86
96
  ['.log', 'text/x-log'],
87
97
  ['.md', 'text/markdown'],
88
98
  ['.markdown', 'text/markdown'],
@@ -90,15 +100,114 @@ const EXT_TO_MIME = new Map([
90
100
  ['.csv', 'text/csv'],
91
101
  ['.tsv', 'text/tab-separated-values'],
92
102
  ['.json', 'application/json'],
103
+ ['.ipynb', 'application/x-ipynb+json'],
93
104
  ['.yaml', 'application/x-yaml'],
94
105
  ['.yml', 'application/x-yaml'],
95
106
  ['.toml', 'application/toml'],
96
107
  ['.html', 'text/html'],
97
108
  ['.htm', 'text/html'],
109
+ ['.xml', 'application/xml'],
98
110
  ['.rst', 'text/x-rst'],
99
111
  ['.pdf', 'application/pdf'],
112
+ // Config + shell + database extensions users actually paste in as
113
+ // attachments (Supabase seeds, .env for triage, Dockerfile review, …).
114
+ ['.sql', 'text/x-sql'],
115
+ ['.env', 'text/x-env'],
116
+ ['.ini', 'text/x-ini'],
117
+ ['.conf', 'text/plain'],
118
+ ['.cfg', 'text/plain'],
119
+ ['.properties', 'text/plain'],
120
+ ['.editorconfig', 'text/plain'],
121
+ ['.gitignore', 'text/plain'],
122
+ ['.gitattributes', 'text/plain'],
123
+ ['.dockerignore', 'text/plain'],
124
+ ['.dockerfile', 'text/x-dockerfile'],
125
+ ['.sh', 'text/x-sh'],
126
+ ['.bash', 'text/x-sh'],
127
+ ['.zsh', 'text/x-sh'],
100
128
  ]);
101
129
 
130
+ function sourceToText(value) {
131
+ if (Array.isArray(value)) return value.map(part => String(part ?? '')).join('');
132
+ if (typeof value === 'string') return value;
133
+ return '';
134
+ }
135
+
136
+ function outputText(output) {
137
+ if (!output || typeof output !== 'object') return '';
138
+ if (output.output_type === 'error') {
139
+ const traceback = Array.isArray(output.traceback)
140
+ ? output.traceback.map(line => String(line ?? '')).join('\n')
141
+ : '';
142
+ const header = [output.ename, output.evalue].filter(Boolean).join(': ');
143
+ return [header, traceback].filter(Boolean).join('\n');
144
+ }
145
+ if (output.text) return sourceToText(output.text);
146
+ const data = output.data && typeof output.data === 'object' ? output.data : null;
147
+ if (data?.['text/plain']) return sourceToText(data['text/plain']);
148
+ if (data?.['text/markdown']) return sourceToText(data['text/markdown']);
149
+ if (data?.['text/html']) return sourceToText(data['text/html']).replace(/<[^>]*>/g, ' ');
150
+ return '';
151
+ }
152
+
153
+ function notebookLanguage(notebook) {
154
+ const languageInfo = notebook?.metadata?.language_info;
155
+ const kernelspec = notebook?.metadata?.kernelspec;
156
+ return String(languageInfo?.name || kernelspec?.language || '').trim() || 'python';
157
+ }
158
+
159
+ export function formatNotebookText(rawNotebook, { maxOutputChars = 12_000 } = {}) {
160
+ let notebook;
161
+ try {
162
+ notebook = typeof rawNotebook === 'string'
163
+ ? JSON.parse(rawNotebook)
164
+ : JSON.parse(Buffer.from(rawNotebook || '').toString('utf8'));
165
+ } catch {
166
+ return '';
167
+ }
168
+
169
+ const cells = Array.isArray(notebook?.cells) ? notebook.cells : [];
170
+ if (!cells.length) return '';
171
+
172
+ const language = notebookLanguage(notebook);
173
+ const parts = [
174
+ `Jupyter notebook (${cells.length} cell${cells.length === 1 ? '' : 's'}, language=${language})`,
175
+ ];
176
+
177
+ cells.forEach((cell, index) => {
178
+ const type = String(cell?.cell_type || 'raw').toLowerCase();
179
+ const execution = cell?.execution_count != null ? ` execution_count=${cell.execution_count}` : '';
180
+ const source = sourceToText(cell?.source).trimEnd();
181
+ parts.push('');
182
+ parts.push(`Cell ${index + 1} [${type}${execution}]`);
183
+ if (type === 'code') {
184
+ parts.push(`\`\`\`${language}`);
185
+ parts.push(source);
186
+ parts.push('```');
187
+ } else {
188
+ parts.push(source || '(empty)');
189
+ }
190
+
191
+ const outputs = Array.isArray(cell?.outputs) ? cell.outputs : [];
192
+ const renderedOutputs = outputs
193
+ .map(outputText)
194
+ .map(text => text.trimEnd())
195
+ .filter(Boolean);
196
+ if (renderedOutputs.length) {
197
+ let output = renderedOutputs.join('\n\n');
198
+ if (output.length > maxOutputChars) {
199
+ output = `${output.slice(0, maxOutputChars)}\n... [output truncated]`;
200
+ }
201
+ parts.push('Output:');
202
+ parts.push('```text');
203
+ parts.push(output);
204
+ parts.push('```');
205
+ }
206
+ });
207
+
208
+ return parts.join('\n').trim();
209
+ }
210
+
102
211
  /**
103
212
  * Best-effort mime for a local file (extension-only, no magic-byte sniff).
104
213
  * Files with no matching extension return 'application/octet-stream',
@@ -200,6 +309,16 @@ export async function extractFromBytes(buffer, mime, opts = {}) {
200
309
  }));
201
310
  }
202
311
 
312
+ if (NOTEBOOK_MIMES.has(normalizedMime)) {
313
+ const text = formatNotebookText(buffer);
314
+ return chunkText(text).map(c => ({
315
+ page: null,
316
+ chunk_no: c.chunk_no,
317
+ text: c.text,
318
+ tokens: c.tokens,
319
+ }));
320
+ }
321
+
203
322
  if (PDF_MIMES.has(normalizedMime)) {
204
323
  const pages = await extractPdfPages(buffer);
205
324
  const out = [];
@@ -217,6 +336,32 @@ export async function extractFromBytes(buffer, mime, opts = {}) {
217
336
  return [];
218
337
  }
219
338
 
339
+ // Sniff whether a buffer is UTF-8 text vs binary. Cheap heuristic used
340
+ // only as a fallback when the file extension didn't map to a known text
341
+ // mime — the goal is to let obscure but genuinely-textual files (.sql,
342
+ // .env, Dockerfile, .terraformrc, whatever) through instead of dropping
343
+ // them at the mime gate.
344
+ //
345
+ // Rules:
346
+ // * NUL byte in the first probe window → binary
347
+ // * Not decodable as strict UTF-8 → binary
348
+ // * Otherwise → text
349
+ //
350
+ // Probe window bounded to 8KB — enough to catch a binary header on
351
+ // large files, small enough to be a no-op on the hot path.
352
+ const _TEXT_PROBE_BYTES = 8192;
353
+ function looksLikeText(buffer) {
354
+ if (!buffer || buffer.length === 0) return false;
355
+ const probe = buffer.length > _TEXT_PROBE_BYTES ? buffer.subarray(0, _TEXT_PROBE_BYTES) : buffer;
356
+ if (probe.includes(0x00)) return false;
357
+ try {
358
+ new TextDecoder('utf-8', { fatal: true }).decode(probe);
359
+ return true;
360
+ } catch {
361
+ return false;
362
+ }
363
+ }
364
+
220
365
  /**
221
366
  * Read a local file and chunk it. Returns { mime, chunks }; both empty
222
367
  * on unresolvable path or unsupported mime — same contract as Python's
@@ -237,12 +382,11 @@ export async function extractFromPath(absPath, opts = {}) {
237
382
  }
238
383
  if (!stat.isFile()) return { mime: '', chunks: [] };
239
384
 
240
- const mime = guessMime(absPath);
385
+ let mime = guessMime(absPath);
241
386
  const textMimes = opts.textMimes || TEXT_LIKE_MIMES;
242
- if (!textMimes.has(mime) && !PDF_MIMES.has(mime)) {
243
- return { mime, chunks: [] };
244
- }
245
387
 
388
+ // Read the buffer up front so the text-sniff fallback can see the
389
+ // actual bytes when the extension didn't map to a known mime.
246
390
  let buffer;
247
391
  try {
248
392
  buffer = fs.readFileSync(absPath);
@@ -250,6 +394,19 @@ export async function extractFromPath(absPath, opts = {}) {
250
394
  return { mime, chunks: [] };
251
395
  }
252
396
 
397
+ // Fallback for unknown extensions: if guessMime returned octet-stream
398
+ // (or any mime not in our allow-list), sniff the buffer. If the bytes
399
+ // look like valid UTF-8 text with no NULs in the first probe window,
400
+ // treat as text/plain. This unblocks .sql, .env, Dockerfile, and any
401
+ // other text file we haven't explicitly listed.
402
+ if (!textMimes.has(mime) && !PDF_MIMES.has(mime) && !NOTEBOOK_MIMES.has(mime)) {
403
+ if (looksLikeText(buffer)) {
404
+ mime = 'text/plain';
405
+ } else {
406
+ return { mime, chunks: [] };
407
+ }
408
+ }
409
+
253
410
  const chunks = await extractFromBytes(buffer, mime, opts);
254
411
  return { mime, chunks };
255
412
  }
@@ -3,6 +3,7 @@ import * as path from 'node:path';
3
3
  import * as os from 'node:os';
4
4
  import { createHash, randomUUID } from 'node:crypto';
5
5
  import { execFileSync } from 'node:child_process';
6
+ import { formatNotebookText } from '../context/prose-chunker.mjs';
6
7
 
7
8
  const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
8
9
  // Text-first document formats we can extract client-side and inline into the
@@ -12,7 +13,7 @@ const DOCUMENT_EXTENSIONS = new Set([
12
13
  '.pdf',
13
14
  '.txt', '.md', '.mdx', '.rst',
14
15
  '.csv', '.tsv', '.log',
15
- '.json', '.yaml', '.yml', '.toml', '.ini', '.env',
16
+ '.json', '.ipynb', '.yaml', '.yml', '.toml', '.ini', '.env',
16
17
  '.html', '.htm', '.xml',
17
18
  ]);
18
19
  const DEFAULT_MAX_IMAGE_BYTES = 8 * 1024 * 1024;
@@ -143,6 +144,53 @@ function sniffImage(buffer) {
143
144
  return null;
144
145
  }
145
146
 
147
+ export function detectImageFile(filePath, { cwd = process.cwd() } = {}) {
148
+ const resolved = resolveAttachmentPath(filePath, cwd);
149
+ let stat;
150
+ try {
151
+ stat = fs.statSync(resolved);
152
+ } catch {
153
+ return null;
154
+ }
155
+ if (!stat.isFile() || stat.size <= 0) return null;
156
+
157
+ const ext = path.extname(resolved).toLowerCase();
158
+ const probe = Buffer.alloc(Math.min(64, stat.size));
159
+ let fd = null;
160
+ try {
161
+ fd = fs.openSync(resolved, 'r');
162
+ fs.readSync(fd, probe, 0, probe.length, 0);
163
+ } catch {
164
+ return null;
165
+ } finally {
166
+ if (fd !== null) {
167
+ try { fs.closeSync(fd); } catch {}
168
+ }
169
+ }
170
+
171
+ const sniffed = sniffImage(probe);
172
+ if (sniffed) {
173
+ return {
174
+ path: resolved,
175
+ mime_type: sniffed.mime_type,
176
+ bytes: stat.size,
177
+ ext: sniffed.ext,
178
+ verified: true,
179
+ };
180
+ }
181
+ if (!IMAGE_EXTENSIONS.has(ext)) return null;
182
+ const mimeType = ext === '.jpg' || ext === '.jpeg'
183
+ ? 'image/jpeg'
184
+ : `image/${ext.slice(1)}`;
185
+ return {
186
+ path: resolved,
187
+ mime_type: mimeType,
188
+ bytes: stat.size,
189
+ ext,
190
+ verified: false,
191
+ };
192
+ }
193
+
146
194
  function pngDimensions(buffer) {
147
195
  if (buffer.length < 24) return {};
148
196
  return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
@@ -510,6 +558,17 @@ function extractPlainText(filePath, { maxChars }) {
510
558
  };
511
559
  }
512
560
 
561
+ function extractNotebookText(filePath, { maxChars }) {
562
+ const buffer = fs.readFileSync(filePath);
563
+ const raw = formatNotebookText(buffer);
564
+ const truncated = raw.length > maxChars;
565
+ return {
566
+ text: truncated ? raw.slice(0, maxChars) : raw,
567
+ pages: 0,
568
+ truncated,
569
+ };
570
+ }
571
+
513
572
  /**
514
573
  * Load a single document attachment. Reads locally, extracts text, returns
515
574
  * a public metadata block plus the extracted text. Errors bubble as
@@ -537,9 +596,12 @@ export async function loadDocumentAttachment(filePath, {
537
596
 
538
597
  const ext = path.extname(resolved).toLowerCase();
539
598
  const isPdf = ext === '.pdf';
599
+ const isNotebook = ext === '.ipynb';
540
600
  const { text, pages, truncated } = isPdf
541
601
  ? await extractPdfText(resolved, { maxChars })
542
- : extractPlainText(resolved, { maxChars });
602
+ : isNotebook
603
+ ? extractNotebookText(resolved, { maxChars })
604
+ : extractPlainText(resolved, { maxChars });
543
605
 
544
606
  const buffer = fs.readFileSync(resolved);
545
607
  const sha256 = createHash('sha256').update(buffer).digest('hex');
@@ -549,7 +611,7 @@ export async function loadDocumentAttachment(filePath, {
549
611
  path: resolved,
550
612
  name: path.basename(resolved),
551
613
  ext,
552
- kind: isPdf ? 'pdf' : 'text',
614
+ kind: isPdf ? 'pdf' : isNotebook ? 'notebook' : 'text',
553
615
  bytes: stat.size,
554
616
  chars: text.length,
555
617
  pages,
@@ -165,7 +165,7 @@ function _clearLicenseActivationState() {
165
165
  if (process.platform === 'darwin') {
166
166
  try {
167
167
  spawnSync('security',
168
- ['delete-generic-password', '-s', 'com.axplusb.agent.runtime', '-a', 'af_state'],
168
+ ['delete-generic-password', '-s', 'ai.bahulam.agent.runtime', '-a', 'af_state'],
169
169
  { stdio: 'ignore' });
170
170
  } catch { /* best-effort */ }
171
171
  }
@@ -22,6 +22,7 @@ import { ApprovalManager } from './approval.mjs';
22
22
  // is invisible to attach clients and to paired mobile devices.
23
23
  import { tapSseEvent, registerBroadcaster } from '../daemon/event-tap.mjs';
24
24
  import { startSocketServer } from '../daemon/socket-server.mjs';
25
+ import { startHttpDashboard } from '../daemon/http-dashboard.mjs';
25
26
  import { resolvePending } from '../daemon/approval-store.mjs';
26
27
  import { startRelayBridge } from '../daemon/relay-client.mjs';
27
28
  import { loadRemoteConfig } from '../commands/remote.mjs';
@@ -348,6 +349,18 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
348
349
  });
349
350
  registerBroadcaster(evt => server.broadcastEvent(evt));
350
351
 
352
+ // Optional HTTP browser dashboard (opt-in with BAHULAM_DASHBOARD=1)
353
+ let dashboardUrl = null;
354
+ if (process.env.BAHULAM_DASHBOARD === '1') {
355
+ try {
356
+ const dashboard = await startHttpDashboard({ sessionId: _sid });
357
+ dashboardUrl = dashboard.url;
358
+ process.stderr.write(`[prd-092] dashboard: ${dashboard.url}\n`);
359
+ } catch (err) {
360
+ try { process.stderr.write(`[prd-092] dashboard start: ${err.message}\n`); } catch {}
361
+ }
362
+ }
363
+
351
364
  // Write meta.json + daemon.pid so `bahulam list` /
352
365
  // `bahulam stop` and the mobile session directory
353
366
  // can find this session.
@@ -361,6 +374,7 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
361
374
  sock_path: server.sockPath,
362
375
  opened_at: new Date().toISOString(),
363
376
  headless: true,
377
+ dashboard_url: dashboardUrl,
364
378
  },
365
379
  });
366
380
  fsSync.writeFileSync(