@bahulam/code 0.1.7 → 0.1.8

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 (47) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/src/auth/{tarang-auth.mjs → bahulam-auth.mjs} +1 -1
  4. package/src/commands/agent.mjs +3 -3
  5. package/src/commands/device.mjs +2 -2
  6. package/src/commands/pair.mjs +2 -2
  7. package/src/commands/remote.mjs +3 -3
  8. package/src/commands/workflow.mjs +5 -5
  9. package/src/config/env.mjs +9 -2
  10. package/src/config/memory-loader.mjs +1 -1
  11. package/src/config/settings.mjs +2 -2
  12. package/src/core/agent-loop.mjs +17 -0
  13. package/src/core/attachments.mjs +1 -1
  14. package/src/core/backend-url.mjs +11 -12
  15. package/src/core/callback-client.mjs +1 -1
  16. package/src/core/headless.mjs +4 -4
  17. package/src/core/jsonl-writer.mjs +15 -15
  18. package/src/core/local-agent.mjs +26 -5
  19. package/src/core/local-store.mjs +1 -1
  20. package/src/core/mode-selector.mjs +1 -1
  21. package/src/core/output-filter.mjs +1 -1
  22. package/src/core/project-artifacts.mjs +3 -3
  23. package/src/core/project-context-loader.mjs +9 -9
  24. package/src/core/settings-sync.mjs +1 -1
  25. package/src/core/stagnation.mjs +61 -1
  26. package/src/core/stream-client.mjs +4 -4
  27. package/src/core/system-prompt.mjs +2 -2
  28. package/src/core/tool-executor.mjs +78 -60
  29. package/src/local-service/agent-relay.mjs +8 -8
  30. package/src/local-service/server.mjs +2 -2
  31. package/src/mcp/client.mjs +4 -4
  32. package/src/onboarding/preflight.mjs +1 -1
  33. package/src/terminal/agents.mjs +2 -2
  34. package/src/terminal/main.mjs +6 -6
  35. package/src/terminal/repl-render.mjs +2 -2
  36. package/src/terminal/repl-resume.mjs +2 -2
  37. package/src/terminal/repl.mjs +17 -17
  38. package/src/terminal/tool-display.mjs +1 -1
  39. package/src/tools/agent.mjs +2 -2
  40. package/src/tools/analyze-image.mjs +2 -2
  41. package/src/tools/generate-image.mjs +4 -4
  42. package/src/tools/project-overview.mjs +12 -12
  43. package/src/ui/formatter.mjs +2 -2
  44. package/src/ui/input-dock.mjs +7 -7
  45. package/src/ui/spinner.mjs +1 -1
  46. package/src/ui/term.mjs +2 -6
  47. package/src/ui/tool-card.mjs +74 -3
@@ -22,7 +22,7 @@ import { execSync as _execSync } from 'node:child_process';
22
22
  import { Writable as _WritableStream } from 'node:stream';
23
23
  import { c, progressBar, spinner, inPlace, renderMarkdown, renderDiff, formatElapsed, formatCost, stripAnsi } from './ansi.mjs';
24
24
  import { calculateCost, formatCostValue, formatTokens, costToCredits, formatCredits } from '../core/pricing.mjs';
25
- import { TarangStreamClient, EVENT_TYPES } from '../core/stream-client.mjs';
25
+ import { BahulamStreamClient, EVENT_TYPES } from '../core/stream-client.mjs';
26
26
  import { AgentHistoryTurnBuilder } from '../core/agent-history.mjs';
27
27
  import { JsonlWriter } from '../core/jsonl-writer.mjs';
28
28
  import { tapSseEvent, registerBroadcaster } from '../daemon/event-tap.mjs';
@@ -53,7 +53,7 @@ import {
53
53
  MODES as V_MODES,
54
54
  } from '../state/verbosity.mjs';
55
55
  import { persistProjectArtifacts } from '../core/project-artifacts.mjs';
56
- import { TarangAuth } from '../auth/tarang-auth.mjs';
56
+ import { BahulamAuth } from '../auth/bahulam-auth.mjs';
57
57
  import { ApprovalManager } from '../core/approval.mjs';
58
58
  import * as telemetry from '../telemetry/index.mjs';
