@bahulam/code 2.6.1 → 2.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/package.json +8 -3
- package/pulse/app/activity/page.tsx +1 -1
- package/pulse/app/globals.css +3 -3
- package/pulse/app/help/page.tsx +10 -10
- package/pulse/app/layout.tsx +2 -2
- package/pulse/app/memory/page.tsx +1 -1
- package/pulse/app/overview-client.tsx +1 -1
- package/pulse/app/page.tsx +2 -2
- package/pulse/app/plans/page.tsx +1 -1
- package/pulse/app/sessions/page.tsx +1 -1
- package/pulse/app/settings/page.tsx +1 -1
- package/pulse/app/tools/page.tsx +1 -1
- package/pulse/cli.js +8 -8
- package/pulse/components/layout/sidebar.tsx +2 -2
- package/pulse/package.json +2 -2
- package/src/agents/workflow_scaffold.mjs +1 -1
- package/src/core/attachments.mjs +287 -1
- package/src/core/backend-url.mjs +18 -5
- package/src/core/bundled-runtime.mjs +379 -0
- package/src/core/headless.mjs +1 -1
- package/src/core/settings-sync.mjs +1 -1
- package/src/core/stream-client.mjs +122 -3
- package/src/core/tool-executor.mjs +10 -3
- package/src/index.mjs +16 -16
- package/src/onboarding/preflight.mjs +14 -0
- package/src/terminal/main.mjs +19 -19
- package/src/terminal/repl-explore.mjs +9 -0
- package/src/terminal/repl.mjs +122 -11
- package/src/terminal/skills.mjs +1 -1
- package/src/tools/project-overview.mjs +66 -4
- package/src/ui/banner.mjs +5 -5
- package/src/ui/input-dock.mjs +76 -18
- package/src/ui/palette.mjs +2 -2
- package/src/ui/tool-card.mjs +23 -3
|
@@ -42,6 +42,20 @@ export async function checkAuthAndBackend(auth, { timeoutMs } = {}) {
|
|
|
42
42
|
const creds = auth.loadCredentials();
|
|
43
43
|
const hasToken = !!creds.token;
|
|
44
44
|
const url = creds.backendUrl;
|
|
45
|
+
|
|
46
|
+
// Bundled runtime mode: the local Python runtime replaces the cloud
|
|
47
|
+
// backend for agent execution. Probing a remote /api/user/me here would
|
|
48
|
+
// falsely paint the CLI as "Offline" even though the local runtime
|
|
49
|
+
// is fully operational. Report bundled state directly.
|
|
50
|
+
const bundledMode =
|
|
51
|
+
process.env.BAHULAM_RUNTIME_MODE === 'bundled' ||
|
|
52
|
+
process.env.TARANG_ENV === 'bundled';
|
|
53
|
+
if (bundledMode) {
|
|
54
|
+
return hasToken
|
|
55
|
+
? { status: 'ok', label: 'Bundled runtime · authenticated' }
|
|
56
|
+
: { status: 'warn', label: 'Bundled runtime', hint: '/login to enable metered calls' };
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
// Local Docker backends round-trip Supabase and often take 2–4s. Give them
|
|
46
60
|
// more headroom so preflight doesn't falsely report Offline.
|
|
47
61
|
const isLocal = /^https?:\/\/(127\.0\.0\.1|localhost)(:|$|\/)/i.test(url || '');
|
package/src/terminal/main.mjs
CHANGED
|
@@ -180,29 +180,29 @@ async function main() {
|
|
|
180
180
|
\x1b[1m\x1b[36mBahulam Code\x1b[0m — Bahulam's coding agent — bahulam.ai
|
|
181
181
|
|
|
182
182
|
\x1b[1mUsage:\x1b[0m
|
|
183
|
-
bahulam
|
|
184
|
-
bahulam
|
|
185
|
-
bahulam
|
|
186
|
-
bahulam
|
|
183
|
+
bahulam Start interactive REPL
|
|
184
|
+
bahulam "instruction" Run a single instruction
|
|
185
|
+
bahulam --headless -p "x" Non-interactive: auto-approve, JSONL output
|
|
186
|
+
bahulam --headless -p "x" --vision screenshot.png
|
|
187
187
|
Attach an image via the vision analysis pipeline
|
|
188
|
-
bahulam
|
|
189
|
-
bahulam
|
|
190
|
-
bahulam
|
|
191
|
-
bahulam
|
|
192
|
-
bahulam
|
|
193
|
-
bahulam
|
|
188
|
+
bahulam --resume Resume last conversation
|
|
189
|
+
bahulam dashboard Open analytics dashboard
|
|
190
|
+
bahulam login Sign in via browser
|
|
191
|
+
bahulam logout Sign out and clear credentials
|
|
192
|
+
bahulam init Scaffold .bahulam config, memory, hooks, tasks
|
|
193
|
+
bahulam version Show version
|
|
194
194
|
|
|
195
195
|
\x1b[1mAnalytics:\x1b[0m
|
|
196
|
-
bahulam
|
|
197
|
-
bahulam
|
|
198
|
-
bahulam
|
|
196
|
+
bahulam sessions List recent local sessions
|
|
197
|
+
bahulam stats Show aggregate local session stats
|
|
198
|
+
bahulam history Show recent prompt history
|
|
199
199
|
|
|
200
200
|
\x1b[1mSkills:\x1b[0m
|
|
201
|
-
bahulam
|
|
202
|
-
bahulam
|
|
203
|
-
bahulam
|
|
204
|
-
bahulam
|
|
205
|
-
bahulam
|
|
201
|
+
bahulam skills list [--all|--project]
|
|
202
|
+
bahulam skills view <name> [resource]
|
|
203
|
+
bahulam skills install <path-or-git-url> [--project] [--force]
|
|
204
|
+
bahulam skills update <name> [--project]
|
|
205
|
+
bahulam skills remove <name> [--project]
|
|
206
206
|
|
|
207
207
|
\x1b[1mREPL Commands:\x1b[0m
|
|
208
208
|
/help Show available commands
|
|
@@ -239,7 +239,7 @@ async function main() {
|
|
|
239
239
|
Max reconnect window for dropped streams
|
|
240
240
|
KEPLER_BLOCK_SEPARATOR Tool/content separator: space, dotted, or off
|
|
241
241
|
|
|
242
|
-
\x1b[2mDocs: https://
|
|
242
|
+
\x1b[2mDocs: https://bahulam.ai\x1b[0m
|
|
243
243
|
`);
|
|
244
244
|
return;
|
|
245
245
|
}
|
|
@@ -10,9 +10,18 @@
|
|
|
10
10
|
const EXPLORE_TOOL_CATEGORY = new Map([
|
|
11
11
|
['read_file', 'read'], ['read', 'read'], ['read_files', 'read'],
|
|
12
12
|
['read_batch', 'read'], ['get_file_info', 'read'],
|
|
13
|
+
// analyze_code is the "cheap 10x-lighter than read_file" tool the system
|
|
14
|
+
// prompt tells the agent to prefer for structure lookups. Burst usage is
|
|
15
|
+
// as common as read bursts, so classify it as a read for collapse.
|
|
16
|
+
['analyze_code', 'read'],
|
|
13
17
|
['list_files', 'list'], ['glob', 'list'], ['ls', 'list'],
|
|
14
18
|
['search_code', 'search'], ['search_files', 'search'], ['grep', 'search'],
|
|
19
|
+
// validate_* tools are read-only structure/build checks the agent chains
|
|
20
|
+
// during post-write verification. They fit naturally in a search-ish bucket
|
|
21
|
+
// ("checking") rather than opening a discrete card per call.
|
|
22
|
+
['validate_file', 'search'], ['validate_structure', 'search'],
|
|
15
23
|
['index_project', 'index'], ['register_project', 'index'],
|
|
24
|
+
['get_project_overview', 'index'],
|
|
16
25
|
]);
|
|
17
26
|
|
|
18
27
|
export function exploreCollapseEnabled() {
|
package/src/terminal/repl.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import * as readline from 'node:readline';
|
|
|
19
19
|
import * as fs from 'node:fs';
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import { execSync as _execSync } from 'node:child_process';
|
|
22
|
+
import { Writable as _WritableStream } from 'node:stream';
|
|
22
23
|
import { c, progressBar, spinner, inPlace, renderMarkdown, renderDiff, formatElapsed, formatCost, stripAnsi } from './ansi.mjs';
|
|
23
24
|
import { calculateCost, formatCostValue, formatTokens, costToCredits, formatCredits } from '../core/pricing.mjs';
|
|
24
25
|
import { TarangStreamClient, EVENT_TYPES } from '../core/stream-client.mjs';
|
|
@@ -56,9 +57,13 @@ import { appendTask, ensureTaskFiles, loadTaskBoard, moveTask, removeTask, taskC
|
|
|
56
57
|
import { applyCompactSummary, localCompactSummary, parseCompactTailCount, prepareCompactHistory } from '../core/compact-history.mjs';
|
|
57
58
|
import {
|
|
58
59
|
appendVisionAnalysisToInstruction,
|
|
60
|
+
appendDocumentsToInstruction,
|
|
59
61
|
attachmentSummaryLine,
|
|
62
|
+
documentSummaryLine,
|
|
60
63
|
prepareImageAttachments,
|
|
64
|
+
prepareDocumentAttachments,
|
|
61
65
|
publicAttachmentMetadata,
|
|
66
|
+
publicDocumentMetadata,
|
|
62
67
|
resolveAttachmentPath,
|
|
63
68
|
writeClipboardImageToTemp,
|
|
64
69
|
} from '../core/attachments.mjs';
|
|
@@ -1259,6 +1264,30 @@ function renderEvent(event) {
|
|
|
1259
1264
|
}
|
|
1260
1265
|
break;
|
|
1261
1266
|
|
|
1267
|
+
// Live steering ack events (PRD-081 §5.2). renderEvent is called from
|
|
1268
|
+
// the SSE loop (see `for await ... jsonlWriter.writeKeplerEvent(event)`
|
|
1269
|
+
// in the /execute path) so every event already lands in the transcript.
|
|
1270
|
+
// Here we only render the user-visible surface — no direct jsonlWriter
|
|
1271
|
+
// calls (it's out of scope in this top-level dispatcher anyway).
|
|
1272
|
+
case 'user_intervention_accepted': {
|
|
1273
|
+
// Endpoint response already told the CLI "sent to running agent".
|
|
1274
|
+
// The SSE echo here is durable-replay backup; nothing to render.
|
|
1275
|
+
break;
|
|
1276
|
+
}
|
|
1277
|
+
case 'user_intervention_delivered': {
|
|
1278
|
+
renderBlockBoundary('status', { compactSame: true });
|
|
1279
|
+
const tool = data?.delivered_at_tool ? ` ${c.dim(`(via ${data.delivered_at_tool})`)}` : '';
|
|
1280
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('follow-up delivered to agent')}${tool}\n`);
|
|
1281
|
+
runtime.lastRenderedBlock = 'status';
|
|
1282
|
+
break;
|
|
1283
|
+
}
|
|
1284
|
+
case 'user_intervention_queued': {
|
|
1285
|
+
// Task ended before delivery; endpoint returned queued_next_turn to
|
|
1286
|
+
// the submission call and the CLI already rendered that ack. Nothing
|
|
1287
|
+
// more to show; the outer SSE loop persists this event to JSONL.
|
|
1288
|
+
break;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1262
1291
|
case 'complete': {
|
|
1263
1292
|
stopSpinner();
|
|
1264
1293
|
flushContent();
|
|
@@ -2924,9 +2953,28 @@ export async function startTerminalRepl() {
|
|
|
2924
2953
|
return 'type any extra context (paths, corrections, follow-ups) · [Enter] send · [Esc] cancel · [Ctrl+P] pause';
|
|
2925
2954
|
}
|
|
2926
2955
|
|
|
2956
|
+
// Proxy stream: swallows writes when the dock owns the input row so
|
|
2957
|
+
// readline's echoes and _refreshLine cursor moves can't fight the dock's
|
|
2958
|
+
// absolute cursor placement (PRD-081 §5.1 — cursor race). When the dock
|
|
2959
|
+
// is not mounted (plain-mode fallback, KEPLER_FIXED_INPUT=0, non-TTY),
|
|
2960
|
+
// writes pass through so history navigation / backspace still redraw.
|
|
2961
|
+
const readlineOutputProxy = new _WritableStream({
|
|
2962
|
+
write(chunk, _enc, cb) {
|
|
2963
|
+
if (!isInputDockMounted()) {
|
|
2964
|
+
try { process.stderr.write(chunk); } catch {}
|
|
2965
|
+
}
|
|
2966
|
+
cb();
|
|
2967
|
+
},
|
|
2968
|
+
});
|
|
2969
|
+
// Preserve enough of stderr's TTY surface so readline still treats us as
|
|
2970
|
+
// a terminal (raw mode, keypress events, history navigation).
|
|
2971
|
+
readlineOutputProxy.isTTY = process.stderr.isTTY;
|
|
2972
|
+
readlineOutputProxy.columns = process.stderr.columns;
|
|
2973
|
+
readlineOutputProxy.rows = process.stderr.rows;
|
|
2974
|
+
|
|
2927
2975
|
const rl = readline.createInterface({
|
|
2928
2976
|
input: process.stdin,
|
|
2929
|
-
output:
|
|
2977
|
+
output: readlineOutputProxy,
|
|
2930
2978
|
prompt: userPrompt(),
|
|
2931
2979
|
completer: (line) => {
|
|
2932
2980
|
if (line.startsWith('/')) {
|
|
@@ -2973,6 +3021,14 @@ export async function startTerminalRepl() {
|
|
|
2973
3021
|
}
|
|
2974
3022
|
|
|
2975
3023
|
function restoreReadlineCursor() {
|
|
3024
|
+
// When the dock owns the input row, delegate to it so INPUT_INDENT +
|
|
3025
|
+
// meta/tips row math stays authoritative. Falling back to
|
|
3026
|
+
// readline.cursorTo(col) here lands INPUT_INDENT chars too far left
|
|
3027
|
+
// and yields the "/-shifts-cursor" bug (PRD-081 §5.1 cursor race).
|
|
3028
|
+
if (isInputDockMounted()) {
|
|
3029
|
+
focusDockInput(userPrompt(), rl.line || '', typeof rl.cursor === 'number' ? rl.cursor : null);
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
2976
3032
|
const col = Math.max(0, promptColumns() + Number(rl.cursor || 0));
|
|
2977
3033
|
readline.cursorTo(process.stderr, col);
|
|
2978
3034
|
}
|
|
@@ -3080,10 +3136,15 @@ export async function startTerminalRepl() {
|
|
|
3080
3136
|
|
|
3081
3137
|
function renderIdleDockInput() {
|
|
3082
3138
|
if (!isInputDockMounted()) return false;
|
|
3139
|
+
// rl.cursor is readline's byte offset within rl.line. Threading it
|
|
3140
|
+
// through to focusDockInput makes arrow-key navigation visually move
|
|
3141
|
+
// the terminal cursor within the buffer instead of always landing at
|
|
3142
|
+
// the end of the string.
|
|
3083
3143
|
return renderDockInput(userPrompt(), rl.line || '', {
|
|
3084
3144
|
context: buildContextStrip(),
|
|
3085
3145
|
meta: buildDockMeta(),
|
|
3086
3146
|
tips: idleInputTips(),
|
|
3147
|
+
cursor: typeof rl.cursor === 'number' ? rl.cursor : null,
|
|
3087
3148
|
});
|
|
3088
3149
|
}
|
|
3089
3150
|
|
|
@@ -3278,8 +3339,28 @@ export async function startTerminalRepl() {
|
|
|
3278
3339
|
}
|
|
3279
3340
|
|
|
3280
3341
|
try {
|
|
3342
|
+
// ── Document attachments (client-side, PRD-091 shape 1) ──
|
|
3343
|
+
// Extract text locally (PDF via pdf-parse, others as UTF-8). We stage
|
|
3344
|
+
// the docs here and inline their text into the user turn LAST, after
|
|
3345
|
+
// vision/image processing, because the image parser normalizes
|
|
3346
|
+
// whitespace and would collapse the doc block's newlines.
|
|
3347
|
+
const docPrep = await prepareDocumentAttachments(originalInput, { cwd: safeCwd() });
|
|
3348
|
+
if (docPrep.documents.length) {
|
|
3349
|
+
process.stderr.write(
|
|
3350
|
+
` ${c.brand('◇')} ${c.dim(`attached ${docPrep.documents.length} document${docPrep.documents.length === 1 ? '' : 's'}:`)} ` +
|
|
3351
|
+
`${docPrep.documents.map(documentSummaryLine).join(c.dim(' · '))}\n`
|
|
3352
|
+
);
|
|
3353
|
+
jsonlWriter.writeKeplerEvent({
|
|
3354
|
+
type: 'attachments',
|
|
3355
|
+
data: { documents: docPrep.documents.map(publicDocumentMetadata) },
|
|
3356
|
+
});
|
|
3357
|
+
}
|
|
3358
|
+
// Image parser operates on the doc-stripped instruction so @doc.pdf
|
|
3359
|
+
// refs are already gone by the time it sees the text.
|
|
3360
|
+
const imageSourceInput = docPrep.instruction || originalInput;
|
|
3361
|
+
|
|
3281
3362
|
const pending = pendingVisionPaths(ctx);
|
|
3282
|
-
const prepared = prepareImageAttachments(
|
|
3363
|
+
const prepared = prepareImageAttachments(imageSourceInput, {
|
|
3283
3364
|
cwd: safeCwd(),
|
|
3284
3365
|
extraPaths: pending,
|
|
3285
3366
|
});
|
|
@@ -3315,6 +3396,11 @@ export async function startTerminalRepl() {
|
|
|
3315
3396
|
} else {
|
|
3316
3397
|
input = prepared.instruction || originalInput;
|
|
3317
3398
|
}
|
|
3399
|
+
// Inline documents AFTER image/vision handling so their newlines
|
|
3400
|
+
// survive the image parser's whitespace normalization.
|
|
3401
|
+
if (docPrep.documents.length) {
|
|
3402
|
+
input = appendDocumentsToInstruction(input, docPrep.documents);
|
|
3403
|
+
}
|
|
3318
3404
|
} catch (err) {
|
|
3319
3405
|
process.stderr.write(` ${c.red('Vision error: ' + (err.message || String(err)))}\n`);
|
|
3320
3406
|
showPrompt();
|
|
@@ -3431,16 +3517,41 @@ export async function startTerminalRepl() {
|
|
|
3431
3517
|
process.stderr.write('\n');
|
|
3432
3518
|
}
|
|
3433
3519
|
executionInputVisible = false;
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3520
|
+
// Live steering (PRD-081 §5.2): submit through the dedicated
|
|
3521
|
+
// /api/intervention/{task_id} path, not /resume. The stream client
|
|
3522
|
+
// returns a status object so we render the true backend decision
|
|
3523
|
+
// (accepted vs queued-for-next-turn vs duplicate) instead of guessing.
|
|
3524
|
+
const result = await client.sendIntervention(instruction);
|
|
3525
|
+
const taskId = client.currentTaskId || null;
|
|
3526
|
+
const status = result && result.status;
|
|
3527
|
+
const interventionId = result && result.interventionId;
|
|
3528
|
+
|
|
3529
|
+
// Persist the local record regardless of outcome so the transcript
|
|
3530
|
+
// reflects what the user typed. Delivered/queued follow-ups will
|
|
3531
|
+
// get their SSE ack events written separately by the event handler.
|
|
3532
|
+
jsonlWriter.writeKeplerEvent({
|
|
3533
|
+
type: 'user_intervention',
|
|
3534
|
+
data: {
|
|
3535
|
+
instruction,
|
|
3536
|
+
task_id: taskId,
|
|
3537
|
+
intervention_id: interventionId || null,
|
|
3538
|
+
status: status || 'unknown',
|
|
3539
|
+
},
|
|
3540
|
+
});
|
|
3541
|
+
|
|
3542
|
+
if (status === 'accepted') {
|
|
3543
|
+
process.stderr.write(` ${c.green('↳')} ${c.dim('sent to running agent')}\n`);
|
|
3544
|
+
} else if (status === 'duplicate') {
|
|
3545
|
+
process.stderr.write(` ${c.dim('↳ already sent (idempotent)')}\n`);
|
|
3546
|
+
} else if (status === 'queued_next_turn') {
|
|
3547
|
+
_queuedLines.push(instruction);
|
|
3548
|
+
process.stderr.write(` ${c.yellow('↳')} ${c.dim('task ended — queued for next turn')}\n`);
|
|
3549
|
+
} else {
|
|
3550
|
+
// no_task, error, or unknown — fall back to next-turn queue so the
|
|
3551
|
+
// user's text is never silently lost.
|
|
3442
3552
|
_queuedLines.push(instruction);
|
|
3443
|
-
|
|
3553
|
+
const errBits = result && result.error ? ` ${c.dim(`(${String(result.error).slice(0, 80)})`)}` : '';
|
|
3554
|
+
process.stderr.write(` ${c.yellow('↳')} ${c.dim('queued for next turn')}${errBits}\n`);
|
|
3444
3555
|
}
|
|
3445
3556
|
}
|
|
3446
3557
|
|
package/src/terminal/skills.mjs
CHANGED
|
@@ -29,7 +29,7 @@ export async function runSkillsCommand(args, { cwd = process.cwd() } = {}) {
|
|
|
29
29
|
}
|
|
30
30
|
if (action === 'view') {
|
|
31
31
|
const name = rest.find(arg => !arg.startsWith('--'));
|
|
32
|
-
if (!name) throw new Error('Usage: bahulam
|
|
32
|
+
if (!name) throw new Error('Usage: bahulam skills view <name> [resource-path]');
|
|
33
33
|
const nameIndex = rest.indexOf(name);
|
|
34
34
|
const resource = rest.slice(nameIndex + 1).find(arg => !arg.startsWith('--')) || null;
|
|
35
35
|
print(loader.view(name, resource));
|
|
@@ -55,6 +55,52 @@ const IGNORED_DIRS = new Set([
|
|
|
55
55
|
'build', 'dist', 'node_modules', 'venv',
|
|
56
56
|
]);
|
|
57
57
|
|
|
58
|
+
// Files or directories whose presence at the root implies this IS a project.
|
|
59
|
+
// One is enough. Kept broad so we accept Node/Python/Rust/Go/Ruby/Java/C++
|
|
60
|
+
// projects, container-only repos, and Bahulam/agent-configured directories.
|
|
61
|
+
const PROJECT_MARKERS = [
|
|
62
|
+
'.git', '.hg', '.svn',
|
|
63
|
+
'.bahulam', '.kepler', // Bahulam project state
|
|
64
|
+
'package.json', // Node
|
|
65
|
+
'pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt', 'Pipfile',
|
|
66
|
+
'Cargo.toml', // Rust
|
|
67
|
+
'go.mod', // Go
|
|
68
|
+
'Gemfile', // Ruby
|
|
69
|
+
'pom.xml', 'build.gradle', 'build.gradle.kts', 'settings.gradle', // Java/Kotlin
|
|
70
|
+
'Makefile', 'CMakeLists.txt', // C/C++
|
|
71
|
+
'Dockerfile', 'docker-compose.yml', 'docker-compose.yaml',
|
|
72
|
+
'AGENTS.md', 'CLAUDE.md', 'KEPLER.md', // Agent config lives at root
|
|
73
|
+
'.editorconfig', // Broad but a strong "this is a repo" signal
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
// System / user-home roots we refuse outright — indexing these would sweep
|
|
77
|
+
// every project the user has ever touched and produce noise, not signal.
|
|
78
|
+
// Compared per-realpath so symlinks don't sneak past.
|
|
79
|
+
function _dangerousRootSet() {
|
|
80
|
+
const set = new Set([
|
|
81
|
+
'/', '/tmp', '/var', '/etc', '/usr', '/opt',
|
|
82
|
+
'/Applications', '/Library', '/System',
|
|
83
|
+
'/Users', '/home', '/root',
|
|
84
|
+
'/Volumes', '/mnt', '/media',
|
|
85
|
+
]);
|
|
86
|
+
try { set.add(os.homedir()); } catch {}
|
|
87
|
+
try { set.add(path.parse(os.homedir()).root); } catch {}
|
|
88
|
+
return set;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isDangerousRoot(root) {
|
|
92
|
+
return _dangerousRootSet().has(root);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function hasProjectMarkers(root) {
|
|
96
|
+
for (const marker of PROJECT_MARKERS) {
|
|
97
|
+
try {
|
|
98
|
+
if (fs.existsSync(path.join(root, marker))) return true;
|
|
99
|
+
} catch { /* skip unreadable entries */ }
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
58
104
|
function projectId(canonicalPath) {
|
|
59
105
|
return crypto.createHash('sha256').update(canonicalPath).digest('hex').slice(0, 12);
|
|
60
106
|
}
|
|
@@ -372,7 +418,7 @@ export class ProjectRegistry {
|
|
|
372
418
|
return resource;
|
|
373
419
|
}
|
|
374
420
|
|
|
375
|
-
async register(rawPath, { forceRefresh = false, force_refresh = false } = {}) {
|
|
421
|
+
async register(rawPath, { forceRefresh = false, force_refresh = false, bypassProjectMarkers = false } = {}) {
|
|
376
422
|
if (!rawPath) {
|
|
377
423
|
throw new Error('get_project_overview requires a project path');
|
|
378
424
|
}
|
|
@@ -403,9 +449,25 @@ export class ProjectRegistry {
|
|
|
403
449
|
if (!fs.statSync(root).isDirectory()) {
|
|
404
450
|
throw new Error(`Project path is not a directory: ${root}`);
|
|
405
451
|
}
|
|
406
|
-
if (
|
|
452
|
+
if (isDangerousRoot(root)) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
`Refusing to index ${root} — too broad or system-level. ` +
|
|
455
|
+
`Pass a specific project directory, or answer the user's question ` +
|
|
456
|
+
`without get_project_overview if it doesn't need project files.`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
// Programmatic file-read registration (registerFileRead) bypasses the
|
|
460
|
+
// marker check — the user has a specific file in hand and we index its
|
|
461
|
+
// parent so tool guards don't block the read. The user-facing
|
|
462
|
+
// get_project_overview tool DOES enforce the check.
|
|
463
|
+
if (!bypassProjectMarkers && !hasProjectMarkers(root)) {
|
|
407
464
|
throw new Error(
|
|
408
|
-
`
|
|
465
|
+
`No project markers found at ${root} (checked for .git, package.json, ` +
|
|
466
|
+
`pyproject.toml, Cargo.toml, go.mod, Gemfile, pom.xml, Makefile, ` +
|
|
467
|
+
`Dockerfile, AGENTS.md, .bahulam/). If the user's request does not ` +
|
|
468
|
+
`require this codebase, do NOT call get_project_overview again for ` +
|
|
469
|
+
`this session — answer the question directly. If it does require code, ` +
|
|
470
|
+
`ask the user to point at the correct project directory.`
|
|
409
471
|
);
|
|
410
472
|
}
|
|
411
473
|
|
|
@@ -504,7 +566,7 @@ export class ProjectRegistry {
|
|
|
504
566
|
const dir = path.dirname(filePath);
|
|
505
567
|
if (dir === path.parse(dir).root || dir === os.homedir()) return null;
|
|
506
568
|
|
|
507
|
-
const registered = await this.register(dir);
|
|
569
|
+
const registered = await this.register(dir, { bypassProjectMarkers: true });
|
|
508
570
|
const owner = this.projects.get(registered.resource.project_id);
|
|
509
571
|
if (!owner) return null;
|
|
510
572
|
|
package/src/ui/banner.mjs
CHANGED
|
@@ -60,11 +60,11 @@ export function printBanner(version = '') {
|
|
|
60
60
|
const vTag = version ? `${dim(' — ')}${dim('v' + version)}` : '';
|
|
61
61
|
|
|
62
62
|
write('\n');
|
|
63
|
-
write(` ${orbit} ${dim('·
|
|
63
|
+
write(` ${orbit} ${dim('· bahulam — abundance')}\n`);
|
|
64
64
|
write('\n');
|
|
65
65
|
write(`${wordmarkFrame()}\n`);
|
|
66
66
|
write('\n');
|
|
67
|
-
write(` ${dim('
|
|
67
|
+
write(` ${dim('abundance, in your terminal')}\n`);
|
|
68
68
|
write(` ${dev}${vTag}\n`);
|
|
69
69
|
write('\n');
|
|
70
70
|
}
|
|
@@ -139,7 +139,7 @@ export function printStyledConfig(creds) {
|
|
|
139
139
|
|
|
140
140
|
const env = process.env.TARANG_ENV || process.env.NODE_ENV || 'production';
|
|
141
141
|
|
|
142
|
-
write(`\n${paint.bold('Bahulam Code
|
|
142
|
+
write(`\n${paint.bold('Bahulam Code · Abundance')} ${dim('(~/.bahulam/config.json)')}\n`);
|
|
143
143
|
write(`${dim('─'.repeat(50))}\n`);
|
|
144
144
|
write(` Token: ${mask(creds.token)}\n`);
|
|
145
145
|
write(` OpenRouter: ${mask(creds.openRouterKey)}\n`);
|
|
@@ -151,7 +151,7 @@ export function printStyledConfig(creds) {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
export function printGoodbye() {
|
|
154
|
-
write(`\n${paint.bold(paint.brand.primary('
|
|
154
|
+
write(`\n${paint.bold(paint.brand.primary('until next time — abundance awaits'))}\n\n`);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
// ── Git probe ────────────────────────────────────────────────────────────
|
|
@@ -204,7 +204,7 @@ export function getLoginSuccessHTML() {
|
|
|
204
204
|
}
|
|
205
205
|
.dev { color: #3fb950; }
|
|
206
206
|
.bahulam { color: #58a6ff; }
|
|
207
|
-
.bahulam { color: #
|
|
207
|
+
.bahulam { color: #06b6d4; letter-spacing: 4px; }
|
|
208
208
|
.check {
|
|
209
209
|
font-size: 64px;
|
|
210
210
|
color: #3fb950;
|
package/src/ui/input-dock.mjs
CHANGED
|
@@ -64,7 +64,12 @@ let inputRowsMax = DEFAULT_MAX_INPUT_ROWS;
|
|
|
64
64
|
let inputRows = MIN_INPUT_ROWS;
|
|
65
65
|
let reservedRows = FIXED_ROWS + MIN_INPUT_ROWS;
|
|
66
66
|
let unsubResize = null;
|
|
67
|
-
|
|
67
|
+
// prefix + value are tracked here so any code path that redraws the dock
|
|
68
|
+
// (renderFrame, applyLayout, redrawDockFrame, resize) can end by parking
|
|
69
|
+
// the cursor at the correct input position. Without this, readline echoes
|
|
70
|
+
// land on rows the dock briefly moved through mid-render and characters
|
|
71
|
+
// appear above/below the input row.
|
|
72
|
+
let lastFrame = { context: '', meta: '', tips: '', prefix: '', value: '', cursor: null };
|
|
68
73
|
let resetting = false;
|
|
69
74
|
|
|
70
75
|
function write(s) { try { OUT.write(s); } catch {} }
|
|
@@ -123,9 +128,26 @@ function computeInputRowsForBuffer(prefix, value) {
|
|
|
123
128
|
// Resize the input area to a new row count. Moves the scroll region so
|
|
124
129
|
// content above shifts accordingly; the dock frame is redrawn at the
|
|
125
130
|
// new position. No-op if the count didn't change.
|
|
131
|
+
//
|
|
132
|
+
// CRITICAL: when the dock SHRINKS (inputRows decreases), the frame moves
|
|
133
|
+
// down and previously-dock rows become part of the scroll region. Those
|
|
134
|
+
// rows still hold their old dock content (rule chars, prior input text,
|
|
135
|
+
// meta line) which then leaks into the transcript as streamed content
|
|
136
|
+
// scrolls past them. We clear the old dock region BEFORE moving the frame
|
|
137
|
+
// so the freed rows are blank when they enter the scroll region.
|
|
126
138
|
function setInputRowsTo(nextRows) {
|
|
127
139
|
const clamped = Math.max(MIN_INPUT_ROWS, Math.min(inputRowsMax, Math.floor(nextRows)));
|
|
128
140
|
if (clamped === inputRows) return false;
|
|
141
|
+
const shrinking = clamped < inputRows;
|
|
142
|
+
if (mounted && shrinking) {
|
|
143
|
+
// Clear the entire old reserved region — top rule through safety row —
|
|
144
|
+
// so nothing that lived here leaks into the scroll region after we
|
|
145
|
+
// move the frame down.
|
|
146
|
+
for (let row = topRuleRow(); row <= rows(); row++) {
|
|
147
|
+
moveTo(row, 1);
|
|
148
|
+
clearLine();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
129
151
|
inputRows = clamped;
|
|
130
152
|
reservedRows = FIXED_ROWS + inputRows;
|
|
131
153
|
if (mounted) {
|
|
@@ -189,18 +211,40 @@ function bottomRuleLine() {
|
|
|
189
211
|
return paint.brand.primary(ruleChars(Math.max(0, cols() - 1)));
|
|
190
212
|
}
|
|
191
213
|
|
|
214
|
+
// Always park the cursor at the tracked (prefix, value) input position.
|
|
215
|
+
// Called at the END of every dock render path so readline echoes land in
|
|
216
|
+
// the input row, not on whichever row a mid-render moveTo left them on.
|
|
217
|
+
// When there is no tracked input (nothing to focus yet), park at the
|
|
218
|
+
// scroll-region bottom so writes at least stay above the dock frame.
|
|
219
|
+
function parkCursorAtInput() {
|
|
220
|
+
if (!mounted) return;
|
|
221
|
+
const prefix = lastFrame.prefix || '';
|
|
222
|
+
const value = lastFrame.value || '';
|
|
223
|
+
if (!prefix && !value) {
|
|
224
|
+
moveTo(inputRowStart(), INPUT_INDENT + 1);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
focusDockInput(prefix, value, lastFrame.cursor);
|
|
228
|
+
}
|
|
229
|
+
|
|
192
230
|
function applyLayout() {
|
|
193
231
|
if (!mounted) return;
|
|
194
232
|
const bottom = contentBottomRow();
|
|
195
233
|
setScrollRegion(1, bottom);
|
|
196
234
|
renderFrame(lastFrame);
|
|
197
|
-
|
|
235
|
+
// On (re)mount and resize, park at input if we have one; otherwise sit at
|
|
236
|
+
// the bottom of the content region so any pending content writes flush
|
|
237
|
+
// above the dock rather than into a stale mid-frame position.
|
|
238
|
+
if (lastFrame.prefix || lastFrame.value) {
|
|
239
|
+
parkCursorAtInput();
|
|
240
|
+
} else {
|
|
241
|
+
moveTo(bottom, 1);
|
|
242
|
+
}
|
|
198
243
|
}
|
|
199
244
|
|
|
200
245
|
function renderFrame(frame = {}) {
|
|
201
246
|
if (!mounted) return;
|
|
202
247
|
lastFrame = { ...lastFrame, ...frame };
|
|
203
|
-
saveCursor();
|
|
204
248
|
|
|
205
249
|
moveTo(topRuleRow(), 1);
|
|
206
250
|
clearLine();
|
|
@@ -220,7 +264,7 @@ function renderFrame(frame = {}) {
|
|
|
220
264
|
clearLine();
|
|
221
265
|
write(padLine(bottomRuleLine()));
|
|
222
266
|
|
|
223
|
-
// Meta row:
|
|
267
|
+
// Meta row: cwd ⎇ branch · turn N · tokens
|
|
224
268
|
moveTo(metaRow(), 1);
|
|
225
269
|
clearLine();
|
|
226
270
|
const metaIndent = ' '.repeat(META_INDENT);
|
|
@@ -238,16 +282,19 @@ function renderFrame(frame = {}) {
|
|
|
238
282
|
// Safety row.
|
|
239
283
|
moveTo(rows(), 1);
|
|
240
284
|
clearLine();
|
|
241
|
-
|
|
285
|
+
|
|
286
|
+
// Deliberately NOT using saveCursor/restoreCursor. VT100 has a single
|
|
287
|
+
// cursor-save slot per terminal; nested calls in drawInputLines /
|
|
288
|
+
// pinned-status writers clobber the outer save and restore to the wrong
|
|
289
|
+
// place. Instead, callers that need the cursor parked at the input row
|
|
290
|
+
// invoke parkCursorAtInput() after renderFrame returns.
|
|
242
291
|
}
|
|
243
292
|
|
|
244
293
|
function clearInputRows() {
|
|
245
|
-
saveCursor();
|
|
246
294
|
for (let row = inputRowStart(); row <= inputRowEnd(); row++) {
|
|
247
295
|
moveTo(row, 1);
|
|
248
296
|
clearLine();
|
|
249
297
|
}
|
|
250
|
-
restoreCursor();
|
|
251
298
|
}
|
|
252
299
|
|
|
253
300
|
export function clearDockArea({ restore = true } = {}) {
|
|
@@ -281,7 +328,6 @@ function layoutInput(prefix, value) {
|
|
|
281
328
|
}
|
|
282
329
|
|
|
283
330
|
function drawInputLines(lines) {
|
|
284
|
-
saveCursor();
|
|
285
331
|
const indent = ' '.repeat(INPUT_INDENT);
|
|
286
332
|
for (let i = 0; i < inputRows; i++) {
|
|
287
333
|
const row = inputRowStart() + i;
|
|
@@ -291,7 +337,7 @@ function drawInputLines(lines) {
|
|
|
291
337
|
write(`${indent}${lines[i]}`);
|
|
292
338
|
}
|
|
293
339
|
}
|
|
294
|
-
|
|
340
|
+
// No save/restore — caller parks cursor via focusDockInput.
|
|
295
341
|
}
|
|
296
342
|
|
|
297
343
|
export function isInputDockMounted() {
|
|
@@ -378,6 +424,7 @@ export function clearPinnedStatus() {
|
|
|
378
424
|
export function redrawDockFrame() {
|
|
379
425
|
if (!mounted) return false;
|
|
380
426
|
renderFrame(lastFrame);
|
|
427
|
+
parkCursorAtInput();
|
|
381
428
|
return true;
|
|
382
429
|
}
|
|
383
430
|
|
|
@@ -385,7 +432,7 @@ export function prepareInputPrompt({ context = '', tips = '', meta = '' } = {})
|
|
|
385
432
|
if (!mounted) return false;
|
|
386
433
|
setInputRowsTo(MIN_INPUT_ROWS);
|
|
387
434
|
clearInputRows();
|
|
388
|
-
renderFrame({ context, tips, meta });
|
|
435
|
+
renderFrame({ context, tips, meta, prefix: '', value: '' });
|
|
389
436
|
moveTo(inputRowStart(), INPUT_INDENT + 1);
|
|
390
437
|
return true;
|
|
391
438
|
}
|
|
@@ -393,29 +440,40 @@ export function prepareInputPrompt({ context = '', tips = '', meta = '' } = {})
|
|
|
393
440
|
export function clearInputPrompt() {
|
|
394
441
|
if (!mounted) return false;
|
|
395
442
|
clearInputRows();
|
|
443
|
+
lastFrame.value = '';
|
|
396
444
|
renderFrame(lastFrame);
|
|
445
|
+
parkCursorAtInput();
|
|
397
446
|
return true;
|
|
398
447
|
}
|
|
399
448
|
|
|
400
|
-
export function renderDockInput(prefix, value, { context = '', tips = '', meta = '' } = {}) {
|
|
449
|
+
export function renderDockInput(prefix, value, { context = '', tips = '', meta = '', cursor = null } = {}) {
|
|
401
450
|
if (!mounted) return false;
|
|
402
451
|
setInputRowsTo(computeInputRowsForBuffer(prefix, value));
|
|
403
|
-
renderFrame({ context, tips, meta });
|
|
452
|
+
renderFrame({ context, tips, meta, prefix, value, cursor });
|
|
404
453
|
const layout = layoutInput(prefix, value);
|
|
405
454
|
drawInputLines(layout.lines);
|
|
406
|
-
focusDockInput(prefix, value);
|
|
455
|
+
focusDockInput(prefix, value, cursor);
|
|
407
456
|
return true;
|
|
408
457
|
}
|
|
409
458
|
|
|
410
459
|
/**
|
|
411
|
-
* Move the terminal cursor to the position that corresponds to
|
|
412
|
-
*
|
|
413
|
-
* area.
|
|
460
|
+
* Move the terminal cursor to the position that corresponds to
|
|
461
|
+
* `prefix + value[0..cursorInValue]` within the (possibly wrapped and
|
|
462
|
+
* truncated) input area. When `cursorInValue` is null/undefined, the cursor
|
|
463
|
+
* lands at the logical end of `value` (append-mode default).
|
|
464
|
+
*
|
|
465
|
+
* `cursorInValue` is a char index into the RAW value string (matching
|
|
466
|
+
* readline's `rl.cursor`), not into the wrapped/rendered output.
|
|
414
467
|
*/
|
|
415
|
-
export function focusDockInput(prefix, value = '') {
|
|
468
|
+
export function focusDockInput(prefix, value = '', cursorInValue = null) {
|
|
416
469
|
if (!mounted) return false;
|
|
417
470
|
const layout = layoutInput(prefix, value);
|
|
418
|
-
const
|
|
471
|
+
const valueStr = String(value || '');
|
|
472
|
+
const rawCursor = cursorInValue == null
|
|
473
|
+
? valueStr.length
|
|
474
|
+
: Math.max(0, Math.min(valueStr.length, Math.floor(cursorInValue)));
|
|
475
|
+
const cursorSlice = valueStr.slice(0, rawCursor);
|
|
476
|
+
const offset = visibleWidth(`${prefix || ''}${cursorSlice}`);
|
|
419
477
|
const pos = cursorPositionInLines(layout.wrapped, offset);
|
|
420
478
|
const visibleRowIdx = Math.max(
|
|
421
479
|
0,
|