@bahulam/code 2.6.8 → 2.6.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bahulam/code",
3
- "version": "2.6.8",
3
+ "version": "2.6.11",
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": {
@@ -49,7 +49,8 @@
49
49
  "web-tree-sitter": "^0.26.9"
50
50
  },
51
51
  "optionalDependencies": {
52
- "@bahulam/runtime-darwin-arm64": "2.6.8",
53
- "@bahulam/runtime-linux-x64": "2.6.8"
52
+ "@bahulam/runtime-darwin-arm64": "2.6.11",
53
+ "@bahulam/runtime-linux-x64": "2.6.11",
54
+ "@bahulam/runtime-win32-x64": "2.6.11"
54
55
  }
55
56
  }
@@ -90,7 +90,17 @@ function _runtimeRoot() {
90
90
  }
91
91
 
92
92
  function _runtimeBin() {
93
- return path.join(_runtimeRoot(), 'bin', IS_WINDOWS ? 'bahulam-agent.exe' : 'bahulam-agent');
93
+ const binDir = path.join(_runtimeRoot(), 'bin');
94
+ if (IS_WINDOWS) {
95
+ // Prefer whichever launcher ships in the Windows runtime package.
96
+ // Current build emits .cmd; keep .exe as a future option.
97
+ for (const name of ['bahulam-agent.cmd', 'bahulam-agent.exe', 'bahulam-agent.bat']) {
98
+ const p = path.join(binDir, name);
99
+ if (fs.existsSync(p)) return p;
100
+ }
101
+ return path.join(binDir, 'bahulam-agent.cmd');
102
+ }
103
+ return path.join(binDir, 'bahulam-agent');
94
104
  }
95
105
 
96
106
  // Read the framework LICENSE_KEY from a config file so shipped end-users
