pleumrouter 0.5.25 → 0.5.27
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 +10 -3
- package/package.json +1 -1
- package/src/balance.mjs +170 -9
- package/src/config.mjs +34 -3
- package/src/interactive.mjs +2 -1
- package/src/launch.mjs +182 -9
- package/src/locales/es.mjs +12 -0
- package/src/locales/ja.mjs +12 -0
- package/src/locales/ko.mjs +12 -0
- package/src/locales/zh.mjs +12 -0
- package/src/menu.mjs +64 -22
- package/src/recipes.mjs +53 -33
- package/src/settings.mjs +11 -2
package/bin/pleum.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { top } from "../src/top.mjs";
|
|
|
11
11
|
import { health } from "../src/health.mjs";
|
|
12
12
|
import { completion } from "../src/completion.mjs";
|
|
13
13
|
import { upgrade } from "../src/upgrade.mjs";
|
|
14
|
-
import { mainMenu } from "../src/menu.mjs";
|
|
14
|
+
import { mainMenu, runAgentMenu } from "../src/menu.mjs";
|
|
15
15
|
import { lang } from "../src/lang.mjs";
|
|
16
16
|
import { RECIPES } from "../src/recipes.mjs";
|
|
17
17
|
import { t } from "../src/i18n.mjs";
|
|
@@ -98,6 +98,11 @@ try {
|
|
|
98
98
|
case "launch": {
|
|
99
99
|
const tool = rest[0];
|
|
100
100
|
if (!tool) {
|
|
101
|
+
const io = process.stdin;
|
|
102
|
+
if (io.isTTY && process.stdout.isTTY && typeof io.setRawMode === "function") {
|
|
103
|
+
await runAgentMenu({ loop: false });
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
101
106
|
launchHelp();
|
|
102
107
|
break;
|
|
103
108
|
}
|
|
@@ -119,9 +124,11 @@ try {
|
|
|
119
124
|
case "models":
|
|
120
125
|
await listModels(rest);
|
|
121
126
|
break;
|
|
122
|
-
case "balance":
|
|
123
|
-
|
|
127
|
+
case "balance": {
|
|
128
|
+
const isSwitch = rest.includes("--switch");
|
|
129
|
+
await balance({ interactive: isSwitch, switchOnly: isSwitch });
|
|
124
130
|
break;
|
|
131
|
+
}
|
|
125
132
|
case "usage":
|
|
126
133
|
await usage(rest);
|
|
127
134
|
break;
|
package/package.json
CHANGED
package/src/balance.mjs
CHANGED
|
@@ -1,16 +1,177 @@
|
|
|
1
|
-
import { authFetch } from "./config.mjs";
|
|
1
|
+
import { authFetch, getActiveContext, setActiveContext } from "./config.mjs";
|
|
2
|
+
import { style } from "./ansi.mjs";
|
|
3
|
+
import { select } from "./interactive.mjs";
|
|
4
|
+
import { t } from "./i18n.mjs";
|
|
2
5
|
|
|
3
|
-
export
|
|
6
|
+
export function formatKrwCredit(amount) {
|
|
7
|
+
return Number(amount || 0).toLocaleString("ko-KR", {
|
|
8
|
+
minimumFractionDigits: 1,
|
|
9
|
+
maximumFractionDigits: 1,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function fetchCreditSummary() {
|
|
4
14
|
const res = await authFetch("/credits");
|
|
5
15
|
if (!res.ok) {
|
|
6
16
|
const body = await res.text().catch(() => "");
|
|
7
|
-
|
|
17
|
+
throw new Error(`잔액 조회 실패 (HTTP ${res.status})${body ? `: ${body}` : ""}`);
|
|
18
|
+
}
|
|
19
|
+
const personal = await res.json();
|
|
20
|
+
|
|
21
|
+
let orgs = [];
|
|
22
|
+
try {
|
|
23
|
+
const orgRes = await authFetch("/orgs");
|
|
24
|
+
if (orgRes.ok) {
|
|
25
|
+
orgs = await orgRes.json();
|
|
26
|
+
}
|
|
27
|
+
} catch {}
|
|
28
|
+
|
|
29
|
+
const teamWallets = [];
|
|
30
|
+
if (Array.isArray(orgs) && orgs.length > 0) {
|
|
31
|
+
await Promise.all(
|
|
32
|
+
orgs.map(async (org) => {
|
|
33
|
+
try {
|
|
34
|
+
const wRes = await authFetch(`/orgs/${org.id}/wallet`);
|
|
35
|
+
if (wRes.ok) {
|
|
36
|
+
const wallet = await wRes.json();
|
|
37
|
+
teamWallets.push({
|
|
38
|
+
id: String(org.id),
|
|
39
|
+
name: org.name,
|
|
40
|
+
role: org.role,
|
|
41
|
+
balance_krw: wallet.available_krw ?? wallet.balance_krw ?? 0,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
} catch {}
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
personal: {
|
|
51
|
+
balance_krw: personal.balance_krw ?? 0,
|
|
52
|
+
balance_usd: personal.balance_usd,
|
|
53
|
+
},
|
|
54
|
+
teams: teamWallets,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildCreditOptions({ personal, teams = [], activeContext = { type: "personal" } }) {
|
|
59
|
+
const options = [];
|
|
60
|
+
const isPersonalActive = activeContext.type === "personal" || !activeContext.org_id;
|
|
61
|
+
const personalBalanceStr = `${formatKrwCredit(personal.balance_krw)} ${t("credits")}${
|
|
62
|
+
personal.balance_usd != null ? ` / $${personal.balance_usd.toFixed(4)}` : ""
|
|
63
|
+
}`;
|
|
64
|
+
|
|
65
|
+
options.push({
|
|
66
|
+
id: "personal",
|
|
67
|
+
type: "personal",
|
|
68
|
+
name: t("Personal"),
|
|
69
|
+
title: t("Personal Credit"),
|
|
70
|
+
balance: personalBalanceStr,
|
|
71
|
+
isActive: isPersonalActive,
|
|
72
|
+
label: `${isPersonalActive ? "✓ " : " "}${t("Personal Credit")}: ${personalBalanceStr}`,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
for (const team of teams) {
|
|
76
|
+
const isTeamActive = activeContext.type === "team" && String(activeContext.org_id) === String(team.id);
|
|
77
|
+
const teamBalanceStr = `${formatKrwCredit(team.balance_krw)} ${t("credits")}`;
|
|
78
|
+
const roleStr = team.role ? ` (${team.role})` : "";
|
|
79
|
+
const teamTitle = `${t("Team")}: ${team.name}${roleStr}`;
|
|
80
|
+
options.push({
|
|
81
|
+
id: String(team.id),
|
|
82
|
+
type: "team",
|
|
83
|
+
name: team.name,
|
|
84
|
+
title: teamTitle,
|
|
85
|
+
balance: teamBalanceStr,
|
|
86
|
+
isActive: isTeamActive,
|
|
87
|
+
label: `${isTeamActive ? "✓ " : " "}${teamTitle}: ${teamBalanceStr}`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return options;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function switchCreditWorkspace({ summary, selectFn = select } = {}) {
|
|
95
|
+
const data = summary || (await fetchCreditSummary());
|
|
96
|
+
const active = getActiveContext();
|
|
97
|
+
const options = buildCreditOptions({
|
|
98
|
+
personal: data.personal,
|
|
99
|
+
teams: data.teams,
|
|
100
|
+
activeContext: active,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (options.length <= 1) {
|
|
104
|
+
console.log(t("You are not a member of any team. Using personal credit."));
|
|
105
|
+
return active;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const picked = await selectFn(options, {
|
|
109
|
+
message: t("Select credit workspace"),
|
|
110
|
+
formatItem: (x) => x.label,
|
|
111
|
+
limit: 10,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!picked) return active;
|
|
115
|
+
|
|
116
|
+
const targetOrgId = picked.type === "team" ? picked.id : null;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const res = await authFetch("/auth/cli-context", {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: { "Content-Type": "application/json" },
|
|
122
|
+
body: JSON.stringify({ org_id: targetOrgId }),
|
|
123
|
+
});
|
|
124
|
+
if (!res.ok) {
|
|
125
|
+
const err = await res.text().catch(() => "");
|
|
126
|
+
console.error(`✗ ${t("Failed to update credit context")}: ${err}`);
|
|
127
|
+
return active;
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
console.error(`✗ ${t("Failed to update credit context")}: ${e.message}`);
|
|
131
|
+
return active;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const newContext = setActiveContext({
|
|
135
|
+
type: picked.type,
|
|
136
|
+
org_id: targetOrgId,
|
|
137
|
+
org_name: picked.type === "team" ? picked.name : null,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
console.log(`✓ ${t("Active credit set to: {name}", { name: picked.title })}`);
|
|
141
|
+
return newContext;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function balance({ interactive = false, switchOnly = false } = {}) {
|
|
145
|
+
let summary;
|
|
146
|
+
try {
|
|
147
|
+
summary = await fetchCreditSummary();
|
|
148
|
+
} catch (e) {
|
|
149
|
+
console.error(e.message);
|
|
8
150
|
process.exit(1);
|
|
9
151
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
152
|
+
|
|
153
|
+
if (switchOnly) {
|
|
154
|
+
return await switchCreditWorkspace({ summary });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const active = getActiveContext();
|
|
158
|
+
const options = buildCreditOptions({
|
|
159
|
+
personal: summary.personal,
|
|
160
|
+
teams: summary.teams,
|
|
161
|
+
activeContext: active,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (!interactive || options.length <= 1) {
|
|
165
|
+
console.log(t("Credit Balance:"));
|
|
166
|
+
for (const opt of options) {
|
|
167
|
+
const mark = opt.isActive ? style.bold(style.plum("▸ [Active] ")) : " [ ] ";
|
|
168
|
+
console.log(`${mark}${opt.title}: ${opt.balance}`);
|
|
169
|
+
}
|
|
170
|
+
if (options.length > 1 && !interactive) {
|
|
171
|
+
console.log(`\n${style.dim(t("Run `pleum balance --switch` to switch credit workspace."))}`);
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return await switchCreditWorkspace({ summary });
|
|
16
177
|
}
|
package/src/config.mjs
CHANGED
|
@@ -194,11 +194,41 @@ export async function refreshAccessToken(cfg = readConfig()) {
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
/**
|
|
197
|
+
/** 활성 크레딧/워크스페이스 컨텍스트 조회 (기본값 personal) */
|
|
198
|
+
export function getActiveContext(cfg = readConfig()) {
|
|
199
|
+
const ctx = cfg?.active_context;
|
|
200
|
+
if (ctx && ctx.type === "team" && ctx.org_id) {
|
|
201
|
+
return { type: "team", org_id: ctx.org_id, org_name: ctx.org_name || ctx.org_id };
|
|
202
|
+
}
|
|
203
|
+
return { type: "personal", org_id: null, org_name: null };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 활성 크레딧/워크스페이스 컨텍스트 저장 */
|
|
207
|
+
export function setActiveContext(context, cfg = readConfig()) {
|
|
208
|
+
const updated = { ...(cfg ?? {}) };
|
|
209
|
+
if (!context || context.type === "personal" || !context.org_id) {
|
|
210
|
+
updated.active_context = { type: "personal" };
|
|
211
|
+
} else {
|
|
212
|
+
updated.active_context = {
|
|
213
|
+
type: "team",
|
|
214
|
+
org_id: context.org_id,
|
|
215
|
+
org_name: context.org_name || null,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
writeConfig(updated);
|
|
219
|
+
return updated.active_context;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Bearer 요청 — 401이면 1회 refresh 후 재시도. 활성 팀 컨텍스트가 있으면 X-Pleum-Org 첨부. */
|
|
198
223
|
export async function authFetch(path, init = {}) {
|
|
199
224
|
const token = await requireAuth();
|
|
200
225
|
const url = path.startsWith("http") ? path : `${V1}${path.startsWith("/") ? path : `/${path}`}`;
|
|
201
|
-
const
|
|
226
|
+
const ctx = getActiveContext();
|
|
227
|
+
const customHeaders = {};
|
|
228
|
+
if (ctx.type === "team" && ctx.org_id) {
|
|
229
|
+
customHeaders["X-Pleum-Org"] = ctx.org_id;
|
|
230
|
+
}
|
|
231
|
+
const headers = { ...customHeaders, ...(init.headers || {}), Authorization: `Bearer ${token}` };
|
|
202
232
|
let res = await fetch(url, { ...init, headers });
|
|
203
233
|
if (res.status !== 401) return res;
|
|
204
234
|
|
|
@@ -207,6 +237,7 @@ export async function authFetch(path, init = {}) {
|
|
|
207
237
|
if (!refreshed) return res;
|
|
208
238
|
return fetch(url, {
|
|
209
239
|
...init,
|
|
210
|
-
headers: { ...(init.headers || {}), Authorization: `Bearer ${refreshed}` },
|
|
240
|
+
headers: { ...customHeaders, ...(init.headers || {}), Authorization: `Bearer ${refreshed}` },
|
|
211
241
|
});
|
|
212
242
|
}
|
|
243
|
+
|
package/src/interactive.mjs
CHANGED
|
@@ -100,6 +100,7 @@ export async function select(items, {
|
|
|
100
100
|
formatItem = String,
|
|
101
101
|
matches = (all, q) => (q ? all.filter((it) => String(formatItem(it)).toLowerCase().includes(q.toLowerCase())) : all),
|
|
102
102
|
limit = 10,
|
|
103
|
+
initialCursor = 0,
|
|
103
104
|
input = process.stdin,
|
|
104
105
|
output = process.stdout,
|
|
105
106
|
} = {}) {
|
|
@@ -107,7 +108,7 @@ export async function select(items, {
|
|
|
107
108
|
|
|
108
109
|
return new Promise((resolve) => {
|
|
109
110
|
let query = "";
|
|
110
|
-
let cursor = 0;
|
|
111
|
+
let cursor = Math.max(0, Math.min(initialCursor, Math.max(0, (items?.length ?? 1) - 1)));
|
|
111
112
|
let lastRows = 0;
|
|
112
113
|
|
|
113
114
|
const rowsFor = (s) => Math.max(1, Math.ceil(displayWidth(s) / Math.max(terminalWidth(), 1)));
|
package/src/launch.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join, delimiter, dirname, resolve } from "node:path";
|
|
5
|
-
import { requireAuth } from "./config.mjs";
|
|
5
|
+
import { requireAuth, getActiveContext } from "./config.mjs";
|
|
6
6
|
import {
|
|
7
7
|
RECIPES,
|
|
8
8
|
MANUAL,
|
|
@@ -95,6 +95,8 @@ export function launchHelp() {
|
|
|
95
95
|
|
|
96
96
|
// exit:false면 process.exit 대신 자식 종료 코드를 반환 — 런처 메뉴의 실행 후 복귀용.
|
|
97
97
|
export async function launch(tool, model, passthrough, { exit = true } = {}) {
|
|
98
|
+
recoverAgySettings();
|
|
99
|
+
|
|
98
100
|
const key = await requireAuth();
|
|
99
101
|
const resolved = resolveLaunchTool(tool);
|
|
100
102
|
const r = RECIPES[resolved];
|
|
@@ -126,7 +128,41 @@ export async function launch(tool, model, passthrough, { exit = true } = {}) {
|
|
|
126
128
|
if (r.type === "crush-home") {
|
|
127
129
|
env.CRUSH_GLOBAL_CONFIG = writeIsolatedHome("crush-home", "crush.json", crushJson(model));
|
|
128
130
|
}
|
|
129
|
-
|
|
131
|
+
|
|
132
|
+
let restoreAgy = null;
|
|
133
|
+
if (r.type === "agy-settings") {
|
|
134
|
+
restoreAgy = setupAgySettings(model);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const cleanup = () => {
|
|
138
|
+
if (restoreAgy) {
|
|
139
|
+
restoreAgy();
|
|
140
|
+
restoreAgy = null;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const onSignal = () => {
|
|
145
|
+
cleanup();
|
|
146
|
+
if (exit) process.exit(130);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
process.once("SIGINT", onSignal);
|
|
150
|
+
process.once("SIGTERM", onSignal);
|
|
151
|
+
process.once("SIGHUP", onSignal);
|
|
152
|
+
process.once("exit", cleanup);
|
|
153
|
+
|
|
154
|
+
const removeListeners = () => {
|
|
155
|
+
process.removeListener("SIGINT", onSignal);
|
|
156
|
+
process.removeListener("SIGTERM", onSignal);
|
|
157
|
+
process.removeListener("SIGHUP", onSignal);
|
|
158
|
+
process.removeListener("exit", cleanup);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const activeCtx = getActiveContext();
|
|
162
|
+
const creditMsg = activeCtx.type === "team"
|
|
163
|
+
? `${t("Team")}: ${activeCtx.org_name}`
|
|
164
|
+
: t("Personal");
|
|
165
|
+
console.log(`ℹ ${t("Credit:")} ${creditMsg}`);
|
|
130
166
|
|
|
131
167
|
const args = [...r.args(model), ...passthrough];
|
|
132
168
|
if (r.note) console.log(`ℹ ${t(r.note)}`);
|
|
@@ -136,6 +172,8 @@ export async function launch(tool, model, passthrough, { exit = true } = {}) {
|
|
|
136
172
|
try {
|
|
137
173
|
command = resolveLaunchCommand(r.bin, args);
|
|
138
174
|
} catch (e) {
|
|
175
|
+
cleanup();
|
|
176
|
+
removeListeners();
|
|
139
177
|
console.error(`✗ ${sanitizeTerminalText(e.message)}`);
|
|
140
178
|
if (exit) process.exit(1);
|
|
141
179
|
return 1;
|
|
@@ -148,6 +186,8 @@ export async function launch(tool, model, passthrough, { exit = true } = {}) {
|
|
|
148
186
|
shell: false,
|
|
149
187
|
});
|
|
150
188
|
child.on("error", (e) => {
|
|
189
|
+
cleanup();
|
|
190
|
+
removeListeners();
|
|
151
191
|
if (e.code === "ENOENT") {
|
|
152
192
|
console.error(`✗ ${t("'{bin}' not found. Install:\n {install}", { bin: r.bin, install: r.install })}`);
|
|
153
193
|
if (exit) process.exit(127);
|
|
@@ -158,6 +198,8 @@ export async function launch(tool, model, passthrough, { exit = true } = {}) {
|
|
|
158
198
|
resolve(1);
|
|
159
199
|
});
|
|
160
200
|
child.on("exit", (code) => {
|
|
201
|
+
cleanup();
|
|
202
|
+
removeListeners();
|
|
161
203
|
if (exit) process.exit(code ?? 0);
|
|
162
204
|
resolve(code ?? 0);
|
|
163
205
|
});
|
|
@@ -171,12 +213,142 @@ function writeIsolatedHome(name, filename, contents) {
|
|
|
171
213
|
return home;
|
|
172
214
|
}
|
|
173
215
|
|
|
174
|
-
function
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
216
|
+
export function getAgyPaths() {
|
|
217
|
+
const settingsPath = join(homedir(), ".gemini", "antigravity-cli", "settings.json");
|
|
218
|
+
const backupPath = join(homedir(), ".pleum", "agy-settings-backup.json");
|
|
219
|
+
return { settingsPath, backupPath };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function setupAgySettings(model, {
|
|
223
|
+
paths = getAgyPaths(),
|
|
224
|
+
read = readFileSync,
|
|
225
|
+
write = writeFileSync,
|
|
226
|
+
exists = existsSync,
|
|
227
|
+
mkdir = mkdirSync,
|
|
228
|
+
rm = rmSync,
|
|
229
|
+
} = {}) {
|
|
230
|
+
const { settingsPath, backupPath } = paths;
|
|
231
|
+
const originalExists = exists(settingsPath);
|
|
232
|
+
let originalContent = null;
|
|
233
|
+
let parsed = null;
|
|
234
|
+
|
|
235
|
+
if (originalExists) {
|
|
236
|
+
try {
|
|
237
|
+
originalContent = read(settingsPath, "utf8");
|
|
238
|
+
parsed = JSON.parse(originalContent);
|
|
239
|
+
} catch {
|
|
240
|
+
parsed = null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 1. 비정상 종료 대비 백업 메타데이터 저장
|
|
245
|
+
try {
|
|
246
|
+
mkdir(dirname(backupPath), { recursive: true, mode: 0o700 });
|
|
247
|
+
write(
|
|
248
|
+
backupPath,
|
|
249
|
+
JSON.stringify(
|
|
250
|
+
{
|
|
251
|
+
settingsPath,
|
|
252
|
+
originalExists,
|
|
253
|
+
originalHadModelProvider: Boolean(parsed && "modelProvider" in parsed),
|
|
254
|
+
originalModelProvider: parsed?.modelProvider ?? null,
|
|
255
|
+
originalHadModel: Boolean(parsed && "model" in parsed),
|
|
256
|
+
originalModel: parsed?.model ?? null,
|
|
257
|
+
originalContent,
|
|
258
|
+
},
|
|
259
|
+
null,
|
|
260
|
+
2,
|
|
261
|
+
),
|
|
262
|
+
{ mode: 0o600 },
|
|
263
|
+
);
|
|
264
|
+
} catch {}
|
|
265
|
+
|
|
266
|
+
// 2. 임시 설정 병합
|
|
267
|
+
mkdir(dirname(settingsPath), { recursive: true, mode: 0o700 });
|
|
268
|
+
write(settingsPath, `${JSON.stringify(mergeAgySettings(originalContent || "", model), null, 2)}\n`, { mode: 0o600 });
|
|
269
|
+
|
|
270
|
+
// 3. 종료 시 스마트 복구 클로저 (세션 중 사용자가 추가한 권한/워크스페이스 보존)
|
|
271
|
+
let restored = false;
|
|
272
|
+
return function restore() {
|
|
273
|
+
if (restored) return;
|
|
274
|
+
restored = true;
|
|
275
|
+
try {
|
|
276
|
+
if (!originalExists) {
|
|
277
|
+
if (exists(settingsPath)) rm(settingsPath, { force: true });
|
|
278
|
+
} else {
|
|
279
|
+
const currentContent = exists(settingsPath) ? read(settingsPath, "utf8") : null;
|
|
280
|
+
if (currentContent) {
|
|
281
|
+
try {
|
|
282
|
+
const currentObj = JSON.parse(currentContent);
|
|
283
|
+
if (parsed && "modelProvider" in parsed) {
|
|
284
|
+
currentObj.modelProvider = parsed.modelProvider;
|
|
285
|
+
} else {
|
|
286
|
+
delete currentObj.modelProvider;
|
|
287
|
+
}
|
|
288
|
+
if (parsed && "model" in parsed) {
|
|
289
|
+
currentObj.model = parsed.model;
|
|
290
|
+
} else {
|
|
291
|
+
delete currentObj.model;
|
|
292
|
+
}
|
|
293
|
+
write(settingsPath, `${JSON.stringify(currentObj, null, 2)}\n`, { mode: 0o600 });
|
|
294
|
+
} catch {
|
|
295
|
+
if (originalContent != null) write(settingsPath, originalContent, { mode: 0o600 });
|
|
296
|
+
}
|
|
297
|
+
} else if (originalContent != null) {
|
|
298
|
+
write(settingsPath, originalContent, { mode: 0o600 });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
// 복구 실패 시 최선 처리
|
|
303
|
+
} finally {
|
|
304
|
+
try {
|
|
305
|
+
if (exists(backupPath)) rm(backupPath, { force: true });
|
|
306
|
+
} catch {}
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function recoverAgySettings({
|
|
312
|
+
paths = getAgyPaths(),
|
|
313
|
+
read = readFileSync,
|
|
314
|
+
write = writeFileSync,
|
|
315
|
+
exists = existsSync,
|
|
316
|
+
rm = rmSync,
|
|
317
|
+
} = {}) {
|
|
318
|
+
const { backupPath } = paths;
|
|
319
|
+
if (!exists(backupPath)) return;
|
|
320
|
+
try {
|
|
321
|
+
const raw = read(backupPath, "utf8");
|
|
322
|
+
const backup = JSON.parse(raw);
|
|
323
|
+
const settingsPath = backup?.settingsPath;
|
|
324
|
+
if (settingsPath && exists(settingsPath)) {
|
|
325
|
+
if (!backup.originalExists) {
|
|
326
|
+
rm(settingsPath, { force: true });
|
|
327
|
+
} else {
|
|
328
|
+
const curContent = read(settingsPath, "utf8");
|
|
329
|
+
try {
|
|
330
|
+
const curObj = JSON.parse(curContent);
|
|
331
|
+
if (backup.originalHadModelProvider) {
|
|
332
|
+
curObj.modelProvider = backup.originalModelProvider;
|
|
333
|
+
} else {
|
|
334
|
+
delete curObj.modelProvider;
|
|
335
|
+
}
|
|
336
|
+
if (backup.originalHadModel) {
|
|
337
|
+
curObj.model = backup.originalModel;
|
|
338
|
+
} else {
|
|
339
|
+
delete curObj.model;
|
|
340
|
+
}
|
|
341
|
+
write(settingsPath, `${JSON.stringify(curObj, null, 2)}\n`, { mode: 0o600 });
|
|
342
|
+
} catch {
|
|
343
|
+
if (backup.originalContent != null) write(settingsPath, backup.originalContent, { mode: 0o600 });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
} catch {} finally {
|
|
348
|
+
try {
|
|
349
|
+
if (exists(backupPath)) rm(backupPath, { force: true });
|
|
350
|
+
} catch {}
|
|
351
|
+
}
|
|
180
352
|
}
|
|
181
353
|
|
|
182
354
|
function printManual(tool, key) {
|
|
@@ -186,3 +358,4 @@ function printManual(tool, key) {
|
|
|
186
358
|
console.log(m.snippet());
|
|
187
359
|
console.log(`\n${t("2) Export the key and run:")}\n export PLEUM_API_KEY=${key}\n`);
|
|
188
360
|
}
|
|
361
|
+
|
package/src/locales/es.mjs
CHANGED
|
@@ -262,5 +262,17 @@ export const ES = {
|
|
|
262
262
|
"✓ installed": "✓ instalado",
|
|
263
263
|
"✗ not installed —": "✗ no instalado —",
|
|
264
264
|
"Log out": "Cerrar sesión",
|
|
265
|
+
"Credit:": "Crédito:",
|
|
266
|
+
"Team": "Equipo",
|
|
267
|
+
"Personal": "Personal",
|
|
268
|
+
"Personal Credit": "Crédito personal",
|
|
269
|
+
"Credit workspace": "Espacio de crédito",
|
|
270
|
+
"Credit Balance:": "Saldo de crédito:",
|
|
271
|
+
"credits": "créditos",
|
|
272
|
+
"Select credit workspace": "Seleccionar espacio de crédito",
|
|
273
|
+
"Active credit set to: {name}": "Crédito activo configurado en: {name}",
|
|
274
|
+
"Failed to update credit context": "Error al actualizar el contexto de crédito",
|
|
275
|
+
"You are not a member of any team. Using personal credit.": "No eres miembro de ningún equipo. Usando crédito personal.",
|
|
276
|
+
"Run `pleum balance --switch` to switch credit workspace.": "Ejecuta `pleum balance --switch` para cambiar de espacio de crédito.",
|
|
265
277
|
};
|
|
266
278
|
|
package/src/locales/ja.mjs
CHANGED
|
@@ -262,5 +262,17 @@ export const JA = {
|
|
|
262
262
|
"✓ installed": "✓ インストール済み",
|
|
263
263
|
"✗ not installed —": "✗ 未インストール —",
|
|
264
264
|
"Log out": "ログアウト",
|
|
265
|
+
"Credit:": "クレジット:",
|
|
266
|
+
"Team": "チーム",
|
|
267
|
+
"Personal": "個人",
|
|
268
|
+
"Personal Credit": "個人クレジット",
|
|
269
|
+
"Credit workspace": "クレジットワークスペース",
|
|
270
|
+
"Credit Balance:": "クレジット残高:",
|
|
271
|
+
"credits": "クレジット",
|
|
272
|
+
"Select credit workspace": "クレジットワークスペースを選択",
|
|
273
|
+
"Active credit set to: {name}": "有効なクレジットを {name} に設定しました。",
|
|
274
|
+
"Failed to update credit context": "クレジットコンテキストの更新に失敗しました",
|
|
275
|
+
"You are not a member of any team. Using personal credit.": "所属しているチームがありません。個人クレジットを使用します。",
|
|
276
|
+
"Run `pleum balance --switch` to switch credit workspace.": "`pleum balance --switch` でクレジットワークスペースを変更できます。",
|
|
265
277
|
};
|
|
266
278
|
|
package/src/locales/ko.mjs
CHANGED
|
@@ -325,5 +325,17 @@ export const KO = {
|
|
|
325
325
|
"↑/↓ move • enter select • esc back": "↑/↓ 이동 • enter 선택 • esc 뒤로",
|
|
326
326
|
"↑/↓ move • enter set model • esc back": "↑/↓ 이동 • enter 모델 설정 • esc 뒤로",
|
|
327
327
|
"Log out": "로그아웃",
|
|
328
|
+
"Credit:": "크레딧:",
|
|
329
|
+
"Team": "팀",
|
|
330
|
+
"Personal": "개인",
|
|
331
|
+
"Personal Credit": "개인 크레딧",
|
|
332
|
+
"Credit workspace": "크레딧 워크스페이스",
|
|
333
|
+
"Credit Balance:": "크레딧 잔액:",
|
|
334
|
+
"credits": "크레딧",
|
|
335
|
+
"Select credit workspace": "크레딧 워크스페이스 선택",
|
|
336
|
+
"Active credit set to: {name}": "활성 크레딧이 {name}으로 설정되었습니다.",
|
|
337
|
+
"Failed to update credit context": "크레딧 컨텍스트 변경 실패",
|
|
338
|
+
"You are not a member of any team. Using personal credit.": "소속된 팀이 없습니다. 개인 크레딧을 사용합니다.",
|
|
339
|
+
"Run `pleum balance --switch` to switch credit workspace.": "`pleum balance --switch` 명령으로 크레딧 워크스페이스를 변경할 수 있습니다.",
|
|
328
340
|
};
|
|
329
341
|
|
package/src/locales/zh.mjs
CHANGED
|
@@ -262,5 +262,17 @@ export const ZH = {
|
|
|
262
262
|
"✓ installed": "✓ 已安装",
|
|
263
263
|
"✗ not installed —": "✗ 未安装 —",
|
|
264
264
|
"Log out": "登出",
|
|
265
|
+
"Credit:": "额度:",
|
|
266
|
+
"Team": "团队",
|
|
267
|
+
"Personal": "个人",
|
|
268
|
+
"Personal Credit": "个人额度",
|
|
269
|
+
"Credit workspace": "额度工作区",
|
|
270
|
+
"Credit Balance:": "额度余额:",
|
|
271
|
+
"credits": "额度",
|
|
272
|
+
"Select credit workspace": "选择额度工作区",
|
|
273
|
+
"Active credit set to: {name}": "已将有效额度设置为: {name}",
|
|
274
|
+
"Failed to update credit context": "更新额度上下文失败",
|
|
275
|
+
"You are not a member of any team. Using personal credit.": "您不属于任何团队。使用个人额度。",
|
|
276
|
+
"Run `pleum balance --switch` to switch credit workspace.": "运行 `pleum balance --switch` 可切换额度工作区。",
|
|
265
277
|
};
|
|
266
278
|
|
package/src/menu.mjs
CHANGED
|
@@ -13,8 +13,8 @@ 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, isLoggedIn, touchSession } from "./config.mjs";
|
|
17
|
-
import { RECIPES, MANUAL } from "./recipes.mjs";
|
|
16
|
+
import { readConfig, writeConfig, isLoggedIn, touchSession, getActiveContext } from "./config.mjs";
|
|
17
|
+
import { RECIPES, MANUAL, DEFAULT_MODEL } from "./recipes.mjs";
|
|
18
18
|
import { onPath, launch } from "./launch.mjs";
|
|
19
19
|
import { fetchModels, matchModels, listModels, formatModelLabel } from "./models.mjs";
|
|
20
20
|
import { run } from "./run.mjs";
|
|
@@ -56,7 +56,7 @@ const autoInstallable = (r) => !/^https?:/.test(r.install); // URL이면 수동
|
|
|
56
56
|
// 최상위 메뉴 항목(순수 함수 — 테스트 대상). 에이전트는 없음 — "agents" 한 항목으로 서브메뉴 진입.
|
|
57
57
|
// 로그인 항목도 없다 — CLI는 로그인 사용자용이라 mainMenu()가 진입 시점에 게이트로 처리하고,
|
|
58
58
|
// 로그인/로그아웃 전환은 설정>계정이 소유한다.
|
|
59
|
-
export function buildItems({ savedModels = {} }) {
|
|
59
|
+
export function buildItems({ savedModels = {}, activeContext = { type: "personal" } }) {
|
|
60
60
|
const items = [];
|
|
61
61
|
items.push({
|
|
62
62
|
id: "chat",
|
|
@@ -65,11 +65,14 @@ export function buildItems({ savedModels = {} }) {
|
|
|
65
65
|
desc: `${t("Start an interactive chat")}${savedModels.chat ? ` · ${t("model")}: ${savedModels.chat}` : ""}`,
|
|
66
66
|
});
|
|
67
67
|
items.push({ id: "agents", title: t("Run an agent"), desc: t("Pick a coding agent to run through PleumRouter") });
|
|
68
|
+
const contextDesc = activeContext?.type === "team" && activeContext.org_name
|
|
69
|
+
? `${t("Team")}: ${activeContext.org_name}`
|
|
70
|
+
: t("Personal");
|
|
68
71
|
items.push(
|
|
69
72
|
{ id: "models", title: t("Model list"), desc: t("All models — price and context included") },
|
|
70
73
|
{ id: "top", title: t("Leaderboard"), desc: t("Model rankings · provider share") },
|
|
71
74
|
{ id: "usage", title: t("Usage report"), desc: t("Spend over the last 30 days") },
|
|
72
|
-
{ id: "balance", title: t("Balance"), desc: t("Check your credit balance") },
|
|
75
|
+
{ id: "balance", title: t("Balance"), desc: `${t("Check your credit balance")} · ${contextDesc}` },
|
|
73
76
|
{ id: "health", title: t("Health check"), desc: t("Gateway connectivity · login status") },
|
|
74
77
|
{ id: "settings", title: t("Settings"), desc: t("Language, default models, account, updates") },
|
|
75
78
|
);
|
|
@@ -120,7 +123,7 @@ function saveLastSelection(id) {
|
|
|
120
123
|
writeConfig(cfg);
|
|
121
124
|
}
|
|
122
125
|
|
|
123
|
-
async function pickModelFor(item) {
|
|
126
|
+
export async function pickModelFor(item, defaultModel) {
|
|
124
127
|
let models;
|
|
125
128
|
try {
|
|
126
129
|
models = await fetchModels();
|
|
@@ -128,11 +131,14 @@ async function pickModelFor(item) {
|
|
|
128
131
|
console.log(`${style.red("✗")} ${t("Failed to fetch model list: {error}", { error: sanitizeTerminalText(e.message) })}`);
|
|
129
132
|
return undefined;
|
|
130
133
|
}
|
|
134
|
+
const targetId = defaultModel || DEFAULT_MODEL;
|
|
135
|
+
const initialIndex = models.findIndex((x) => x.id === targetId);
|
|
131
136
|
const m = await select(models, {
|
|
132
137
|
message: `${item.title} — ${t("select a model")}`,
|
|
133
138
|
formatItem: (x) => formatModelLabel(x),
|
|
134
139
|
matches: matchModels,
|
|
135
|
-
limit:
|
|
140
|
+
limit: 10,
|
|
141
|
+
initialCursor: initialIndex >= 0 ? initialIndex : 0,
|
|
136
142
|
});
|
|
137
143
|
return m?.id;
|
|
138
144
|
}
|
|
@@ -229,7 +235,8 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
|
|
|
229
235
|
// 선택되면 ollama처럼 즉시 실행으로 이어가고, 취소면 메뉴 복귀.
|
|
230
236
|
const configure = async (it) => {
|
|
231
237
|
suspend();
|
|
232
|
-
const
|
|
238
|
+
const currentModel = it.tool ? (readConfig()?.launch_models?.[it.tool] || DEFAULT_MODEL) : undefined;
|
|
239
|
+
const picked = await pickModelFor(it, currentModel);
|
|
233
240
|
if (picked) {
|
|
234
241
|
input.pause();
|
|
235
242
|
resolve({ item: it, model: picked });
|
|
@@ -271,7 +278,10 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
|
|
|
271
278
|
async function menuSelect() {
|
|
272
279
|
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
273
280
|
const cfg = readConfig();
|
|
274
|
-
const items = buildItems({
|
|
281
|
+
const items = buildItems({
|
|
282
|
+
savedModels: cfg?.launch_models ?? {},
|
|
283
|
+
activeContext: getActiveContext(cfg),
|
|
284
|
+
});
|
|
275
285
|
const last = cfg?.last_selection;
|
|
276
286
|
const initialCursor = last ? Math.max(0, items.findIndex((it) => it.id === last)) : 0;
|
|
277
287
|
return runList(items, {
|
|
@@ -292,6 +302,48 @@ async function agentSelect() {
|
|
|
292
302
|
});
|
|
293
303
|
}
|
|
294
304
|
|
|
305
|
+
// 에이전트 실행 메뉴 — bare pleum의 "에이전트 실행"과 pleum launch (인자 없음)가 공유.
|
|
306
|
+
// 에이전트를 고르면 곧바로 핵심 10개 모델 피커를 띄워 기본 모델(deepseek-v4.1-flash) 또는
|
|
307
|
+
// 선택한 우리 모델로 코딩 에이전트를 실행한다.
|
|
308
|
+
export async function runAgentMenu({ loop = false } = {}) {
|
|
309
|
+
const io = process.stdin;
|
|
310
|
+
if (!io.isTTY || !process.stdout.isTTY || typeof io.setRawMode !== "function") return false;
|
|
311
|
+
if (!isLoggedIn()) await login();
|
|
312
|
+
else await touchSession().catch(() => {});
|
|
313
|
+
|
|
314
|
+
for (;;) {
|
|
315
|
+
const agentPicked = await agentSelect();
|
|
316
|
+
if (agentPicked === "quit") return "quit";
|
|
317
|
+
if (!agentPicked) return null;
|
|
318
|
+
|
|
319
|
+
const { item: agentItem } = agentPicked;
|
|
320
|
+
if (agentItem.manual) {
|
|
321
|
+
await launch(agentItem.tool, undefined, [], { exit: false });
|
|
322
|
+
if (!loop) return true;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const r = RECIPES[agentItem.tool];
|
|
327
|
+
if (!onPath(r.bin) && !(await ensureInstalled(r))) {
|
|
328
|
+
if (!loop) return false;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const agentSaved = readConfig()?.launch_models ?? {};
|
|
333
|
+
let chosenModel = agentPicked.model;
|
|
334
|
+
if (!chosenModel) {
|
|
335
|
+
const defaultForTool = agentSaved[agentItem.tool] || DEFAULT_MODEL;
|
|
336
|
+
chosenModel = await pickModelFor(agentItem, defaultForTool);
|
|
337
|
+
if (!chosenModel) continue; // Esc 취소 시 에이전트 목록으로 복귀
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
saveModel(agentItem.tool, chosenModel);
|
|
341
|
+
await launch(agentItem.tool, chosenModel, [], { exit: false });
|
|
342
|
+
console.log();
|
|
343
|
+
if (!loop) return true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
295
347
|
// bare `pleum` 진입점. 비TTY면 false(호출자가 HELP 출력), 그 외 true.
|
|
296
348
|
// ollama처럼 실행이 끝나면 메뉴로 복귀하는 루프(REPL 채팅·ctrl+c 종료 제외).
|
|
297
349
|
export async function mainMenu() {
|
|
@@ -331,7 +383,7 @@ export async function mainMenu() {
|
|
|
331
383
|
await usage([]);
|
|
332
384
|
continue;
|
|
333
385
|
case "balance":
|
|
334
|
-
await balance();
|
|
386
|
+
await balance({ interactive: true });
|
|
335
387
|
continue;
|
|
336
388
|
case "health":
|
|
337
389
|
await health();
|
|
@@ -342,19 +394,9 @@ export async function mainMenu() {
|
|
|
342
394
|
continue; // esc/q — 메인 메뉴로 복귀
|
|
343
395
|
}
|
|
344
396
|
case "agents": {
|
|
345
|
-
const
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
const { item: agentItem, model: agentModel } = agentPicked;
|
|
349
|
-
if (agentModel && !agentItem.manual) saveModel(agentItem.tool, agentModel);
|
|
350
|
-
if (!agentItem.manual) {
|
|
351
|
-
const r = RECIPES[agentItem.tool];
|
|
352
|
-
if (!onPath(r.bin) && !(await ensureInstalled(r))) continue;
|
|
353
|
-
}
|
|
354
|
-
const agentSaved = readConfig()?.launch_models ?? {};
|
|
355
|
-
await launch(agentItem.tool, agentModel ?? agentSaved[agentItem.tool], [], { exit: false });
|
|
356
|
-
console.log(); // 에이전트 출력과 다음 메뉴 사이 여백
|
|
357
|
-
continue; // ollama처럼 에이전트 종료 후 메뉴 복귀
|
|
397
|
+
const r = await runAgentMenu({ loop: true });
|
|
398
|
+
if (r === "quit") return true;
|
|
399
|
+
continue;
|
|
358
400
|
}
|
|
359
401
|
}
|
|
360
402
|
}
|
package/src/recipes.mjs
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { ROOT, V1 } from "./config.mjs";
|
|
2
2
|
|
|
3
|
+
export const DEFAULT_MODEL = "deepseek-ai/DeepSeek-V4.1-Flash";
|
|
4
|
+
|
|
5
|
+
export const CORE_MODELS = [
|
|
6
|
+
"deepseek-ai/DeepSeek-V4.1-Flash",
|
|
7
|
+
"claude-sonnet-5",
|
|
8
|
+
"claude-opus-5-5",
|
|
9
|
+
"gpt-6-astra",
|
|
10
|
+
"claude-fable-5-1",
|
|
11
|
+
"gemini-3.8-flash",
|
|
12
|
+
"glm-5.3-flash",
|
|
13
|
+
"kimi-k3",
|
|
14
|
+
"gpt-6-sol",
|
|
15
|
+
"gpt-6-luna",
|
|
16
|
+
];
|
|
17
|
+
|
|
3
18
|
// 도구별 설정의 진실 원천. 출처: frontend .../docs/cookbook/{codex,claude-code,...} + 에이전트별 공식 문서.
|
|
4
19
|
// type "env" → 환경변수만 세팅하고 바로 실행.
|
|
5
20
|
// type "codex-home" → 격리 CODEX_HOME에 config.toml (사용자 ~/.codex 보존).
|
|
@@ -34,7 +49,7 @@ export const RECIPES = {
|
|
|
34
49
|
// /model 이 GET {ANTHROPIC_BASE_URL}/v1/models 를 치게 한다. 기본값은 꺼져 있다.
|
|
35
50
|
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
|
|
36
51
|
}),
|
|
37
|
-
args: (m) =>
|
|
52
|
+
args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("anthropic/") ? (m || DEFAULT_MODEL) : `anthropic/${m || DEFAULT_MODEL}`],
|
|
38
53
|
},
|
|
39
54
|
codex: {
|
|
40
55
|
label: "Codex CLI",
|
|
@@ -58,7 +73,7 @@ export const RECIPES = {
|
|
|
58
73
|
referer: "https://aider.chat",
|
|
59
74
|
},
|
|
60
75
|
env: (key) => ({ OPENAI_API_BASE: V1, OPENAI_API_KEY: key }),
|
|
61
|
-
args: (m) =>
|
|
76
|
+
args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("openai/") ? (m || DEFAULT_MODEL) : `openai/${m || DEFAULT_MODEL}`],
|
|
62
77
|
},
|
|
63
78
|
goose: {
|
|
64
79
|
label: "Goose",
|
|
@@ -98,20 +113,25 @@ export const RECIPES = {
|
|
|
98
113
|
title: "opencode",
|
|
99
114
|
referer: "https://opencode.ai",
|
|
100
115
|
},
|
|
101
|
-
env: (key, model) =>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
env: (key, model) => {
|
|
117
|
+
const active = model || DEFAULT_MODEL;
|
|
118
|
+
const models = Object.fromEntries(CORE_MODELS.map((id) => [id, {}]));
|
|
119
|
+
models[active] = {};
|
|
120
|
+
return {
|
|
121
|
+
PLEUM_API_KEY: key,
|
|
122
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
123
|
+
provider: {
|
|
124
|
+
pleum: {
|
|
125
|
+
npm: "@ai-sdk/openai-compatible",
|
|
126
|
+
name: "PleumRouter",
|
|
127
|
+
options: { baseURL: V1, apiKey: "{env:PLEUM_API_KEY}" },
|
|
128
|
+
models,
|
|
129
|
+
},
|
|
110
130
|
},
|
|
111
|
-
},
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
args: (m) => ["--model", `pleum/${m ||
|
|
131
|
+
}),
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
args: (m) => ["--model", `pleum/${m || DEFAULT_MODEL}`],
|
|
115
135
|
},
|
|
116
136
|
omp: {
|
|
117
137
|
label: "omp (Oh My Pi)",
|
|
@@ -126,7 +146,7 @@ export const RECIPES = {
|
|
|
126
146
|
referer: "https://omp.sh",
|
|
127
147
|
},
|
|
128
148
|
env: (key) => ({ ANTHROPIC_BASE_URL: ROOT, ANTHROPIC_API_KEY: key }),
|
|
129
|
-
args: (m) =>
|
|
149
|
+
args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("anthropic/") ? (m || DEFAULT_MODEL) : `anthropic/${m || DEFAULT_MODEL}`],
|
|
130
150
|
},
|
|
131
151
|
hermes: {
|
|
132
152
|
label: "Hermes Agent",
|
|
@@ -150,7 +170,7 @@ export const RECIPES = {
|
|
|
150
170
|
title: "Qwen Code",
|
|
151
171
|
referer: "https://github.com/QwenLM/qwen-code",
|
|
152
172
|
},
|
|
153
|
-
env: (key, model) => ({ OPENAI_BASE_URL: V1, OPENAI_API_KEY: key, OPENAI_MODEL: model ||
|
|
173
|
+
env: (key, model) => ({ OPENAI_BASE_URL: V1, OPENAI_API_KEY: key, OPENAI_MODEL: model || DEFAULT_MODEL }),
|
|
154
174
|
args: () => [],
|
|
155
175
|
},
|
|
156
176
|
copilot: {
|
|
@@ -167,7 +187,7 @@ export const RECIPES = {
|
|
|
167
187
|
COPILOT_PROVIDER_BASE_URL: V1,
|
|
168
188
|
COPILOT_PROVIDER_TYPE: "openai",
|
|
169
189
|
COPILOT_PROVIDER_API_KEY: key,
|
|
170
|
-
COPILOT_MODEL: model ||
|
|
190
|
+
COPILOT_MODEL: model || DEFAULT_MODEL,
|
|
171
191
|
}),
|
|
172
192
|
args: () => [],
|
|
173
193
|
},
|
|
@@ -189,7 +209,7 @@ export const RECIPES = {
|
|
|
189
209
|
GEMINI_API_KEY: key,
|
|
190
210
|
GOOGLE_API_KEY: key,
|
|
191
211
|
}),
|
|
192
|
-
args: (m) =>
|
|
212
|
+
args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("google/") ? (m || DEFAULT_MODEL) : `google/${m || DEFAULT_MODEL}`],
|
|
193
213
|
},
|
|
194
214
|
agy: {
|
|
195
215
|
label: "Antigravity CLI",
|
|
@@ -206,7 +226,7 @@ export const RECIPES = {
|
|
|
206
226
|
GEMINI_API_KEY: key,
|
|
207
227
|
GOOGLE_API_KEY: key,
|
|
208
228
|
}),
|
|
209
|
-
args: (m) =>
|
|
229
|
+
args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("google/") ? (m || DEFAULT_MODEL) : `google/${m || DEFAULT_MODEL}`],
|
|
210
230
|
},
|
|
211
231
|
pi: {
|
|
212
232
|
label: "Pi",
|
|
@@ -219,7 +239,7 @@ export const RECIPES = {
|
|
|
219
239
|
referer: "https://pi.dev",
|
|
220
240
|
},
|
|
221
241
|
env: (key) => ({ PLEUM_API_KEY: key }),
|
|
222
|
-
args: (m) => ["--model", m ||
|
|
242
|
+
args: (m) => ["--model", m || DEFAULT_MODEL],
|
|
223
243
|
},
|
|
224
244
|
crush: {
|
|
225
245
|
label: "Crush",
|
|
@@ -246,9 +266,9 @@ export const RECIPES = {
|
|
|
246
266
|
env: (key, model) => ({
|
|
247
267
|
OPENAI_BASE_URL: V1,
|
|
248
268
|
OPENAI_API_KEY: key,
|
|
249
|
-
|
|
269
|
+
OPENAI_MODEL: model || DEFAULT_MODEL,
|
|
250
270
|
}),
|
|
251
|
-
args: (m) =>
|
|
271
|
+
args: (m) => ["--model", m || DEFAULT_MODEL],
|
|
252
272
|
},
|
|
253
273
|
mimo: {
|
|
254
274
|
label: "MiMo Code",
|
|
@@ -262,9 +282,9 @@ export const RECIPES = {
|
|
|
262
282
|
env: (key, model) => ({
|
|
263
283
|
OPENAI_BASE_URL: V1,
|
|
264
284
|
OPENAI_API_KEY: key,
|
|
265
|
-
|
|
285
|
+
OPENAI_MODEL: model || DEFAULT_MODEL,
|
|
266
286
|
}),
|
|
267
|
-
args: (m) =>
|
|
287
|
+
args: (m) => ["--model", m || DEFAULT_MODEL],
|
|
268
288
|
},
|
|
269
289
|
openclaw: {
|
|
270
290
|
label: "OpenClaw",
|
|
@@ -278,9 +298,9 @@ export const RECIPES = {
|
|
|
278
298
|
env: (key, model) => ({
|
|
279
299
|
OPENAI_BASE_URL: V1,
|
|
280
300
|
OPENAI_API_KEY: key,
|
|
281
|
-
|
|
301
|
+
OPENAI_MODEL: model || DEFAULT_MODEL,
|
|
282
302
|
}),
|
|
283
|
-
args: (m) =>
|
|
303
|
+
args: (m) => ["--model", m || DEFAULT_MODEL],
|
|
284
304
|
},
|
|
285
305
|
plandex: {
|
|
286
306
|
label: "Plandex",
|
|
@@ -310,7 +330,7 @@ export const RECIPES = {
|
|
|
310
330
|
OPENAI_API_BASE: V1,
|
|
311
331
|
OPENAI_API_KEY: key,
|
|
312
332
|
}),
|
|
313
|
-
args: (m) =>
|
|
333
|
+
args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("openai/") ? (m || DEFAULT_MODEL) : `openai/${m || DEFAULT_MODEL}`],
|
|
314
334
|
},
|
|
315
335
|
};
|
|
316
336
|
|
|
@@ -357,7 +377,7 @@ export function tomlBasicString(value) {
|
|
|
357
377
|
}
|
|
358
378
|
|
|
359
379
|
export function codexToml(model) {
|
|
360
|
-
const m = model ||
|
|
380
|
+
const m = model || DEFAULT_MODEL;
|
|
361
381
|
return `# Generated by pleum CLI. Isolated CODEX_HOME — your ~/.codex is untouched.
|
|
362
382
|
model = ${tomlBasicString(m)}
|
|
363
383
|
model_provider = "pleum"
|
|
@@ -371,7 +391,7 @@ supports_websockets = false
|
|
|
371
391
|
`;
|
|
372
392
|
}
|
|
373
393
|
|
|
374
|
-
export function mergeAgySettings(existingJson) {
|
|
394
|
+
export function mergeAgySettings(existingJson, model) {
|
|
375
395
|
let obj = {};
|
|
376
396
|
if (existingJson && String(existingJson).trim()) {
|
|
377
397
|
obj = JSON.parse(existingJson);
|
|
@@ -379,11 +399,11 @@ export function mergeAgySettings(existingJson) {
|
|
|
379
399
|
throw new Error("agy settings.json must be a JSON object");
|
|
380
400
|
}
|
|
381
401
|
}
|
|
382
|
-
return { ...obj, modelProvider: "gemini" };
|
|
402
|
+
return { ...obj, modelProvider: "gemini", model: model || DEFAULT_MODEL };
|
|
383
403
|
}
|
|
384
404
|
|
|
385
405
|
export function piModelsJson(model) {
|
|
386
|
-
const m = model ||
|
|
406
|
+
const m = model || DEFAULT_MODEL;
|
|
387
407
|
return `${JSON.stringify({
|
|
388
408
|
providers: {
|
|
389
409
|
pleum: {
|
|
@@ -397,7 +417,7 @@ export function piModelsJson(model) {
|
|
|
397
417
|
}
|
|
398
418
|
|
|
399
419
|
export function crushJson(model) {
|
|
400
|
-
const m = model ||
|
|
420
|
+
const m = model || DEFAULT_MODEL;
|
|
401
421
|
return `${JSON.stringify({
|
|
402
422
|
providers: {
|
|
403
423
|
pleum: {
|
package/src/settings.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
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, isLoggedIn } from "./config.mjs";
|
|
11
|
+
import { readConfig, writeConfig, isLoggedIn, getActiveContext } 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";
|
|
@@ -16,13 +16,18 @@ 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";
|
|
19
|
+
import { switchCreditWorkspace } from "./balance.mjs";
|
|
19
20
|
import { runList, saveModel } from "./menu.mjs";
|
|
20
21
|
|
|
21
22
|
// Settings 서브메뉴 항목(순수 함수 — 테스트 대상).
|
|
22
|
-
export function buildSettingsItems({ lang, loggedIn, keyPrefix, version }) {
|
|
23
|
+
export function buildSettingsItems({ lang, loggedIn, keyPrefix, version, activeContext = { type: "personal" } }) {
|
|
24
|
+
const contextDesc = activeContext?.type === "team" && activeContext.org_name
|
|
25
|
+
? `${t("Team")}: ${activeContext.org_name}`
|
|
26
|
+
: t("Personal Credit");
|
|
23
27
|
return [
|
|
24
28
|
{ id: "settings:lang", title: t("Language"), desc: `${t("current")}: ${LANGS[lang] ?? lang}` },
|
|
25
29
|
{ id: "settings:models", title: t("Default models"), desc: t("Set the default model used per agent") },
|
|
30
|
+
{ id: "settings:context", title: t("Credit workspace"), desc: contextDesc },
|
|
26
31
|
{
|
|
27
32
|
id: "settings:account",
|
|
28
33
|
// 제목 자체가 다음 동작을 말한다 — 로그인 상태에서 Enter는 로그아웃이다.
|
|
@@ -73,6 +78,7 @@ async function settingsSelect() {
|
|
|
73
78
|
loggedIn: isLoggedIn(cfg),
|
|
74
79
|
keyPrefix: cfg?.access_token ? "oauth" : cfg?.key ? cfg.key.slice(0, 8) : null,
|
|
75
80
|
version: readVersion(),
|
|
81
|
+
activeContext: getActiveContext(cfg),
|
|
76
82
|
});
|
|
77
83
|
return runList(items, {
|
|
78
84
|
header: style.bold(style.plum(t("Settings"))),
|
|
@@ -152,6 +158,9 @@ export async function runSettings() {
|
|
|
152
158
|
if (r === "quit") return "quit";
|
|
153
159
|
continue;
|
|
154
160
|
}
|
|
161
|
+
case "settings:context":
|
|
162
|
+
await switchCreditWorkspace();
|
|
163
|
+
continue;
|
|
155
164
|
case "settings:account":
|
|
156
165
|
await runAccount();
|
|
157
166
|
continue;
|