59
59
  import { resolveBackendUrl } from '../core/backend-url.mjs';
@@ -1586,8 +1586,8 @@ function foldSubAgentToolResult(data = {}) {
1586
1586
  fold.entries.push(entry);
1587
1587
  }
1588
1588
  const durationMs = data?.duration_ms ?? (data?.duration_s != null ? data.duration_s * 1000 : null);
1589
- const summary = summarizeResult(tool, data);
1590
1589
  entry.args = entry.args && Object.keys(entry.args).length ? entry.args : args;
1590
+ const summary = summarizeResult(tool, data, entry.args);
1591
1591
  entry.result = data;
1592
1592
  entry.durationMs = durationMs;
1593
1593
  entry.outcome = summary.text || '';
@@ -2358,7 +2358,7 @@ function renderEvent(event) {
2358
2358
  break;
2359
2359
 
2360
2360
  // Live steering ack events (PRD-081 §5.2). renderEvent is called from
2361
- // the SSE loop (see `for await ... jsonlWriter.writeKeplerEvent(event)`
2361
+ // the SSE loop (see `for await ... jsonlWriter.writeBahulamEvent(event)`
2362
2362
  // in the /execute path) so every event already lands in the transcript.
2363
2363
  // Here we only render the user-visible surface — no direct jsonlWriter
2364
2364
  // calls (it's out of scope in this top-level dispatcher anyway).
@@ -3776,7 +3776,7 @@ export async function startTerminalRepl() {
3776
3776
  safeCwd(); // prime the cache in repl-utils.mjs for later recovery
3777
3777
 
3778
3778
  const cliArgs = parseArgs(process.argv.slice(2));
3779
- const auth = new TarangAuth();
3779
+ const auth = new BahulamAuth();
3780
3780
 
3781
3781
  // Projects are registered and indexed on demand through get_project_overview.
3782
3782
  // CheckpointManager records per-file snapshots before edits so /undo works.
@@ -4123,7 +4123,7 @@ export async function startTerminalRepl() {
4123
4123
  && richHistory.summary
4124
4124
  && Number(richHistory.summaryCoveredMessageCount) > Number(richHistory.summaryCheckpointMessageCount || 0)
4125
4125
  ) {
4126
- jsonlWriter.writeKeplerEvent({
4126
+ jsonlWriter.writeBahulamEvent({
4127
4127
  type: 'resume_summary',
4128
4128
  data: {
4129
4129
  session_id: sessionId,
@@ -4138,7 +4138,7 @@ export async function startTerminalRepl() {
4138
4138
  },
4139
4139
  });
4140
4140
  }
4141
- jsonlWriter.writeKeplerEvent({
4141
+ jsonlWriter.writeBahulamEvent({
4142
4142
  type: 'resume_context',
4143
4143
  data: {
4144
4144
  session_id: sessionId,
@@ -4424,7 +4424,7 @@ export async function startTerminalRepl() {
4424
4424
  // Proxy stream: swallows writes when the dock owns the input row so
4425
4425
  // readline's echoes and _refreshLine cursor moves can't fight the dock's
4426
4426
  // absolute cursor placement (PRD-081 §5.1 — cursor race). When the dock
4427
- // is not mounted (plain-mode fallback, KEPLER_FIXED_INPUT=0, non-TTY),
4427
+ // is not mounted (plain-mode fallback, BAHULAM_FIXED_INPUT=0, non-TTY),
4428
4428
  // writes pass through so history navigation / backspace still redraw.
4429
4429
  const readlineOutputProxy = new _WritableStream({
4430
4430
  write(chunk, _enc, cb) {
@@ -4471,7 +4471,7 @@ export async function startTerminalRepl() {
4471
4471
  // 3 rows fits comfortably in the dock's reservation without leaking
4472
4472
  // past the safety row.
4473
4473
  if (isInputDockMounted()) return 3;
4474
- const raw = process.env.KEPLER_PROMPT_BOTTOM_PADDING ?? '5';
4474
+ const raw = process.env.BAHULAM_PROMPT_BOTTOM_PADDING ?? '5';
4475
4475
  const n = Number.parseInt(raw, 10);
4476
4476
  if (!Number.isFinite(n) || n <= 0) return 0;
4477
4477
  return Math.min(8, n);
@@ -4744,14 +4744,14 @@ export async function startTerminalRepl() {
4744
4744
  // 1. If the terminal supports bracketed paste, we hold flushing until we
4745
4745
  // see the ESC[201~ end marker — reliable regardless of paste latency.
4746
4746
  // 2. Otherwise we fall back to a short timer that merges bursts arriving
4747
- // within KEPLER_PASTE_FLUSH_MS.
4747
+ // within BAHULAM_PASTE_FLUSH_MS.
4748
4748
  let _lineInFlight = false;
4749
4749
  const _queuedLines = [];
4750
4750
  let _pasteLines = [];
4751
4751
  let _pasteFlushTimer = null;
4752
4752
 
4753
4753
  function pasteFlushDelayMs() {
4754
- const raw = Number.parseInt(process.env.KEPLER_PASTE_FLUSH_MS || '35', 10);
4754
+ const raw = Number.parseInt(process.env.BAHULAM_PASTE_FLUSH_MS || '35', 10);
4755
4755
  return Number.isFinite(raw) && raw >= 0 ? Math.min(250, raw) : 35;
4756
4756
  }
4757
4757
 
@@ -4897,7 +4897,7 @@ export async function startTerminalRepl() {
4897
4897
  // Create or reuse stream client — sessionId persists across turns.
4898
4898
  // The same client also owns the authenticated vision-analysis preflight.
4899
4899
  if (!streamClient || streamClient.baseUrl !== creds.backendUrl || streamClient.token !== creds.token) {
4900
- streamClient = new TarangStreamClient({
4900
+ streamClient = new BahulamStreamClient({
4901
4901
  baseUrl: creds.backendUrl,
4902
4902
  token: creds.token,
4903
4903
  toolExecutor,
@@ -4921,7 +4921,7 @@ export async function startTerminalRepl() {
4921
4921
  ` ${c.brand('◇')} ${c.dim(`attached ${docPrep.documents.length} document${docPrep.documents.length === 1 ? '' : 's'}:`)} ` +
4922
4922
  `${docPrep.documents.map(documentSummaryLine).join(c.dim(' · '))}\n`
4923
4923
  );
4924
- jsonlWriter.writeKeplerEvent({
4924
+ jsonlWriter.writeBahulamEvent({
4925
4925
  type: 'attachments',
4926
4926
  data: { documents: docPrep.documents.map(publicDocumentMetadata) },
4927
4927
  });
@@ -4937,7 +4937,7 @@ export async function startTerminalRepl() {
4937
4937
  });
4938
4938
  if (prepared.attachments.length) {
4939
4939
  process.stderr.write(` ${c.brand('◇')} ${c.dim(`attached ${prepared.attachments.length} image${prepared.attachments.length === 1 ? '' : 's'}:`)} ${prepared.attachments.map(attachmentSummaryLine).join(c.dim(' · '))}\n`);
4940
- jsonlWriter.writeKeplerEvent({
4940
+ jsonlWriter.writeBahulamEvent({
4941
4941
  type: 'attachments',
4942
4942
  data: { attachments: prepared.attachments.map(publicAttachmentMetadata) },
4943
4943
  });
@@ -4954,7 +4954,7 @@ export async function startTerminalRepl() {
4954
4954
  });
4955
4955
  process.stderr.write(`\r${' '.repeat(80)}\r`);
4956
4956
  process.stderr.write(` ${c.green('✓')} ${c.dim(`Vision analysis completed for ${prepared.attachments.length} image${prepared.attachments.length === 1 ? '' : 's'}`)}${analysis.model ? c.dim(` · ${analysis.model}`) : ''}\n`);
4957
- jsonlWriter.writeKeplerEvent({
4957
+ jsonlWriter.writeBahulamEvent({
4958
4958
  type: 'vision_analysis',
4959
4959
  data: {
4960
4960
  model: analysis.model || '',
@@ -5140,7 +5140,7 @@ export async function startTerminalRepl() {
5140
5140
  // Persist the local record regardless of outcome so the transcript
5141
5141
  // reflects what the user typed. Delivered/queued follow-ups will
5142
5142
  // get their SSE ack events written separately by the event handler.
5143
- jsonlWriter.writeKeplerEvent({
5143
+ jsonlWriter.writeBahulamEvent({
5144
5144
  type: 'user_intervention',
5145
5145
  data: {
5146
5146
  instruction,
@@ -5511,7 +5511,7 @@ export async function startTerminalRepl() {
5511
5511
  _turnIterable = client.execute(input, execContext, session.agentHistory);
5512
5512
  }
5513
5513
  for await (const event of _turnIterable) {
5514
- jsonlWriter.writeKeplerEvent(event);
5514
+ jsonlWriter.writeBahulamEvent(event);
5515
5515
  // . daemon event log. Env-var gated (off by default) —
5516
5516
  // when BAHULAM_DAEMON_EVENTLOG=1, mirror each SSE frame that maps
5517
5517
  // to a first-class type into ~/.bahulam/sessions/<id>/events.jsonl.
@@ -90,7 +90,7 @@ function currentWorkingDirectory() {
90
90
  // handles the display width from here)
91
91
  //
92
92
  // The point: keep the basename + parent visible even when the full path
93
- // is long, so users don't see "Editing /Users/sree/Sites/Tarang-Orca/
93
+ // is long, so users don't see "Editing /Users/sree/Sites/.../
94
94
  // codekepler-ba…" with the actual filename lost off the right edge.
95
95
  function shortPath(filePath, cwd = currentWorkingDirectory()) {
96
96
  const value = String(filePath || '');
@@ -87,10 +87,10 @@ export const AgentTool = {
87
87
  settings: {
88
88
  stream: false,
89
89
  stagnationDetection: !['0', 'false', 'no', 'off'].includes(
90
- (process.env.KEPLER_STAGNATION_DETECTION ?? '0').toLowerCase(),
90
+ (process.env.BAHULAM_STAGNATION_DETECTION ?? '0').toLowerCase(),
91
91
  ),
92
92
  stagnationThreshold: Number.parseInt(
93
- process.env.KEPLER_STAGNATION_THRESHOLD
93
+ process.env.BAHULAM_STAGNATION_THRESHOLD
94
94
  ?? '3',
95
95
  10,
96
96
  ),
@@ -5,7 +5,7 @@
5
5
  * sends the same bounded attachment shape used by the preflight vision path to
6
6
  * the authenticated backend `/api/vision/analyze` endpoint.
7
7
  */
8
- import { TarangAuth } from '../auth/tarang-auth.mjs';
8
+ import { BahulamAuth } from '../auth/bahulam-auth.mjs';
9
9
  import {
10
10
  loadImageAttachment,
11
11
  publicAttachmentMetadata,
@@ -88,7 +88,7 @@ export const AnalyzeImageTool = {
88
88
  };
89
89
  }
90
90
 
91
- const creds = new TarangAuth().loadCredentials();
91
+ const creds = new BahulamAuth().loadCredentials();
92
92
  if (!creds.backendUrl || !creds.token) {
93
93
  return {
94
94
  success: false,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * generate_image — text-to-image generation for Kepler Code.
2
+ * generate_image — text-to-image generation for Bahulam Code.
3
3
  *
4
4
  * Uses the OpenRouter-compatible image API shape (`POST /v1/images`) and saves
5
5
  * generated bytes to local files so the agent can use the assets in code.
@@ -7,7 +7,7 @@
7
7
  import fs from 'node:fs/promises';
8
8
  import path from 'node:path';
9
9
 
10
- import { TarangAuth } from '../auth/tarang-auth.mjs';
10
+ import { BahulamAuth } from '../auth/bahulam-auth.mjs';
11
11
 
12
12
  const DEFAULT_IMAGE_GENERATION_MODEL = 'google/gemini-3-pro-image';
13
13
  const MAX_GENERATED_IMAGE_BYTES = 10 * 1024 * 1024;
@@ -97,7 +97,7 @@ export const GenerateImageTool = {
97
97
  output_path: optionalString(input.output_path),
98
98
  filename: optionalString(input.filename),
99
99
  };
100
- const creds = new TarangAuth().loadCredentials();
100
+ const creds = new BahulamAuth().loadCredentials();
101
101
  const apiKey = gatewayApiKey(creds);
102
102
  let result;
103
103
  if (creds.backendUrl && creds.token) {
@@ -241,7 +241,7 @@ async function callImageApi({ apiKey, model, prompt, n, aspect_ratio, resolution
241
241
  Authorization: `Bearer ${apiKey}`,
242
242
  'Content-Type': 'application/json',
243
243
  'X-Product': process.env.BAHULAM_PRODUCT || 'kepler',
244
- 'X-Bahulam-Client': 'kepler-code-image-generation',
244
+ 'X-Bahulam-Client': 'bahulam-code-image-generation',
245
245
  },
246
246
  body: JSON.stringify(body),
247
247
  });
@@ -322,8 +322,8 @@ function _readIfExists(dir, filename, maxChars = 8000) {
322
322
  } catch { return ''; }
323
323
  }
324
324
 
325
- function _scanSkills(keplerDir) {
326
- const skillsDir = path.join(keplerDir, 'skills');
325
+ function _scanSkills(bahulamDir) {
326
+ const skillsDir = path.join(bahulamDir, 'skills');
327
327
  if (!fs.existsSync(skillsDir)) return [];
328
328
  try {
329
329
  return fs.readdirSync(skillsDir, { withFileTypes: true })
@@ -402,15 +402,15 @@ export class ProjectRegistry {
402
402
  // .bahulam/KEPLER.md, goal/plan/style, skills, AGENTS.md, etc. changes.
403
403
  _attachLiveContext(resource, root) {
404
404
  // Resolver — prefers .bahulam/, falls back to .kepler/ for legacy projects.
405
- const keplerDir = projectConfigDir(root);
405
+ const bahulamDir = projectConfigDir(root);
406
406
  resource.environment = detectEnvironment();
407
- resource.project_context = _readIfExists(keplerDir, 'KEPLER.md', 10000) ||
407
+ resource.project_context = _readIfExists(bahulamDir, 'KEPLER.md', 10000) ||
408
408
  _readIfExists(root, 'KEPLER.md', 10000) ||
409
- _readIfExists(keplerDir, 'project.md', 8000);
410
- resource.style = _readIfExists(keplerDir, 'style.md', 4000);
411
- resource.goal = _readIfExists(keplerDir, 'goal.md', 2000);
412
- resource.plan = _readIfExists(keplerDir, 'plan.md', 6000);
413
- resource.skills_index = _scanSkills(keplerDir);
409
+ _readIfExists(bahulamDir, 'project.md', 8000);
410
+ resource.style = _readIfExists(bahulamDir, 'style.md', 4000);
411
+ resource.goal = _readIfExists(bahulamDir, 'goal.md', 2000);
412
+ resource.plan = _readIfExists(bahulamDir, 'plan.md', 6000);
413
+ resource.skills_index = _scanSkills(bahulamDir);
414
414
 
415
415
  if (!resource.project_context) {
416
416
  for (const name of ['.bahulam.md', 'AGENTS.md', 'CLAUDE.md']) {
@@ -426,7 +426,7 @@ export class ProjectRegistry {
426
426
  throw new Error('get_project_overview requires a project path');
427
427
  }
428
428
 
429
- // LLM sometimes passes shell-escaped paths ("Tarang\ Orca") or paths
429
+ // LLM sometimes passes shell-escaped paths ("Bahulam") or paths
430
430
  // beginning with "~". Normalize defensively so the tool does not bounce
431
431
  // back a "not found" error on a path that's correct apart from quoting.
432
432
  rawPath = normalizePathInput(rawPath);
@@ -600,7 +600,7 @@ export class ProjectRegistry {
600
600
  }
601
601
 
602
602
  // LLM frequently passes shell-quoted paths copied from a terminal,
603
- // e.g. "Tarang\ Orca/src/app/\(kepler\)/page.tsx". Normalize here so
603
+ // e.g. "Bahulam/src/app/\(kepler\)/page.tsx". Normalize here so
604
604
  // every tool benefits, not just get_project_overview.
605
605
  rawPath = normalizePathInput(rawPath);
606
606
 
@@ -633,7 +633,7 @@ export class ProjectRegistry {
633
633
  let containingScratchRoot = containingProject ? null : findScratchRoot(candidate);
634
634
 
635
635
  // Two reasons to try the unescaped variant:
636
- // (1) candidate is outside every project root (literal "Tarang\ Orca"
636
+ // (1) candidate is outside every project root (literal "Bahulam"
637
637
  // does not contain a real project), or
638
638
  // (2) candidate is inside a root but does not exist on disk because
639
639
  // a path segment like "\(kepler\)" only resolves once unescaped.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Formatter — Clean terminal output for Tarang events.
2
+ * Formatter — Clean terminal output for Bahulam events.
3
3
  *
4
4
  * Style reference: Claude Code screenshot
5
5
  * ◐ Cyan spinner + cyan text for working/thinking
@@ -135,7 +135,7 @@ export class EventFormatter {
135
135
  if (!this._lastBlock) return;
136
136
  if (compactSame && this._lastBlock === nextBlock) return;
137
137
 
138
- const mode = String(process.env.KEPLER_BLOCK_SEPARATOR || 'space').toLowerCase();
138
+ const mode = String(process.env.BAHULAM_BLOCK_SEPARATOR || 'space').toLowerCase();
139
139
  if (mode === 'off' || mode === 'none') return;
140
140
  if (mode === 'dotted' || mode === 'dots') {
141
141
  const cols = Math.max(24, process.stderr.columns || process.stdout.columns || 80);
@@ -23,15 +23,15 @@
23
23
  * ... ← up to MAX_INPUT_ROWS
24
24
  * ← spacer
25
25
  * ══════════════════════════════════════════════════════ ← bottom rule (colored)
26
- * deepseek-chat-v3 · Tarang-Orca ⎇ main · turn 4 ← meta row
26
+ * deepseek-chat-v3 · Bahulam ⎇ main · turn 4 ← meta row
27
27
  * [Enter] send · [/] commands … ← tips row
28
28
  * (one blank row at very bottom for cursor safety)
29
29
  *
30
30
  * Config:
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
34
- * BAHULAM_FIXED_INPUT=0 / KEPLER_FIXED_INPUT=0 disable dock entirely (fallback readline)
31
+ * BAHULAM_INPUT_ROWS_MAX 1..12 hard cap on input row growth (default 6)
32
+ * BAHULAM_TTY_MODE=stable scrollback-safe transcript, no fixed dock
33
+ * BAHULAM_PLAIN=1 deterministic no-ANSI output for automation
34
+ * BAHULAM_FIXED_INPUT=0 disable dock entirely (fallback readline)
35
35
  */
36
36
 
37
37
  import { paint, width as visibleWidth } from './palette.mjs';
@@ -214,7 +214,7 @@ function inputTextBudget() {
214
214
  function resolveMaxInputRows(requested) {
215
215
  const raw = requested != null
216
216
  ? requested
217
- : (process.env.BAHULAM_INPUT_ROWS_MAX || process.env.KEPLER_INPUT_ROWS_MAX);
217
+ : process.env.BAHULAM_INPUT_ROWS_MAX;
218
218
  const n = Number.parseInt(String(raw), 10);
219
219
  if (!Number.isFinite(n)) return DEFAULT_MAX_INPUT_ROWS;
220
220
  return Math.max(MIN_INPUT_ROWS, Math.min(MAX_INPUT_ROWS_CAP, n));
@@ -530,7 +530,7 @@ export function mountInputDock({
530
530
  const t = term();
531
531
  if (!t.isTTY || t.plain) return false;
532
532
  if (t.ttyMode !== 'rich' || t.fixedInput === false) return false;
533
- if (process.env.BAHULAM_FIXED_INPUT === '0' || process.env.KEPLER_FIXED_INPUT === '0') return false;
533
+ if (process.env.BAHULAM_FIXED_INPUT === '0') return false;
534
534
  if (mounted) return true;
535
535
 
536
536
  inputRowsMax = resolveMaxInputRows(requestedMax);
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * Behavior:
18
18
  * - 120ms per frame.
19
- * - Suppressed entirely when stdout is not a TTY or when KEPLER_PLAIN=1.
19
+ * - Suppressed entirely when stdout is not a TTY or when BAHULAM_PLAIN=1.
20
20
  * The `start`/`stop` API is still safe to call (no-op).
21
21
  * - Color follows the current orbit (orchestrated by the caller via the
22
22
  * palette token). Default: brand.primary.
package/src/ui/term.mjs CHANGED
@@ -34,9 +34,7 @@ function readEnv() {
34
34
  NO_COLOR: env.NO_COLOR,
35
35
  FORCE_COLOR: env.FORCE_COLOR,
36
36
  BAHULAM_PLAIN: env.BAHULAM_PLAIN,
37
- KEPLER_PLAIN: env.KEPLER_PLAIN,
38
37
  BAHULAM_TTY_MODE: env.BAHULAM_TTY_MODE,
39
- KEPLER_TTY_MODE: env.KEPLER_TTY_MODE,
40
38
  COLORTERM: (env.COLORTERM || '').toLowerCase(),
41
39
  TERM: (env.TERM || '').toLowerCase(),
42
40
  TERM_PROGRAM: env.TERM_PROGRAM || '',
@@ -48,7 +46,6 @@ function detectColorLevel(env, isTTY) {
48
46
  // Hard opt-out (https://no-color.org). Honored even on TTYs.
49
47
  if (env.NO_COLOR !== undefined && env.NO_COLOR !== '') return 'none';
50
48
  if (env.BAHULAM_PLAIN === '1') return 'none';
51
- if (env.KEPLER_PLAIN === '1') return 'none';
52
49
 
53
50
  // Hard opt-in. FORCE_COLOR=1|2|3 maps to ansi16|ansi256|truecolor.
54
51
  // FORCE_COLOR with no value or =true falls through to detection.
@@ -84,7 +81,6 @@ function detectColorLevel(env, isTTY) {
84
81
 
85
82
  function detectUnicode(env) {
86
83
  if (env.BAHULAM_PLAIN === '1') return false;
87
- if (env.KEPLER_PLAIN === '1') return false;
88
84
  // Most modern terminals on macOS/Linux handle UTF-8.
89
85
  // Windows ConEmu / older terminals are the main holdouts; conservative
90
86
  // fallback when LANG and LC_* are missing or explicitly POSIX.
@@ -95,8 +91,8 @@ function detectUnicode(env) {
95
91
  }
96
92
 
97
93
  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';
94
+ const raw = String(env.BAHULAM_TTY_MODE || '').trim().toLowerCase();
95
+ if (env.BAHULAM_PLAIN === '1') return 'plain';
100
96
  if (raw === 'stable' || raw === 'simple' || raw === 'static' || raw === 'transcript') return 'stable';
101
97
  return 'rich';
102
98
  }
@@ -78,12 +78,15 @@ const paintShellAdapter = {
78
78
  *
79
79
  * @returns {{ text: string, tone: 'success'|'warn'|'danger'|'dim' }}
80
80
  */
81
- export function summarizeResult(tool, data) {
81
+ export function summarizeResult(tool, data, args = {}) {
82
82
  if (!data) return { text: '', tone: 'dim' };
83
83
 
84
84
  if (data._blocked) {
85
85
  return { text: firstOutputLine(data) || 'blocked', tone: 'danger' };
86
86
  }
87
+ if (isNoChangeEditResult(tool, data)) {
88
+ return { text: 'no changes', tone: 'warn' };
89
+ }
87
90
  if (data._observation_timeout) {
88
91
  const ms = data._observation_timeout_ms;
89
92
  const duration = typeof ms === 'number' ? formatDuration(ms) : '';
@@ -111,7 +114,8 @@ export function summarizeResult(tool, data) {
111
114
 
112
115
  switch (tool) {
113
116
  case 'read_file': {
114
- const lines = data._total_lines || lineCount(data.output || data.output_preview);
117
+ const lines = readFileLineCount(data, args);
118
+ if (lines == null) return { text: 'read', tone: 'success' };
115
119
  return { text: `${lines} line${lines === 1 ? '' : 's'}`, tone: 'success' };
116
120
  }
117
121
  case 'read_files':
@@ -136,6 +140,9 @@ export function summarizeResult(tool, data) {
136
140
  case 'write_file':
137
141
  case 'write_project': {
138
142
  const delta = diffDelta(data);
143
+ if (tool === 'edit_file' && delta === '+0 −0') {
144
+ return { text: 'no changes', tone: 'warn' };
145
+ }
139
146
  if (delta) return { text: delta, tone: 'success' };
140
147
  return { text: 'updated', tone: 'success' };
141
148
  }
@@ -448,6 +455,64 @@ function lineCount(s) {
448
455
  return String(s).split('\n').filter(Boolean).length;
449
456
  }
450
457
 
458
+ function physicalLineCount(text) {
459
+ const value = String(text ?? '').replace(/\r\n?/g, '\n');
460
+ if (!value) return 0;
461
+ const lines = value.split('\n');
462
+ if (lines[lines.length - 1] === '') lines.pop();
463
+ return lines.length;
464
+ }
465
+
466
+ function readFileLineCount(data = {}, args = {}) {
467
+ const explicit = explicitReadLineCount(data);
468
+ if (explicit != null) return explicit;
469
+
470
+ const text = firstReadTextPayload(data);
471
+ if (text != null) return physicalLineCount(text);
472
+
473
+ const range = requestedRangeLineCount(args || data?.args || {});
474
+ if (range != null) return range;
475
+
476
+ return null;
477
+ }
478
+
479
+ function explicitReadLineCount(data = {}) {
480
+ if (!data || typeof data !== 'object') return null;
481
+ for (const key of ['_total_lines', 'line_count', 'lines_count', 'total_lines', 'lineCount', 'totalLines']) {
482
+ const value = data[key];
483
+ if (value == null || value === '') continue;
484
+ const number = Number(value);
485
+ if (Number.isFinite(number) && number >= 0) return Math.floor(number);
486
+ }
487
+ if (Array.isArray(data.lines)) return data.lines.length;
488
+ if (data.result && typeof data.result === 'object' && data.result !== data) {
489
+ return explicitReadLineCount(data.result);
490
+ }
491
+ return null;
492
+ }
493
+
494
+ function firstReadTextPayload(data = {}) {
495
+ if (!data || typeof data !== 'object') return null;
496
+ for (const key of ['content', 'text', 'output', 'output_preview', 'message']) {
497
+ if (typeof data[key] === 'string') return data[key];
498
+ }
499
+ if (data.result && typeof data.result === 'object' && data.result !== data) {
500
+ return firstReadTextPayload(data.result);
501
+ }
502
+ return null;
503
+ }
504
+
505
+ function requestedRangeLineCount(args = {}) {
506
+ const start = Number(args.start_line ?? args.startLine);
507
+ const end = Number(args.end_line ?? args.endLine);
508
+ if (Number.isFinite(start) && Number.isFinite(end) && start > 0 && end >= start) {
509
+ return Math.floor(end - start + 1);
510
+ }
511
+ const limit = Number(args.limit ?? args.max_lines ?? args.maxLines);
512
+ if (Number.isFinite(limit) && limit >= 0) return Math.floor(limit);
513
+ return null;
514
+ }
515
+
451
516
  function countMatches(data) {
452
517
  if (typeof data?.match_count === 'number') return data.match_count;
453
518
  return lineCount(data?.output);
@@ -474,6 +539,12 @@ function diffDelta(data) {
474
539
  return `+${a} −${r}`;
475
540
  }
476
541
 
542
+ function isNoChangeEditResult(tool, data = {}) {
543
+ if (tool !== 'edit_file') return false;
544
+ if (data._no_change || data.no_change) return true;
545
+ return data.success !== false && data.lines_added === 0 && data.lines_removed === 0;
546
+ }
547
+
477
548
  function tone(text, t) {
478
549
  switch (t) {
479
550
  case 'success': return paint.state.success(text);
@@ -577,7 +648,7 @@ export function formatCard({ tool, args, result, durationMs, indent, columns, cw
577
648
  const cols = columns || term().columns || 120;
578
649
  const head = formatCardHead(tool, args, { indent, columns: cols, cwd });
579
650
 
580
- const summary = summarizeResult(tool, result);
651
+ const summary = summarizeResult(tool, result, args);
581
652
  const duration = formatDuration(durationMs ?? result?.duration_ms ?? (result?.duration_s != null ? result.duration_s * 1000 : null));
582
653
 
583
654
  if (!summary.text && !duration) return head;