@bike4mind/cli 0.18.5 → 0.20.1
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/LICENSE +1 -1
- package/README.md +204 -35
- package/bin/bike4mind-cli.mjs +137 -24
- package/bin/hearth-hook.mjs +292 -0
- package/dist/AgentHistoryStore-BQiATPsQ.mjs +35755 -0
- package/dist/ApiClient-BPmlalut.mjs +277 -0
- package/dist/{ConfigStore-D39UqFnY.mjs → ConfigStore-CNfbeaJf.mjs} +6702 -4122
- package/dist/{ImageStore-BVmEG1xc.mjs → ImageStore-kVo-oHoS.mjs} +2 -2
- package/dist/PluginStore-DwvOJ-G3.mjs +206 -0
- package/dist/ProxyManager-Bqr7Lmsd.mjs +3 -0
- package/dist/{ProxyManager-CV94yZUW.mjs → ProxyManager-C5H0pUyK.mjs} +2 -2
- package/dist/{SandboxOrchestrator-BS6gALNq.mjs → SandboxOrchestrator-BFPVpmB5.mjs} +1 -1
- package/dist/{SandboxOrchestrator-BoINxbX4.mjs → SandboxOrchestrator-C8uleDn2.mjs} +7 -7
- package/dist/ShellSessionManager-6o8KZzl1-vrbPAUTq.mjs +252 -0
- package/dist/{ViolationLogStore-B-plqJfn.mjs → ViolationLogStore-byEhxa2A.mjs} +1 -1
- package/dist/WorkItemsClient-Cow6nXx7.mjs +382 -0
- package/dist/{bashExecute-B1N1lMOS-TZVDbcQ4.mjs → bashExecute-CrdPpBqk-DCATrE-D.mjs} +116 -16
- package/dist/buildAgent-DwPvcTpz.mjs +824 -0
- package/dist/commands/acpCommand.mjs +798 -0
- package/dist/commands/apiCommand.mjs +14 -16
- package/dist/commands/doctorCommand.mjs +5 -5
- package/dist/commands/envCommand.mjs +1 -1
- package/dist/commands/headlessCommand.mjs +272 -76
- package/dist/commands/mcpCommand.mjs +14 -1
- package/dist/commands/pluginCommand.mjs +232 -0
- package/dist/commands/updateCommand.mjs +10 -9
- package/dist/{grepSearch-DJs-cubo-Bm0Y8oS3.mjs → grepSearch-BaYUfIYs-C-fxWc9G.mjs} +3 -3
- package/dist/index.mjs +3284 -2307
- package/dist/{package-I_v_WFUn.mjs → package-CxHSRXdp.mjs} +1 -1
- package/dist/serve-Du3HiqAH.mjs +772 -0
- package/dist/store-BG3e54c8.mjs +3 -0
- package/dist/{store-DV5s-qni.mjs → store-CvjTpQPs.mjs} +70 -3
- package/dist/{terminalSetup-BbJt04ZG.mjs → terminalSetup-DjXAwpDy.mjs} +2 -3
- package/dist/{treeSitterEngine-BRbQ9b7I.mjs → treeSitterEngine-QBE3YkmG.mjs} +51 -1
- package/dist/{updateChecker-C8xsNY2L.mjs → updateChecker-CQW8bxo6.mjs} +10 -10
- package/package.json +48 -43
- package/dist/BackgroundAgentManager-D-xsWd3C.mjs +0 -27303
- package/dist/ProxyManager-ByuAHFMq.mjs +0 -3
- package/dist/store-DgzCTRkN.mjs +0 -3
- package/dist/utils-Cdktpk_k.mjs +0 -158
- package/dist/utils-DEizxshI.mjs +0 -3
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as logger, t as ConfigStore } from "./ConfigStore-CNfbeaJf.mjs";
|
|
3
|
+
import { t as version } from "./package-CxHSRXdp.mjs";
|
|
4
|
+
import axios, { isAxiosError } from "axios";
|
|
5
|
+
//#region src/auth/OAuthClient.ts
|
|
6
|
+
/**
|
|
7
|
+
* OAuth 2.0 Device Authorization Flow client
|
|
8
|
+
* Implements RFC 8628 for CLI authentication
|
|
9
|
+
*/
|
|
10
|
+
var OAuthClient = class {
|
|
11
|
+
constructor(baseURL = "http://localhost:3000") {
|
|
12
|
+
this.clientId = "b4m-cli";
|
|
13
|
+
this.apiClient = axios.create({
|
|
14
|
+
baseURL,
|
|
15
|
+
headers: { "Content-Type": "application/json" }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Initiate device authorization flow
|
|
20
|
+
* Returns device code, user code, and verification URL
|
|
21
|
+
*/
|
|
22
|
+
async initiateDeviceFlow() {
|
|
23
|
+
return (await this.apiClient.post("/api/oauth/device/initiate", { client_id: this.clientId })).data;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Poll for access token
|
|
27
|
+
* Returns token response if approved, or throws error with status
|
|
28
|
+
*/
|
|
29
|
+
async pollForToken(deviceCode) {
|
|
30
|
+
try {
|
|
31
|
+
const response = await this.apiClient.post("/api/oauth/device/token", {
|
|
32
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
33
|
+
device_code: deviceCode,
|
|
34
|
+
client_id: this.clientId
|
|
35
|
+
}, { validateStatus: () => true });
|
|
36
|
+
if ("error" in response.data) throw new Error(response.data.error);
|
|
37
|
+
return response.data;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (axios.isAxiosError(error) && error.response?.data?.error) throw new Error(error.response.data.error);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Wait for user authorization with automatic polling
|
|
45
|
+
* Implements exponential backoff and respects server's interval
|
|
46
|
+
*/
|
|
47
|
+
async waitForAuthorization(deviceCode, interval, onStatus) {
|
|
48
|
+
let pollInterval = interval * 1e3;
|
|
49
|
+
const maxInterval = 3e4;
|
|
50
|
+
onStatus?.("Waiting for user authorization...");
|
|
51
|
+
await this.sleep(pollInterval);
|
|
52
|
+
while (true) try {
|
|
53
|
+
return await this.pollForToken(deviceCode);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error instanceof Error) {
|
|
56
|
+
const errorMessage = error.message;
|
|
57
|
+
if (errorMessage === "authorization_pending") {
|
|
58
|
+
onStatus?.("Waiting for user authorization...");
|
|
59
|
+
await this.sleep(pollInterval);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (errorMessage === "slow_down") {
|
|
63
|
+
pollInterval = Math.min(pollInterval + 5e3, maxInterval);
|
|
64
|
+
onStatus?.("Slowing down polling...");
|
|
65
|
+
await this.sleep(pollInterval);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (errorMessage === "access_denied") throw new Error("User denied the authorization request");
|
|
69
|
+
if (errorMessage === "expired_token") throw new Error("Authorization code has expired");
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Refresh an expired access token
|
|
77
|
+
*/
|
|
78
|
+
async refreshToken(refreshToken) {
|
|
79
|
+
return (await this.apiClient.post("/api/oauth/refresh", {
|
|
80
|
+
grant_type: "refresh_token",
|
|
81
|
+
refresh_token: refreshToken,
|
|
82
|
+
client_id: this.clientId
|
|
83
|
+
})).data;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Sleep for specified milliseconds
|
|
87
|
+
*/
|
|
88
|
+
sleep(ms) {
|
|
89
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/auth/ApiClient.ts
|
|
94
|
+
const USER_AGENT = `b4m-cli/${version}`;
|
|
95
|
+
/**
|
|
96
|
+
* Per-request timeout. Generous by design: a waited chat (POST /api/chat with wait:true)
|
|
97
|
+
* runs a full server-side quest, so the bound only exists to stop a hung backend from
|
|
98
|
+
* wedging a caller forever, not to cap a normal long quest. Override with B4M_API_TIMEOUT_MS
|
|
99
|
+
* (0 disables the timeout entirely).
|
|
100
|
+
*/
|
|
101
|
+
const DEFAULT_API_TIMEOUT_MS = 6e5;
|
|
102
|
+
function resolveTimeoutMs() {
|
|
103
|
+
const raw = process.env.B4M_API_TIMEOUT_MS;
|
|
104
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_API_TIMEOUT_MS;
|
|
105
|
+
const parsed = Number(raw);
|
|
106
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_API_TIMEOUT_MS;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Thrown by the response interceptor only when the session is DEFINITIVELY revoked - the
|
|
110
|
+
* refresh token was rejected (400/401 invalid_grant), or a request still 401s after a
|
|
111
|
+
* successful refresh. A transient refresh outage (5xx / network / timeout) throws a plain
|
|
112
|
+
* Error instead, so callers that must distinguish "log out" from "retry" (e.g.
|
|
113
|
+
* checkSessionValid / the WS reconnect loop) can key on the type. The human-readable
|
|
114
|
+
* message is preserved on both paths so existing `error.message.includes(...)` callers are
|
|
115
|
+
* unaffected.
|
|
116
|
+
*/
|
|
117
|
+
var SessionRevokedError = class extends Error {
|
|
118
|
+
constructor(message) {
|
|
119
|
+
super(message);
|
|
120
|
+
this.name = "SessionRevokedError";
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Authenticated API client for B4M services
|
|
125
|
+
* Automatically injects access tokens from ConfigStore
|
|
126
|
+
*/
|
|
127
|
+
var ApiClient = class {
|
|
128
|
+
/**
|
|
129
|
+
* @param apiKey - When set, requests authenticate with this instance API key via
|
|
130
|
+
* the `x-api-key` header and the OAuth-JWT path (Bearer injection + refresh-on-401)
|
|
131
|
+
* is bypassed entirely. Omit to keep the default stored-JWT behavior unchanged.
|
|
132
|
+
*/
|
|
133
|
+
constructor(baseURL = "http://localhost:3000", configStore, apiKey) {
|
|
134
|
+
this.configStore = configStore || new ConfigStore();
|
|
135
|
+
this.oauthClient = new OAuthClient(baseURL);
|
|
136
|
+
this.apiKey = apiKey;
|
|
137
|
+
this.client = axios.create({
|
|
138
|
+
baseURL,
|
|
139
|
+
timeout: resolveTimeoutMs(),
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
"User-Agent": USER_AGENT,
|
|
143
|
+
"X-B4M-Client": USER_AGENT
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
this.client.interceptors.request.use(async (config) => {
|
|
147
|
+
if (this.apiKey) {
|
|
148
|
+
config.headers["x-api-key"] = this.apiKey;
|
|
149
|
+
return config;
|
|
150
|
+
}
|
|
151
|
+
const tokens = await this.configStore.getAuthTokens();
|
|
152
|
+
if (tokens) config.headers.Authorization = `Bearer ${tokens.accessToken}`;
|
|
153
|
+
return config;
|
|
154
|
+
}, (error) => Promise.reject(error));
|
|
155
|
+
this.client.interceptors.response.use((response) => response, async (error) => {
|
|
156
|
+
const originalRequest = error.config;
|
|
157
|
+
if (error.response?.status === 401) logger.debug("AUTH: Received 401 Unauthorized");
|
|
158
|
+
else if (error.response?.status === 403) logger.error("403 Forbidden", error);
|
|
159
|
+
if (this.apiKey) return Promise.reject(error);
|
|
160
|
+
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
161
|
+
originalRequest._retry = true;
|
|
162
|
+
try {
|
|
163
|
+
const tokens = await this.configStore.getAuthTokens();
|
|
164
|
+
if (!tokens) throw new Error("Not authenticated");
|
|
165
|
+
if (Date.now() - (new Date(tokens.expiresAt).getTime() - 6048e5) < 36e5) {
|
|
166
|
+
logger.debug("AUTH: Access token is fresh, skipping refresh — 401 is likely transient");
|
|
167
|
+
return Promise.reject(error);
|
|
168
|
+
}
|
|
169
|
+
logger.debug("AUTH: Attempting token refresh");
|
|
170
|
+
const newTokens = await this.oauthClient.refreshToken(tokens.refreshToken);
|
|
171
|
+
logger.debug("AUTH: Token refresh successful");
|
|
172
|
+
const expiresAt = new Date(Date.now() + newTokens.expires_in * 1e3).toISOString();
|
|
173
|
+
await this.configStore.setAuthTokens({
|
|
174
|
+
accessToken: newTokens.access_token,
|
|
175
|
+
refreshToken: newTokens.refresh_token,
|
|
176
|
+
expiresAt,
|
|
177
|
+
userId: tokens.userId
|
|
178
|
+
});
|
|
179
|
+
originalRequest.headers.Authorization = `Bearer ${newTokens.access_token}`;
|
|
180
|
+
logger.debug("AUTH: Retrying request with new token");
|
|
181
|
+
return this.client(originalRequest);
|
|
182
|
+
} catch (refreshError) {
|
|
183
|
+
const refreshMsg = refreshError instanceof Error ? refreshError.message : "Unknown error";
|
|
184
|
+
logger.warn(`AUTH: Token refresh failed: ${refreshMsg}`);
|
|
185
|
+
const tokens = await this.configStore.getAuthTokens();
|
|
186
|
+
if (tokens && new Date(tokens.expiresAt) <= /* @__PURE__ */ new Date()) await this.configStore.clearAuthTokens();
|
|
187
|
+
const msg = "Authentication expired. Please run `b4m login` again.";
|
|
188
|
+
const refreshStatus = isAxiosError(refreshError) ? refreshError.response?.status : void 0;
|
|
189
|
+
if (refreshStatus === 400 || refreshStatus === 401) throw new SessionRevokedError(msg);
|
|
190
|
+
throw new Error(msg);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (error.response?.status === 401 && originalRequest._retry) {
|
|
194
|
+
logger.debug("AUTH: Token refresh retry failed");
|
|
195
|
+
const tokens = await this.configStore.getAuthTokens();
|
|
196
|
+
if (tokens && new Date(tokens.expiresAt) <= /* @__PURE__ */ new Date()) await this.configStore.clearAuthTokens();
|
|
197
|
+
throw new SessionRevokedError("Authentication failed. Please run /login to authenticate.");
|
|
198
|
+
}
|
|
199
|
+
return Promise.reject(error);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Make a GET request
|
|
204
|
+
*/
|
|
205
|
+
async get(url, config) {
|
|
206
|
+
return (await this.client.get(url, config)).data;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Make a POST request
|
|
210
|
+
*/
|
|
211
|
+
async post(url, data, config) {
|
|
212
|
+
logger.debug(`[ApiClient] POST ${this.client.defaults.baseURL}${url}`);
|
|
213
|
+
logger.debug(`[ApiClient] Request body: ${JSON.stringify(data)}`);
|
|
214
|
+
const response = await this.client.post(url, data, config);
|
|
215
|
+
logger.debug(`[ApiClient] Response status: ${response.status}`);
|
|
216
|
+
return response.data;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Make a PUT request
|
|
220
|
+
*/
|
|
221
|
+
async put(url, data, config) {
|
|
222
|
+
return (await this.client.put(url, data, config)).data;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Make a DELETE request
|
|
226
|
+
*/
|
|
227
|
+
async delete(url, config) {
|
|
228
|
+
return (await this.client.delete(url, config)).data;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Get the underlying axios instance for advanced use cases (e.g., streaming)
|
|
232
|
+
*/
|
|
233
|
+
getAxiosInstance() {
|
|
234
|
+
return this.client;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Check if user is authenticated
|
|
238
|
+
*/
|
|
239
|
+
async isAuthenticated() {
|
|
240
|
+
return this.configStore.isAuthenticated();
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Get current user information
|
|
244
|
+
*/
|
|
245
|
+
async getCurrentUser() {
|
|
246
|
+
try {
|
|
247
|
+
const tokens = await this.configStore.getAuthTokens();
|
|
248
|
+
if (!tokens) return null;
|
|
249
|
+
return { id: tokens.userId };
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Verifies the session is still valid via a cheap authed GET. The response interceptor
|
|
256
|
+
* above already attempts a token refresh and retries on 401, so a resolved call means the
|
|
257
|
+
* session is valid (fresh or transparently refreshed). Returns false ONLY on a
|
|
258
|
+
* `SessionRevokedError` (the refresh token was rejected, or a 401 survived a refresh);
|
|
259
|
+
* every other outcome - a transient refresh outage, a network blip, or the interceptor's
|
|
260
|
+
* fresh-token 401 skip - is treated as transient and returns true, so callers keep
|
|
261
|
+
* retrying rather than tearing down on a blip.
|
|
262
|
+
*
|
|
263
|
+
* Used by WebSocketConnectionManager to distinguish "session revoked" from "transient
|
|
264
|
+
* network issue" when a WS connect attempt fails to open - a WS close event carries no
|
|
265
|
+
* HTTP status, so this is the only way to tell the two apart.
|
|
266
|
+
*/
|
|
267
|
+
async checkSessionValid() {
|
|
268
|
+
try {
|
|
269
|
+
await this.get("/api/identify");
|
|
270
|
+
return true;
|
|
271
|
+
} catch (err) {
|
|
272
|
+
return !(err instanceof SessionRevokedError);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
//#endregion
|
|
277
|
+
export { OAuthClient as n, ApiClient as t };
|