@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.
Files changed (169) hide show
  1. package/dist/__tests__/capture.test.js +2 -11
  2. package/dist/__tests__/capture.test.js.map +1 -1
  3. package/dist/__tests__/config.test.d.ts +8 -0
  4. package/dist/__tests__/config.test.d.ts.map +1 -0
  5. package/dist/__tests__/config.test.js +166 -0
  6. package/dist/__tests__/config.test.js.map +1 -0
  7. package/dist/__tests__/constellation.test.js +2 -8
  8. package/dist/__tests__/constellation.test.js.map +1 -1
  9. package/dist/__tests__/fields.test.js +14 -14
  10. package/dist/__tests__/fields.test.js.map +1 -1
  11. package/dist/__tests__/handshake.test.js +9 -17
  12. package/dist/__tests__/handshake.test.js.map +1 -1
  13. package/dist/__tests__/init.test.d.ts +7 -0
  14. package/dist/__tests__/init.test.d.ts.map +1 -0
  15. package/dist/__tests__/init.test.js +146 -0
  16. package/dist/__tests__/init.test.js.map +1 -0
  17. package/dist/__tests__/login.test.js +28 -29
  18. package/dist/__tests__/login.test.js.map +1 -1
  19. package/dist/__tests__/onboarding.test.d.ts +6 -0
  20. package/dist/__tests__/onboarding.test.d.ts.map +1 -0
  21. package/dist/__tests__/onboarding.test.js +199 -0
  22. package/dist/__tests__/onboarding.test.js.map +1 -0
  23. package/dist/__tests__/promote.test.js +4 -16
  24. package/dist/__tests__/promote.test.js.map +1 -1
  25. package/dist/__tests__/prompts.test.d.ts +6 -0
  26. package/dist/__tests__/prompts.test.d.ts.map +1 -0
  27. package/dist/__tests__/prompts.test.js +146 -0
  28. package/dist/__tests__/prompts.test.js.map +1 -0
  29. package/dist/__tests__/proposals.test.js +6 -29
  30. package/dist/__tests__/proposals.test.js.map +1 -1
  31. package/dist/__tests__/relate.test.js +6 -23
  32. package/dist/__tests__/relate.test.js.map +1 -1
  33. package/dist/__tests__/runner.test.js +19 -15
  34. package/dist/__tests__/runner.test.js.map +1 -1
  35. package/dist/__tests__/session.test.js +2 -8
  36. package/dist/__tests__/session.test.js.map +1 -1
  37. package/dist/__tests__/setup.test.js +39 -25
  38. package/dist/__tests__/setup.test.js.map +1 -1
  39. package/dist/__tests__/spinner-labels.test.d.ts +2 -0
  40. package/dist/__tests__/spinner-labels.test.d.ts.map +1 -0
  41. package/dist/__tests__/spinner-labels.test.js +23 -0
  42. package/dist/__tests__/spinner-labels.test.js.map +1 -0
  43. package/dist/__tests__/update.test.js +27 -61
  44. package/dist/__tests__/update.test.js.map +1 -1
  45. package/dist/__tests__/workspace.test.d.ts +2 -0
  46. package/dist/__tests__/workspace.test.d.ts.map +1 -0
  47. package/dist/__tests__/workspace.test.js +308 -0
  48. package/dist/__tests__/workspace.test.js.map +1 -0
  49. package/dist/commands/accept.d.ts.map +1 -1
  50. package/dist/commands/accept.js +6 -2
  51. package/dist/commands/accept.js.map +1 -1
  52. package/dist/commands/brief.d.ts.map +1 -1
  53. package/dist/commands/brief.js +6 -1
  54. package/dist/commands/brief.js.map +1 -1
  55. package/dist/commands/capture.d.ts.map +1 -1
  56. package/dist/commands/capture.js +17 -10
  57. package/dist/commands/capture.js.map +1 -1
  58. package/dist/commands/chain-walk.d.ts.map +1 -1
  59. package/dist/commands/chain-walk.js +6 -1
  60. package/dist/commands/chain-walk.js.map +1 -1
  61. package/dist/commands/changes.d.ts.map +1 -1
  62. package/dist/commands/changes.js +6 -1
  63. package/dist/commands/changes.js.map +1 -1
  64. package/dist/commands/codex-prep.d.ts.map +1 -1
  65. package/dist/commands/codex-prep.js +6 -2
  66. package/dist/commands/codex-prep.js.map +1 -1
  67. package/dist/commands/collections.d.ts.map +1 -1
  68. package/dist/commands/collections.js +6 -2
  69. package/dist/commands/collections.js.map +1 -1
  70. package/dist/commands/constellation.d.ts.map +1 -1
  71. package/dist/commands/constellation.js +6 -1
  72. package/dist/commands/constellation.js.map +1 -1
  73. package/dist/commands/context.d.ts.map +1 -1
  74. package/dist/commands/context.js +6 -1
  75. package/dist/commands/context.js.map +1 -1
  76. package/dist/commands/doctor.d.ts.map +1 -1
  77. package/dist/commands/doctor.js +30 -20
  78. package/dist/commands/doctor.js.map +1 -1
  79. package/dist/commands/doctor.test.d.ts +1 -0
  80. package/dist/commands/doctor.test.d.ts.map +1 -1
  81. package/dist/commands/doctor.test.js +54 -21
  82. package/dist/commands/doctor.test.js.map +1 -1
  83. package/dist/commands/fields.d.ts.map +1 -1
  84. package/dist/commands/fields.js +6 -2
  85. package/dist/commands/fields.js.map +1 -1
  86. package/dist/commands/get.d.ts.map +1 -1
  87. package/dist/commands/get.js +11 -3
  88. package/dist/commands/get.js.map +1 -1
  89. package/dist/commands/handshake.d.ts.map +1 -1
  90. package/dist/commands/handshake.js +21 -22
  91. package/dist/commands/handshake.js.map +1 -1
  92. package/dist/commands/ingest.d.ts.map +1 -1
  93. package/dist/commands/ingest.js +11 -3
  94. package/dist/commands/ingest.js.map +1 -1
  95. package/dist/commands/init.d.ts +14 -0
  96. package/dist/commands/init.d.ts.map +1 -0
  97. package/dist/commands/init.js +117 -0
  98. package/dist/commands/init.js.map +1 -0
  99. package/dist/commands/login.d.ts.map +1 -1
  100. package/dist/commands/login.js +32 -20
  101. package/dist/commands/login.js.map +1 -1
  102. package/dist/commands/promote.d.ts.map +1 -1
  103. package/dist/commands/promote.js +21 -7
  104. package/dist/commands/promote.js.map +1 -1
  105. package/dist/commands/reject.d.ts.map +1 -1
  106. package/dist/commands/reject.js +11 -5
  107. package/dist/commands/reject.js.map +1 -1
  108. package/dist/commands/relate.d.ts.map +1 -1
  109. package/dist/commands/relate.js +21 -10
  110. package/dist/commands/relate.js.map +1 -1
  111. package/dist/commands/session.d.ts.map +1 -1
  112. package/dist/commands/session.js +16 -6
  113. package/dist/commands/session.js.map +1 -1
  114. package/dist/commands/setup.d.ts +1 -2
  115. package/dist/commands/setup.d.ts.map +1 -1
  116. package/dist/commands/setup.js +17 -38
  117. package/dist/commands/setup.js.map +1 -1
  118. package/dist/commands/update.d.ts.map +1 -1
  119. package/dist/commands/update.js +45 -27
  120. package/dist/commands/update.js.map +1 -1
  121. package/dist/commands/verify.d.ts.map +1 -1
  122. package/dist/commands/verify.js +11 -5
  123. package/dist/commands/verify.js.map +1 -1
  124. package/dist/commands/workspace.d.ts +41 -0
  125. package/dist/commands/workspace.d.ts.map +1 -0
  126. package/dist/commands/workspace.js +239 -0
  127. package/dist/commands/workspace.js.map +1 -0
  128. package/dist/index.js +109 -21
  129. package/dist/index.js.map +1 -1
  130. package/dist/lib/client.d.ts +2 -0
  131. package/dist/lib/client.d.ts.map +1 -1
  132. package/dist/lib/client.js +113 -57
  133. package/dist/lib/client.js.map +1 -1
  134. package/dist/lib/config.d.ts +57 -4
  135. package/dist/lib/config.d.ts.map +1 -1
  136. package/dist/lib/config.js +189 -32
  137. package/dist/lib/config.js.map +1 -1
  138. package/dist/lib/errors.d.ts +1 -0
  139. package/dist/lib/errors.d.ts.map +1 -1
  140. package/dist/lib/errors.js +2 -0
  141. package/dist/lib/errors.js.map +1 -1
  142. package/dist/lib/format.d.ts +10 -0
  143. package/dist/lib/format.d.ts.map +1 -0
  144. package/dist/lib/format.js +27 -0
  145. package/dist/lib/format.js.map +1 -0
  146. package/dist/lib/onboarding.d.ts +19 -0
  147. package/dist/lib/onboarding.d.ts.map +1 -0
  148. package/dist/lib/onboarding.js +373 -0
  149. package/dist/lib/onboarding.js.map +1 -0
  150. package/dist/lib/prompts.d.ts +38 -0
  151. package/dist/lib/prompts.d.ts.map +1 -0
  152. package/dist/lib/prompts.js +90 -0
  153. package/dist/lib/prompts.js.map +1 -0
  154. package/dist/lib/runner.d.ts.map +1 -1
  155. package/dist/lib/runner.js +4 -19
  156. package/dist/lib/runner.js.map +1 -1
  157. package/dist/lib/spinner.d.ts +27 -0
  158. package/dist/lib/spinner.d.ts.map +1 -0
  159. package/dist/lib/spinner.js +76 -0
  160. package/dist/lib/spinner.js.map +1 -0
  161. package/dist/lib/spinner.test.d.ts +2 -0
  162. package/dist/lib/spinner.test.d.ts.map +1 -0
  163. package/dist/lib/spinner.test.js +39 -0
  164. package/dist/lib/spinner.test.js.map +1 -0
  165. package/dist/lib/workspace-probe.d.ts +16 -0
  166. package/dist/lib/workspace-probe.d.ts.map +1 -0
  167. package/dist/lib/workspace-probe.js +33 -0
  168. package/dist/lib/workspace-probe.js.map +1 -0
  169. package/package.json +2 -1
