brainclaw 0.22.1 → 0.23.0
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/dist/cli.js +26 -2
- package/dist/commands/mcp.js +3 -3
- package/dist/commands/switch.js +141 -0
- package/dist/core/active-project.js +50 -0
- package/dist/core/store-resolution.js +93 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -78,6 +78,8 @@ import { runExplore } from './commands/explore.js';
|
|
|
78
78
|
import { getInstalledBrainclawVersion } from './core/brainclaw-version.js';
|
|
79
79
|
import { cleanOrphanFiles, memoryDir } from './core/io.js';
|
|
80
80
|
import { initLogLevel, logger } from './core/logger.js';
|
|
81
|
+
import { resolveEffectiveCwd } from './core/store-resolution.js';
|
|
82
|
+
import { runSwitch } from './commands/switch.js';
|
|
81
83
|
const program = new Command();
|
|
82
84
|
function collect(value, previous) {
|
|
83
85
|
return [...previous, value];
|
|
@@ -88,12 +90,19 @@ program
|
|
|
88
90
|
.version(getInstalledBrainclawVersion())
|
|
89
91
|
.option('--verbose', 'Show info-level log messages on stderr')
|
|
90
92
|
.option('--debug', 'Show debug-level log messages on stderr')
|
|
93
|
+
.option('--cwd <path>', 'Override working directory for this invocation')
|
|
91
94
|
.hook('preAction', (_thisCommand, actionCommand) => {
|
|
92
95
|
const root = actionCommand.optsWithGlobals();
|
|
93
96
|
initLogLevel({ verbose: root.verbose, debug: root.debug });
|
|
94
|
-
|
|
97
|
+
// Resolve effective cwd (--cwd > BRAINCLAW_PROJECT > active-project > process.cwd)
|
|
98
|
+
const effectiveCwd = resolveEffectiveCwd({ explicitCwd: root.cwd });
|
|
99
|
+
if (effectiveCwd !== process.cwd()) {
|
|
100
|
+
// Store resolved cwd so commands can read it via optsWithGlobals().cwd
|
|
101
|
+
actionCommand.setOptionValue('cwd', effectiveCwd);
|
|
102
|
+
}
|
|
103
|
+
const removed = cleanOrphanFiles(memoryDir(effectiveCwd));
|
|
95
104
|
if (removed > 0) {
|
|
96
|
-
logger.info(`Cleaned ${removed} orphan lock/tmp file(s) in ${memoryDir()}`);
|
|
105
|
+
logger.info(`Cleaned ${removed} orphan lock/tmp file(s) in ${memoryDir(effectiveCwd)}`);
|
|
97
106
|
}
|
|
98
107
|
});
|
|
99
108
|
// --- init ---
|
|
@@ -1090,6 +1099,21 @@ program
|
|
|
1090
1099
|
.action((options) => {
|
|
1091
1100
|
runExplore({ query: options.query });
|
|
1092
1101
|
});
|
|
1102
|
+
program
|
|
1103
|
+
.command('switch [project]')
|
|
1104
|
+
.description('Set the active project for subsequent commands')
|
|
1105
|
+
.option('--list', 'List available projects in the workspace')
|
|
1106
|
+
.option('--clear', 'Clear the active project (revert to cwd)')
|
|
1107
|
+
.option('--json', 'Output as JSON')
|
|
1108
|
+
.action((project, options) => {
|
|
1109
|
+
const globalOpts = options.parent?.parent ? program.opts() : {};
|
|
1110
|
+
runSwitch(project, {
|
|
1111
|
+
list: options.list,
|
|
1112
|
+
clear: options.clear,
|
|
1113
|
+
json: options.json,
|
|
1114
|
+
cwd: globalOpts.cwd,
|
|
1115
|
+
});
|
|
1116
|
+
});
|
|
1093
1117
|
program.parseAsync(process.argv).catch((err) => {
|
|
1094
1118
|
console.error(err);
|
|
1095
1119
|
process.exit(1);
|
package/dist/commands/mcp.js
CHANGED
|
@@ -29,7 +29,7 @@ import { validateMcpInput, validateMcpField } from '../core/input-validation.js'
|
|
|
29
29
|
import { buildEstimationReport } from './estimation-report.js';
|
|
30
30
|
import { detectAiAgent } from '../core/ai-agent-detection.js';
|
|
31
31
|
import { checkGitPresence, scanGitRepos, parseRoots, parseRepoSelection, parseAgentSelection, runGlobalInstall, initReposAndConfigureAgents, readSetupState, ALL_KNOWN_AGENTS, } from './setup.js';
|
|
32
|
-
import { resolveTargetStore, resolveStoreChain } from '../core/store-resolution.js';
|
|
32
|
+
import { resolveEffectiveCwd, resolveTargetStore, resolveStoreChain } from '../core/store-resolution.js';
|
|
33
33
|
import { probeForQuickSetup, buildQuickSetupProbeResponse, buildOnboardingPreview } from '../core/setup-flow.js';
|
|
34
34
|
import { ensureUserStore } from '../core/setup-state.js';
|
|
35
35
|
import { readUnseenEvents, buildNotificationSummary } from '../core/event-log.js';
|
|
@@ -888,7 +888,7 @@ export class McpServerConnection {
|
|
|
888
888
|
}
|
|
889
889
|
}
|
|
890
890
|
export function runMcp() {
|
|
891
|
-
const cwd =
|
|
891
|
+
const cwd = resolveEffectiveCwd();
|
|
892
892
|
if (!memoryExists(cwd)) {
|
|
893
893
|
console.error('Project memory not initialized. Run `brainclaw init` first.');
|
|
894
894
|
process.exit(1);
|
|
@@ -991,7 +991,7 @@ function getReviewAssignee(tags) {
|
|
|
991
991
|
return undefined;
|
|
992
992
|
}
|
|
993
993
|
export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
994
|
-
const cwd = context.cwd ??
|
|
994
|
+
const cwd = context.cwd ?? resolveEffectiveCwd();
|
|
995
995
|
if (name === 'bclaw_get_context') {
|
|
996
996
|
const result = buildContext({
|
|
997
997
|
target: args.path,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { loadActiveProject, saveActiveProject, clearActiveProject } from '../core/active-project.js';
|
|
3
|
+
import { memoryExists } from '../core/io.js';
|
|
4
|
+
import { resolveProjectRef, resolveWorkspaceRoot } from '../core/store-resolution.js';
|
|
5
|
+
import { scanNestedBrainclawProjects } from '../core/workspace-projects.js';
|
|
6
|
+
import { loadConfig } from '../core/config.js';
|
|
7
|
+
export function runSwitch(projectRef, options = {}) {
|
|
8
|
+
const cwd = options.cwd ?? process.cwd();
|
|
9
|
+
const wsRoot = resolveWorkspaceRoot(cwd);
|
|
10
|
+
if (!wsRoot) {
|
|
11
|
+
console.error('Error: no brainclaw workspace found. Run `brainclaw init` first.');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
// --list: show available projects
|
|
15
|
+
if (options.list) {
|
|
16
|
+
listProjects(wsRoot, options.json ?? false);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// --clear: remove active project
|
|
20
|
+
if (options.clear) {
|
|
21
|
+
clearActiveProject(wsRoot);
|
|
22
|
+
if (options.json) {
|
|
23
|
+
console.log(JSON.stringify({ cleared: true }));
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
console.log('✔ Active project cleared. Commands will use current directory.');
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// No argument: show current active project
|
|
31
|
+
if (!projectRef) {
|
|
32
|
+
showCurrent(wsRoot, options.json ?? false);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Switch to project
|
|
36
|
+
const resolved = resolveProjectRef(projectRef, cwd);
|
|
37
|
+
if (!resolved) {
|
|
38
|
+
console.error(`Error: cannot resolve project "${projectRef}".`);
|
|
39
|
+
console.error('Use `brainclaw switch --list` to see available projects.');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
let projectName;
|
|
43
|
+
try {
|
|
44
|
+
const config = loadConfig(resolved);
|
|
45
|
+
projectName = config.project_name;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// name is optional
|
|
49
|
+
}
|
|
50
|
+
saveActiveProject(wsRoot, {
|
|
51
|
+
path: resolved,
|
|
52
|
+
name: projectName,
|
|
53
|
+
switched_at: new Date().toISOString(),
|
|
54
|
+
switched_by: process.env.BRAINCLAW_AGENT_NAME ?? process.env.USER ?? 'unknown',
|
|
55
|
+
});
|
|
56
|
+
if (options.json) {
|
|
57
|
+
console.log(JSON.stringify({ switched: true, path: resolved, name: projectName }));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const rel = path.relative(wsRoot, resolved) || '.';
|
|
61
|
+
console.log(`✔ Switched to ${projectName ? `"${projectName}" (${rel})` : rel}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function showCurrent(wsRoot, json) {
|
|
65
|
+
const active = loadActiveProject(wsRoot);
|
|
66
|
+
if (!active) {
|
|
67
|
+
if (json) {
|
|
68
|
+
console.log(JSON.stringify({ active: false }));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
console.log('No active project. Commands use current directory.');
|
|
72
|
+
console.log('Use `brainclaw switch <project>` to set one.');
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const rel = path.relative(wsRoot, active.path) || '.';
|
|
77
|
+
if (json) {
|
|
78
|
+
console.log(JSON.stringify({ active: true, ...active, relative_path: rel }));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
console.log(`Active project: ${active.name ? `"${active.name}" (${rel})` : rel}`);
|
|
82
|
+
console.log(` switched at: ${active.switched_at}`);
|
|
83
|
+
if (active.switched_by)
|
|
84
|
+
console.log(` switched by: ${active.switched_by}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function listProjects(wsRoot, json) {
|
|
88
|
+
const active = loadActiveProject(wsRoot);
|
|
89
|
+
const projects = [];
|
|
90
|
+
// Add workspace root itself
|
|
91
|
+
if (memoryExists(wsRoot)) {
|
|
92
|
+
try {
|
|
93
|
+
const config = loadConfig(wsRoot);
|
|
94
|
+
projects.push({
|
|
95
|
+
name: config.project_name,
|
|
96
|
+
path: wsRoot,
|
|
97
|
+
relative_path: '.',
|
|
98
|
+
active: active?.path === wsRoot,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
projects.push({
|
|
103
|
+
path: wsRoot,
|
|
104
|
+
relative_path: '.',
|
|
105
|
+
active: active?.path === wsRoot,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Discover child projects
|
|
110
|
+
const children = scanNestedBrainclawProjects(wsRoot, 4);
|
|
111
|
+
for (const child of children) {
|
|
112
|
+
const childPath = path.resolve(child.path);
|
|
113
|
+
if (childPath === wsRoot)
|
|
114
|
+
continue;
|
|
115
|
+
const rel = path.relative(wsRoot, childPath) || '.';
|
|
116
|
+
projects.push({
|
|
117
|
+
name: child.project_name,
|
|
118
|
+
path: childPath,
|
|
119
|
+
relative_path: rel,
|
|
120
|
+
active: active?.path === childPath,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (json) {
|
|
124
|
+
console.log(JSON.stringify({ workspace: wsRoot, projects }, null, 2));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (projects.length === 0) {
|
|
128
|
+
console.log('No brainclaw projects found in this workspace.');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
console.log(`Projects in ${wsRoot}:\n`);
|
|
132
|
+
for (const p of projects) {
|
|
133
|
+
const marker = p.active ? '→ ' : ' ';
|
|
134
|
+
const name = p.name ? `${p.name} (${p.relative_path})` : p.relative_path;
|
|
135
|
+
console.log(`${marker}${name}`);
|
|
136
|
+
}
|
|
137
|
+
if (!active) {
|
|
138
|
+
console.log('\nNo active project. Use `brainclaw switch <project>` to set one.');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=switch.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { MEMORY_DIR } from './io.js';
|
|
4
|
+
const ACTIVE_PROJECT_FILE = 'active-project.json';
|
|
5
|
+
/**
|
|
6
|
+
* Load the active project for a workspace.
|
|
7
|
+
* Returns undefined when no active project is set or the file is unreadable.
|
|
8
|
+
*/
|
|
9
|
+
export function loadActiveProject(workspaceRoot) {
|
|
10
|
+
const filePath = path.join(workspaceRoot, MEMORY_DIR, ACTIVE_PROJECT_FILE);
|
|
11
|
+
if (!fs.existsSync(filePath)) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
16
|
+
if (typeof raw.path !== 'string' || !raw.path) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
path: raw.path,
|
|
21
|
+
name: typeof raw.name === 'string' ? raw.name : undefined,
|
|
22
|
+
switched_at: typeof raw.switched_at === 'string' ? raw.switched_at : new Date().toISOString(),
|
|
23
|
+
switched_by: typeof raw.switched_by === 'string' ? raw.switched_by : undefined,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Persist the active project for a workspace.
|
|
32
|
+
*/
|
|
33
|
+
export function saveActiveProject(workspaceRoot, project) {
|
|
34
|
+
const dir = path.join(workspaceRoot, MEMORY_DIR);
|
|
35
|
+
if (!fs.existsSync(dir)) {
|
|
36
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
const filePath = path.join(dir, ACTIVE_PROJECT_FILE);
|
|
39
|
+
fs.writeFileSync(filePath, JSON.stringify(project, null, 2) + '\n', 'utf-8');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Clear the active project (revert to process.cwd() default).
|
|
43
|
+
*/
|
|
44
|
+
export function clearActiveProject(workspaceRoot) {
|
|
45
|
+
const filePath = path.join(workspaceRoot, MEMORY_DIR, ACTIVE_PROJECT_FILE);
|
|
46
|
+
if (fs.existsSync(filePath)) {
|
|
47
|
+
fs.unlinkSync(filePath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=active-project.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { loadActiveProject } from './active-project.js';
|
|
4
5
|
import { loadConfig } from './config.js';
|
|
5
6
|
import { MEMORY_DIR } from './io.js';
|
|
6
7
|
import { summarizeWorkspaceProjects } from './workspace-projects.js';
|
|
@@ -84,6 +85,98 @@ export function resolveTargetStore(cwd = process.cwd(), target = 'local', option
|
|
|
84
85
|
const match = chain.find((s) => s.role === 'user');
|
|
85
86
|
return match?.cwd ?? os.homedir();
|
|
86
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Single source of truth for the effective working directory.
|
|
90
|
+
*
|
|
91
|
+
* Priority:
|
|
92
|
+
* 1. explicitCwd (--cwd flag)
|
|
93
|
+
* 2. BRAINCLAW_PROJECT env var → resolved by name/path from workspace
|
|
94
|
+
* 3. active-project.json in workspace root
|
|
95
|
+
* 4. process.cwd()
|
|
96
|
+
*/
|
|
97
|
+
export function resolveEffectiveCwd(options = {}) {
|
|
98
|
+
// 1. Explicit --cwd flag
|
|
99
|
+
if (options.explicitCwd) {
|
|
100
|
+
return path.resolve(options.explicitCwd);
|
|
101
|
+
}
|
|
102
|
+
// 2. BRAINCLAW_PROJECT env var
|
|
103
|
+
const envProject = process.env.BRAINCLAW_PROJECT;
|
|
104
|
+
if (envProject) {
|
|
105
|
+
const resolved = resolveProjectRef(envProject, process.cwd(), options.storeChainOptions);
|
|
106
|
+
if (resolved)
|
|
107
|
+
return resolved;
|
|
108
|
+
}
|
|
109
|
+
// 3. active-project.json from workspace root
|
|
110
|
+
const wsRoot = resolveWorkspaceRoot(process.cwd(), options.storeChainOptions);
|
|
111
|
+
if (wsRoot) {
|
|
112
|
+
const active = loadActiveProject(wsRoot);
|
|
113
|
+
if (active && fs.existsSync(path.join(active.path, MEMORY_DIR, 'config.yaml'))) {
|
|
114
|
+
return active.path;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// 4. Default
|
|
118
|
+
return process.cwd();
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Find the workspace root (farthest store in the chain, or the one with
|
|
122
|
+
* role=workspace). Returns undefined when no store exists.
|
|
123
|
+
*/
|
|
124
|
+
export function resolveWorkspaceRoot(cwd = process.cwd(), options = {}) {
|
|
125
|
+
const chain = resolveStoreChain(cwd, options);
|
|
126
|
+
if (chain.length === 0)
|
|
127
|
+
return undefined;
|
|
128
|
+
const ws = chain.find((s) => s.role === 'workspace');
|
|
129
|
+
return ws?.cwd ?? chain[chain.length - 1].cwd;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Resolve a project reference (name or relative path) to an absolute path.
|
|
133
|
+
* Returns undefined when the reference cannot be resolved to a valid brainclaw project.
|
|
134
|
+
*/
|
|
135
|
+
export function resolveProjectRef(ref, cwd = process.cwd(), storeChainOptions) {
|
|
136
|
+
const wsRoot = resolveWorkspaceRoot(cwd, storeChainOptions);
|
|
137
|
+
if (!wsRoot)
|
|
138
|
+
return undefined;
|
|
139
|
+
// Try as absolute path
|
|
140
|
+
if (path.isAbsolute(ref)) {
|
|
141
|
+
return fs.existsSync(path.join(ref, MEMORY_DIR, 'config.yaml')) ? ref : undefined;
|
|
142
|
+
}
|
|
143
|
+
// Try as relative path from workspace root
|
|
144
|
+
const asPath = path.resolve(wsRoot, ref);
|
|
145
|
+
if (fs.existsSync(path.join(asPath, MEMORY_DIR, 'config.yaml'))) {
|
|
146
|
+
return asPath;
|
|
147
|
+
}
|
|
148
|
+
// Try by project name: scan child stores for matching project_name
|
|
149
|
+
const chain = resolveStoreChain(wsRoot, storeChainOptions);
|
|
150
|
+
for (const store of chain) {
|
|
151
|
+
if (store.cwd === wsRoot)
|
|
152
|
+
continue; // skip workspace itself
|
|
153
|
+
try {
|
|
154
|
+
const config = loadConfig(store.cwd);
|
|
155
|
+
if (config.project_name === ref)
|
|
156
|
+
return store.cwd;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// skip unreadable configs
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Try discovering child projects by scanning filesystem
|
|
163
|
+
try {
|
|
164
|
+
const wsConfig = loadConfig(wsRoot);
|
|
165
|
+
const summary = summarizeWorkspaceProjects(wsRoot, wsConfig);
|
|
166
|
+
for (const project of summary.discovered_projects) {
|
|
167
|
+
const projectPath = path.resolve(wsRoot, project.path);
|
|
168
|
+
if (project.project_name === ref) {
|
|
169
|
+
if (fs.existsSync(path.join(projectPath, MEMORY_DIR, 'config.yaml'))) {
|
|
170
|
+
return projectPath;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// fall through
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
87
180
|
/**
|
|
88
181
|
* Resolve the most specific child store that should answer a context request.
|
|
89
182
|
*
|