@@ -195,7 +205,7 @@ export async function ensureRuntimeReady() {
195
205
  if (!fs.existsSync(bin)) {
196
206
  throw new Error(
197
207
  `Bahulam runtime not found at ${bin}. ` +
198
- `Re-run \`npm install -g @bahulamai/code\` to fetch the runtime, ` +
208
+ `Re-run \`npm install -g @bahulam/code\` to fetch the runtime, ` +
199
209
  `or set BAHULAM_RUNTIME_ROOT to a valid install path.`,
200
210
  );
201
211
  }
@@ -221,10 +231,16 @@ export async function ensureRuntimeReady() {
221
231
  env: {
222
232
  ...process.env,
223
233
  BAHULAM_CLI_LOCAL: '1',
234
+ // Framework validates LICENSE_KEY against the license portal on import.
235
+ // The portal is now embedded inside the Bahulam Gateway itself
236
+ // (see codekepler-bahulam-gateway/app/main.py mount at /portal), so
237
+ // the phone-home shares gateway uptime. Framework code (_license.py)
238
+ // reads LICENSE_PORTAL_URL and hits {url}/api/heartbeat.
239
+ LICENSE_PORTAL_URL: process.env.LICENSE_PORTAL_URL || 'https://gateway.bahulam.ai/portal',
224
240
  // Framework requires LICENSE_KEY at import time. Sourced from
225
241
  // ~/.bahulam/license.jwt so end users don't need to export a var.
226
242
  ...(licenseKey ? { LICENSE_KEY: licenseKey } : {}),
227
- // Bahulam CLI token from `bahulam-code login` (saved in
243
+ // Bahulam CLI token from `bahulam login` (saved in
228
244
  // ~/.bahulam/config.json). BahulamGateway reads it from any of
229
245
  // BAHULAM_API_KEY / BAHULAM_CLI_TOKEN / BAHULAM_GATEWAY_API_KEY —
230
246
  // we set both so it works whether the framework prefers one over
@@ -345,7 +361,7 @@ export function isRuntimeInstalled() {
345
361
  return fs.existsSync(_runtimeBin());
346
362
  }
347
363
 
348
- /** Diagnostic snapshot for `bahulam-code doctor`. */
364
+ /** Diagnostic snapshot for `bahulam doctor`. */
349
365
  export function runtimeInfo() {
350
366
  return {
351
367
  installed: isRuntimeInstalled(),
package/src/index.mjs CHANGED
@@ -231,7 +231,7 @@ import { startTerminalRepl } from './terminal/repl.mjs';
231
231
 
232
232
  async function main() {
233
233
  const args = parseArgs(process.argv.slice(2));
234
- if (args.version) { console.log(`@bahulamai/code ${VERSION}`); process.exit(0); }
234
+ if (args.version) { console.log(`@bahulam/code ${VERSION}`); process.exit(0); }
235
235
  if (args.help) { printUsage(); process.exit(0); }
236
236
 
237
237
  const auth = new TarangAuth();
@@ -237,6 +237,7 @@ async function main() {
237
237
  KEPLER_CONFIG_DIR Legacy config directory override
238
238
  KEPLER_RECONNECT_MAX_ELAPSED_MS
239
239
  Max reconnect window for dropped streams
240
+ BAHULAM_TTY_MODE=stable Scrollback-safe transcript if fixed dock redraws leak
240
241
  KEPLER_BLOCK_SEPARATOR Tool/content separator: space, dotted, or off
241
242
 
242
243
  \x1b[2mDocs: https://bahulam.ai\x1b[0m
@@ -64,21 +64,23 @@ export function defaultOptions(tier) {
64
64
  * @param {number} [opts.selected] — index of the highlighted option
65
65
  */
66
66
  export function renderApprovalPrompt({
67
- tool, args = {}, tier, why: _why = '', width,
67
+ tool, args = {}, tier, why = '', width,
68
68
  options, selected = 0,
69
69
  } = {}) {
70
70
  const cols = Math.max(60, Math.min(width || process.stderr.columns || 96, 120));
71
71
  const explicit = requiresExplicitApproval(tier);
72
72
  const accent = explicit ? paint.brand.accent : paint.brand.data;
73
73
  const opts = options || defaultOptions(tier);
74
- const title = `${riskIcon(tier)} ${approvalTitle(tier)} · ${tierLabel(tier)} · ${tool || 'tool'}`;
74
+ const title = `${approvalTitle(tier).toLowerCase()} · ${tierLabel(tier)} · ${tool || 'tool'}`;
75
75
 
76
76
  const lines = [
77
- horizontalRule(title, cols, accent),
77
+ blockHeader(title, accent),
78
78
  ...subjectRows(tool, args, cols, accent),
79
+ ...riskRows(tool, args, tier, accent),
80
+ ...reasonRows(why, cols, accent),
81
+ blockLine(accent),
79
82
  ...decisionRows(opts, selected, accent),
80
- ` ${paint.text.dim('↑↓ move · Enter pick · letter shortcut · Esc cancel')}`,
81
- horizontalRule('', cols, accent),
83
+ blockLine(accent, paint.text.dim('↑↓ move · Enter pick · letter shortcut · Esc cancel')),
82
84
  ];
83
85
 
84
86
  return '\n' + lines.join('\n');
@@ -110,24 +112,6 @@ export function renderInlinePrompt({ tool, args = {}, tier, why = '' } = {}) {
110
112
 
111
113
  export { TIERS };
112
114
 
113
- function riskIcon(tier) {
114
- switch (tier) {
115
- case TIERS.SENSITIVE_READ:
116
- return '🔐';
117
- case TIERS.SHELL_DANGEROUS:
118
- case TIERS.DESTRUCTIVE:
119
- return '🔴';
120
- case TIERS.SHELL_MEDIUM:
121
- return '⚠';
122
- case TIERS.NETWORK:
123
- return '🌐';
124
- case TIERS.LOCAL_EDIT:
125
- return '✎';
126
- default:
127
- return '◈';
128
- }
129
- }
130
-
131
115
  function tierTitle(tier) {
132
116
  switch (tier) {
133
117
  case TIERS.SENSITIVE_READ: return 'SENSITIVE';
@@ -153,40 +137,89 @@ function approvalTitle(tier) {
153
137
  }
154
138
  }
155
139
 
156
- function horizontalRule(title, cols, accent) {
157
- if (!title) return ` ${accent(''.repeat(Math.max(24, cols - 2)))}`;
158
- const label = ` ${paint.bold(accent(title))} `;
159
- const remaining = Math.max(8, cols - 2 - visibleWidth(label) - 2);
160
- return ` ${accent('')} ${label}${accent('─'.repeat(remaining))}`;
140
+ function blockHeader(title, accent) {
141
+ return ` ${accent('╭─')} ${paint.bold(accent(title))}`;
142
+ }
143
+
144
+ function blockLine(accent, text = '') {
145
+ return ` ${accent('│')}${text ? ` ${text}` : ''}`;
161
146
  }
162
147
 
163
148
  function subjectRows(tool, args, cols, accent) {
164
149
  const rows = [];
165
- const available = Math.max(24, cols - 4);
150
+ const available = Math.max(24, cols - 5);
166
151
  const summary = toolDisplaySummary(tool, args, {});
167
152
  const label = `${icon(tool)} ${paint.text.primary(toolDisplayLabel(tool))}`;
168
153
  const details = subjectDetails(tool, args, summary, Math.max(24, available - visibleWidth(label) - 1));
169
154
 
170
155
  if (details.length === 1 && visibleWidth(`${label} ${details[0]}`) <= available) {
171
- rows.push(` ${label} ${paint.text.primary(details[0])}`);
156
+ rows.push(blockLine(accent, `${label} ${paint.text.primary(details[0])}`));
172
157
  return rows;
173
158
  }
174
159
 
175
- rows.push(` ${label}`);
160
+ rows.push(blockLine(accent, label));
176
161
  for (const line of subjectDetails(tool, args, summary, Math.max(24, available - 2))) {
177
- rows.push(` ${paint.text.primary(line)}`);
162
+ rows.push(blockLine(accent, `${paint.text.dim(' ')}${paint.text.primary(line)}`));
178
163
  }
179
164
  return rows;
180
165
  }
181
166
 
182
167
  function decisionRows(opts, selected, accent) {
183
- const rows = [` ${paint.text.dim('Decision')}`];
168
+ const rows = [blockLine(accent, paint.text.dim('Decision'))];
184
169
  for (let i = 0; i < opts.length; i++) {
185
- rows.push(` ${optionToken(opts[i], i === selected, accent)}`);
170
+ rows.push(blockLine(accent, optionToken(opts[i], i === selected, accent)));
186
171
  }
187
172
  return rows;
188
173
  }
189
174
 
175
+ function reasonRows(why, cols, accent) {
176
+ const reason = String(why || '').replace(/\s+/g, ' ').trim();
177
+ if (!reason) return [];
178
+ const label = paint.text.dim('reason ');
179
+ const firstWidth = Math.max(20, cols - 5 - visibleWidth(label));
180
+ const restWidth = Math.max(20, cols - 5 - 7);
181
+ const wrapped = wrapText(reason, firstWidth);
182
+ const rows = [];
183
+ wrapped.forEach((line, index) => {
184
+ if (index === 0) {
185
+ rows.push(blockLine(accent, `${label}${paint.text.primary(line)}`));
186
+ } else {
187
+ for (const rest of wrapText(line, restWidth)) {
188
+ rows.push(blockLine(accent, `${paint.text.dim(' ')}${paint.text.primary(rest)}`));
189
+ }
190
+ }
191
+ });
192
+ return rows;
193
+ }
194
+
195
+ function riskRows(tool, args = {}, tier, accent) {
196
+ const terms = riskTerms(tool, args, tier);
197
+ if (!terms.length) return [];
198
+ return [blockLine(accent, `${paint.text.dim('risk ')}${paint.state.warn(terms.join(', '))}`)];
199
+ }
200
+
201
+ function riskTerms(tool, args = {}, tier) {
202
+ const terms = [];
203
+ if (tool === 'shell') {
204
+ const command = String(args.command || args.cmd || '');
205
+ const patterns = [
206
+ [/rm\s+-[^\n]*r[^\n]*f|rm\s+-[^\n]*f[^\n]*r/i, 'rm -rf'],
207
+ [/\bsudo\b/i, 'sudo'],
208
+ [/\bgit\s+push\b[^\n]*(?:--force|-f)\b/i, 'force push'],
209
+ [/\bnpm\s+publish\b/i, 'publish'],
210
+ [/\b(?:kill|pkill)\b/i, 'process kill'],
211
+ [/\bchmod\s+777\b/i, 'chmod 777'],
212
+ [/\bcurl\b|\bwget\b/i, 'network download'],
213
+ ];
214
+ for (const [re, label] of patterns) {
215
+ if (re.test(command)) terms.push(label);
216
+ }
217
+ }
218
+ if (tier === TIERS.SENSITIVE_READ) terms.push('sensitive read');
219
+ if (tier === TIERS.DESTRUCTIVE) terms.push('destructive');
220
+ return [...new Set(terms)].slice(0, 3);
221
+ }
222
+
190
223
  function optionToken(option, selected, accent) {
191
224
  const cursor = selected ? accent('▸ ') : paint.text.dim(' ');
192
225
  const keyTag = paint.text.dim('[') + (selected ? accent(option.key) : paint.brand.data(option.key)) + paint.text.dim('] ');
package/src/ui/banner.mjs CHANGED
@@ -8,7 +8,6 @@
8
8
  import { execSync } from 'node:child_process';
9
9
  import * as path from 'node:path';
10
10
 
11
- import { icons } from './icons.mjs';
12
11
  import { paint, strip } from './palette.mjs';
13
12
  import { term } from './term.mjs';
14
13
 
@@ -17,56 +16,54 @@ const write = (s) => { try { out.write(s); } catch {} };
17
16
 
18
17
  // ── Brand banner ─────────────────────────────────────────────────────────
19
18
 
20
- // BAHULAM CODE is the product wordmark; the 0xB0 motif is preserved as a
21
- // dev-culture callback beneath it.
22
-
23
- // Renders a gradient purple → magenta → cyan across the given string, one
24
- // codepoint at a time. Falls back to a plain (no-color) string when the
25
- // terminal doesn't support color at all.
26
- function gradientText(text) {
27
- if (!term().color) return text;
28
- const chars = [...text];
29
- const painters = [paint.brand.primary, paint.brand.accent, paint.brand.data];
30
- return chars.map((ch, i) => painters[Math.floor((i / Math.max(1, chars.length)) * painters.length)](ch)).join('');
19
+ function abundanceGlyph() {
20
+ const loop = term().unicode ? '∞' : 'o';
21
+ return [
22
+ ` ${loop}${loop} ${loop}${loop} `,
23
+ `${loop} ${loop} ${loop} ${loop}`,
24
+ `${loop} ${loop} ${loop}`,
25
+ `${loop} ${loop} ${loop} ${loop}`,
26
+ ` ${loop}${loop} ${loop}${loop} `,
27
+ ];
28
+ }
29
+
30
+ function wordmarkLines() {
31
+ return [
32
+ '████ ███ █ █ █ █ █ ███ █ █',
33
+ '█ █ █ █ █ █ █ █ █ █ █ ██ ██',
34
+ '████ █████ █████ █ █ █ █████ █ █ █',
35
+ '█ █ █ █ █ █ █ █ █ █ █ █ █',
36
+ '████ █ █ █ █ ███ █████ █ █ █ █',
37
+ ];
31
38
  }
32
39
 
33
40
  /**
34
- * Render the framed BAHULAM CODE wordmark. Box is 24 cols wide, 3 rows
35
- * tall, and fits any terminal ≥40 cols.
41
+ * Render the branded startup banner.
36
42
  */
37
- function wordmarkFrame() {
43
+ export function renderBanner(version = '') {
38
44
  const dim = paint.text.dim;
39
- const top = dim('╔════════════════════════╗');
40
- const bottom = dim('╚════════════════════════╝');
41
- const inner = dim('');
42
- // ║ BAHULAM CODE ║ — 3-space pad each side around a 18-char wordmark
43
- const glyph = paint.bold(gradientText('BAHULAM CODE'));
44
- const bodyPad = ' '.repeat(5);
45
- const tailPad = ' '.repeat(6);
46
- return [
47
- ` ${top}`,
48
- ` ${inner}${bodyPad}${glyph}${tailPad}${inner}`,
49
- ` ${bottom}`,
50
- ].join('\n');
45
+ const mark = abundanceGlyph();
46
+ const wordmark = wordmarkLines();
47
+ const code = paint.brand.data('code');
48
+ const vTag = version ? `${dim(' · v' + version)}` : '';
49
+ const markWidth = 11;
50
+ const wordmarkIndent = ' '.repeat(2 + markWidth + 2);
51
+ const lines = [''];
52
+
53
+ for (let i = 0; i < wordmark.length; i++) {
54
+ lines.push(` ${paint.brand.accent(mark[i].padEnd(markWidth))} ${paint.bold(paint.brand.primary(wordmark[i]))}`);
55
+ }
56
+
57
+ lines.push(`${wordmarkIndent}${code} ${dim('· abundance in your terminal')}${vTag}`);
58
+ lines.push('');
59
+ return lines.join('\n');
51
60
  }
52
61
 
53
62
  /**
54
- * Print the branded orbital banner.
63
+ * Print the branded startup banner.
55
64
  */
56
65
  export function printBanner(version = '') {
57
- const dim = paint.text.dim;
58
- const orbit = paint.brand.accent(icons.orbit);
59
- const dev = paint.brand.data('0xB0');
60
- const vTag = version ? `${dim(' — ')}${dim('v' + version)}` : '';
61
-
62
- write('\n');
63
- write(` ${orbit} ${dim('· bahulam — abundance')}\n`);
64
- write('\n');
65
- write(`${wordmarkFrame()}\n`);
66
- write('\n');
67
- write(` ${dim('abundance, in your terminal')}\n`);
68
- write(` ${dev}${vTag}\n`);
69
- write('\n');
66
+ write(renderBanner(version));
70
67
  }
71
68
 
72
69
  // ── Project info bar ─────────────────────────────────────────────────────
@@ -29,6 +29,8 @@
29
29
  *
30
30
  * Config:
31
31
  * BAHULAM_INPUT_ROWS_MAX / KEPLER_INPUT_ROWS_MAX 1..12 hard cap on input row growth (default 6)
32
+ * BAHULAM_TTY_MODE=stable / KEPLER_TTY_MODE=stable scrollback-safe transcript, no fixed dock
33
+ * BAHULAM_PLAIN=1 / KEPLER_PLAIN=1 deterministic no-ANSI output for automation
32
34
  * BAHULAM_FIXED_INPUT=0 / KEPLER_FIXED_INPUT=0 disable dock entirely (fallback readline)
33
35
  */
34
36
 
@@ -347,6 +349,7 @@ export function isInputDockMounted() {
347
349
  export function mountInputDock({ inputRowsMax: requestedMax } = {}) {
348
350
  const t = term();
349
351
  if (!t.isTTY || t.plain) return false;
352
+ if (t.ttyMode !== 'rich' || t.fixedInput === false) return false;
350
353
  if (process.env.BAHULAM_FIXED_INPUT === '0' || process.env.KEPLER_FIXED_INPUT === '0') return false;
351
354
  if (mounted) return true;
352
355
 
package/src/ui/term.mjs CHANGED
@@ -33,7 +33,10 @@ function readEnv() {
33
33
  return {
34
34
  NO_COLOR: env.NO_COLOR,
35
35
  FORCE_COLOR: env.FORCE_COLOR,
36
+ BAHULAM_PLAIN: env.BAHULAM_PLAIN,
36
37
  KEPLER_PLAIN: env.KEPLER_PLAIN,
38
+ BAHULAM_TTY_MODE: env.BAHULAM_TTY_MODE,
39
+ KEPLER_TTY_MODE: env.KEPLER_TTY_MODE,
37
40
  COLORTERM: (env.COLORTERM || '').toLowerCase(),
38
41
  TERM: (env.TERM || '').toLowerCase(),
39
42
  TERM_PROGRAM: env.TERM_PROGRAM || '',
@@ -44,6 +47,7 @@ function readEnv() {
44
47
  function detectColorLevel(env, isTTY) {
45
48
  // Hard opt-out (https://no-color.org). Honored even on TTYs.
46
49
  if (env.NO_COLOR !== undefined && env.NO_COLOR !== '') return 'none';
50
+ if (env.BAHULAM_PLAIN === '1') return 'none';
47
51
  if (env.KEPLER_PLAIN === '1') return 'none';
48
52
 
49
53
  // Hard opt-in. FORCE_COLOR=1|2|3 maps to ansi16|ansi256|truecolor.
@@ -79,6 +83,7 @@ function detectColorLevel(env, isTTY) {
79
83
  }
80
84
 
81
85
  function detectUnicode(env) {
86
+ if (env.BAHULAM_PLAIN === '1') return false;
82
87
  if (env.KEPLER_PLAIN === '1') return false;
83
88
  // Most modern terminals on macOS/Linux handle UTF-8.
84
89
  // Windows ConEmu / older terminals are the main holdouts; conservative
@@ -89,10 +94,18 @@ function detectUnicode(env) {
89
94
  return false;
90
95
  }
91
96
 
97
+ function detectTtyMode(env) {
98
+ const raw = String(env.BAHULAM_TTY_MODE || env.KEPLER_TTY_MODE || '').trim().toLowerCase();
99
+ if (env.BAHULAM_PLAIN === '1' || env.KEPLER_PLAIN === '1') return 'plain';
100
+ if (raw === 'stable' || raw === 'simple' || raw === 'static' || raw === 'transcript') return 'stable';
101
+ return 'rich';
102
+ }
103
+
92
104
  function compute() {
93
105
  const env = readEnv();
94
106
  const isTTY = !!(process.stdout && process.stdout.isTTY);
95
107
  const level = detectColorLevel(env, isTTY);
108
+ const ttyMode = detectTtyMode(env);
96
109
  return {
97
110
  isTTY,
98
111
  colorLevel: level, // 'none' | 'ansi16' | 'ansi256' | 'truecolor'
@@ -100,7 +113,9 @@ function compute() {
100
113
  truecolor: level === 'truecolor',
101
114
  ansi256: level === 'ansi256' || level === 'truecolor',
102
115
  unicode: detectUnicode(env),
103
- plain: env.KEPLER_PLAIN === '1',
116
+ ttyMode, // 'rich' | 'stable' | 'plain'
117
+ plain: ttyMode === 'plain',
118
+ fixedInput: ttyMode === 'rich',
104
119
  columns: (process.stdout && process.stdout.columns) || 80,
105
120
  rows: (process.stdout && process.stdout.rows) || 24,
106
121
  ci: !!env.CI,