@productbrain/cli 0.1.0-beta.32 → 0.1.0-beta.34
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/__tests__/capture.test.js +2 -11
- package/dist/__tests__/capture.test.js.map +1 -1
- package/dist/__tests__/config.test.d.ts +8 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +166 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/constellation.test.js +2 -8
- package/dist/__tests__/constellation.test.js.map +1 -1
- package/dist/__tests__/fields.test.js +14 -14
- package/dist/__tests__/fields.test.js.map +1 -1
- package/dist/__tests__/handshake.test.js +9 -17
- package/dist/__tests__/handshake.test.js.map +1 -1
- package/dist/__tests__/init.test.d.ts +7 -0
- package/dist/__tests__/init.test.d.ts.map +1 -0
- package/dist/__tests__/init.test.js +146 -0
- package/dist/__tests__/init.test.js.map +1 -0
- package/dist/__tests__/login.test.js +28 -29
- package/dist/__tests__/login.test.js.map +1 -1
- package/dist/__tests__/onboarding.test.d.ts +6 -0
- package/dist/__tests__/onboarding.test.d.ts.map +1 -0
- package/dist/__tests__/onboarding.test.js +199 -0
- package/dist/__tests__/onboarding.test.js.map +1 -0
- package/dist/__tests__/promote.test.js +4 -16
- package/dist/__tests__/promote.test.js.map +1 -1
- package/dist/__tests__/prompts.test.d.ts +6 -0
- package/dist/__tests__/prompts.test.d.ts.map +1 -0
- package/dist/__tests__/prompts.test.js +146 -0
- package/dist/__tests__/prompts.test.js.map +1 -0
- package/dist/__tests__/proposals.test.js +6 -29
- package/dist/__tests__/proposals.test.js.map +1 -1
- package/dist/__tests__/relate.test.js +6 -23
- package/dist/__tests__/relate.test.js.map +1 -1
- package/dist/__tests__/runner.test.js +19 -15
- package/dist/__tests__/runner.test.js.map +1 -1
- package/dist/__tests__/session.test.js +2 -8
- package/dist/__tests__/session.test.js.map +1 -1
- package/dist/__tests__/setup.test.js +39 -25
- package/dist/__tests__/setup.test.js.map +1 -1
- package/dist/__tests__/spinner-labels.test.d.ts +2 -0
- package/dist/__tests__/spinner-labels.test.d.ts.map +1 -0
- package/dist/__tests__/spinner-labels.test.js +23 -0
- package/dist/__tests__/spinner-labels.test.js.map +1 -0
- package/dist/__tests__/update.test.js +27 -61
- package/dist/__tests__/update.test.js.map +1 -1
- package/dist/__tests__/workspace.test.d.ts +2 -0
- package/dist/__tests__/workspace.test.d.ts.map +1 -0
- package/dist/__tests__/workspace.test.js +308 -0
- package/dist/__tests__/workspace.test.js.map +1 -0
- package/dist/commands/accept.d.ts.map +1 -1
- package/dist/commands/accept.js +6 -2
- package/dist/commands/accept.js.map +1 -1
- package/dist/commands/brief.d.ts.map +1 -1
- package/dist/commands/brief.js +6 -1
- package/dist/commands/brief.js.map +1 -1
- package/dist/commands/capture.d.ts.map +1 -1
- package/dist/commands/capture.js +17 -10
- package/dist/commands/capture.js.map +1 -1
- package/dist/commands/chain-walk.d.ts.map +1 -1
- package/dist/commands/chain-walk.js +6 -1
- package/dist/commands/chain-walk.js.map +1 -1
- package/dist/commands/changes.d.ts.map +1 -1
- package/dist/commands/changes.js +6 -1
- package/dist/commands/changes.js.map +1 -1
- package/dist/commands/codex-prep.d.ts.map +1 -1
- package/dist/commands/codex-prep.js +6 -2
- package/dist/commands/codex-prep.js.map +1 -1
- package/dist/commands/collections.d.ts.map +1 -1
- package/dist/commands/collections.js +6 -2
- package/dist/commands/collections.js.map +1 -1
- package/dist/commands/constellation.d.ts.map +1 -1
- package/dist/commands/constellation.js +6 -1
- package/dist/commands/constellation.js.map +1 -1
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +6 -1
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +30 -20
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.d.ts +1 -0
- package/dist/commands/doctor.test.d.ts.map +1 -1
- package/dist/commands/doctor.test.js +54 -21
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/fields.d.ts.map +1 -1
- package/dist/commands/fields.js +6 -2
- package/dist/commands/fields.js.map +1 -1
- package/dist/commands/get.d.ts.map +1 -1
- package/dist/commands/get.js +11 -3
- package/dist/commands/get.js.map +1 -1
- package/dist/commands/handshake.d.ts.map +1 -1
- package/dist/commands/handshake.js +21 -22
- package/dist/commands/handshake.js.map +1 -1
- package/dist/commands/ingest.d.ts.map +1 -1
- package/dist/commands/ingest.js +11 -3
- package/dist/commands/ingest.js.map +1 -1
- package/dist/commands/init.d.ts +14 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +117 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +32 -20
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/promote.d.ts.map +1 -1
- package/dist/commands/promote.js +21 -7
- package/dist/commands/promote.js.map +1 -1
- package/dist/commands/reject.d.ts.map +1 -1
- package/dist/commands/reject.js +11 -5
- package/dist/commands/reject.js.map +1 -1
- package/dist/commands/relate.d.ts.map +1 -1
- package/dist/commands/relate.js +21 -10
- package/dist/commands/relate.js.map +1 -1
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +16 -6
- package/dist/commands/session.js.map +1 -1
- package/dist/commands/setup.d.ts +1 -2
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +17 -38
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +45 -27
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +11 -5
- package/dist/commands/verify.js.map +1 -1
- package/dist/commands/workspace.d.ts +41 -0
- package/dist/commands/workspace.d.ts.map +1 -0
- package/dist/commands/workspace.js +239 -0
- package/dist/commands/workspace.js.map +1 -0
- package/dist/index.js +109 -21
- package/dist/index.js.map +1 -1
- package/dist/lib/client.d.ts +2 -0
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +113 -57
- package/dist/lib/client.js.map +1 -1
- package/dist/lib/config.d.ts +57 -4
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +189 -32
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/errors.d.ts +1 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +2 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/format.d.ts +10 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +27 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/onboarding.d.ts +19 -0
- package/dist/lib/onboarding.d.ts.map +1 -0
- package/dist/lib/onboarding.js +373 -0
- package/dist/lib/onboarding.js.map +1 -0
- package/dist/lib/prompts.d.ts +38 -0
- package/dist/lib/prompts.d.ts.map +1 -0
- package/dist/lib/prompts.js +90 -0
- package/dist/lib/prompts.js.map +1 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +4 -19
- package/dist/lib/runner.js.map +1 -1
- package/dist/lib/spinner.d.ts +27 -0
- package/dist/lib/spinner.d.ts.map +1 -0
- package/dist/lib/spinner.js +76 -0
- package/dist/lib/spinner.js.map +1 -0
- package/dist/lib/spinner.test.d.ts +2 -0
- package/dist/lib/spinner.test.d.ts.map +1 -0
- package/dist/lib/spinner.test.js +39 -0
- package/dist/lib/spinner.test.js.map +1 -0
- package/dist/lib/workspace-probe.d.ts +16 -0
- package/dist/lib/workspace-probe.d.ts.map +1 -0
- package/dist/lib/workspace-probe.js +33 -0
- package/dist/lib/workspace-probe.js.map +1 -0
- package/package.json +2 -1
package/dist/lib/client.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { getConfig } from './config.js';
|
|
13
13
|
import { readSession } from './session.js';
|
|
14
14
|
import { CLIError, ErrorCode } from './errors.js';
|
|
15
|
+
import { withSpinner } from './spinner.js';
|
|
15
16
|
/**
|
|
16
17
|
* Map MCP server error codes to CLIError categories.
|
|
17
18
|
* Keeps backward compat: McpError.code is still a free string from the server,
|
|
@@ -86,43 +87,99 @@ function rethrowWithGuidance(err, siteUrl) {
|
|
|
86
87
|
* Throws on network/timeout errors so the caller can distinguish "bad key" from "can't reach server".
|
|
87
88
|
*/
|
|
88
89
|
export async function validateKey(apiKey, siteUrl, timeoutMs = 5000) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
90
|
+
return withSpinner('Validating API key', async () => {
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(`${siteUrl}/api/mcp`, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: {
|
|
97
|
+
'Content-Type': 'application/json',
|
|
98
|
+
Authorization: `Bearer ${apiKey}`,
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify({ fn: 'resolveWorkspace', args: {} }),
|
|
101
|
+
signal: controller.signal,
|
|
102
|
+
});
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
if (res.status === 401 || res.status === 403) {
|
|
105
|
+
return { valid: false, error: 'Invalid API key.' };
|
|
106
|
+
}
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
const json = (await res.json().catch(() => ({})));
|
|
109
|
+
return { valid: false, error: json.error ?? `Server error (${res.status})` };
|
|
110
|
+
}
|
|
111
|
+
const json = (await res.json());
|
|
112
|
+
if (json.error) {
|
|
113
|
+
return { valid: false, error: json.error };
|
|
114
|
+
}
|
|
115
|
+
return { valid: true, workspaceId: json.data?._id ?? 'unknown' };
|
|
104
116
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const json = (await res.json());
|
|
110
|
-
if (json.error) {
|
|
111
|
-
return { valid: false, error: json.error };
|
|
117
|
+
catch (err) {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
// Re-throw network/timeout errors — caller handles the "offline" case
|
|
120
|
+
throw err;
|
|
112
121
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Map MCP function names to human-friendly spinner labels.
|
|
126
|
+
* Unknown functions get a generic fallback.
|
|
127
|
+
*/
|
|
128
|
+
const SPINNER_LABELS = {
|
|
129
|
+
orient: 'Loading workspace overview',
|
|
130
|
+
'entries.get': 'Loading entry',
|
|
131
|
+
'entries.search': 'Searching entries',
|
|
132
|
+
'entries.create': 'Creating entry',
|
|
133
|
+
'entries.update': 'Updating entry',
|
|
134
|
+
'capture-quick': 'Capturing to Chain',
|
|
135
|
+
'propose-capture': 'Preparing capture',
|
|
136
|
+
'commit-entry': 'Committing entry',
|
|
137
|
+
'agent.startSession': 'Starting session',
|
|
138
|
+
'agent.closeSession': 'Closing session',
|
|
139
|
+
'agent.touchSession': 'Refreshing session',
|
|
140
|
+
resolveWorkspace: 'Resolving workspace',
|
|
141
|
+
'onboarding.chat': 'Thinking',
|
|
142
|
+
context: 'Loading context',
|
|
143
|
+
'entries.relate': 'Creating relation',
|
|
144
|
+
'entries.unrelate': 'Removing relation',
|
|
145
|
+
constellation: 'Loading constellation',
|
|
146
|
+
};
|
|
147
|
+
/** Get a human-friendly spinner label for an MCP function name. */
|
|
148
|
+
export function spinnerLabel(fn) {
|
|
149
|
+
return SPINNER_LABELS[fn] || `Loading ${fn.replace(/\./g, ' ')}`;
|
|
120
150
|
}
|
|
121
151
|
export async function mcpCall(fn, args = {}) {
|
|
122
152
|
const { apiKey, siteUrl } = getConfig();
|
|
123
|
-
|
|
153
|
+
const label = spinnerLabel(fn);
|
|
154
|
+
return withSpinner(label, async () => {
|
|
155
|
+
let res;
|
|
156
|
+
try {
|
|
157
|
+
res = await fetch(`${siteUrl}/api/mcp`, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: {
|
|
160
|
+
'Content-Type': 'application/json',
|
|
161
|
+
Authorization: `Bearer ${apiKey}`,
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify({ fn, args }),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
rethrowWithGuidance(err, siteUrl);
|
|
168
|
+
}
|
|
169
|
+
const json = (await res.json());
|
|
170
|
+
if (!res.ok || json.error) {
|
|
171
|
+
throw new McpError(json.error ?? res.statusText, json.code, json.details);
|
|
172
|
+
}
|
|
173
|
+
return json.data;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Raw fetch to the MCP gateway — no spinner, swallows errors.
|
|
178
|
+
* Used for fire-and-forget operations (e.g. session touch) where visible feedback is noise.
|
|
179
|
+
*/
|
|
180
|
+
async function mcpFetchRaw(fn, args, apiKey, siteUrl) {
|
|
124
181
|
try {
|
|
125
|
-
|
|
182
|
+
await fetch(`${siteUrl}/api/mcp`, {
|
|
126
183
|
method: 'POST',
|
|
127
184
|
headers: {
|
|
128
185
|
'Content-Type': 'application/json',
|
|
@@ -131,14 +188,9 @@ export async function mcpCall(fn, args = {}) {
|
|
|
131
188
|
body: JSON.stringify({ fn, args }),
|
|
132
189
|
});
|
|
133
190
|
}
|
|
134
|
-
catch
|
|
135
|
-
|
|
191
|
+
catch {
|
|
192
|
+
// Best-effort — silently swallow all errors
|
|
136
193
|
}
|
|
137
|
-
const json = (await res.json());
|
|
138
|
-
if (!res.ok || json.error) {
|
|
139
|
-
throw new McpError(json.error ?? res.statusText, json.code, json.details);
|
|
140
|
-
}
|
|
141
|
-
return json.data;
|
|
142
194
|
}
|
|
143
195
|
/**
|
|
144
196
|
* mcpCall variant that injects X-Agent-Session-Id header for write operations.
|
|
@@ -153,6 +205,7 @@ export async function mcpCall(fn, args = {}) {
|
|
|
153
205
|
export async function mcpCallWithSession(fn, args = {}) {
|
|
154
206
|
const { apiKey, siteUrl } = getConfig();
|
|
155
207
|
const session = readSession();
|
|
208
|
+
const label = spinnerLabel(fn);
|
|
156
209
|
const headers = {
|
|
157
210
|
'Content-Type': 'application/json',
|
|
158
211
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -160,27 +213,30 @@ export async function mcpCallWithSession(fn, args = {}) {
|
|
|
160
213
|
if (session?.sessionId) {
|
|
161
214
|
headers['X-Agent-Session-Id'] = session.sessionId;
|
|
162
215
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
216
|
+
const result = await withSpinner(label, async () => {
|
|
217
|
+
let res;
|
|
218
|
+
try {
|
|
219
|
+
res = await fetch(`${siteUrl}/api/mcp`, {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers,
|
|
222
|
+
body: JSON.stringify({ fn, args }),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
rethrowWithGuidance(err, siteUrl);
|
|
227
|
+
}
|
|
228
|
+
const json = (await res.json());
|
|
229
|
+
if (!res.ok || json.error) {
|
|
230
|
+
throw new McpError(json.error ?? res.statusText, json.code, json.details);
|
|
231
|
+
}
|
|
232
|
+
return json.data;
|
|
233
|
+
});
|
|
178
234
|
// Fire-and-forget session touch — best-effort, never blocks the write response.
|
|
179
|
-
//
|
|
180
|
-
//
|
|
235
|
+
// Uses mcpFetchRaw (not mcpCall) to avoid recursion and suppress the visible spinner
|
|
236
|
+
// that would appear if mcpCall were used here (background ops are silent noise).
|
|
181
237
|
if (session?.sessionId) {
|
|
182
|
-
|
|
238
|
+
void mcpFetchRaw('agent.touchSession', { sessionId: session.sessionId }, apiKey, siteUrl);
|
|
183
239
|
}
|
|
184
|
-
return
|
|
240
|
+
return result;
|
|
185
241
|
}
|
|
186
242
|
//# sourceMappingURL=client.js.map
|
package/dist/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI config — reads PRODUCTBRAIN_API_KEY + CONVEX_SITE_URL from:
|
|
3
3
|
* 1. Environment variables (always highest priority)
|
|
4
|
-
* 2.
|
|
5
|
-
* 3.
|
|
6
|
-
* 4.
|
|
4
|
+
* 2. Project config: .productbrain/config.json in CWD (WP-303 S0)
|
|
5
|
+
* 3. Profile system (~/.config/productbrain/profiles/{name}.env)
|
|
6
|
+
* 4. Legacy ~/.config/productbrain/.env (auto-migrated to profiles on first use)
|
|
7
|
+
* 5. CWD .env.mcp / packages/mcp-server/.env.mcp / .env
|
|
7
8
|
*
|
|
8
9
|
* Config loading is lazy — loadEnv() runs on first getConfig()/getConfigOrGuide() call.
|
|
9
10
|
* This enables profile switching without import-time side effects (TEN-1276).
|
|
@@ -12,6 +13,36 @@
|
|
|
12
13
|
*/
|
|
13
14
|
declare const HOME_CONFIG_DIR: string;
|
|
14
15
|
declare const HOME_ENV_PATH: string;
|
|
16
|
+
/** Project config shape — .productbrain/config.json */
|
|
17
|
+
interface ProjectConfig {
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
siteUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Read .productbrain/config.json from CWD.
|
|
23
|
+
* Returns null if file missing. Warns to stderr on invalid JSON but does not throw.
|
|
24
|
+
*/
|
|
25
|
+
declare function readProjectConfig(): ProjectConfig | null;
|
|
26
|
+
/** Config source in resolution priority order. */
|
|
27
|
+
export type ConfigSource = 'env' | 'project' | 'profile' | 'cwd' | 'default';
|
|
28
|
+
/** Result of the full config resolution cascade. */
|
|
29
|
+
export interface ResolvedConfig {
|
|
30
|
+
apiKey?: string;
|
|
31
|
+
siteUrl: string;
|
|
32
|
+
source: ConfigSource;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Run the full config resolution cascade and return a typed result.
|
|
36
|
+
* Resolution order:
|
|
37
|
+
* 1. Environment variables (highest priority)
|
|
38
|
+
* 2. Project config (.productbrain/config.json in CWD)
|
|
39
|
+
* 3. Profile system (~/.config/productbrain/profiles/{name}.env)
|
|
40
|
+
* 4. Legacy home config (~/.config/productbrain/.env)
|
|
41
|
+
* 5. CWD .env files (.env.mcp, packages/mcp-server/.env.mcp, .env)
|
|
42
|
+
*
|
|
43
|
+
* siteUrl always has a value (falls back to DEFAULT_SITE_URL).
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveConfig(): ResolvedConfig;
|
|
15
46
|
/** Reset env loaded state — used for profile switching. */
|
|
16
47
|
export declare function resetConfigCache(): void;
|
|
17
48
|
export declare function getConfig(): {
|
|
@@ -22,11 +53,33 @@ export type Config = {
|
|
|
22
53
|
apiKey: string;
|
|
23
54
|
siteUrl: string;
|
|
24
55
|
};
|
|
56
|
+
/** Discriminated union result for non-throwing config access. */
|
|
57
|
+
export type ConfigResult = {
|
|
58
|
+
ok: true;
|
|
59
|
+
apiKey: string;
|
|
60
|
+
siteUrl: string;
|
|
61
|
+
source: ConfigSource;
|
|
62
|
+
} | {
|
|
63
|
+
ok: false;
|
|
64
|
+
reason: string;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Non-throwing alternative to getConfig().
|
|
68
|
+
*
|
|
69
|
+
* Architecture: getConfig() is the primary path for commands that require auth — it
|
|
70
|
+
* throws CLIError when the key is missing, which is correct for API call paths.
|
|
71
|
+
* getConfigSafe() is for status/probe surfaces (no-args dashboard, init) that need
|
|
72
|
+
* to check config availability without crashing. Both paths are intentional and
|
|
73
|
+
* coexist by design — see WP-303 review (DW#5).
|
|
74
|
+
*
|
|
75
|
+
* Uses resolveConfig() directly (no loadEnv) to avoid env var pollution affecting source attribution.
|
|
76
|
+
*/
|
|
77
|
+
export declare function getConfigSafe(): ConfigResult;
|
|
25
78
|
/**
|
|
26
79
|
* Get config; if key is missing and stdin is a TTY, run guided flow (y/n → paste key → save → retry).
|
|
27
80
|
* @param retry Called after saving key so the original command runs in-process.
|
|
28
81
|
* @returns Config, or null if we ran retry (caller should return without running again).
|
|
29
82
|
*/
|
|
30
83
|
export declare function getConfigOrGuide(retry: () => Promise<void>): Promise<Config | null>;
|
|
31
|
-
export { HOME_CONFIG_DIR, HOME_ENV_PATH };
|
|
84
|
+
export { HOME_CONFIG_DIR, HOME_ENV_PATH, readProjectConfig };
|
|
32
85
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/lib/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAUH,QAAA,MAAM,eAAe,QAAgD,CAAC;AACtE,QAAA,MAAM,aAAa,QAAmC,CAAC;AAUvD,uDAAuD;AACvD,UAAU,aAAa;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,iBAAS,iBAAiB,IAAI,aAAa,GAAG,IAAI,CAuBjD;AAED,kDAAkD;AAClD,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,KAAK,GAAG,SAAS,CAAC;AAE7E,oDAAoD;AACpD,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,IAAI,cAAc,CAkE9C;AAiED,2DAA2D;AAC3D,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AA6DD,wBAAgB,SAAS,IAAI;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CA0B/D;AAED,MAAM,MAAM,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD,iEAAiE;AACjE,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GACnE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,IAAI,YAAY,CAiB5C;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoBzF;AAED,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,iBAAiB,EAAE,CAAC"}
|
package/dist/lib/config.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI config — reads PRODUCTBRAIN_API_KEY + CONVEX_SITE_URL from:
|
|
3
3
|
* 1. Environment variables (always highest priority)
|
|
4
|
-
* 2.
|
|
5
|
-
* 3.
|
|
6
|
-
* 4.
|
|
4
|
+
* 2. Project config: .productbrain/config.json in CWD (WP-303 S0)
|
|
5
|
+
* 3. Profile system (~/.config/productbrain/profiles/{name}.env)
|
|
6
|
+
* 4. Legacy ~/.config/productbrain/.env (auto-migrated to profiles on first use)
|
|
7
|
+
* 5. CWD .env.mcp / packages/mcp-server/.env.mcp / .env
|
|
7
8
|
*
|
|
8
9
|
* Config loading is lazy — loadEnv() runs on first getConfig()/getConfigOrGuide() call.
|
|
9
10
|
* This enables profile switching without import-time side effects (TEN-1276).
|
|
@@ -13,9 +14,10 @@
|
|
|
13
14
|
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
14
15
|
import { resolve } from 'path';
|
|
15
16
|
import { homedir } from 'os';
|
|
16
|
-
import { createInterface } from 'readline';
|
|
17
17
|
import { DEFAULT_SITE_URL, isDevMode } from './constants.js';
|
|
18
18
|
import { resolveProfileConfig } from './profiles.js';
|
|
19
|
+
import { CLIError, ErrorCode } from './errors.js';
|
|
20
|
+
import { confirm as promptConfirm, password as promptPassword } from './prompts.js';
|
|
19
21
|
const HOME_CONFIG_DIR = resolve(homedir(), '.config', 'productbrain');
|
|
20
22
|
const HOME_ENV_PATH = resolve(HOME_CONFIG_DIR, '.env');
|
|
21
23
|
const CWD_ENV_PATHS = ['.env.mcp', 'packages/mcp-server/.env.mcp', '.env'];
|
|
@@ -23,6 +25,119 @@ const CWD_ENV_PATHS = ['.env.mcp', 'packages/mcp-server/.env.mcp', '.env'];
|
|
|
23
25
|
const NON_TTY_MESSAGE = 'No API key. Set PRODUCTBRAIN_API_KEY or run pb login.';
|
|
24
26
|
/** Track whether env has been loaded to avoid redundant file reads. */
|
|
25
27
|
let envLoaded = false;
|
|
28
|
+
/**
|
|
29
|
+
* Read .productbrain/config.json from CWD.
|
|
30
|
+
* Returns null if file missing. Warns to stderr on invalid JSON but does not throw.
|
|
31
|
+
*/
|
|
32
|
+
function readProjectConfig() {
|
|
33
|
+
const configPath = resolve(process.cwd(), '.productbrain', 'config.json');
|
|
34
|
+
if (!existsSync(configPath))
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
const raw = readFileSync(configPath, 'utf8');
|
|
38
|
+
const parsed = JSON.parse(raw);
|
|
39
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
40
|
+
process.stderr.write('Warning: .productbrain/config.json is not a JSON object, skipping.\n');
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const result = {};
|
|
44
|
+
if (typeof parsed.apiKey === 'string' && parsed.apiKey) {
|
|
45
|
+
result.apiKey = parsed.apiKey;
|
|
46
|
+
}
|
|
47
|
+
if (typeof parsed.siteUrl === 'string' && parsed.siteUrl) {
|
|
48
|
+
result.siteUrl = parsed.siteUrl;
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
process.stderr.write('Warning: .productbrain/config.json contains invalid JSON, skipping.\n');
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run the full config resolution cascade and return a typed result.
|
|
59
|
+
* Resolution order:
|
|
60
|
+
* 1. Environment variables (highest priority)
|
|
61
|
+
* 2. Project config (.productbrain/config.json in CWD)
|
|
62
|
+
* 3. Profile system (~/.config/productbrain/profiles/{name}.env)
|
|
63
|
+
* 4. Legacy home config (~/.config/productbrain/.env)
|
|
64
|
+
* 5. CWD .env files (.env.mcp, packages/mcp-server/.env.mcp, .env)
|
|
65
|
+
*
|
|
66
|
+
* siteUrl always has a value (falls back to DEFAULT_SITE_URL).
|
|
67
|
+
*/
|
|
68
|
+
export function resolveConfig() {
|
|
69
|
+
// 1. Env vars — check directly, don't mutate process.env
|
|
70
|
+
const envKey = process.env.PRODUCTBRAIN_API_KEY;
|
|
71
|
+
const envSite = process.env.CONVEX_SITE_URL;
|
|
72
|
+
if (envKey) {
|
|
73
|
+
return {
|
|
74
|
+
apiKey: envKey,
|
|
75
|
+
siteUrl: (envSite || DEFAULT_SITE_URL).replace(/\/$/, ''),
|
|
76
|
+
source: 'env',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// 2. Project config
|
|
80
|
+
const projectConfig = readProjectConfig();
|
|
81
|
+
if (projectConfig?.apiKey) {
|
|
82
|
+
return {
|
|
83
|
+
apiKey: projectConfig.apiKey,
|
|
84
|
+
siteUrl: (projectConfig.siteUrl || envSite || DEFAULT_SITE_URL).replace(/\/$/, ''),
|
|
85
|
+
source: 'project',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// 3. Profile system
|
|
89
|
+
const profileConfig = resolveProfileConfig();
|
|
90
|
+
if (profileConfig?.apiKey) {
|
|
91
|
+
return {
|
|
92
|
+
apiKey: profileConfig.apiKey,
|
|
93
|
+
siteUrl: (profileConfig.siteUrl || envSite || DEFAULT_SITE_URL).replace(/\/$/, ''),
|
|
94
|
+
source: 'profile',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// 4. Legacy home config
|
|
98
|
+
if (existsSync(HOME_ENV_PATH)) {
|
|
99
|
+
const vars = parseEnvVars(readFileSync(HOME_ENV_PATH, 'utf8'));
|
|
100
|
+
if (vars['PRODUCTBRAIN_API_KEY']) {
|
|
101
|
+
return {
|
|
102
|
+
apiKey: vars['PRODUCTBRAIN_API_KEY'],
|
|
103
|
+
siteUrl: (vars['CONVEX_SITE_URL'] || envSite || DEFAULT_SITE_URL).replace(/\/$/, ''),
|
|
104
|
+
source: 'profile',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// 5. CWD .env files
|
|
109
|
+
for (const p of CWD_ENV_PATHS) {
|
|
110
|
+
const full = resolve(process.cwd(), p);
|
|
111
|
+
if (existsSync(full)) {
|
|
112
|
+
const vars = parseEnvVars(readFileSync(full, 'utf8'));
|
|
113
|
+
if (vars['PRODUCTBRAIN_API_KEY']) {
|
|
114
|
+
return {
|
|
115
|
+
apiKey: vars['PRODUCTBRAIN_API_KEY'],
|
|
116
|
+
siteUrl: (vars['CONVEX_SITE_URL'] || envSite || DEFAULT_SITE_URL).replace(/\/$/, ''),
|
|
117
|
+
source: 'cwd',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// 6. No API key found anywhere — return with siteUrl default
|
|
124
|
+
return {
|
|
125
|
+
apiKey: undefined,
|
|
126
|
+
siteUrl: (envSite || DEFAULT_SITE_URL).replace(/\/$/, ''),
|
|
127
|
+
source: 'default',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Parse env file content and return key-value pairs (without mutating process.env). */
|
|
131
|
+
function parseEnvVars(content) {
|
|
132
|
+
const vars = {};
|
|
133
|
+
for (const line of content.split('\n')) {
|
|
134
|
+
const m = line.match(/^([^#=]+)=(.*)$/);
|
|
135
|
+
if (m) {
|
|
136
|
+
vars[m[1].trim()] = m[2].trim().replace(/^["']|["']$/g, '');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return vars;
|
|
140
|
+
}
|
|
26
141
|
function parseEnvContent(content) {
|
|
27
142
|
for (const line of content.split('\n')) {
|
|
28
143
|
const m = line.match(/^([^#=]+)=(.*)$/);
|
|
@@ -35,10 +150,20 @@ function loadEnv() {
|
|
|
35
150
|
if (envLoaded)
|
|
36
151
|
return;
|
|
37
152
|
envLoaded = true;
|
|
38
|
-
// 1. Try
|
|
153
|
+
// 1. Try project config first (WP-303 S0)
|
|
154
|
+
const projectConfig = readProjectConfig();
|
|
155
|
+
if (projectConfig) {
|
|
156
|
+
if (!process.env.PRODUCTBRAIN_API_KEY && projectConfig.apiKey) {
|
|
157
|
+
process.env.PRODUCTBRAIN_API_KEY = projectConfig.apiKey;
|
|
158
|
+
}
|
|
159
|
+
if (!process.env.CONVEX_SITE_URL && projectConfig.siteUrl) {
|
|
160
|
+
process.env.CONVEX_SITE_URL = projectConfig.siteUrl;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// 2. Try profile system (handles auto-migration from legacy .env)
|
|
39
164
|
const profileConfig = resolveProfileConfig();
|
|
40
165
|
if (profileConfig) {
|
|
41
|
-
// Only set if not already set by env vars
|
|
166
|
+
// Only set if not already set by env vars or project config
|
|
42
167
|
if (!process.env.PRODUCTBRAIN_API_KEY) {
|
|
43
168
|
process.env.PRODUCTBRAIN_API_KEY = profileConfig.apiKey;
|
|
44
169
|
}
|
|
@@ -46,11 +171,11 @@ function loadEnv() {
|
|
|
46
171
|
process.env.CONVEX_SITE_URL = profileConfig.siteUrl;
|
|
47
172
|
}
|
|
48
173
|
}
|
|
49
|
-
//
|
|
174
|
+
// 3. Legacy home config (in case profiles didn't find it)
|
|
50
175
|
if (existsSync(HOME_ENV_PATH)) {
|
|
51
176
|
parseEnvContent(readFileSync(HOME_ENV_PATH, 'utf8'));
|
|
52
177
|
}
|
|
53
|
-
//
|
|
178
|
+
// 4. CWD env files
|
|
54
179
|
for (const p of CWD_ENV_PATHS) {
|
|
55
180
|
const full = resolve(process.cwd(), p);
|
|
56
181
|
if (existsSync(full)) {
|
|
@@ -63,36 +188,30 @@ function loadEnv() {
|
|
|
63
188
|
export function resetConfigCache() {
|
|
64
189
|
envLoaded = false;
|
|
65
190
|
}
|
|
66
|
-
function question(rl, prompt) {
|
|
67
|
-
return new Promise((resolve) => {
|
|
68
|
-
rl.question(prompt, (answer) => resolve((answer ?? '').trim().toLowerCase()));
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
function questionRaw(rl, prompt) {
|
|
72
|
-
return new Promise((resolve) => {
|
|
73
|
-
rl.question(prompt, (answer) => resolve((answer ?? '').trim()));
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
191
|
/**
|
|
77
192
|
* Guided flow when key is missing (TTY only). Per docs/cli-unauthenticated-user-journey.md.
|
|
78
193
|
* Returns true if we saved and ran retry; false if user said No (caller should exit).
|
|
79
194
|
*/
|
|
80
195
|
async function runGuidedFlow(retry) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (hasKey
|
|
196
|
+
const hasKey = await promptConfirm({
|
|
197
|
+
message: 'Do you have an API key from Product Brain?',
|
|
198
|
+
initialValue: false,
|
|
199
|
+
});
|
|
200
|
+
if (!hasKey) {
|
|
86
201
|
console.log('Get your key: Product Brain app → Settings → API Keys. Then run: pb login');
|
|
87
|
-
rl.close();
|
|
88
202
|
process.exit(0);
|
|
89
203
|
}
|
|
90
|
-
let apiKey = await
|
|
91
|
-
|
|
204
|
+
let apiKey = await promptPassword({
|
|
205
|
+
message: 'Paste your API key (pb_sk_...)',
|
|
206
|
+
validate: (v) => (!v ? 'Key is required' : undefined),
|
|
207
|
+
});
|
|
208
|
+
if (!apiKey.startsWith('pb_sk_')) {
|
|
92
209
|
console.log('Key must start with pb_sk_. Try again or run pb login.');
|
|
93
|
-
apiKey = await
|
|
210
|
+
apiKey = await promptPassword({
|
|
211
|
+
message: 'Paste your API key (pb_sk_...)',
|
|
212
|
+
validate: (v) => (!v || !v.startsWith('pb_sk_') ? 'Key must start with pb_sk_' : undefined),
|
|
213
|
+
});
|
|
94
214
|
}
|
|
95
|
-
rl.close();
|
|
96
215
|
if (!apiKey || !apiKey.startsWith('pb_sk_')) {
|
|
97
216
|
console.log('Get your key: Product Brain app → Settings → API Keys. Then run: pb login');
|
|
98
217
|
process.exit(0);
|
|
@@ -125,14 +244,48 @@ export function getConfig() {
|
|
|
125
244
|
const apiKey = process.env.PRODUCTBRAIN_API_KEY ?? '';
|
|
126
245
|
const siteUrl = (process.env.CONVEX_SITE_URL ?? DEFAULT_SITE_URL).replace(/\/$/, '');
|
|
127
246
|
if (!apiKey || !apiKey.startsWith('pb_sk_')) {
|
|
128
|
-
throw new
|
|
247
|
+
throw new CLIError(NON_TTY_MESSAGE, {
|
|
248
|
+
code: ErrorCode.AUTH_MISSING,
|
|
249
|
+
category: 'auth',
|
|
250
|
+
guidance: 'Set PRODUCTBRAIN_API_KEY or run `pb login`.',
|
|
251
|
+
});
|
|
129
252
|
}
|
|
130
253
|
const allowHttp = isDevMode() && siteUrl.startsWith('http://localhost');
|
|
131
254
|
if (!siteUrl.startsWith('https://') && !allowHttp) {
|
|
132
|
-
throw new
|
|
255
|
+
throw new CLIError(`CONVEX_SITE_URL must use HTTPS (got "${siteUrl.slice(0, 30)}…"). API keys must not be sent over unencrypted connections.`, {
|
|
256
|
+
code: ErrorCode.CONFIG_INVALID,
|
|
257
|
+
category: 'config',
|
|
258
|
+
guidance: 'Set CONVEX_SITE_URL to an https:// URL.',
|
|
259
|
+
});
|
|
133
260
|
}
|
|
134
261
|
return { apiKey, siteUrl };
|
|
135
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Non-throwing alternative to getConfig().
|
|
265
|
+
*
|
|
266
|
+
* Architecture: getConfig() is the primary path for commands that require auth — it
|
|
267
|
+
* throws CLIError when the key is missing, which is correct for API call paths.
|
|
268
|
+
* getConfigSafe() is for status/probe surfaces (no-args dashboard, init) that need
|
|
269
|
+
* to check config availability without crashing. Both paths are intentional and
|
|
270
|
+
* coexist by design — see WP-303 review (DW#5).
|
|
271
|
+
*
|
|
272
|
+
* Uses resolveConfig() directly (no loadEnv) to avoid env var pollution affecting source attribution.
|
|
273
|
+
*/
|
|
274
|
+
export function getConfigSafe() {
|
|
275
|
+
const resolved = resolveConfig();
|
|
276
|
+
if (!resolved.apiKey || !resolved.apiKey.startsWith('pb_sk_')) {
|
|
277
|
+
return { ok: false, reason: 'No API key. Run pb login or set PRODUCTBRAIN_API_KEY.' };
|
|
278
|
+
}
|
|
279
|
+
const siteUrl = resolved.siteUrl;
|
|
280
|
+
const allowHttp = isDevMode() && siteUrl.startsWith('http://localhost');
|
|
281
|
+
if (!siteUrl.startsWith('https://') && !allowHttp) {
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
reason: `CONVEX_SITE_URL must use HTTPS (got "${siteUrl.slice(0, 30)}…"). API keys must not be sent over unencrypted connections.`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return { ok: true, apiKey: resolved.apiKey, siteUrl, source: resolved.source };
|
|
288
|
+
}
|
|
136
289
|
/**
|
|
137
290
|
* Get config; if key is missing and stdin is a TTY, run guided flow (y/n → paste key → save → retry).
|
|
138
291
|
* @param retry Called after saving key so the original command runs in-process.
|
|
@@ -147,10 +300,14 @@ export async function getConfigOrGuide(retry) {
|
|
|
147
300
|
return { apiKey, siteUrl };
|
|
148
301
|
}
|
|
149
302
|
if (!process.stdin.isTTY) {
|
|
150
|
-
throw new
|
|
303
|
+
throw new CLIError(NON_TTY_MESSAGE, {
|
|
304
|
+
code: ErrorCode.AUTH_MISSING,
|
|
305
|
+
category: 'auth',
|
|
306
|
+
guidance: 'Set PRODUCTBRAIN_API_KEY or run `pb login`.',
|
|
307
|
+
});
|
|
151
308
|
}
|
|
152
309
|
const didRetry = await runGuidedFlow(retry);
|
|
153
310
|
return didRetry ? null : null;
|
|
154
311
|
}
|
|
155
|
-
export { HOME_CONFIG_DIR, HOME_ENV_PATH };
|
|
312
|
+
export { HOME_CONFIG_DIR, HOME_ENV_PATH, readProjectConfig };
|
|
156
313
|
//# sourceMappingURL=config.js.map
|