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 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
- const cfg = readConfig();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pleumrouter",
3
- "version": "0.5.16",
3
+ "version": "0.5.17",
4
4
  "description": "Official PleumRouter terminal CLI — launch any coding agent against PleumRouter.",
5
5
  "type": "module",
6
6
  "bin": {
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;
@@ -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
+
@@ -223,4 +223,6 @@ export const JA = {
223
223
  "⏹ generation aborted — the partial reply stays in context": "⏹ 生成を中断しました — 途中までの応答はコンテキストに残ります",
224
224
  "✓ installed": "✓ インストール済み",
225
225
  "✗ not installed —": "✗ 未インストール —",
226
+ "Log out": "ログアウト",
226
227
  };
228
+
@@ -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
+
@@ -223,4 +223,6 @@ export const ZH = {
223
223
  "⏹ generation aborted — the partial reply stays in context": "⏹ 生成已中止 — 部分回复保留在上下文中",
224
224
  "✓ installed": "✓ 已安装",
225
225
  "✗ not installed —": "✗ 未安装 —",
226
+ "Log out": "登出",
226
227
  };
228
+
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
- export function buildItems({ loggedIn, savedModels = {} }) {
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({ loggedIn: Boolean(cfg?.key), savedModels: cfg?.launch_models ?? {} });
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
- if (item.id !== "login") saveLastSelection(item.id);
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, clearCredentials } from "./config.mjs";
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
- title: t("Account"),
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: Boolean(cfg?.access_token || cfg?.key),
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
- const cfg = readConfig();
121
- if (cfg?.key) {
122
- const ok = await confirm(t("Log out? [Y/n] "), { defaultYes: true });
123
- if (ok) {
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
  }