@bike4mind/cli 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{AgentHistoryStore-DIMzvf-y.mjs → AgentHistoryStore-DVxv1kSO.mjs} +7940 -7014
- package/dist/{ConfigStore-DIs-K9oD.mjs → ConfigStore-51UaP9pJ.mjs} +12333 -7008
- package/dist/{ImageStore-kVo-oHoS.mjs → ImageStore-DuI3tgwQ.mjs} +8 -6
- package/dist/SandboxRuntimeAdapter-Bt1wPpld.mjs +2 -0
- package/dist/{WorkItemsClient-Cow6nXx7.mjs → WorkItemsClient-BMY0Qq1s.mjs} +78 -9
- package/dist/{buildAgent-DOvEp92a.mjs → buildAgent-CrSRKYU_.mjs} +106 -43
- package/dist/commands/acpCommand.mjs +26 -35
- package/dist/commands/apiCommand.mjs +1 -1
- package/dist/commands/doctorCommand.mjs +3 -3
- package/dist/commands/envCommand.mjs +1 -1
- package/dist/commands/headlessCommand.mjs +15 -24
- package/dist/commands/mcpCommand.mjs +3 -3
- package/dist/commands/pluginCommand.mjs +2 -2
- package/dist/commands/updateCommand.mjs +5 -15
- package/dist/dist-BsG2Hlp-.mjs +739 -0
- package/dist/index.mjs +670 -340
- package/dist/{package-Dl_F2Fah.mjs → package-DEsNSP54.mjs} +1 -1
- package/dist/serve-BaHzVbcm.mjs +1511 -0
- package/dist/store-BkMur6uk.mjs +2 -0
- package/dist/{store-CvjTpQPs.mjs → store-BxmPEI3v.mjs} +10 -0
- package/dist/types-I45lMh7Q.mjs +2 -0
- package/dist/{updateChecker-CQW8bxo6.mjs → updateChecker-CCLVVdH0.mjs} +36 -13
- package/package.json +27 -26
- package/dist/ApiClient-BTI0-m0g.mjs +0 -316
- package/dist/SandboxRuntimeAdapter-BmHELLuM.mjs +0 -3
- package/dist/serve-Y2zMbfGs.mjs +0 -938
- package/dist/store-BG3e54c8.mjs +0 -3
- package/dist/types-CdIKgWWe.mjs +0 -3
- package/dist/{bashExecute-CrdPpBqk-DCATrE-D.mjs → bashExecute-CrdPpBqk-CgK1gZa2.mjs} +1 -1
- package/dist/{grepSearch-BgoOOwGe-DtlV8Gn-.mjs → grepSearch-BgoOOwGe-B388GxRD.mjs} +2 -2
|
@@ -75,6 +75,16 @@ const useCliStore = create((set) => ({
|
|
|
75
75
|
}
|
|
76
76
|
};
|
|
77
77
|
}),
|
|
78
|
+
sessionHolds: 0,
|
|
79
|
+
holdSession: () => {
|
|
80
|
+
set((state) => ({ sessionHolds: state.sessionHolds + 1 }));
|
|
81
|
+
let released = false;
|
|
82
|
+
return () => {
|
|
83
|
+
if (released) return;
|
|
84
|
+
released = true;
|
|
85
|
+
set((state) => ({ sessionHolds: state.sessionHolds - 1 }));
|
|
86
|
+
};
|
|
87
|
+
},
|
|
78
88
|
messageQueue: [],
|
|
79
89
|
enqueueMessage: (message) => set((state) => ({ messageQueue: [...state.messageQueue, message] })),
|
|
80
90
|
dequeueAllMessages: () => {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { execSync } from "child_process";
|
|
3
2
|
import { constants, promises } from "fs";
|
|
4
3
|
import { homedir } from "os";
|
|
5
4
|
import path from "path";
|
|
6
5
|
import axios from "axios";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
7
|
//#region src/utils/updateChecker.ts
|
|
8
8
|
/**
|
|
9
9
|
* Update checker utility for B4M CLI
|
|
@@ -163,9 +163,40 @@ async function readCache() {
|
|
|
163
163
|
async function writeCache(cache) {
|
|
164
164
|
try {
|
|
165
165
|
await promises.mkdir(path.dirname(CACHE_FILE), { recursive: true });
|
|
166
|
-
|
|
166
|
+
const tempFile = `${CACHE_FILE}.${process.pid}.tmp`;
|
|
167
|
+
await promises.writeFile(tempFile, JSON.stringify(cache, null, 2), "utf-8");
|
|
168
|
+
await promises.rename(tempFile, CACHE_FILE);
|
|
167
169
|
} catch {}
|
|
168
170
|
}
|
|
171
|
+
function toResult(currentVersion, latestVersion) {
|
|
172
|
+
return {
|
|
173
|
+
currentVersion,
|
|
174
|
+
latestVersion,
|
|
175
|
+
updateAvailable: compareSemver(latestVersion, currentVersion) > 0
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The last known update status, from the cache alone. Never touches the network,
|
|
180
|
+
* so it is safe to await before first paint. A cache of any age is used: a newer
|
|
181
|
+
* published version stays newer, and checkForUpdate() refreshes the cache for the
|
|
182
|
+
* next launch. Null when nothing is cached for this version.
|
|
183
|
+
*/
|
|
184
|
+
async function readCachedUpdate(currentVersion) {
|
|
185
|
+
const cache = await readCache();
|
|
186
|
+
if (!cache || cache.currentVersion !== currentVersion) return null;
|
|
187
|
+
return toResult(currentVersion, cache.latestVersion);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Startup update status: the cached result now, plus a background refresh of
|
|
191
|
+
* the cache (registry fetch only when it is stale) for the next launch. This
|
|
192
|
+
* refresh is the only automatic writer of the cache, which both the banner and
|
|
193
|
+
* the launch auto-update prompt read.
|
|
194
|
+
*/
|
|
195
|
+
async function getStartupUpdateStatus(currentVersion) {
|
|
196
|
+
const cached = await readCachedUpdate(currentVersion);
|
|
197
|
+
checkForUpdate(currentVersion);
|
|
198
|
+
return cached;
|
|
199
|
+
}
|
|
169
200
|
/**
|
|
170
201
|
* Check for updates using cache when fresh.
|
|
171
202
|
* Non-throwing - returns null on any error.
|
|
@@ -174,11 +205,7 @@ async function checkForUpdate(currentVersion) {
|
|
|
174
205
|
try {
|
|
175
206
|
const cache = await readCache();
|
|
176
207
|
if (cache && cache.currentVersion === currentVersion) {
|
|
177
|
-
if (Date.now() - new Date(cache.lastChecked).getTime() < CACHE_TTL_MS) return
|
|
178
|
-
currentVersion,
|
|
179
|
-
latestVersion: cache.latestVersion,
|
|
180
|
-
updateAvailable: compareSemver(cache.latestVersion, currentVersion) > 0
|
|
181
|
-
};
|
|
208
|
+
if (Date.now() - new Date(cache.lastChecked).getTime() < CACHE_TTL_MS) return toResult(currentVersion, cache.latestVersion);
|
|
182
209
|
}
|
|
183
210
|
const latestVersion = await fetchLatestVersion();
|
|
184
211
|
if (!latestVersion) return null;
|
|
@@ -187,11 +214,7 @@ async function checkForUpdate(currentVersion) {
|
|
|
187
214
|
latestVersion,
|
|
188
215
|
currentVersion
|
|
189
216
|
});
|
|
190
|
-
return
|
|
191
|
-
currentVersion,
|
|
192
|
-
latestVersion,
|
|
193
|
-
updateAvailable: compareSemver(latestVersion, currentVersion) > 0
|
|
194
|
-
};
|
|
217
|
+
return toResult(currentVersion, latestVersion);
|
|
195
218
|
} catch {
|
|
196
219
|
return null;
|
|
197
220
|
}
|
|
@@ -215,4 +238,4 @@ async function forceCheckForUpdate(currentVersion) {
|
|
|
215
238
|
};
|
|
216
239
|
}
|
|
217
240
|
//#endregion
|
|
218
|
-
export {
|
|
241
|
+
export { forceCheckForUpdate as a, isNpmPrefixWritable as c, shouldAttemptAutoUpdate as d, fetchLatestVersion as i, readCachedUpdate as l, REEXEC_GUARD_ENV as n, getAutoUpdatePreference as o, compareSemver as r, getStartupUpdateStatus as s, INSTALL_CMD as t, setAutoUpdatePreference as u };
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bike4mind/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"mcpName": "io.github.bike4mind/bike4mind",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Interactive CLI tool for Bike4Mind with ReAct agents",
|
|
7
|
-
"license": "
|
|
7
|
+
"license": "BUSL-1.1",
|
|
8
8
|
"author": "Bike4Mind",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -37,23 +37,23 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
39
39
|
"@anthropic-ai/sdk": "^0.111.0",
|
|
40
|
-
"@aws-sdk/client-apigatewaymanagementapi": "^3.
|
|
41
|
-
"@aws-sdk/client-bedrock-runtime": "^3.
|
|
42
|
-
"@aws-sdk/client-cloudwatch": "^3.
|
|
43
|
-
"@aws-sdk/client-lambda": "^3.
|
|
44
|
-
"@aws-sdk/client-s3": "^3.
|
|
45
|
-
"@aws-sdk/client-sqs": "^3.
|
|
46
|
-
"@aws-sdk/client-transcribe": "^3.
|
|
47
|
-
"@aws-sdk/credential-provider-node": "^3.972.
|
|
48
|
-
"@aws-sdk/s3-request-presigner": "^3.
|
|
40
|
+
"@aws-sdk/client-apigatewaymanagementapi": "^3.1150.0",
|
|
41
|
+
"@aws-sdk/client-bedrock-runtime": "^3.1150.0",
|
|
42
|
+
"@aws-sdk/client-cloudwatch": "^3.1150.0",
|
|
43
|
+
"@aws-sdk/client-lambda": "^3.1150.0",
|
|
44
|
+
"@aws-sdk/client-s3": "^3.1150.0",
|
|
45
|
+
"@aws-sdk/client-sqs": "^3.1150.0",
|
|
46
|
+
"@aws-sdk/client-transcribe": "^3.1150.0",
|
|
47
|
+
"@aws-sdk/credential-provider-node": "^3.972.86",
|
|
48
|
+
"@aws-sdk/s3-request-presigner": "^3.1150.0",
|
|
49
49
|
"@casl/ability": "^6.8.1",
|
|
50
50
|
"@google/genai": "^1.46.0",
|
|
51
51
|
"@joplin/turndown-plugin-gfm": "^1.0.67",
|
|
52
52
|
"@mendable/firecrawl-js": "^1.29.3",
|
|
53
|
-
"@modelcontextprotocol/sdk": "1.
|
|
53
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
54
54
|
"@octokit/rest": "^22.0.1",
|
|
55
55
|
"@opensearch-project/opensearch": "2.11.0",
|
|
56
|
-
"@smithy/node-http-handler": "^4.
|
|
56
|
+
"@smithy/node-http-handler": "^4.12.1",
|
|
57
57
|
"async-mutex": "^0.5.0",
|
|
58
58
|
"axios": "1.20.0",
|
|
59
59
|
"bcryptjs": "^3.0.3",
|
|
@@ -65,7 +65,6 @@
|
|
|
65
65
|
"dayjs": "^1.11.23",
|
|
66
66
|
"diff": "^9.0.0",
|
|
67
67
|
"eventsource-parser": "^3.1.0",
|
|
68
|
-
"fdir": "^6.5.0",
|
|
69
68
|
"file-type": "^22.1.0",
|
|
70
69
|
"fuse.js": "^7.5.0",
|
|
71
70
|
"fzf": "^0.5.2",
|
|
@@ -90,9 +89,9 @@
|
|
|
90
89
|
"p-limit": "^7.3.2",
|
|
91
90
|
"picomatch": "^4.0.7",
|
|
92
91
|
"qrcode": "^1.5.4",
|
|
93
|
-
"react": "^19.
|
|
92
|
+
"react": "^19.3.0",
|
|
94
93
|
"sharp": "^0.35.4",
|
|
95
|
-
"shell-quote": "^1.
|
|
94
|
+
"shell-quote": "^1.12.0",
|
|
96
95
|
"speakeasy": "^2.0.0",
|
|
97
96
|
"tiktoken": "^1.0.22",
|
|
98
97
|
"tldts": "^7.4.13",
|
|
@@ -107,7 +106,7 @@
|
|
|
107
106
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
|
108
107
|
"yargs": "^18.1.0",
|
|
109
108
|
"yauzl": "^3.4.0",
|
|
110
|
-
"zod": "^4.
|
|
109
|
+
"zod": "^4.6.6",
|
|
111
110
|
"zod-validation-error": "^5.0.0",
|
|
112
111
|
"zustand": "^5.0.13"
|
|
113
112
|
},
|
|
@@ -116,7 +115,7 @@
|
|
|
116
115
|
"@types/jsonwebtoken": "^9.0.4",
|
|
117
116
|
"@types/node": "^24.0.0",
|
|
118
117
|
"@types/picomatch": "^4.0.3",
|
|
119
|
-
"@types/react": "^19.
|
|
118
|
+
"@types/react": "^19.3.0",
|
|
120
119
|
"@types/shell-quote": "^1.7.5",
|
|
121
120
|
"@types/ws": "^8.18.1",
|
|
122
121
|
"@types/yargs": "^17.0.35",
|
|
@@ -125,15 +124,17 @@
|
|
|
125
124
|
"tsx": "^4.23.0",
|
|
126
125
|
"typescript": "^5.9.3",
|
|
127
126
|
"vitest": "^4.1.11",
|
|
128
|
-
"@bike4mind/agents": "1.
|
|
129
|
-
"@bike4mind/
|
|
130
|
-
"@bike4mind/
|
|
131
|
-
"@bike4mind/fab-pipeline": "1.
|
|
132
|
-
"@bike4mind/
|
|
133
|
-
"@bike4mind/
|
|
127
|
+
"@bike4mind/agents": "1.2.0",
|
|
128
|
+
"@bike4mind/client-auth": "0.2.0",
|
|
129
|
+
"@bike4mind/common": "15.0.0",
|
|
130
|
+
"@bike4mind/fab-pipeline": "2.1.0",
|
|
131
|
+
"@bike4mind/hearth": "0.3.1",
|
|
132
|
+
"@bike4mind/llm-adapters": "1.1.0",
|
|
133
|
+
"@bike4mind/mcp": "2.1.0",
|
|
134
134
|
"@bike4mind/observability": "0.2.3",
|
|
135
|
-
"@bike4mind/
|
|
136
|
-
"@bike4mind/services": "
|
|
135
|
+
"@bike4mind/sdk": "1.0.0",
|
|
136
|
+
"@bike4mind/services": "17.0.0",
|
|
137
|
+
"@bike4mind/utils": "7.1.0"
|
|
137
138
|
},
|
|
138
139
|
"optionalDependencies": {
|
|
139
140
|
"@vscode/ripgrep": "^1.18.0"
|
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { n as logger, t as ConfigStore } from "./ConfigStore-DIs-K9oD.mjs";
|
|
3
|
-
import { t as version } from "./package-Dl_F2Fah.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/providerKeyFailure.ts
|
|
94
|
-
const PROVIDER_KEY_ERROR_CODES = /* @__PURE__ */ new Set(["provider_not_configured", "provider_rejected"]);
|
|
95
|
-
/**
|
|
96
|
-
* A 401 whose body names a missing or rejected AI-provider key (e.g. /api/ai/tts). The
|
|
97
|
-
* Bike4Mind credential was accepted, so refreshing or re-authenticating cannot fix it.
|
|
98
|
-
* Shared by the ApiClient refresh interceptor and mapApiError in mcp/b4mApiClient.ts.
|
|
99
|
-
*/
|
|
100
|
-
function isProviderKeyFailure(data) {
|
|
101
|
-
const body = decodeBinaryBody(data);
|
|
102
|
-
if (!body || typeof body !== "object") return false;
|
|
103
|
-
const { errorCode } = body;
|
|
104
|
-
return typeof errorCode === "string" && PROVIDER_KEY_ERROR_CODES.has(errorCode);
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* An arraybuffer-typed request (the generated-audio calls) gets its error body as raw
|
|
108
|
-
* bytes, and the refresh interceptor sees it before any caller can decode it.
|
|
109
|
-
*/
|
|
110
|
-
function decodeBinaryBody(data) {
|
|
111
|
-
const bytes = data instanceof ArrayBuffer ? Buffer.from(data) : ArrayBuffer.isView(data) ? Buffer.from(data.buffer, data.byteOffset, data.byteLength) : void 0;
|
|
112
|
-
if (!bytes) return data;
|
|
113
|
-
try {
|
|
114
|
-
return JSON.parse(bytes.toString("utf8"));
|
|
115
|
-
} catch {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
//#endregion
|
|
120
|
-
//#region src/auth/ApiClient.ts
|
|
121
|
-
const USER_AGENT = `b4m-cli/${version}`;
|
|
122
|
-
/**
|
|
123
|
-
* Per-request timeout. Generous by design: a waited chat (POST /api/chat with wait:true)
|
|
124
|
-
* runs a full server-side quest, so the bound only exists to stop a hung backend from
|
|
125
|
-
* wedging a caller forever, not to cap a normal long quest. Override with B4M_API_TIMEOUT_MS
|
|
126
|
-
* (0 disables the timeout entirely).
|
|
127
|
-
*/
|
|
128
|
-
const DEFAULT_API_TIMEOUT_MS = 6e5;
|
|
129
|
-
function resolveTimeoutMs() {
|
|
130
|
-
const raw = process.env.B4M_API_TIMEOUT_MS;
|
|
131
|
-
if (raw === void 0 || raw.trim() === "") return DEFAULT_API_TIMEOUT_MS;
|
|
132
|
-
const parsed = Number(raw);
|
|
133
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_API_TIMEOUT_MS;
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Thrown by the response interceptor only when the session is DEFINITIVELY revoked - the
|
|
137
|
-
* refresh token was rejected (400/401 invalid_grant), or a request still 401s after a
|
|
138
|
-
* successful refresh. A transient refresh outage (5xx / network / timeout) throws a plain
|
|
139
|
-
* Error instead, so callers that must distinguish "log out" from "retry" (e.g.
|
|
140
|
-
* checkSessionValid / the WS reconnect loop) can key on the type. The human-readable
|
|
141
|
-
* message is preserved on both paths so existing `error.message.includes(...)` callers are
|
|
142
|
-
* unaffected.
|
|
143
|
-
*/
|
|
144
|
-
var SessionRevokedError = class extends Error {
|
|
145
|
-
constructor(message) {
|
|
146
|
-
super(message);
|
|
147
|
-
this.name = "SessionRevokedError";
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
/**
|
|
151
|
-
* 401 with no credential at all (no API key, no stored tokens): a config gap, not an expiry.
|
|
152
|
-
* Not a SessionRevokedError, so checkSessionValid still reports "not revoked". The message keeps
|
|
153
|
-
* `Authentication failed` for string-matching callers (turnController, handoff, ServerLlmBackend).
|
|
154
|
-
*/
|
|
155
|
-
var NotAuthenticatedError = class extends Error {
|
|
156
|
-
constructor(message) {
|
|
157
|
-
super(message);
|
|
158
|
-
this.name = "NotAuthenticatedError";
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
/**
|
|
162
|
-
* Authenticated API client for B4M services
|
|
163
|
-
* Automatically injects access tokens from ConfigStore
|
|
164
|
-
*/
|
|
165
|
-
var ApiClient = class {
|
|
166
|
-
/**
|
|
167
|
-
* @param apiKey - When set, requests authenticate with this instance API key via
|
|
168
|
-
* the `x-api-key` header and the OAuth-JWT path (Bearer injection + refresh-on-401)
|
|
169
|
-
* is bypassed entirely. Omit to keep the default stored-JWT behavior unchanged.
|
|
170
|
-
*/
|
|
171
|
-
constructor(baseURL = "http://localhost:3000", configStore, apiKey) {
|
|
172
|
-
this.configStore = configStore || new ConfigStore();
|
|
173
|
-
this.oauthClient = new OAuthClient(baseURL);
|
|
174
|
-
this.apiKey = apiKey;
|
|
175
|
-
this.client = axios.create({
|
|
176
|
-
baseURL,
|
|
177
|
-
timeout: resolveTimeoutMs(),
|
|
178
|
-
headers: {
|
|
179
|
-
"Content-Type": "application/json",
|
|
180
|
-
"User-Agent": USER_AGENT,
|
|
181
|
-
"X-B4M-Client": USER_AGENT
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
this.client.interceptors.request.use(async (config) => {
|
|
185
|
-
if (this.apiKey) {
|
|
186
|
-
config.headers["x-api-key"] = this.apiKey;
|
|
187
|
-
return config;
|
|
188
|
-
}
|
|
189
|
-
const tokens = await this.configStore.getAuthTokens();
|
|
190
|
-
if (tokens) config.headers.Authorization = `Bearer ${tokens.accessToken}`;
|
|
191
|
-
return config;
|
|
192
|
-
}, (error) => Promise.reject(error));
|
|
193
|
-
this.client.interceptors.response.use((response) => response, async (error) => {
|
|
194
|
-
const originalRequest = error.config;
|
|
195
|
-
if (error.response?.status === 401) logger.debug("AUTH: Received 401 Unauthorized");
|
|
196
|
-
else if (error.response?.status === 403) logger.error("403 Forbidden", error);
|
|
197
|
-
if (this.apiKey) return Promise.reject(error);
|
|
198
|
-
if (error.response?.status === 401 && isProviderKeyFailure(error.response.data)) return Promise.reject(error);
|
|
199
|
-
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
200
|
-
originalRequest._retry = true;
|
|
201
|
-
const tokens = await this.configStore.getAuthTokens();
|
|
202
|
-
if (!tokens) throw new NotAuthenticatedError("Authentication failed: not logged in. Please run `b4m login` to authenticate.");
|
|
203
|
-
try {
|
|
204
|
-
if (new Date(tokens.expiresAt).getTime() > Date.now()) {
|
|
205
|
-
logger.debug("AUTH: Access token has not expired, skipping refresh - 401 is likely transient");
|
|
206
|
-
return Promise.reject(error);
|
|
207
|
-
}
|
|
208
|
-
logger.debug("AUTH: Attempting token refresh");
|
|
209
|
-
const newTokens = await this.oauthClient.refreshToken(tokens.refreshToken);
|
|
210
|
-
logger.debug("AUTH: Token refresh successful");
|
|
211
|
-
const expiresAt = new Date(Date.now() + newTokens.expires_in * 1e3).toISOString();
|
|
212
|
-
await this.configStore.setAuthTokens({
|
|
213
|
-
accessToken: newTokens.access_token,
|
|
214
|
-
refreshToken: newTokens.refresh_token ?? tokens.refreshToken,
|
|
215
|
-
expiresAt,
|
|
216
|
-
userId: tokens.userId
|
|
217
|
-
});
|
|
218
|
-
originalRequest.headers.Authorization = `Bearer ${newTokens.access_token}`;
|
|
219
|
-
logger.debug("AUTH: Retrying request with new token");
|
|
220
|
-
return this.client(originalRequest);
|
|
221
|
-
} catch (refreshError) {
|
|
222
|
-
const refreshMsg = refreshError instanceof Error ? refreshError.message : "Unknown error";
|
|
223
|
-
logger.warn(`AUTH: Token refresh failed: ${refreshMsg}`);
|
|
224
|
-
const tokens = await this.configStore.getAuthTokens();
|
|
225
|
-
if (tokens && new Date(tokens.expiresAt) <= /* @__PURE__ */ new Date()) await this.configStore.clearAuthTokens();
|
|
226
|
-
const msg = "Authentication expired. Please run `b4m login` again.";
|
|
227
|
-
const refreshStatus = isAxiosError(refreshError) ? refreshError.response?.status : void 0;
|
|
228
|
-
if (refreshStatus === 400 || refreshStatus === 401) throw new SessionRevokedError(msg);
|
|
229
|
-
throw new Error(msg);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
if (error.response?.status === 401 && originalRequest._retry) {
|
|
233
|
-
logger.debug("AUTH: Token refresh retry failed");
|
|
234
|
-
const tokens = await this.configStore.getAuthTokens();
|
|
235
|
-
if (tokens && new Date(tokens.expiresAt) <= /* @__PURE__ */ new Date()) await this.configStore.clearAuthTokens();
|
|
236
|
-
throw new SessionRevokedError("Authentication failed. Please run /login to authenticate.");
|
|
237
|
-
}
|
|
238
|
-
return Promise.reject(error);
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Make a GET request
|
|
243
|
-
*/
|
|
244
|
-
async get(url, config) {
|
|
245
|
-
return (await this.client.get(url, config)).data;
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* Make a POST request
|
|
249
|
-
*/
|
|
250
|
-
async post(url, data, config) {
|
|
251
|
-
logger.debug(`[ApiClient] POST ${this.client.defaults.baseURL}${url}`);
|
|
252
|
-
logger.debug(`[ApiClient] Request body: ${JSON.stringify(data)}`);
|
|
253
|
-
const response = await this.client.post(url, data, config);
|
|
254
|
-
logger.debug(`[ApiClient] Response status: ${response.status}`);
|
|
255
|
-
return response.data;
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Make a PUT request
|
|
259
|
-
*/
|
|
260
|
-
async put(url, data, config) {
|
|
261
|
-
return (await this.client.put(url, data, config)).data;
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Make a DELETE request
|
|
265
|
-
*/
|
|
266
|
-
async delete(url, config) {
|
|
267
|
-
return (await this.client.delete(url, config)).data;
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Get the underlying axios instance for advanced use cases (e.g., streaming)
|
|
271
|
-
*/
|
|
272
|
-
getAxiosInstance() {
|
|
273
|
-
return this.client;
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Check if user is authenticated
|
|
277
|
-
*/
|
|
278
|
-
async isAuthenticated() {
|
|
279
|
-
return this.configStore.isAuthenticated();
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Get current user information
|
|
283
|
-
*/
|
|
284
|
-
async getCurrentUser() {
|
|
285
|
-
try {
|
|
286
|
-
const tokens = await this.configStore.getAuthTokens();
|
|
287
|
-
if (!tokens) return null;
|
|
288
|
-
return { id: tokens.userId };
|
|
289
|
-
} catch {
|
|
290
|
-
return null;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Verifies the session is still valid via a cheap authed GET. The response interceptor
|
|
295
|
-
* above already attempts a token refresh and retries on 401, so a resolved call means the
|
|
296
|
-
* session is valid (fresh or transparently refreshed). Returns false ONLY on a
|
|
297
|
-
* `SessionRevokedError` (the refresh token was rejected, or a 401 survived a refresh);
|
|
298
|
-
* every other outcome - a transient refresh outage, a network blip, or the interceptor's
|
|
299
|
-
* fresh-token 401 skip - is treated as transient and returns true, so callers keep
|
|
300
|
-
* retrying rather than tearing down on a blip.
|
|
301
|
-
*
|
|
302
|
-
* Used by WebSocketConnectionManager to distinguish "session revoked" from "transient
|
|
303
|
-
* network issue" when a WS connect attempt fails to open - a WS close event carries no
|
|
304
|
-
* HTTP status, so this is the only way to tell the two apart.
|
|
305
|
-
*/
|
|
306
|
-
async checkSessionValid() {
|
|
307
|
-
try {
|
|
308
|
-
await this.get("/api/identify");
|
|
309
|
-
return true;
|
|
310
|
-
} catch (err) {
|
|
311
|
-
return !(err instanceof SessionRevokedError);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
//#endregion
|
|
316
|
-
export { OAuthClient as i, NotAuthenticatedError as n, isProviderKeyFailure as r, ApiClient as t };
|