@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.
- package/package.json +5 -2
- package/src/agents/loader.mjs +26 -4
- package/src/auth/bahulam-auth.mjs +2 -13
- package/src/commands/plugin-manage.mjs +449 -0
- package/src/commands/plugin.mjs +247 -0
- package/src/config/env.mjs +2 -0
- package/src/config/hook-runner.mjs +8 -8
- package/src/config/memory-loader.mjs +7 -3
- package/src/config/settings-loader.mjs +5 -3
- package/src/core/attachments.mjs +2 -2
- package/src/core/headless.mjs +6 -1
- package/src/core/local-store.mjs +10 -10
- package/src/core/paths.mjs +10 -96
- package/src/core/policy-resolver.mjs +1 -1
- package/src/core/project-context-loader.mjs +2 -2
- package/src/core/stream-client.mjs +68 -1
- package/src/core/system-prompt.mjs +31 -12
- package/src/core/tool-executor.mjs +251 -10
- package/src/local-service/agent-relay.mjs +139 -12
- package/src/local-service/server.mjs +230 -7
- package/src/plugins/executor.mjs +121 -0
- package/src/plugins/loader.mjs +123 -123
- package/src/plugins/manifest.mjs +291 -0
- package/src/plugins/preflight.mjs +227 -0
- package/src/plugins/registry.mjs +233 -0
- package/src/plugins/state.mjs +290 -0
- package/src/terminal/agents.mjs +18 -1
- package/src/terminal/init.mjs +2 -2
- package/src/terminal/main.mjs +75 -2
- package/src/terminal/repl-explore.mjs +1 -1
- package/src/terminal/repl-render.mjs +2 -2
- package/src/terminal/repl.mjs +49 -3
- package/src/tools/analyze-image.mjs +1 -1
- package/src/tools/project-overview.mjs +7 -7
- package/src/ui/slash-commands.mjs +18 -0
|
@@ -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
|
+
}
|
package/src/config/env.mjs
CHANGED
|
@@ -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 {
|
|
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 ||
|
|
49
|
+
this.settings = settings || loadBahulamSettings({ cwd }).settings;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
reload() {
|
|
53
|
-
this.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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 =
|
|
26
|
+
const global = readMemoryFile(bahulamHome());
|
|
23
27
|
if (global) files.push({ source: 'global', ...global });
|
|
24
28
|
|
|
25
|
-
const topLevel =
|
|
29
|
+
const topLevel = readMemoryFile(cwd);
|
|
26
30
|
if (topLevel) files.push({ source: 'project-top-level', ...topLevel });
|
|
27
31
|
|
|
28
|
-
const project =
|
|
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
|
|
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
|
|
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:
|
|
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
|
+
|
package/src/core/attachments.mjs
CHANGED
|
@@ -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('
|
|
423
|
-
maxTurnBytes = envInt('
|
|
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 = [
|
package/src/core/headless.mjs
CHANGED
|
@@ -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
|
|
package/src/core/local-store.mjs
CHANGED
|
@@ -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
|
|
14
|
-
const PROJECTS_DIR = path.join(
|
|
15
|
-
const
|
|
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 ||
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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:
|
|
984
|
+
bahulamDir: BAHULAM_DIR,
|
|
985
985
|
projectsDir: PROJECTS_DIR,
|
|
986
|
-
historyPath: path.join(
|
|
986
|
+
historyPath: path.join(BAHULAM_DIR, 'history.jsonl'),
|
|
987
987
|
};
|
|
988
988
|
}
|
package/src/core/paths.mjs
CHANGED
|
@@ -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
|
|
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
|
|
43
|
-
* 2.
|
|
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
|
-
|
|
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
|
|
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
|
|
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/
|
|
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`.
|
|
213
|
-
*
|
|
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
|
-
|
|
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: ['
|
|
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', '
|
|
73
|
-
? '
|
|
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,
|