pleumrouter 0.5.16 → 0.5.17
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/bin/pleum.mjs +3 -13
- package/package.json +1 -1
- package/src/config.mjs +8 -0
- package/src/locales/es.mjs +2 -0
- package/src/locales/ja.mjs +2 -0
- package/src/locales/ko.mjs +2 -0
- package/src/locales/zh.mjs +2 -0
- package/src/login.mjs +14 -1
- package/src/menu.mjs +15 -8
- package/src/settings.mjs +9 -11
package/bin/pleum.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { login, loginDevice } from "../src/login.mjs";
|
|
3
|
+
import { login, loginDevice, logout } from "../src/login.mjs";
|
|
4
4
|
import { launch, launchHelp } from "../src/launch.mjs";
|
|
5
5
|
import { run } from "../src/run.mjs";
|
|
6
6
|
import { listModels } from "../src/models.mjs";
|
|
@@ -10,7 +10,6 @@ import { top } from "../src/top.mjs";
|
|
|
10
10
|
import { health } from "../src/health.mjs";
|
|
11
11
|
import { completion } from "../src/completion.mjs";
|
|
12
12
|
import { upgrade } from "../src/upgrade.mjs";
|
|
13
|
-
import { readConfig, writeConfig, clearCredentials, V1 } from "../src/config.mjs";
|
|
14
13
|
import { mainMenu } from "../src/menu.mjs";
|
|
15
14
|
import { lang } from "../src/lang.mjs";
|
|
16
15
|
import { RECIPES } from "../src/recipes.mjs";
|
|
@@ -85,19 +84,10 @@ try {
|
|
|
85
84
|
if (rest.includes("--device")) await loginDevice();
|
|
86
85
|
else await login();
|
|
87
86
|
break;
|
|
88
|
-
case "logout":
|
|
89
|
-
|
|
90
|
-
if (cfg?.access_token) {
|
|
91
|
-
// best-effort server revoke — 실패해도 로컬 자격증명은 지운다.
|
|
92
|
-
await fetch(`${V1}/auth/logout`, {
|
|
93
|
-
method: "POST",
|
|
94
|
-
headers: { Authorization: `Bearer ${cfg.access_token}` },
|
|
95
|
-
}).catch(() => {});
|
|
96
|
-
}
|
|
97
|
-
if (cfg) writeConfig(clearCredentials(cfg));
|
|
87
|
+
case "logout":
|
|
88
|
+
await logout();
|
|
98
89
|
console.log(`✓ ${t("Logged out")}`);
|
|
99
90
|
break;
|
|
100
|
-
}
|
|
101
91
|
case "lang":
|
|
102
92
|
await lang(rest);
|
|
103
93
|
break;
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -61,6 +61,14 @@ export function clearCredentials(existing) {
|
|
|
61
61
|
return rest;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* 사용 가능한 세션 보유 여부. 레거시 `key`(plm_)만 있는 상태는 requireAuth()가 거부하므로
|
|
66
|
+
* (아래 requireAuth 참조) 미로그인으로 취급한다 — 그래야 런처가 자동으로 재로그인을 태운다.
|
|
67
|
+
*/
|
|
68
|
+
export function isLoggedIn(cfg = readConfig()) {
|
|
69
|
+
return Boolean(cfg?.access_token);
|
|
70
|
+
}
|
|
71
|
+
|
|
64
72
|
/** JWT payload `exp`(epoch sec) — 서명 검증 없이 refresh 트리거 힌트. */
|
|
65
73
|
export function readJwtExp(token) {
|
|
66
74
|
if (typeof token !== "string") return null;
|
package/src/locales/es.mjs
CHANGED
|
@@ -223,4 +223,6 @@ export const ES = {
|
|
|
223
223
|
"⏹ generation aborted — the partial reply stays in context": "⏹ generación abortada — la respuesta parcial permanece en el contexto",
|
|
224
224
|
"✓ installed": "✓ instalado",
|
|
225
225
|
"✗ not installed —": "✗ no instalado —",
|
|
226
|
+
"Log out": "Cerrar sesión",
|
|
226
227
|
};
|
|
228
|
+
|
package/src/locales/ja.mjs
CHANGED
package/src/locales/ko.mjs
CHANGED
|
@@ -286,4 +286,6 @@ export const KO = {
|
|
|
286
286
|
"재설치 중입니다 — 완료되면 pleum이 종료됩니다. 이후 다시 pleum을 실행하세요.",
|
|
287
287
|
"↑/↓ move • enter select • esc back": "↑/↓ 이동 • enter 선택 • esc 뒤로",
|
|
288
288
|
"↑/↓ move • enter set model • esc back": "↑/↓ 이동 • enter 모델 설정 • esc 뒤로",
|
|
289
|
+
"Log out": "로그아웃",
|
|
289
290
|
};
|
|
291
|
+
|
package/src/locales/zh.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 } from "./config.mjs";
|
|
4
|
+
import { ROOT, SITE_ROOT, V1, readConfig, writeConfig, mergeLoginConfig, clearCredentials } from "./config.mjs";
|
|
5
5
|
import { t } from "./i18n.mjs";
|
|
6
6
|
import { sanitizeTerminalText } from "./ansi.mjs";
|
|
7
7
|
|
|
@@ -150,3 +150,16 @@ export async function loginDevice() {
|
|
|
150
150
|
console.error(t("Timed out: the code has expired."));
|
|
151
151
|
process.exit(1);
|
|
152
152
|
}
|
|
153
|
+
|
|
154
|
+
// 서버 세션 폐기 + 로컬 자격증명 삭제. `pleum logout`과 런처 설정>계정이 공유한다.
|
|
155
|
+
export async function logout() {
|
|
156
|
+
const cfg = readConfig();
|
|
157
|
+
if (cfg?.access_token) {
|
|
158
|
+
// best-effort — 서버가 죽어 있어도 로컬 자격증명은 반드시 지운다.
|
|
159
|
+
await fetch(`${V1}/auth/logout`, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: { Authorization: `Bearer ${cfg.access_token}` },
|
|
162
|
+
}).catch(() => {});
|
|
163
|
+
}
|
|
164
|
+
if (cfg) writeConfig(clearCredentials(cfg));
|
|
165
|
+
}
|
package/src/menu.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { spawn } from "node:child_process";
|
|
|
13
13
|
import { createInterface } from "node:readline";
|
|
14
14
|
import { style, displayWidth, terminalWidth, hyperlink, sanitizeTerminalText } from "./ansi.mjs";
|
|
15
15
|
import { keyFeeder, select } from "./interactive.mjs";
|
|
16
|
-
import { readConfig, writeConfig } from "./config.mjs";
|
|
16
|
+
import { readConfig, writeConfig, isLoggedIn } from "./config.mjs";
|
|
17
17
|
import { RECIPES, MANUAL } from "./recipes.mjs";
|
|
18
18
|
import { onPath, launch } from "./launch.mjs";
|
|
19
19
|
import { fetchModels, matchModels, listModels, formatModelLabel } from "./models.mjs";
|
|
@@ -45,9 +45,10 @@ const AGENT_DESC = {
|
|
|
45
45
|
const autoInstallable = (r) => !/^https?:/.test(r.install); // URL이면 수동 설치 안내만
|
|
46
46
|
|
|
47
47
|
// 최상위 메뉴 항목(순수 함수 — 테스트 대상). 에이전트는 없음 — "agents" 한 항목으로 서브메뉴 진입.
|
|
48
|
-
|
|
48
|
+
// 로그인 항목도 없다 — CLI는 로그인 사용자용이라 mainMenu()가 진입 시점에 게이트로 처리하고,
|
|
49
|
+
// 로그인/로그아웃 전환은 설정>계정이 소유한다.
|
|
50
|
+
export function buildItems({ savedModels = {} }) {
|
|
49
51
|
const items = [];
|
|
50
|
-
if (!loggedIn) items.push({ id: "login", title: t("Log in"), desc: t("Log in via browser and save the API key") });
|
|
51
52
|
items.push({
|
|
52
53
|
id: "chat",
|
|
53
54
|
tool: "chat",
|
|
@@ -261,7 +262,7 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
|
|
|
261
262
|
async function menuSelect() {
|
|
262
263
|
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
263
264
|
const cfg = readConfig();
|
|
264
|
-
const items = buildItems({
|
|
265
|
+
const items = buildItems({ savedModels: cfg?.launch_models ?? {} });
|
|
265
266
|
const last = cfg?.last_selection;
|
|
266
267
|
const initialCursor = last ? Math.max(0, items.findIndex((it) => it.id === last)) : 0;
|
|
267
268
|
return runList(items, {
|
|
@@ -285,20 +286,26 @@ async function agentSelect() {
|
|
|
285
286
|
// bare `pleum` 진입점. 비TTY면 false(호출자가 HELP 출력), 그 외 true.
|
|
286
287
|
// ollama처럼 실행이 끝나면 메뉴로 복귀하는 루프(REPL 채팅·ctrl+c 종료 제외).
|
|
287
288
|
export async function mainMenu() {
|
|
289
|
+
// 비TTY(파이프·스크립트)면 메뉴도 로그인도 띄우지 않는다 — 호출자가 HELP를 출력한다.
|
|
290
|
+
// runList()와 같은 3조건을 쓴다. 이 가드가 없으면 `pleum | cat`이 브라우저를 열고 2분 매달린다.
|
|
291
|
+
const io = process.stdin;
|
|
292
|
+
if (!io.isTTY || !process.stdout.isTTY || typeof io.setRawMode !== "function") return false;
|
|
293
|
+
// CLI는 로그인 사용자용이다 — 미로그인 진입이면 여기서 auth 로그인을 태운다.
|
|
294
|
+
// login()의 실패(취소·2분 타임아웃·state 불일치)는 그대로 던져 bin/pleum.mjs의
|
|
295
|
+
// 최상위 catch가 `✗ <메시지>` + exit 1로 처리하게 둔다(메뉴를 띄우지 않는다).
|
|
296
|
+
if (!isLoggedIn()) await login();
|
|
297
|
+
|
|
288
298
|
for (;;) {
|
|
289
299
|
const picked = await menuSelect();
|
|
290
300
|
if (picked === undefined) return false; // 비TTY
|
|
291
301
|
if (picked === null || picked === "quit") return true; // esc/q/ctrl+c — ollama처럼 조용히 종료
|
|
292
302
|
|
|
293
303
|
const { item, model } = picked;
|
|
294
|
-
|
|
304
|
+
saveLastSelection(item.id);
|
|
295
305
|
if (model && item.tool) saveModel(item.tool, model);
|
|
296
306
|
const saved = readConfig()?.launch_models ?? {};
|
|
297
307
|
|
|
298
308
|
switch (item.id) {
|
|
299
|
-
case "login":
|
|
300
|
-
await login();
|
|
301
|
-
continue; // 로그인 후 메뉴로 복귀
|
|
302
309
|
case "chat": {
|
|
303
310
|
const m = model ?? saved.chat;
|
|
304
311
|
await run(m ? [m] : []); // REPL 종료는 run 내부 process.exit — 메뉴 복귀 없음
|
package/src/settings.mjs
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
9
|
import { style, sanitizeTerminalText } from "./ansi.mjs";
|
|
10
10
|
import { select, confirm } from "./interactive.mjs";
|
|
11
|
-
import { readConfig, writeConfig,
|
|
11
|
+
import { readConfig, writeConfig, isLoggedIn } from "./config.mjs";
|
|
12
12
|
import { getLocale, LANGS, t } from "./i18n.mjs";
|
|
13
13
|
import { RECIPES } from "./recipes.mjs";
|
|
14
14
|
import { fetchModels, matchModels, formatModelLabel } from "./models.mjs";
|
|
15
|
-
import { login } from "./login.mjs";
|
|
15
|
+
import { login, logout } from "./login.mjs";
|
|
16
16
|
import { upgrade } from "./upgrade.mjs";
|
|
17
17
|
import { completion } from "./completion.mjs";
|
|
18
18
|
import { lang } from "./lang.mjs";
|
|
@@ -25,7 +25,8 @@ export function buildSettingsItems({ lang, loggedIn, keyPrefix, version }) {
|
|
|
25
25
|
{ id: "settings:models", title: t("Default models"), desc: t("Set the default model used per agent") },
|
|
26
26
|
{
|
|
27
27
|
id: "settings:account",
|
|
28
|
-
|
|
28
|
+
// 제목 자체가 다음 동작을 말한다 — 로그인 상태에서 Enter는 로그아웃이다.
|
|
29
|
+
title: loggedIn ? t("Log out") : t("Log in"),
|
|
29
30
|
desc: loggedIn
|
|
30
31
|
? keyPrefix === "oauth"
|
|
31
32
|
? `${t("Logged in")} (${t("OAuth session")})`
|
|
@@ -69,7 +70,7 @@ async function settingsSelect() {
|
|
|
69
70
|
const cfg = readConfig();
|
|
70
71
|
const items = buildSettingsItems({
|
|
71
72
|
lang: getLocale(),
|
|
72
|
-
loggedIn:
|
|
73
|
+
loggedIn: isLoggedIn(cfg),
|
|
73
74
|
keyPrefix: cfg?.access_token ? "oauth" : cfg?.key ? cfg.key.slice(0, 8) : null,
|
|
74
75
|
version: readVersion(),
|
|
75
76
|
});
|
|
@@ -117,13 +118,10 @@ async function runDefaultModels() {
|
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
async function runAccount() {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
writeConfig(clearCredentials(cfg));
|
|
125
|
-
console.log(`✓ ${t("Logged out")}`);
|
|
126
|
-
}
|
|
121
|
+
if (isLoggedIn()) {
|
|
122
|
+
if (!(await confirm(t("Log out? [Y/n] "), { defaultYes: true }))) return;
|
|
123
|
+
await logout(); // 서버 세션 revoke + 로컬 자격증명 삭제
|
|
124
|
+
console.log(`✓ ${t("Logged out")}`);
|
|
127
125
|
} else {
|
|
128
126
|
await login();
|
|
129
127
|
}
|