@@ -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
- const controller = new AbortController();
90
- const timer = setTimeout(() => controller.abort(), timeoutMs);
91
- try {
92
- const res = await fetch(`${siteUrl}/api/mcp`, {
93
- method: 'POST',
94
- headers: {
95
- 'Content-Type': 'application/json',
96
- Authorization: `Bearer ${apiKey}`,
97
- },
98
- body: JSON.stringify({ fn: 'resolveWorkspace', args: {} }),
99
- signal: controller.signal,
100
- });
101
- clearTimeout(timer);
102
- if (res.status === 401 || res.status === 403) {
103
- return { valid: false, error: 'Invalid API key.' };
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
- if (!res.ok) {
106
- const json = (await res.json().catch(() => ({})));
107
- return { valid: false, error: json.error ?? `Server error (${res.status})` };
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
- return { valid: true, workspaceId: json.data?._id ?? 'unknown' };
114
- }
115
- catch (err) {
116
- clearTimeout(timer);
117
- // Re-throw network/timeout errors caller handles the "offline" case
118
- throw err;
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
- let res;
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
- res = await fetch(`${siteUrl}/api/mcp`, {
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 (err) {
135
- rethrowWithGuidance(err, siteUrl);
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
- let res;
164
- try {
165
- res = await fetch(`${siteUrl}/api/mcp`, {
166
- method: 'POST',
167
- headers,
168
- body: JSON.stringify({ fn, args }),
169
- });
170
- }
171
- catch (err) {
172
- rethrowWithGuidance(err, siteUrl);
173
- }
174
- const json = (await res.json());
175
- if (!res.ok || json.error) {
176
- throw new McpError(json.error ?? res.statusText, json.code, json.details);
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
- // Only touch when a session is active. Uses mcpCall (not mcpCallWithSession) to
180
- // avoid recursion.
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
- mcpCall('agent.touchSession', { sessionId: session.sessionId }).catch(() => { });
238
+ void mcpFetchRaw('agent.touchSession', { sessionId: session.sessionId }, apiKey, siteUrl);
183
239
  }
184
- return json.data;
240
+ return result;
185
241
  }
186
242
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"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"}
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;AAElD,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAY3C;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,IAAa;IACtC,IAAI,CAAC,IAAI;QAAE,OAAO,UAAU,CAAC;IAC7B,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,IAAI,KAAK,cAAc;QAAE,OAAO,MAAM,CAAC;IACtE,IAAI,IAAI,KAAK,mBAAmB;QAAE,OAAO,YAAY,CAAC;IACtD,IAAI,IAAI,KAAK,kBAAkB;QAAE,OAAO,SAAS,CAAC;IAClD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,4FAA4F;AAC5F,MAAM,OAAO,QAAS,SAAQ,QAAQ;IACpC,OAAO,CAA2B;IAElC,YAAY,OAAe,EAAE,IAAa,EAAE,OAAiC;QAC3E,KAAK,CAAC,OAAO,EAAE;YACb,IAAI,EAAG,IAAuB,IAAI,SAAS,CAAC,QAAQ;YACpD,QAAQ,EAAE,iBAAiB,CAAC,IAAI,CAAC;SAClC,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF;AAED,qEAAqE;AACrE,2EAA2E;AAC3E,yEAAyE;AACzE,iDAAiD;AACjD,IAAI,CAAC;IACH,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;IAClE,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,EAAE,UAAU,EAAE,mBAAmB,EAAE,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;QACnE,mBAAmB,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,CAAC;AACH,CAAC;AAAC,MAAM,CAAC;IACP,8EAA8E;AAChF,CAAC;AAED,yEAAyE;AACzE,SAAS,mBAAmB,CAAC,GAAY,EAAE,OAAe;IACxD,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7D,IAAI,GAAG,KAAK,cAAc,IAAI,GAAG,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;QACxF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC;QACvC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;QACjE,MAAM,SAAS,GAAG,KAAK;YACrB,CAAC,CAAC,0BAA0B,KAAK,0CAA0C;gBACzE,gEAAgE;gBAChE,+DAA+D;YACjE,CAAC,CAAC,oFAAoF;gBACpF,uDAAuD;gBACvD,aAAa,IAAI,wCAAwC;gBACzD,qDAAqD;gBACrD,uEAAuE,CAAC;QAC5E,MAAM,IAAI,QAAQ,CAAC,mBAAmB,IAAI,GAAG,EAAE;YAC7C,IAAI,EAAE,SAAS,CAAC,mBAAmB;YACnC,QAAQ,EAAE,SAAS;YACnB,QAAQ,EAAE,SAAS;SACpB,CAAC,CAAC;IACL,CAAC;IACD,MAAM,GAAG,CAAC;AACZ,CAAC;AAOD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAc,EACd,OAAe,EACf,SAAS,GAAG,IAAI;IAEhB,OAAO,WAAW,CAAC,oBAAoB,EAAE,KAAK,IAAI,EAAE;QAClD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAC;QAE9D,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,UAAU,EAAE;gBAC5C,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,aAAa,EAAE,UAAU,MAAM,EAAE;iBAClC;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;gBAC1D,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YAEH,YAAY,CAAC,KAAK,CAAC,CAAC;YAEpB,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBAC7C,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC;YACrD,CAAC;YAED,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAuB,CAAC;gBACxE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,iBAAiB,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC;YAC/E,CAAC;YAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAgD,CAAC;YAC/E,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;gBACf,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;YAC7C,CAAC;YAED,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,SAAS,EAAE,CAAC;QACnE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,sEAAsE;YACtE,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,cAAc,GAA2B;IAC7C,MAAM,EAAE,4BAA4B;IACpC,aAAa,EAAE,eAAe;IAC9B,gBAAgB,EAAE,mBAAmB;IACrC,gBAAgB,EAAE,gBAAgB;IAClC,gBAAgB,EAAE,gBAAgB;IAClC,eAAe,EAAE,oBAAoB;IACrC,iBAAiB,EAAE,mBAAmB;IACtC,cAAc,EAAE,kBAAkB;IAClC,oBAAoB,EAAE,kBAAkB;IACxC,oBAAoB,EAAE,iBAAiB;IACvC,oBAAoB,EAAE,oBAAoB;IAC1C,gBAAgB,EAAE,qBAAqB;IACvC,iBAAiB,EAAE,UAAU;IAC7B,OAAO,EAAE,iBAAiB;IAC1B,gBAAgB,EAAE,mBAAmB;IACrC,kBAAkB,EAAE,mBAAmB;IACvC,aAAa,EAAE,uBAAuB;CACvC,CAAC;AAEF,mEAAmE;AACnE,MAAM,UAAU,YAAY,CAAC,EAAU;IACrC,OAAO,cAAc,CAAC,EAAE,CAAC,IAAI,WAAW,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;AACnE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,EAAU,EACV,OAAgC,EAAE;IAElC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS,EAAE,CAAC;IACxC,MAAM,KAAK,GAAG,YAAY,CAAC,EAAE,CAAC,CAAC;IAE/B,OAAO,WAAW,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;QACnC,IAAI,GAAa,CAAC;QAClB,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,UAAU,EAAE;gBACtC,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,aAAa,EAAE,UAAU,MAAM,EAAE;iBAClC;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;aACnC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,mBAAmB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACpC,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAmF,CAAC;QAClH,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC1B,MAAM,IAAI,QAAQ,CAAC,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5E,CAAC;QACD,OAAO,IAAI,CAAC,IAAS,CAAC;IACxB,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,KAAK,UAAU,WAAW,CACxB,EAAU,EACV,IAA6B,EAC7B,MAAc,EACd,OAAe;IAEf,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,GAAG,OAAO,UAAU,EAAE;YAChC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,aAAa,EAAE,UAAU,MAAM,EAAE;aAClC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;SACnC,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,4CAA4C;IAC9C,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,EAAU,EACV,OAAgC,EAAE;IAElC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS,EAAE,CAAC;IACxC,MAAM,OAAO,GAAG,WAAW,EAAE,CAAC;IAC9B,MAAM,KAAK,GAAG,YAAY,CAAC,EAAE,CAAC,CAAC;IAE/B,MAAM,OAAO,GAA2B;QACtC,cAAc,EAAE,kBAAkB;QAClC,aAAa,EAAE,UAAU,MAAM,EAAE;KAClC,CAAC;IACF,IAAI,OAAO,EAAE,SAAS,EAAE,CAAC;QACvB,OAAO,CAAC,oBAAoB,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC;IACpD,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,WAAW,CAAI,KAAK,EAAE,KAAK,IAAI,EAAE;QACpD,IAAI,GAAa,CAAC;QAClB,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,UAAU,EAAE;gBACtC,MAAM,EAAE,MAAM;gBACd,OAAO;gBACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;aACnC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,mBAAmB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACpC,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAmF,CAAC;QAClH,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC1B,MAAM,IAAI,QAAQ,CAAC,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5E,CAAC;QACD,OAAO,IAAI,CAAC,IAAS,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,gFAAgF;IAChF,qFAAqF;IACrF,iFAAiF;IACjF,IAAI,OAAO,EAAE,SAAS,EAAE,CAAC;QACvB,KAAK,WAAW,CAAC,oBAAoB,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5F,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -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. Profile system (~/.config/productbrain/profiles/{name}.env)
5
- * 3. Legacy ~/.config/productbrain/.env (auto-migrated to profiles on first use)
6
- * 4. CWD .env.mcp / packages/mcp-server/.env.mcp / .env
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
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH,QAAA,MAAM,eAAe,QAAgD,CAAC;AACtE,QAAA,MAAM,aAAa,QAAmC,CAAC;AAkDvD,2DAA2D;AAC3D,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAsED,wBAAgB,SAAS,IAAI;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAiB/D;AAED,MAAM,MAAM,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgBzF;AAED,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,CAAC"}
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"}
@@ -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. Profile system (~/.config/productbrain/profiles/{name}.env)
5
- * 3. Legacy ~/.config/productbrain/.env (auto-migrated to profiles on first use)
6
- * 4. CWD .env.mcp / packages/mcp-server/.env.mcp / .env
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 profile system first (handles auto-migration from legacy .env)
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 (env vars always win)
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
- // 2. Legacy home config (in case profiles didn't find it)
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
- // 3. CWD env files
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 rl = createInterface({ input: process.stdin, output: process.stdout });
82
- console.log('Do you have an API key from Product Brain? (y/n)');
83
- console.log(' Get one: Product Brain app → Settings → API Keys');
84
- const hasKey = await question(rl, '> ');
85
- if (hasKey !== 'y' && hasKey !== 'yes') {
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 questionRaw(rl, 'Paste your API key (pb_sk_...): ');
91
- if (!apiKey || !apiKey.startsWith('pb_sk_')) {
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 questionRaw(rl, 'Paste your API key (pb_sk_...): ');
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 Error(NON_TTY_MESSAGE);
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 Error(`CONVEX_SITE_URL must use HTTPS (got "${siteUrl.slice(0, 30)}…"). API keys must not be sent over unencrypted connections.`);
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 Error(NON_TTY_MESSAGE);
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