@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.
- package/README.md +1 -1
- package/package.json +3 -3
- package/src/auth/{tarang-auth.mjs → bahulam-auth.mjs} +1 -1
- package/src/commands/agent.mjs +3 -3
- package/src/commands/device.mjs +2 -2
- package/src/commands/pair.mjs +2 -2
- package/src/commands/remote.mjs +3 -3
- package/src/commands/workflow.mjs +5 -5
- package/src/config/env.mjs +9 -2
- package/src/config/memory-loader.mjs +1 -1
- package/src/config/settings.mjs +2 -2
- package/src/core/agent-loop.mjs +17 -0
- package/src/core/attachments.mjs +1 -1
- package/src/core/backend-url.mjs +11 -12
- package/src/core/callback-client.mjs +1 -1
- package/src/core/headless.mjs +4 -4
- package/src/core/jsonl-writer.mjs +15 -15
- package/src/core/local-agent.mjs +26 -5
- package/src/core/local-store.mjs +1 -1
- package/src/core/mode-selector.mjs +1 -1
- package/src/core/output-filter.mjs +1 -1
- package/src/core/project-artifacts.mjs +3 -3
- package/src/core/project-context-loader.mjs +9 -9
- package/src/core/settings-sync.mjs +1 -1
- package/src/core/stagnation.mjs +61 -1
- package/src/core/stream-client.mjs +4 -4
- package/src/core/system-prompt.mjs +2 -2
- package/src/core/tool-executor.mjs +78 -60
- package/src/local-service/agent-relay.mjs +8 -8
- package/src/local-service/server.mjs +2 -2
- package/src/mcp/client.mjs +4 -4
- package/src/onboarding/preflight.mjs +1 -1
- package/src/terminal/agents.mjs +2 -2
- package/src/terminal/main.mjs +6 -6
- package/src/terminal/repl-render.mjs +2 -2
- package/src/terminal/repl-resume.mjs +2 -2
- package/src/terminal/repl.mjs +17 -17
- package/src/terminal/tool-display.mjs +1 -1
- package/src/tools/agent.mjs +2 -2
- package/src/tools/analyze-image.mjs +2 -2
- package/src/tools/generate-image.mjs +4 -4
- package/src/tools/project-overview.mjs +12 -12
- package/src/ui/formatter.mjs +2 -2
- package/src/ui/input-dock.mjs +7 -7
- package/src/ui/spinner.mjs +1 -1
- package/src/ui/term.mjs +2 -6
- package/src/ui/tool-card.mjs +74 -3
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ See [RELEASE.md](./RELEASE.md) for the merge, PR, and npm publish checklist.
|
|
|
79
79
|
## Links
|
|
80
80
|
|
|
81
81
|
- Website: https://bahulam.ai
|
|
82
|
-
- Repository: https://github.com/
|
|
82
|
+
- Repository: https://github.com/BahulamAI/BahulamCode-CLI
|
|
83
83
|
|
|
84
84
|
## License
|
|
85
85
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bahulam/code",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Bahulam Code
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"description": "Bahulam Code — abundance, in your terminal. CLI-first, reliability-first, sub-agents, 65.6% SWE-bench Verified.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"bahulam": "src/terminal/main.mjs",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"homepage": "https://bahulam.ai/code",
|
|
45
45
|
"repository": {
|
|
46
46
|
"type": "git",
|
|
47
|
-
"url": "git+https://github.com/
|
|
47
|
+
"url": "git+https://github.com/BahulamAI/BahulamCode-CLI.git"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"mermaid": "^11.17.2",
|
package/src/commands/agent.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { resolveBackendUrl } from '../core/backend-url.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import { BahulamAuth } from '../auth/bahulam-auth.mjs';
|
|
12
12
|
import { listLocalAgents, syncAgentsToBackend, agentToSpec, agentContentHash } from '../agents/scaffold.mjs';
|
|
13
13
|
import { parseAgentDefinition } from '../agents/parser.mjs';
|
|
14
14
|
import fs from 'node:fs';
|
|
@@ -56,7 +56,7 @@ function printAgentUsage() {
|
|
|
56
56
|
// ── Helpers ────────────────────────────────────────────────────
|
|
57
57
|
|
|
58
58
|
function getAuth() {
|
|
59
|
-
const auth = new
|
|
59
|
+
const auth = new BahulamAuth();
|
|
60
60
|
const creds = auth.loadCredentials();
|
|
61
61
|
if (!creds.token) {
|
|
62
62
|
process.stderr.write(`${RED}✗ Not authenticated. Run bahulam-code login first.${RESET}\n`);
|
|
@@ -66,7 +66,7 @@ function getAuth() {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function getBaseUrl() {
|
|
69
|
-
const auth = new
|
|
69
|
+
const auth = new BahulamAuth();
|
|
70
70
|
return auth.loadCredentials().backendUrl || resolveBackendUrl();
|
|
71
71
|
}
|
|
72
72
|
|
package/src/commands/device.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* `bahulam device list`).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { BahulamAuth } from '../auth/bahulam-auth.mjs';
|
|
14
14
|
|
|
15
15
|
const GATEWAY = (process.env.BAHULAM_GATEWAY_URL || 'https://gateway.bahulam.ai').replace(/\/+$/, '').replace(/\/v1$/, '');
|
|
16
16
|
const RESET = '\x1b[0m';
|
|
@@ -22,7 +22,7 @@ const YELLOW = '\x1b[33m';
|
|
|
22
22
|
|
|
23
23
|
export async function runDeviceCommand(args = []) {
|
|
24
24
|
const sub = (args[0] || '').toLowerCase();
|
|
25
|
-
const auth = new
|
|
25
|
+
const auth = new BahulamAuth();
|
|
26
26
|
const creds = auth.loadCredentials();
|
|
27
27
|
if (!creds.token) {
|
|
28
28
|
process.stderr.write(`${RED}Not logged in. Run ${BOLD}bahulam login${RESET}${RED} first.${RESET}\n`);
|
package/src/commands/pair.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import * as fs from 'node:fs';
|
|
|
30
30
|
import * as os from 'node:os';
|
|
31
31
|
import { generateKeyPairSync, createPrivateKey, createPublicKey } from 'node:crypto';
|
|
32
32
|
import { promisify } from 'node:util';
|
|
33
|
-
import {
|
|
33
|
+
import { BahulamAuth } from '../auth/bahulam-auth.mjs';
|
|
34
34
|
|
|
35
35
|
const GATEWAY = (process.env.BAHULAM_GATEWAY_URL || 'https://gateway.bahulam.ai').replace(/\/+$/, '').replace(/\/v1$/, '');
|
|
36
36
|
const RESET = '\x1b[0m';
|
|
@@ -42,7 +42,7 @@ const YELLOW = '\x1b[33m';
|
|
|
42
42
|
const CYAN = '\x1b[36m';
|
|
43
43
|
|
|
44
44
|
export async function runPairCommand(args = []) {
|
|
45
|
-
const auth = new
|
|
45
|
+
const auth = new BahulamAuth();
|
|
46
46
|
const creds = auth.loadCredentials();
|
|
47
47
|
if (!creds.token) {
|
|
48
48
|
process.stderr.write(`${RED}Not logged in. Run ${BOLD}bahulam login${RESET}${RED} first.${RESET}\n`);
|
package/src/commands/remote.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* connection — the daemon must be running. * daemon needs a device_id + keypair to authenticate to the relay.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { BahulamAuth } from '../auth/bahulam-auth.mjs';
|
|
17
17
|
|
|
18
18
|
const RESET = '\x1b[0m';
|
|
19
19
|
const BOLD = '\x1b[1m';
|
|
@@ -26,7 +26,7 @@ const DEFAULT_RELAY = (process.env.BAHULAM_RELAY_URL || 'wss://relay.bahulam.ai'
|
|
|
26
26
|
|
|
27
27
|
export async function runRemoteCommand(args = []) {
|
|
28
28
|
const sub = (args[0] || '').toLowerCase();
|
|
29
|
-
const auth = new
|
|
29
|
+
const auth = new BahulamAuth();
|
|
30
30
|
auth.loadCredentials();
|
|
31
31
|
|
|
32
32
|
if (sub === 'enable') return _enable(auth);
|
|
@@ -93,7 +93,7 @@ function _status(auth) {
|
|
|
93
93
|
* public_key, peer_pubkeys } — or null if we shouldn't dial.
|
|
94
94
|
*/
|
|
95
95
|
export function loadRemoteConfig() {
|
|
96
|
-
const auth = new
|
|
96
|
+
const auth = new BahulamAuth();
|
|
97
97
|
const config = auth.getRawConfig() || auth.loadCredentials() && auth.getRawConfig();
|
|
98
98
|
if (!config?.remote?.enabled) return null;
|
|
99
99
|
if (!config?.pairing?.device_id) return null;
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { resolveBackendUrl } from '../core/backend-url.mjs';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { BahulamAuth } from '../auth/bahulam-auth.mjs';
|
|
20
|
+
import { BahulamStreamClient } from '../core/stream-client.mjs';
|
|
21
21
|
import { createToolExecutor } from '../core/tool-executor.mjs';
|
|
22
22
|
import { ApprovalManager } from '../core/approval.mjs';
|
|
23
23
|
import { EventFormatter } from '../ui/formatter.mjs';
|
|
@@ -87,7 +87,7 @@ function printWorkflowUsage() {
|
|
|
87
87
|
// ── Helpers ────────────────────────────────────────────────────
|
|
88
88
|
|
|
89
89
|
function getAuthHeaders() {
|
|
90
|
-
const auth = new
|
|
90
|
+
const auth = new BahulamAuth();
|
|
91
91
|
const creds = auth.loadCredentials();
|
|
92
92
|
if (!creds.token) {
|
|
93
93
|
process.stderr.write(`${RED}✗ Not authenticated. Run `bahulam-code login` first.${RESET}\n`);
|
|
@@ -100,7 +100,7 @@ function getAuthHeaders() {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
function getBaseUrl() {
|
|
103
|
-
const auth = new
|
|
103
|
+
const auth = new BahulamAuth();
|
|
104
104
|
return auth.loadCredentials().backendUrl || resolveBackendUrl();
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -540,7 +540,7 @@ async function handleRunMulti(args) {
|
|
|
540
540
|
process.exit(1);
|
|
541
541
|
}
|
|
542
542
|
|
|
543
|
-
const client = new
|
|
543
|
+
const client = new BahulamStreamClient({
|
|
544
544
|
baseUrl,
|
|
545
545
|
token: headers.Authorization.replace(/^Bearer\s+/i, ''),
|
|
546
546
|
toolExecutor: createToolExecutor(),
|
package/src/config/env.mjs
CHANGED
|
@@ -27,8 +27,8 @@ export const ENV_SCHEMA = {
|
|
|
27
27
|
CLAUDE_CODE_BRIEF: { type: 'boolean', default: false, description: 'Brief output mode' },
|
|
28
28
|
CLAUDE_CODE_DISABLE_CRON: { type: 'boolean', default: false, description: 'Disable cron tasks' },
|
|
29
29
|
CLAUDE_CODE_ENABLE_TASKS: { type: 'boolean', default: false, description: 'Enable task system' },
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
BAHULAM_STAGNATION_DETECTION: { type: 'boolean', default: false, description: 'Detect repeated tool-call or no-op edit loops' },
|
|
31
|
+
BAHULAM_STAGNATION_THRESHOLD: { type: 'number', default: 3, description: 'Consecutive repeated calls before warning' },
|
|
32
32
|
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: { type: 'boolean', default: false, description: 'Enable agent teams' },
|
|
33
33
|
CLAUDE_CODE_DEBUG: { type: 'boolean', default: false, description: 'Debug mode' },
|
|
34
34
|
CLAUDE_CODE_DISABLE_TELEMETRY: { type: 'boolean', default: false, description: 'Disable telemetry' },
|
|
@@ -44,6 +44,13 @@ export const ENV_SCHEMA = {
|
|
|
44
44
|
// UI and Display
|
|
45
45
|
SHOW_THINKING: { type: 'boolean', default: false, description: 'Show thinking blocks' },
|
|
46
46
|
SHOW_TOOL_RESULTS: { type: 'boolean', default: false, description: 'Show tool results in REPL' },
|
|
47
|
+
BAHULAM_PLAIN: { type: 'boolean', default: false, description: 'Disable ANSI output and fixed terminal UI' },
|
|
48
|
+
BAHULAM_TTY_MODE: { type: 'string', default: 'rich', description: 'Terminal UI mode: rich, stable, transcript, or plain' },
|
|
49
|
+
BAHULAM_FIXED_INPUT: { type: 'boolean', default: true, description: 'Enable the fixed bottom input dock in rich TTY mode' },
|
|
50
|
+
BAHULAM_INPUT_ROWS_MAX: { type: 'number', default: 6, description: 'Maximum input rows reserved by the fixed input dock' },
|
|
51
|
+
BAHULAM_PROMPT_BOTTOM_PADDING: { type: 'number', default: 5, description: 'Blank rows reserved below the prompt when the fixed dock is disabled' },
|
|
52
|
+
BAHULAM_PASTE_FLUSH_MS: { type: 'number', default: 35, description: 'Debounce window for non-bracketed multiline paste input' },
|
|
53
|
+
BAHULAM_BLOCK_SEPARATOR: { type: 'string', default: 'space', description: 'Tool/content separator style: space, dotted, or off' },
|
|
47
54
|
NO_COLOR: { type: 'boolean', default: false, description: 'Disable colored output' },
|
|
48
55
|
TERM: { type: 'string', description: 'Terminal type' },
|
|
49
56
|
|
|
@@ -17,7 +17,7 @@ function readIfExists(filePath, maxChars = 12000) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function
|
|
20
|
+
export function loadBahulamMemory({ cwd = process.cwd() } = {}) {
|
|
21
21
|
const files = [];
|
|
22
22
|
const global = readIfExists(path.join(bahulamHome(), 'KEPLER.md'));
|
|
23
23
|
if (global) files.push({ source: 'global', ...global });
|
package/src/config/settings.mjs
CHANGED
|
@@ -118,13 +118,13 @@ function applyEnvOverrides(settings) {
|
|
|
118
118
|
if (process.env.CLAUDE_CODE_THINKING === '1') settings.alwaysThinkingEnabled = true;
|
|
119
119
|
if (process.env.CLAUDE_CODE_DISABLE_CRON === '1') settings.cronEnabled = false;
|
|
120
120
|
if (process.env.CLAUDE_CODE_ENABLE_TASKS === '1') settings.enableTeams = true;
|
|
121
|
-
const stagnationEnabled = process.env.
|
|
121
|
+
const stagnationEnabled = process.env.BAHULAM_STAGNATION_DETECTION;
|
|
122
122
|
if (stagnationEnabled !== undefined) {
|
|
123
123
|
settings.stagnationDetection = ['1', 'true', 'yes', 'on'].includes(
|
|
124
124
|
stagnationEnabled.toLowerCase(),
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
|
-
const stagnationThreshold = process.env.
|
|
127
|
+
const stagnationThreshold = process.env.BAHULAM_STAGNATION_THRESHOLD;
|
|
128
128
|
if (stagnationThreshold) {
|
|
129
129
|
const n = parseInt(stagnationThreshold, 10);
|
|
130
130
|
if (Number.isInteger(n) && n >= 2) settings.stagnationThreshold = n;
|
package/src/core/agent-loop.mjs
CHANGED
|
@@ -191,6 +191,23 @@ export function createAgentLoop({ model, tools, permissions, settings, hooks })
|
|
|
191
191
|
result = await hooks.runPostToolUse(block.name, result);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
const resultStagnation = stagnation.recordResult(block.name, block.input || {}, result);
|
|
195
|
+
if (resultStagnation.detected) {
|
|
196
|
+
yield {
|
|
197
|
+
type: 'stagnation',
|
|
198
|
+
tool: block.name,
|
|
199
|
+
count: resultStagnation.count,
|
|
200
|
+
kind: resultStagnation.kind,
|
|
201
|
+
target: resultStagnation.target,
|
|
202
|
+
};
|
|
203
|
+
result = {
|
|
204
|
+
...(typeof result === 'object' && result !== null ? result : {}),
|
|
205
|
+
success: false,
|
|
206
|
+
output: stagnationMessage(block.name, resultStagnation.count, resultStagnation),
|
|
207
|
+
_stagnation: true,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
194
211
|
yield { type: 'result', tool: block.name, result };
|
|
195
212
|
|
|
196
213
|
toolResults.push({
|
package/src/core/attachments.mjs
CHANGED
|
@@ -265,7 +265,7 @@ function powershellString(value) {
|
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
function ensureClipboardDir(baseDir = os.tmpdir()) {
|
|
268
|
-
const dir = path.join(baseDir, '
|
|
268
|
+
const dir = path.join(baseDir, 'bahulam-clipboard-images');
|
|
269
269
|
fs.mkdirSync(dir, { recursive: true });
|
|
270
270
|
return dir;
|
|
271
271
|
}
|
package/src/core/backend-url.mjs
CHANGED
|
@@ -8,32 +8,29 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const BACKEND_URLS = {
|
|
11
|
-
//
|
|
12
|
-
// local
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
11
|
+
// Supported environments:
|
|
12
|
+
// local — Docker Compose backend on the developer's own machine.
|
|
13
|
+
// production — Bahulam Cloud live (api.bahulam.ai).
|
|
14
|
+
// bundled — CLI-local Python runtime spawned as a subprocess. URL is
|
|
15
|
+
// overridden by bundled-runtime.mjs at spawn time; sentinel
|
|
16
|
+
// value below is only used if the runtime isn't up yet.
|
|
17
|
+
//
|
|
18
|
+
// For custom dev/staging URLs, set TARANG_BACKEND_URL directly.
|
|
18
19
|
local: 'http://127.0.0.1:8150',
|
|
19
|
-
dev: 'https://codekepler-backend-dev.kindisland-9034322d.eastus.azurecontainerapps.io',
|
|
20
20
|
production: 'https://api.bahulam.ai',
|
|
21
21
|
bundled: 'http://127.0.0.1:0', // sentinel — real URL comes from bundled-runtime.mjs
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
// Aliases (backwards compat + convenience)
|
|
25
25
|
BACKEND_URLS.prod = BACKEND_URLS.production;
|
|
26
|
-
BACKEND_URLS.
|
|
27
|
-
BACKEND_URLS.docker = BACKEND_URLS.local; // convenience alias
|
|
26
|
+
BACKEND_URLS.docker = BACKEND_URLS.local;
|
|
28
27
|
|
|
29
28
|
const WEB_URLS = {
|
|
30
29
|
local: 'http://localhost:3100',
|
|
31
|
-
dev: 'https://treetop.bahulam.ai',
|
|
32
30
|
production: 'https://bahulam.ai',
|
|
33
31
|
bundled: 'http://localhost:3100', // bundled mode reuses local web if user runs it
|
|
34
32
|
};
|
|
35
33
|
WEB_URLS.prod = WEB_URLS.production;
|
|
36
|
-
WEB_URLS.treetop = WEB_URLS.dev; // legacy alias
|
|
37
34
|
WEB_URLS.docker = WEB_URLS.local;
|
|
38
35
|
|
|
39
36
|
/**
|
|
@@ -50,6 +47,8 @@ export function resolveWebUrl() {
|
|
|
50
47
|
|
|
51
48
|
/**
|
|
52
49
|
* Resolve the backend URL from environment.
|
|
50
|
+
*
|
|
51
|
+
* For dev/staging, set TARANG_BACKEND_URL=<your-url> explicitly.
|
|
53
52
|
* @returns {string}
|
|
54
53
|
*/
|
|
55
54
|
export function resolveBackendUrl() {
|
package/src/core/headless.mjs
CHANGED
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* bahulam-code --headless --model deepseek/deepseek-chat-v3-0324 "Add tests"
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { BahulamStreamClient } from './stream-client.mjs';
|
|
15
15
|
import { createToolExecutor } from './tool-executor.mjs';
|
|
16
16
|
import { buildWorkScope, promptProjectRoots } from './work-scope.mjs';
|
|
17
17
|
import { persistProjectArtifacts } from './project-artifacts.mjs';
|
|
18
|
-
import {
|
|
18
|
+
import { BahulamAuth } from '../auth/bahulam-auth.mjs';
|
|
19
19
|
import { ApprovalManager } from './approval.mjs';
|
|
20
20
|
// daemon wiring — headless (and `bahulam daemonize`) also starts the socket
|
|
21
21
|
// server + relay bridge when eventlog is enabled. Without this the daemon
|
|
@@ -58,7 +58,7 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
|
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
// ── Auth ──
|
|
61
|
-
const auth = new
|
|
61
|
+
const auth = new BahulamAuth();
|
|
62
62
|
const creds = auth.loadCredentials();
|
|
63
63
|
if (!creds.token) {
|
|
64
64
|
emit({ type: 'error', error: 'Not logged in. Run: bahulam login' });
|
|
@@ -99,7 +99,7 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
|
|
|
99
99
|
};
|
|
100
100
|
log(`Local mode: ${localModel}`);
|
|
101
101
|
} else {
|
|
102
|
-
client = new
|
|
102
|
+
client = new BahulamStreamClient({
|
|
103
103
|
baseUrl: creds.backendUrl,
|
|
104
104
|
token: creds.token,
|
|
105
105
|
toolExecutor,
|
|
@@ -17,13 +17,13 @@ import { randomUUID } from 'node:crypto';
|
|
|
17
17
|
import * as childProcessModule from 'node:child_process';
|
|
18
18
|
import { bahulamHome } from './paths.mjs';
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const BAHULAM_DIR = bahulamHome();
|
|
21
21
|
const FLUSH_INTERVAL_MS = 500;
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Sanitize a cwd path into a project slug for the directory name.
|
|
25
25
|
* /Users/sree/Sites/myproject → -Users-sree-Sites-myproject
|
|
26
|
-
* Mirrors the Claude Code /
|
|
26
|
+
* Mirrors the Claude Code / Bahulam CLI convention.
|
|
27
27
|
*/
|
|
28
28
|
function sanitizePath(p) {
|
|
29
29
|
return p.replace(/\//g, '-').replace(/^-/, '-');
|
|
@@ -67,7 +67,7 @@ export class JsonlWriter {
|
|
|
67
67
|
this.version = version;
|
|
68
68
|
this.sessionId = null; // set by setSessionId() when backend assigns it
|
|
69
69
|
this.slug = sanitizePath(cwd);
|
|
70
|
-
this.projectDir = path.join(
|
|
70
|
+
this.projectDir = path.join(BAHULAM_DIR, 'projects', this.slug);
|
|
71
71
|
|
|
72
72
|
// UUID chain for parent linking (cc-lens replay)
|
|
73
73
|
this.lastUuid = null;
|
|
@@ -85,7 +85,7 @@ export class JsonlWriter {
|
|
|
85
85
|
this._turnToolResults = []; // [{tool_use_id, content, is_error}, ...]
|
|
86
86
|
this._turnUsage = null;
|
|
87
87
|
this._turnModel = null;
|
|
88
|
-
this.
|
|
88
|
+
this._pendingBahulamEvents = [];
|
|
89
89
|
|
|
90
90
|
// Git branch (captured once at construction)
|
|
91
91
|
this._gitBranch = this._detectGitBranch();
|
|
@@ -101,10 +101,10 @@ export class JsonlWriter {
|
|
|
101
101
|
this.sessionId = id;
|
|
102
102
|
this._transcriptPath = path.join(this.projectDir, `${id}.jsonl`);
|
|
103
103
|
this._ready = true;
|
|
104
|
-
if (this.
|
|
105
|
-
const pending = this.
|
|
106
|
-
this.
|
|
107
|
-
for (const event of pending) this.
|
|
104
|
+
if (this._pendingBahulamEvents.length > 0) {
|
|
105
|
+
const pending = this._pendingBahulamEvents;
|
|
106
|
+
this._pendingBahulamEvents = [];
|
|
107
|
+
for (const event of pending) this.writeBahulamEvent(event);
|
|
108
108
|
}
|
|
109
109
|
// Flush any buffered entries now that we have a path
|
|
110
110
|
if (this._buffer.length > 0) this._flush();
|
|
@@ -146,7 +146,7 @@ export class JsonlWriter {
|
|
|
146
146
|
* Write a Bahulam UI/SSE event for exact-ish terminal replay on /resume.
|
|
147
147
|
* This is a Bahulam extension; cc-lens readers should ignore unknown types.
|
|
148
148
|
*/
|
|
149
|
-
|
|
149
|
+
writeBahulamEvent(event) {
|
|
150
150
|
if (!event || !event.type) return;
|
|
151
151
|
const sanitized = sanitizeEventValue({
|
|
152
152
|
type: event.type,
|
|
@@ -154,12 +154,12 @@ export class JsonlWriter {
|
|
|
154
154
|
});
|
|
155
155
|
|
|
156
156
|
if (!this.sessionId) {
|
|
157
|
-
this.
|
|
157
|
+
this._pendingBahulamEvents.push(sanitized);
|
|
158
158
|
return;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
const entry = {
|
|
162
|
-
type: '
|
|
162
|
+
type: 'bahulam_event',
|
|
163
163
|
timestamp: new Date().toISOString(),
|
|
164
164
|
cwd: this.cwd,
|
|
165
165
|
sessionId: this.sessionId,
|
|
@@ -204,8 +204,8 @@ export class JsonlWriter {
|
|
|
204
204
|
content: normalizeToolResultContent(output),
|
|
205
205
|
is_error: !!isError,
|
|
206
206
|
};
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
207
|
+
const bahulamMeta = compactToolResultMetadata(metadata);
|
|
208
|
+
if (bahulamMeta) entry.bahulam = bahulamMeta;
|
|
209
209
|
this._turnToolResults.push(entry);
|
|
210
210
|
}
|
|
211
211
|
|
|
@@ -305,7 +305,7 @@ export class JsonlWriter {
|
|
|
305
305
|
project: this.cwd,
|
|
306
306
|
sessionId: this.sessionId,
|
|
307
307
|
};
|
|
308
|
-
const historyPath = path.join(
|
|
308
|
+
const historyPath = path.join(BAHULAM_DIR, 'history.jsonl');
|
|
309
309
|
fs.promises.appendFile(historyPath, JSON.stringify(entry) + '\n', { mode: 0o600 })
|
|
310
310
|
.catch(() => {}); // best effort
|
|
311
311
|
}
|
|
@@ -393,7 +393,7 @@ export class JsonlWriter {
|
|
|
393
393
|
fs.mkdirSync(this.projectDir, { recursive: true, mode: 0o700 });
|
|
394
394
|
} catch { /* ignore */ }
|
|
395
395
|
try {
|
|
396
|
-
fs.mkdirSync(path.join(
|
|
396
|
+
fs.mkdirSync(path.join(BAHULAM_DIR, 'projects'), { recursive: true, mode: 0o700 });
|
|
397
397
|
} catch { /* ignore */ }
|
|
398
398
|
}
|
|
399
399
|
|
package/src/core/local-agent.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Local Agent — T18: Direct LLM API calls, <100ms startup, offline.
|
|
3
3
|
* Replaces the SSE backend for --local mode.
|
|
4
|
-
* Yields events matching the same format as
|
|
4
|
+
* Yields events matching the same format as BahulamStreamClient.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { ContextRetriever } from '../context/retriever.mjs';
|
|
@@ -58,15 +58,16 @@ const TOOL_SCHEMAS = [
|
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
name: 'edit_file',
|
|
61
|
-
description: 'Search for a string in a file and replace it. The
|
|
61
|
+
description: 'Search for a string in a file and replace it. The search string must match exactly, including whitespace.',
|
|
62
62
|
input_schema: {
|
|
63
63
|
type: 'object',
|
|
64
64
|
properties: {
|
|
65
65
|
file_path: { type: 'string', description: 'Path to the file' },
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
search: { type: 'string', description: 'Exact string to find in the file' },
|
|
67
|
+
replace: { type: 'string', description: 'Replacement string' },
|
|
68
|
+
replace_all: { type: 'boolean', description: 'Replace every occurrence instead of only the first match' },
|
|
68
69
|
},
|
|
69
|
-
required: ['file_path', '
|
|
70
|
+
required: ['file_path', 'search', 'replace'],
|
|
70
71
|
},
|
|
71
72
|
},
|
|
72
73
|
{
|
|
@@ -310,6 +311,26 @@ export class LocalAgent {
|
|
|
310
311
|
result = { success: false, output: `Error: ${err.message}` };
|
|
311
312
|
}
|
|
312
313
|
const durationMs = Date.now() - toolStart;
|
|
314
|
+
const resultStagnation = stagnation.recordResult(name, input || {}, result);
|
|
315
|
+
if (resultStagnation.detected) {
|
|
316
|
+
const message = stagnationMessage(name, resultStagnation.count, resultStagnation);
|
|
317
|
+
result = {
|
|
318
|
+
...result,
|
|
319
|
+
success: false,
|
|
320
|
+
output: message,
|
|
321
|
+
_stagnation: true,
|
|
322
|
+
};
|
|
323
|
+
yield {
|
|
324
|
+
type: 'stagnation',
|
|
325
|
+
data: {
|
|
326
|
+
tool: name,
|
|
327
|
+
count: resultStagnation.count,
|
|
328
|
+
message,
|
|
329
|
+
kind: resultStagnation.kind,
|
|
330
|
+
target: resultStagnation.target,
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
313
334
|
|
|
314
335
|
yield {
|
|
315
336
|
type: 'tool_done',
|
package/src/core/local-store.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mode Selector
|
|
3
3
|
*
|
|
4
|
-
* remote (default): All requests go to
|
|
4
|
+
* remote (default): All requests go to Bahulam backend.
|
|
5
5
|
* Backend handles orchestration, model selection, tool routing.
|
|
6
6
|
* User's provider and models configured via web Settings page.
|
|
7
7
|
*
|
|
@@ -23,9 +23,9 @@ export function persistProjectArtifacts(data, resources, log = () => {}) {
|
|
|
23
23
|
for (const [field, filename] of artifacts) {
|
|
24
24
|
try {
|
|
25
25
|
// Resolver honors legacy .kepler/ when it's the only dir that exists.
|
|
26
|
-
const
|
|
27
|
-
fs.mkdirSync(
|
|
28
|
-
const artifactPath = path.join(
|
|
26
|
+
const bahulamDir = projectConfigDir(resource.root);
|
|
27
|
+
fs.mkdirSync(bahulamDir, { recursive: true });
|
|
28
|
+
const artifactPath = path.join(bahulamDir, filename);
|
|
29
29
|
fs.writeFileSync(artifactPath, data[field], 'utf-8');
|
|
30
30
|
resource[field] = data[field];
|
|
31
31
|
written.push(artifactPath);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as crypto from 'node:crypto';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { loadBahulamMemory } from '../config/memory-loader.mjs';
|
|
5
5
|
import { bahulamHome, projectConfigDir } from './paths.mjs';
|
|
6
6
|
|
|
7
7
|
function sha(content) {
|
|
@@ -27,8 +27,8 @@ function readFile(filePath, label, maxChars = 12000) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function readTasks(
|
|
31
|
-
const tasksDir = path.join(
|
|
30
|
+
function readTasks(bahulamDir) {
|
|
31
|
+
const tasksDir = path.join(bahulamDir, 'tasks');
|
|
32
32
|
const files = [];
|
|
33
33
|
try {
|
|
34
34
|
if (!fs.existsSync(tasksDir)) return files;
|
|
@@ -66,9 +66,9 @@ function scanSkills(dir, scope) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export function loadProjectContext({ cwd = process.cwd(), previous = null } = {}) {
|
|
69
|
-
const
|
|
69
|
+
const bahulamDir = projectConfigDir(cwd);
|
|
70
70
|
const files = [];
|
|
71
|
-
for (const file of
|
|
71
|
+
for (const file of loadBahulamMemory({ cwd })) {
|
|
72
72
|
const label = file.path.endsWith(path.join('.bahulam', 'KEPLER.md'))
|
|
73
73
|
? 'KEPLER.md'
|
|
74
74
|
: path.basename(file.path);
|
|
@@ -83,10 +83,10 @@ export function loadProjectContext({ cwd = process.cwd(), previous = null } = {}
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
for (const name of ['config.json', 'project.md', 'style.md', 'goal.md', 'plan.md', 'hitl.md']) {
|
|
86
|
-
const file = readFile(path.join(
|
|
86
|
+
const file = readFile(path.join(bahulamDir, name), name, name.endsWith('.json') ? 4000 : 12000);
|
|
87
87
|
if (file) files.push(file);
|
|
88
88
|
}
|
|
89
|
-
files.push(...readTasks(
|
|
89
|
+
files.push(...readTasks(bahulamDir));
|
|
90
90
|
|
|
91
91
|
const previousHashes = new Map((previous?.files || []).map(f => [f.path, f.hash]));
|
|
92
92
|
const changed = files.filter(f => f.hash && previousHashes.get(f.path) && previousHashes.get(f.path) !== f.hash);
|
|
@@ -102,14 +102,14 @@ export function loadProjectContext({ cwd = process.cwd(), previous = null } = {}
|
|
|
102
102
|
|
|
103
103
|
const skills = [
|
|
104
104
|
...scanSkills(bahulamHome(), 'global'),
|
|
105
|
-
...scanSkills(
|
|
105
|
+
...scanSkills(bahulamDir, 'project'),
|
|
106
106
|
];
|
|
107
107
|
const byName = new Map();
|
|
108
108
|
for (const skill of skills) byName.set(skill.name, skill);
|
|
109
109
|
|
|
110
110
|
return {
|
|
111
111
|
root: cwd,
|
|
112
|
-
kepler_dir:
|
|
112
|
+
kepler_dir: bahulamDir,
|
|
113
113
|
files,
|
|
114
114
|
loaded,
|
|
115
115
|
changed,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Settings Sync — fetch user settings from
|
|
2
|
+
* Settings Sync — fetch user settings from Bahulam web and cache locally.
|
|
3
3
|
*
|
|
4
4
|
* Syncs: gateway_type, model preferences, configured providers.
|
|
5
5
|
* Cached in ~/.bahulam/config.json alongside auth token.
|