@bahulam/code 0.1.10 → 0.1.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.
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Plugin CLI Commands — open a workspace with a named plugin loaded.
3
+ *
4
+ * Commands:
5
+ * bahulam-code plugin <name> [path] Open workspace with plugin tools
6
+ *
7
+ * The named plugin is looked up in the standard plugin directories,
8
+ * verified to exist, then a local workspace is started at [path]
9
+ * (or the current directory) with the plugin's tools, handlers,
10
+ * and optionally sub-agents available.
11
+ */
12
+
13
+ import * as fs from 'node:fs';
14
+ import * as os from 'node:os';
15
+ import * as path from 'node:path';
16
+ import {
17
+ createLocalWorkspaceSession,
18
+ listLocalWorkspaceSessions,
19
+ loadLocalWorkspaceSession,
20
+ writeLocalWorkspaceSession,
21
+ } from '../local-service/session-store.mjs';
22
+ import { startLocalWorkspaceService } from '../local-service/server.mjs';
23
+ import { openLocalBrowser } from '../local-service/browser.mjs';
24
+
25
+ const RESET = '\x1b[0m';
26
+ const BOLD = '\x1b[1m';
27
+ const DIM = '\x1b[2m';
28
+ const CYAN = '\x1b[36m';
29
+ const GREEN = '\x1b[32m';
30
+ const YELLOW = '\x1b[33m';
31
+ const RED = '\x1b[31m';
32
+
33
+ /**
34
+ * Standard directories to search for plugins.
35
+ */
36
+ const PLUGIN_SEARCH_DIRS = [
37
+ path.join(process.cwd(), '.bahulam', 'plugins'),
38
+ path.join(os.homedir(), '.bahulam', 'plugins'),
39
+ ];
40
+
41
+ /**
42
+ * Find a plugin directory by name across all standard search paths.
43
+ * Returns the directory path and manifests on success, null on miss.
44
+ */
45
+ function findPluginDir(name) {
46
+ const needle = String(name || '').trim().toLowerCase();
47
+ if (!needle) return null;
48
+
49
+ for (const searchDir of PLUGIN_SEARCH_DIRS) {
50
+ try {
51
+ if (!fs.existsSync(searchDir)) continue;
52
+ const entries = fs.readdirSync(searchDir, { withFileTypes: true });
53
+ for (const entry of entries) {
54
+ if (!entry.isDirectory()) continue;
55
+ const pluginDir = path.join(searchDir, entry.name);
56
+ // Check both plugin.yaml and plugin.json
57
+ const yamlPath = path.join(pluginDir, 'plugin.yaml');
58
+ const jsonPath = path.join(pluginDir, 'plugin.json');
59
+ let manifestPath = null;
60
+ if (fs.existsSync(yamlPath)) manifestPath = yamlPath;
61
+ else if (fs.existsSync(jsonPath)) manifestPath = jsonPath;
62
+
63
+ if (!manifestPath) continue;
64
+
65
+ // Quick name match — match against directory name first (fast path),
66
+ // then parse the manifest for its metadata.name if needed
67
+ if (entry.name.toLowerCase() === needle) {
68
+ return { dir: pluginDir, manifestPath, searchDir };
69
+ }
70
+
71
+ // Parse manifest to check metadata.name
72
+ const raw = fs.readFileSync(manifestPath, 'utf-8');
73
+ let manifest;
74
+ try {
75
+ manifest = JSON.parse(raw);
76
+ } catch {
77
+ // Might be YAML — try simple YAML top-level name extraction
78
+ // For speed, check for `name:` line patterns
79
+ const nameMatch = raw.match(/^(?:name|metadata\.name|metadata:\s*\n\s+name)\s*:\s*(.+)$/m);
80
+ if (nameMatch) {
81
+ const metaName = nameMatch[1].trim().replace(/^["']|["']$/g, '').toLowerCase();
82
+ if (metaName === needle) {
83
+ return { dir: pluginDir, manifestPath, searchDir };
84
+ }
85
+ }
86
+ continue;
87
+ }
88
+
89
+ const metaName = (
90
+ manifest?.metadata?.name ||
91
+ manifest?.name ||
92
+ ''
93
+ ).toLowerCase();
94
+ if (metaName === needle) {
95
+ return { dir: pluginDir, manifestPath, searchDir };
96
+ }
97
+ }
98
+ } catch {
99
+ // Skip unreadable directories
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+
105
+ /**
106
+ * Main entry point for `bahulam-code plugin` subcommand.
107
+ * @param {object} args - parsed CLI args
108
+ * @param {object} [options]
109
+ * @param {string} [options.cwd]
110
+ */
111
+ export async function handlePluginCommand(args, { cwd = process.cwd() } = {}) {
112
+ const pluginName = String(args.pluginName || '').trim();
113
+ const targetPath = String(args.targetPath || cwd).trim();
114
+
115
+ if (!pluginName || args.help) {
116
+ printPluginUsage();
117
+ process.exit(args.help ? 0 : 1);
118
+ }
119
+
120
+ // 1. Find the plugin
121
+ const found = findPluginDir(pluginName);
122
+ if (!found) {
123
+ process.stderr.write(
124
+ `${RED}✗ Plugin "${pluginName}" not found.${RESET}\n` +
125
+ ` ${DIM}Searched:${RESET}\n` +
126
+ PLUGIN_SEARCH_DIRS.map(d => ` ${d}`).join('\n') + '\n' +
127
+ ` ${DIM}Create a plugin.yaml or plugin.json in one of these directories.${RESET}\n`
128
+ );
129
+ process.exit(1);
130
+ }
131
+
132
+ // 2. Verify the target path exists
133
+ let resolvedPath;
134
+ try {
135
+ resolvedPath = path.resolve(cwd, targetPath);
136
+ if (!fs.existsSync(resolvedPath)) {
137
+ process.stderr.write(
138
+ `${RED}✗ Target path does not exist: ${resolvedPath}${RESET}\n`
139
+ );
140
+ process.exit(1);
141
+ }
142
+ } catch (err) {
143
+ process.stderr.write(
144
+ `${RED}✗ Invalid target path: ${err.message}${RESET}\n`
145
+ );
146
+ process.exit(1);
147
+ }
148
+
149
+ // 3. Create a local workspace session with plugin context
150
+ const sessionTitle = `${pluginName} plugin — ${path.basename(resolvedPath) || resolvedPath}`;
151
+ const { session, token } = createLocalWorkspaceSession({
152
+ targetPath: resolvedPath,
153
+ cwd,
154
+ kind: `plugin-${pluginName}`,
155
+ title: sessionTitle,
156
+ });
157
+
158
+ // Augment session with plugin metadata so the workspace knows which
159
+ // plugin to highlight
160
+ const stored = loadLocalWorkspaceSession(session.id);
161
+ if (stored) {
162
+ stored.plugin = {
163
+ name: pluginName,
164
+ plugin_dir: found.dir,
165
+ manifest_path: found.manifestPath,
166
+ };
167
+ writeLocalWorkspaceSession(stored);
168
+ session.plugin = stored.plugin;
169
+ }
170
+
171
+ // 4. Start the workspace service
172
+ const service = await startLocalWorkspaceService({
173
+ session,
174
+ token,
175
+ port: args.port || 0,
176
+ });
177
+
178
+ // 5. Output
179
+ const url = typeof service === 'object' ? service.url : '';
180
+ if (args.json) {
181
+ process.stdout.write(
182
+ `${JSON.stringify({ ok: true, session, plugin: pluginName, url, port: service.port }, null, 2)}\n`
183
+ );
184
+ } else {
185
+ process.stderr.write(`\n${BOLD}${CYAN}Bahulam Plugin Workspace${RESET}\n`);
186
+ process.stderr.write(` ${DIM}plugin${RESET} ${pluginName}\n`);
187
+ process.stderr.write(` ${DIM}session${RESET} ${session.id}\n`);
188
+ process.stderr.write(` ${DIM}root${RESET} ${session.root_path}\n`);
189
+ process.stderr.write(` ${DIM}url${RESET} ${CYAN}${url}${RESET}\n\n`);
190
+ process.stderr.write(
191
+ `${GREEN}ready${RESET} ${DIM}Plugin workspace started at 127.0.0.1:${service.port}. Press Ctrl+C to stop.${RESET}\n`
192
+ );
193
+ }
194
+
195
+ if (args.open !== false) {
196
+ openLocalBrowser(url);
197
+ }
198
+
199
+ // 6. Wait for shutdown
200
+ await new Promise((resolve) => {
201
+ let done = false;
202
+ const stop = async () => {
203
+ if (done) return;
204
+ done = true;
205
+ await service.close();
206
+ resolve();
207
+ };
208
+ process.once('SIGINT', stop);
209
+ process.once('SIGTERM', stop);
210
+ });
211
+ }
212
+
213
+ function printPluginUsage() {
214
+ process.stderr.write(
215
+ `${BOLD}PLUGIN COMMANDS${RESET}\n` +
216
+ ` ${CYAN}bahulam plugin <name> [path]${RESET} Open a workspace with a plugin loaded\n` +
217
+ ` ${CYAN}bahulam plugin install <src>${RESET} Install from git URL, tarball, local dir, or registry name\n` +
218
+ ` ${CYAN}bahulam plugin validate <path|name>${RESET} Preflight without installing (schema + handlers + collisions)\n` +
219
+ ` ${CYAN}bahulam plugin list${RESET} List installed plugins\n` +
220
+ ` ${CYAN}bahulam plugin info <name>${RESET} Show manifest details\n` +
221
+ ` ${CYAN}bahulam plugin remove <name>${RESET} Uninstall a plugin\n` +
222
+ ` ${CYAN}bahulam plugin enable|disable <name>${RESET} Toggle without deleting\n` +
223
+ ` ${CYAN}bahulam plugin update <name>${RESET} git pull the latest for git-installed plugins\n` +
224
+ `\n` +
225
+ ` ${DIM}Install source shapes:${RESET}\n` +
226
+ ` git URL https://github.com/foo/bar.git\n` +
227
+ ` tarball URL https://.../bar-1.0.0.tgz\n` +
228
+ ` local directory ./my-plugin or /abs/path\n` +
229
+ ` registry name seo-toolkit (looked up in awesome-bahulam-plugins)\n` +
230
+ `\n` +
231
+ ` ${DIM}Options:${RESET}\n` +
232
+ ` --port <n> (open) bind a specific localhost port\n` +
233
+ ` --no-open (open) start service without opening the browser\n` +
234
+ ` --json print machine-readable JSON\n` +
235
+ ` --project install into <cwd>/.bahulam/plugins instead of ~/.bahulam/plugins\n` +
236
+ ` --global install into ~/.bahulam/plugins (default)\n` +
237
+ ` --ref <tag|branch> (install/update) pin a git ref\n` +
238
+ ` --force overwrite existing install\n` +
239
+ `\n` +
240
+ ` ${DIM}Search paths (later overrides earlier):${RESET}\n` +
241
+ PLUGIN_SEARCH_DIRS.map(d => ` ${d}`).join('\n') + '\n' +
242
+ `\n` +
243
+ ` ${DIM}Example:${RESET}\n` +
244
+ ` bahulam plugin install https://github.com/community/seo-toolkit\n` +
245
+ ` bahulam plugin seo-toolkit ~/projects/client-site\n`
246
+ );
247
+ }
@@ -116,6 +116,8 @@ export const ENV_SCHEMA = {
116
116
  // Extended: Plugins
117
117
  CLAUDE_CODE_PLUGIN_DIR: { type: 'string', description: 'Custom plugin directory' },
118
118
  CLAUDE_CODE_DISABLE_PLUGINS: { type: 'boolean', default: false, description: 'Disable all plugins' },
119
+ BAHULAM_PLUGIN_DIR: { type: 'string', description: 'Bahulam plugin directory (comma-separated paths)' },
120
+ BAHULAM_DISABLE_PLUGINS: { type: 'string', description: 'Comma-separated plugin names to disable' },
119
121
 
120
122
  // Extended: Session
121
123
  CLAUDE_CODE_SESSION_TTL: { type: 'number', default: 86400000, description: 'Session TTL in ms (default 24h)' },
@@ -1,6 +1,6 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import * as path from 'node:path';
3
- import { loadKeplerSettings } from './settings-loader.mjs';
3
+ import { loadBahulamSettings } from './settings-loader.mjs';
4
4
 
5
5
  function asArray(value) {
6
6
  if (!value) return [];
@@ -46,11 +46,11 @@ export class HookRunner {
46
46
  constructor({ cwd = process.cwd(), settings = null, sessionId = null } = {}) {
47
47
  this.cwd = cwd;
48
48
  this.sessionId = sessionId;
49
- this.settings = settings || loadKeplerSettings({ cwd }).settings;
49
+ this.settings = settings || loadBahulamSettings({ cwd }).settings;
50
50
  }
51
51
 
52
52
  reload() {
53
- this.settings = loadKeplerSettings({ cwd: this.cwd }).settings;
53
+ this.settings = loadBahulamSettings({ cwd: this.cwd }).settings;
54
54
  }
55
55
 
56
56
  hooksFor(event) {
@@ -74,11 +74,11 @@ export class HookRunner {
74
74
  const env = {
75
75
  ...process.env,
76
76
  ...(this.settings?.env || {}),
77
- KEPLER_TOOL_NAME: toolName,
78
- KEPLER_TOOL_INPUT_FILE_PATH: input.tool_input.file_path || input.tool_input.path || '',
79
- KEPLER_PROJECT_DIR: this.cwd,
80
- KEPLER_SESSION_ID: this.sessionId || '',
81
- KEPLER_TURN_ID: payload.turnId || '',
77
+ BAHULAM_TOOL_NAME: toolName,
78
+ BAHULAM_TOOL_INPUT_FILE_PATH: input.tool_input.file_path || input.tool_input.path || '',
79
+ BAHULAM_PROJECT_DIR: this.cwd,
80
+ BAHULAM_SESSION_ID: this.sessionId || '',
81
+ BAHULAM_TURN_ID: payload.turnId || '',
82
82
  };
83
83
  const result = await runCommand(hook.command, {
84
84
  cwd: path.resolve(this.cwd),
@@ -17,15 +17,19 @@ function readIfExists(filePath, maxChars = 12000) {
17
17
  }
18
18
  }
19
19
 
20
+ function readMemoryFile(dir, maxChars) {
21
+ return readIfExists(path.join(dir, 'BAHULAM.md'), maxChars);
22
+ }
23
+
20
24
  export function loadBahulamMemory({ cwd = process.cwd() } = {}) {
21
25
  const files = [];
22
- const global = readIfExists(path.join(bahulamHome(), 'KEPLER.md'));
26
+ const global = readMemoryFile(bahulamHome());
23
27
  if (global) files.push({ source: 'global', ...global });
24
28
 
25
- const topLevel = readIfExists(path.join(cwd, 'KEPLER.md'));
29
+ const topLevel = readMemoryFile(cwd);
26
30
  if (topLevel) files.push({ source: 'project-top-level', ...topLevel });
27
31
 
28
- const project = readIfExists(path.join(projectConfigDir(cwd), 'KEPLER.md'));
32
+ const project = readMemoryFile(projectConfigDir(cwd));
29
33
  if (project) files.push({ source: 'project', ...project });
30
34
 
31
35
  return files;
@@ -2,7 +2,7 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import { deepMerge } from '../core/policy-resolver.mjs';
4
4
 
5
- export const DEFAULT_KEPLER_SETTINGS = Object.freeze({
5
+ export const DEFAULT_BAHULAM_SETTINGS = Object.freeze({
6
6
  env: {},
7
7
  permissions: {
8
8
  shellAllowlist: [],
@@ -16,6 +16,7 @@ export const DEFAULT_KEPLER_SETTINGS = Object.freeze({
16
16
  },
17
17
  });
18
18
 
19
+
19
20
  function readJson(filePath) {
20
21
  try {
21
22
  if (!fs.existsSync(filePath)) return null;
@@ -25,10 +26,10 @@ function readJson(filePath) {
25
26
  }
26
27
  }
27
28
 
28
- export function loadKeplerSettings({ cwd = process.cwd() } = {}) {
29
+ export function loadBahulamSettings({ cwd = process.cwd() } = {}) {
29
30
  const base = path.join(cwd, '.bahulam');
30
31
  const layers = [
31
- { name: 'default', path: null, data: DEFAULT_KEPLER_SETTINGS },
32
+ { name: 'default', path: null, data: DEFAULT_BAHULAM_SETTINGS },
32
33
  ];
33
34
  for (const [name, file] of [
34
35
  ['project', path.join(base, 'settings.json')],
@@ -43,3 +44,4 @@ export function loadKeplerSettings({ cwd = process.cwd() } = {}) {
43
44
  for (const layer of layers) settings = deepMerge(settings, layer.data || {});
44
45
  return { settings, layers };
45
46
  }
47
+
@@ -419,8 +419,8 @@ export function loadImageAttachment(filePath, { cwd = process.cwd(), maxBytes =
419
419
  export function prepareImageAttachments(input, {
420
420
  cwd = process.cwd(),
421
421
  extraPaths = [],
422
- maxImageBytes = envInt('KEPLER_VISION_MAX_IMAGE_BYTES', DEFAULT_MAX_IMAGE_BYTES),
423
- maxTurnBytes = envInt('KEPLER_VISION_MAX_TURN_BYTES', DEFAULT_MAX_TURN_BYTES),
422
+ maxImageBytes = envInt('BAHULAM_VISION_MAX_IMAGE_BYTES', DEFAULT_MAX_IMAGE_BYTES),
423
+ maxTurnBytes = envInt('BAHULAM_VISION_MAX_TURN_BYTES', DEFAULT_MAX_TURN_BYTES),
424
424
  } = {}) {
425
425
  const parsed = parseImageReferences(input, { cwd });
426
426
  const paths = [
@@ -17,6 +17,7 @@ import { buildWorkScope, promptProjectRoots } from './work-scope.mjs';
17
17
  import { persistProjectArtifacts } from './project-artifacts.mjs';
18
18
  import { BahulamAuth } from '../auth/bahulam-auth.mjs';
19
19
  import { ApprovalManager } from './approval.mjs';
20
+ import { PluginRegistry } from '../plugins/registry.mjs';
20
21
  // daemon wiring — headless (and `bahulam daemonize`) also starts the socket
21
22
  // server + relay bridge when eventlog is enabled. Without this the daemon
22
23
  // is invisible to attach clients and to paired mobile devices.
@@ -65,8 +66,11 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
65
66
  process.exit(1);
66
67
  }
67
68
 
69
+ // Scan plugins so client_tools and client_agents are sent to the backend.
70
+ const pluginRegistry = new PluginRegistry().scan();
71
+
68
72
  // Projects are registered and indexed only when the agent requests an overview.
69
- const toolExecutor = createToolExecutor();
73
+ const toolExecutor = createToolExecutor({ pluginRegistry });
70
74
 
71
75
  // Auto-approve everything — no prompts
72
76
  const approval = new ApprovalManager({ autoApprove: true });
@@ -104,6 +108,7 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
104
108
  token: creds.token,
105
109
  toolExecutor,
106
110
  approvalManager: approval,
111
+ pluginRegistry,
107
112
  });
108
113
  }
109
114
 
@@ -10,9 +10,9 @@ import * as path from 'node:path';
10
10
  import * as readline from 'node:readline';
11
11
  import { bahulamHome } from './paths.mjs';
12
12
 
13
- const KEPLER_DIR = bahulamHome();
14
- const PROJECTS_DIR = path.join(KEPLER_DIR, 'projects');
15
- const REPLAY_EVENT_RECORD_TYPES = new Set(['bahulam_event', 'kepler_event']);
13
+ const BAHULAM_DIR = bahulamHome();
14
+ const PROJECTS_DIR = path.join(BAHULAM_DIR, 'projects');
15
+ const REPLAY_EVENT_RECORD_TYPE = 'bahulam_event';
16
16
 
17
17
  function finiteNumber(value) {
18
18
  const n = Number(value);
@@ -28,7 +28,7 @@ function firstFiniteNumber(...values) {
28
28
  }
29
29
 
30
30
  function replayEventFromRecord(record) {
31
- if (!record || !REPLAY_EVENT_RECORD_TYPES.has(record.type) || !record.event) return null;
31
+ if (!record || record.type !== REPLAY_EVENT_RECORD_TYPE || !record.event) return null;
32
32
  const event = record.event;
33
33
  if (!event || typeof event !== 'object' || !event.type) return null;
34
34
  return {
@@ -334,8 +334,7 @@ async function parseSessionMeta(filePath) {
334
334
  if (!meta.endTime || ts > meta.endTime) meta.endTime = ts;
335
335
  }
336
336
 
337
- // Bahulam replay events may carry cost / error markers. Older local
338
- // transcripts used the same payload under the legacy kepler_event type.
337
+ // Bahulam replay events may carry cost / error markers.
339
338
  const ev = replayEventFromRecord(obj);
340
339
  if (ev) {
341
340
  const data = ev.data || {};
@@ -518,7 +517,8 @@ export async function getSessionDetail(sessionId, options = {}) {
518
517
  continue;
519
518
  }
520
519
 
521
- const message = obj.message || {};
520
+ if (!obj.message || typeof obj.message !== 'object') continue;
521
+ const message = obj.message;
522
522
  entries.push({
523
523
  order: entryOrder,
524
524
  type: obj.type || null,
@@ -965,7 +965,7 @@ export async function getModelBreakdown(days = 30) {
965
965
  * @param {number} n — max entries to return (most recent first)
966
966
  */
967
967
  export function getHistory(n = 50) {
968
- const historyPath = path.join(KEPLER_DIR, 'history.jsonl');
968
+ const historyPath = path.join(BAHULAM_DIR, 'history.jsonl');
969
969
  try {
970
970
  const content = fs.readFileSync(historyPath, 'utf-8');
971
971
  const lines = content.trim().split('\n').filter(Boolean);
@@ -981,8 +981,8 @@ export function getHistory(n = 50) {
981
981
 
982
982
  export function getStorePaths() {
983
983
  return {
984
- bahulamDir: KEPLER_DIR,
984
+ bahulamDir: BAHULAM_DIR,
985
985
  projectsDir: PROJECTS_DIR,
986
- historyPath: path.join(KEPLER_DIR, 'history.jsonl'),
986
+ historyPath: path.join(BAHULAM_DIR, 'history.jsonl'),
987
987
  };
988
988
  }
@@ -16,15 +16,8 @@
16
16
  * hooks.json — project-specific hooks
17
17
  * projects.json — slug → project path mapping
18
18
  *
19
- * ── Legacy fallback ─────────────────────────────────────────────────────
20
- * Pre-rename installs stored everything under ~/.kepler/. The resolver below
21
- * prefers the new path but falls back to the legacy directory when it
22
- * exists and the new one doesn't, so existing users keep their config,
23
- * agents, workflows, and history until they explicitly migrate.
24
- *
25
19
  * Env vars:
26
- * BAHULAM_HOME preferred; explicit override for ~/.bahulam
27
- * KEPLER_HOME legacy; still honored for backward compat
20
+ * BAHULAM_HOME explicit override for ~/.bahulam
28
21
  */
29
22
 
30
23
  import * as fs from 'node:fs';
@@ -32,58 +25,14 @@ import * as path from 'node:path';
32
25
  import * as os from 'node:os';
33
26
  import * as crypto from 'node:crypto';
34
27
 
35
- const NEW_HOME_NAME = '.bahulam';
36
- const LEGACY_HOME_NAME = '.kepler';
37
-
38
- let _legacyNoticeShown = false;
39
-
40
28
  /**
41
29
  * Resolve the CLI home directory. Priority:
42
- * 1. $BAHULAM_HOME (explicit new)
43
- * 2. $KEPLER_HOME (explicit legacy prints a one-time deprecation notice)
44
- * 3. ~/.bahulam (if it exists)
45
- * 4. ~/.kepler (if it exists — prints a one-time migration hint)
46
- * 5. ~/.bahulam (fresh install, will be created on first write)
30
+ * 1. $BAHULAM_HOME (explicit override)
31
+ * 2. ~/.bahulam (standard location, created on first write)
47
32
  */
48
33
  function resolveHome() {
49
34
  if (process.env.BAHULAM_HOME) return process.env.BAHULAM_HOME;
50
- if (process.env.KEPLER_HOME) {
51
- maybeNoticeLegacyEnv();
52
- return process.env.KEPLER_HOME;
53
- }
54
- const home = os.homedir();
55
- const newPath = path.join(home, NEW_HOME_NAME);
56
- const legacyPath = path.join(home, LEGACY_HOME_NAME);
57
- try {
58
- if (fs.existsSync(newPath)) return newPath;
59
- } catch {}
60
- try {
61
- if (fs.existsSync(legacyPath)) {
62
- maybeNoticeLegacyDir(legacyPath, newPath);
63
- return legacyPath;
64
- }
65
- } catch {}
66
- return newPath;
67
- }
68
-
69
- function maybeNoticeLegacyEnv() {
70
- if (_legacyNoticeShown || process.env.B0_QUIET_MIGRATION === '1') return;
71
- _legacyNoticeShown = true;
72
- try {
73
- process.stderr.write(
74
- ' \x1b[2mnote: KEPLER_HOME is deprecated; set BAHULAM_HOME instead.\x1b[0m\n'
75
- );
76
- } catch {}
77
- }
78
-
79
- function maybeNoticeLegacyDir(legacyPath, newPath) {
80
- if (_legacyNoticeShown || process.env.B0_QUIET_MIGRATION === '1') return;
81
- _legacyNoticeShown = true;
82
- try {
83
- process.stderr.write(
84
- ` \x1b[2mnote: reading legacy ${legacyPath}. Move to ${newPath} when convenient (silence with B0_QUIET_MIGRATION=1).\x1b[0m\n`
85
- );
86
- } catch {}
35
+ return path.join(os.homedir(), '.bahulam');
87
36
  }
88
37
 
89
38
  /**
@@ -91,7 +40,6 @@ function maybeNoticeLegacyDir(legacyPath, newPath) {
91
40
  * Uses first 16 chars of SHA-256 (same as Claude Code).
92
41
  */
93
42
  export function projectHash(projectDir) {
94
- // Resolve symlinks (macOS: /tmp → /private/tmp) so the hash is stable
95
43
  let resolved = projectDir;
96
44
  try {
97
45
  resolved = fs.realpathSync(projectDir);
@@ -104,14 +52,11 @@ export function projectHash(projectDir) {
104
52
  .slice(0, 16);
105
53
  }
106
54
 
107
- /** Root ~/.bahulam/ directory (or legacy ~/.kepler/ if that's what's present). */
55
+ /** Root ~/.bahulam/ directory. */
108
56
  export function bahulamHome() {
109
57
  return resolveHome();
110
58
  }
111
59
 
112
- /** Backward-compat alias. Prefer `bahulamHome()` in new code. */
113
- export const keplerHome = bahulamHome;
114
-
115
60
  /** ~/.bahulam/projects/{hash}/ for a given project path. */
116
61
  export function projectDir(projectPath) {
117
62
  return path.join(bahulamHome(), 'projects', projectHash(projectPath));
@@ -163,20 +108,6 @@ export function historyPath() {
163
108
  }
164
109
 
165
110
  // ── daemon session paths ─────────────────────────────────────
166
- //
167
- // Daemon-owned sessions (bahulamd, detach/attach) live at:
168
- // ~/.bahulam/sessions/<sess_id>/ per-session dir
169
- // meta.json cwd, model, opened_at, ...
170
- // events.jsonl (+ events-1.jsonl, ...) append-only event log
171
- // snapshot-<seq>.json periodic compacted snapshot
172
- // approvals/ pending + decided approvals
173
- // input-lock.json who holds input right now
174
- // daemon.pid pid of the owning daemon
175
- // ~/.bahulam/sockets/<sess_id>.sock Unix socket (0600)
176
- //
177
- // These are DIFFERENT from the projects/<hash>/sessions/ archive above.
178
- // The archive is a historical index keyed on project path; daemon sessions
179
- // are keyed on session id and are the live source of truth while running.
180
111
 
181
112
  /** ~/.bahulam/sessions/ — root for daemon-owned sessions. */
182
113
  export function daemonSessionsRoot() {
@@ -188,7 +119,7 @@ export function daemonSessionDir(sessionId) {
188
119
  return path.join(daemonSessionsRoot(), sessionId);
189
120
  }
190
121
 
191
- /** ~/.bahulam/sockets/ — root for daemon Unix sockets (Phase 1). */
122
+ /** ~/.bahulam/sockets/ — root for daemon Unix sockets. */
192
123
  export function daemonSocketsDir() {
193
124
  return path.join(bahulamHome(), 'sockets');
194
125
  }
@@ -198,29 +129,12 @@ export function daemonSocketPath(sessionId) {
198
129
  return path.join(daemonSocketsDir(), `${sessionId}.sock`);
199
130
  }
200
131
 
201
- // ── Project-local config directory (.bahulam/ next to CLAUDE.md/etc) ────
202
- //
203
- // Project-scoped stuff (agents/*.yaml, memory/*.md, hooks/, settings.json,
204
- // tasks/) used to live in .kepler/ inside the project. Same resolver logic
205
- // applies — prefer .bahulam/, fall back to .kepler/ when only the legacy
206
- // dir exists.
207
-
208
- const PROJECT_NEW_NAME = '.bahulam';
209
- const PROJECT_LEGACY_NAME = '.kepler';
132
+ // ── Project-local config directory (.bahulam/ inside the project) ────
210
133
 
211
134
  /**
212
- * Resolve the project-local config directory for `cwd`. Same priority as
213
- * the home resolver. Returns an absolute path; the directory may not
214
- * exist yet (callers that write should mkdir -p first).
135
+ * Resolve the project-local config directory for `cwd`.
136
+ * Returns an absolute path; the directory may not exist yet.
215
137
  */
216
138
  export function projectConfigDir(cwd = process.cwd()) {
217
- const newPath = path.join(cwd, PROJECT_NEW_NAME);
218
- const legacyPath = path.join(cwd, PROJECT_LEGACY_NAME);
219
- try {
220
- if (fs.existsSync(newPath)) return newPath;
221
- } catch {}
222
- try {
223
- if (fs.existsSync(legacyPath)) return legacyPath;
224
- } catch {}
225
- return newPath;
139
+ return path.join(cwd, '.bahulam');
226
140
  }
@@ -5,7 +5,7 @@ import * as path from 'node:path';
5
5
  export const DEFAULT_POLICY = Object.freeze({
6
6
  version: 1,
7
7
  context: {
8
- loadEveryTurn: ['KEPLER.md', 'project.md', 'style.md', 'goal.md', 'plan.md', 'tasks/*.md'],
8
+ loadEveryTurn: ['BAHULAM.md', 'project.md', 'style.md', 'goal.md', 'plan.md', 'tasks/*.md'],
9
9
  showReloadNotice: true,
10
10
  injectCommandOptions: true,
11
11
  injectActionableTips: true,
@@ -69,8 +69,8 @@ export function loadProjectContext({ cwd = process.cwd(), previous = null } = {}
69
69
  const bahulamDir = projectConfigDir(cwd);
70
70
  const files = [];
71
71
  for (const file of loadBahulamMemory({ cwd })) {
72
- const label = file.path.endsWith(path.join('.bahulam', 'KEPLER.md'))
73
- ? 'KEPLER.md'
72
+ const label = file.path.endsWith(path.join('.bahulam', 'BAHULAM.md'))
73
+ ? 'BAHULAM.md'
74
74
  : path.basename(file.path);
75
75
  files.push({
76
76
  label,