pleumrouter 0.5.51 → 0.5.54
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/package.json +1 -1
- package/src/completion.mjs +1 -0
- package/src/login.mjs +39 -8
- package/src/recipes.mjs +7 -1
package/package.json
CHANGED
package/src/completion.mjs
CHANGED
package/src/login.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
-
import { ROOT, SITE_ROOT, V1, readConfig, writeConfig, mergeLoginConfig, clearCredentials, setActiveContext, refreshAccessToken, accessTokenIsStale } from "./config.mjs";
|
|
4
|
+
import { ROOT, SITE_ROOT, V1, readConfig, writeConfig, mergeLoginConfig, clearCredentials, setActiveContext, refreshAccessToken, accessTokenIsStale, readJwtExp } from "./config.mjs";
|
|
5
5
|
import { t } from "./i18n.mjs";
|
|
6
6
|
import { sanitizeTerminalText } from "./ansi.mjs";
|
|
7
7
|
|
|
@@ -30,13 +30,16 @@ function openBrowser(url) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function saveTokens({ access_token, refresh_token, expires_in }) {
|
|
33
|
+
export function saveTokens({ access_token, refresh_token, expires_in }) {
|
|
34
34
|
const existing = readConfig() || {};
|
|
35
35
|
const { key: _legacy, ...rest } = existing;
|
|
36
|
+
const exp = readJwtExp(access_token);
|
|
36
37
|
const token_expires_at =
|
|
37
|
-
|
|
38
|
-
?
|
|
39
|
-
:
|
|
38
|
+
exp != null
|
|
39
|
+
? exp * 1000
|
|
40
|
+
: typeof expires_in === "number"
|
|
41
|
+
? Date.now() + expires_in * 1000
|
|
42
|
+
: undefined;
|
|
40
43
|
writeConfig(
|
|
41
44
|
mergeLoginConfig(rest, {
|
|
42
45
|
access_token,
|
|
@@ -151,10 +154,31 @@ export async function login() {
|
|
|
151
154
|
console.log(`✓ ${t("Logged in. Saved the session to ~/.pleum/config.json.")}`);
|
|
152
155
|
}
|
|
153
156
|
|
|
157
|
+
export function retryAfterSeconds(response) {
|
|
158
|
+
const value = response.headers.get("Retry-After");
|
|
159
|
+
const seconds = value === null ? NaN : Number(value);
|
|
160
|
+
return Number.isFinite(seconds) && seconds > 0 ? Math.ceil(seconds) : 60;
|
|
161
|
+
}
|
|
162
|
+
|
|
154
163
|
// device-code 로그인 — 브라우저 없는 환경(SSH·CI).
|
|
155
164
|
// 1) code 발급 → 2) 사용자가 /device에서 확인 → 3) CLI가 폴링으로 JWT 수신.
|
|
156
165
|
export async function loginDevice() {
|
|
157
|
-
const
|
|
166
|
+
const codeDeadline = Date.now() + 120_000;
|
|
167
|
+
let res;
|
|
168
|
+
while (true) {
|
|
169
|
+
try {
|
|
170
|
+
res = await fetch(`${V1}/auth/device/code`, { method: "POST", signal: AbortSignal.timeout(Math.max(1, codeDeadline - Date.now())) });
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (error.name !== "TimeoutError") throw error;
|
|
173
|
+
console.error(t("Login timed out (2 minutes)"));
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
if (res.status !== 429) break;
|
|
177
|
+
const wait = retryAfterSeconds(res) * 1000;
|
|
178
|
+
if (Date.now() + wait >= codeDeadline) break;
|
|
179
|
+
console.error(t("Failed to issue a device code (server responded {status})", { status: res.status }));
|
|
180
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
181
|
+
}
|
|
158
182
|
if (!res.ok) {
|
|
159
183
|
console.error(t("Failed to issue a device code (server responded {status})", { status: res.status }));
|
|
160
184
|
process.exit(1);
|
|
@@ -170,19 +194,26 @@ export async function loginDevice() {
|
|
|
170
194
|
const expiresIn = Number(expires_in) || 600;
|
|
171
195
|
const pollInterval = Math.max(1, Number(interval) || 5);
|
|
172
196
|
const deadline = Date.now() + expiresIn * 1000;
|
|
173
|
-
|
|
174
|
-
|
|
197
|
+
let nextWait = pollInterval * 1000;
|
|
198
|
+
while (Date.now() + nextWait < deadline) {
|
|
199
|
+
await new Promise((r) => setTimeout(r, nextWait));
|
|
200
|
+
nextWait = pollInterval * 1000;
|
|
175
201
|
let poll;
|
|
176
202
|
try {
|
|
177
203
|
poll = await fetch(`${V1}/auth/device/token`, {
|
|
178
204
|
method: "POST",
|
|
179
205
|
headers: { "Content-Type": "application/json" },
|
|
180
206
|
body: JSON.stringify({ device_code }),
|
|
207
|
+
signal: AbortSignal.timeout(Math.max(1, deadline - Date.now())),
|
|
181
208
|
});
|
|
182
209
|
} catch {
|
|
183
210
|
continue; // Transient network error; retry on next poll tick
|
|
184
211
|
}
|
|
185
212
|
if (poll.status === 202) continue; // authorization_pending
|
|
213
|
+
if (poll.status === 429) {
|
|
214
|
+
nextWait = Math.max(pollInterval, retryAfterSeconds(poll)) * 1000;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
186
217
|
if (!poll.ok) {
|
|
187
218
|
const e = await poll.json().catch(() => ({}));
|
|
188
219
|
if (e.detail === "expired_token") { console.error(t("The code has expired.")); process.exit(1); }
|
package/src/recipes.mjs
CHANGED
|
@@ -193,7 +193,13 @@ export const RECIPES = {
|
|
|
193
193
|
title: "Goose",
|
|
194
194
|
referer: "https://block.github.io/goose",
|
|
195
195
|
},
|
|
196
|
-
env: (key) => ({
|
|
196
|
+
env: (key) => ({
|
|
197
|
+
GOOSE_PROVIDER: "openai",
|
|
198
|
+
OPENAI_HOST: ROOT,
|
|
199
|
+
OPENAI_BASE_PATH: "v1/chat/completions",
|
|
200
|
+
OPENAI_API_BASE: V1,
|
|
201
|
+
OPENAI_API_KEY: key,
|
|
202
|
+
}),
|
|
197
203
|
args: () => [],
|
|
198
204
|
},
|
|
199
205
|
openhands: {
|