pleumrouter 0.5.45 → 0.5.46
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/balance.mjs +38 -137
- package/src/config.mjs +5 -18
- package/src/launch.mjs +2 -6
- package/src/locales/es.mjs +3 -0
- package/src/locales/ja.mjs +3 -0
- package/src/locales/ko.mjs +3 -0
- package/src/locales/zh.mjs +3 -0
- package/src/menu.mjs +3 -5
- package/src/run.mjs +19 -2
- package/src/settings.mjs +3 -6
- package/src/wire.mjs +230 -142
- package/src/workspace.mjs +3 -3
package/package.json
CHANGED
package/src/balance.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { authFetch, getActiveContext
|
|
1
|
+
import { authFetch, getActiveContext } from "./config.mjs";
|
|
2
2
|
import { style } from "./ansi.mjs";
|
|
3
3
|
import { select } from "./interactive.mjs";
|
|
4
4
|
import { t } from "./i18n.mjs";
|
|
5
|
+
import { fetchWorkspaceContext, selectAndBindWorkspace, workspaceLabel } from "./workspace.mjs";
|
|
5
6
|
|
|
6
7
|
export function formatKrwCredit(amount) {
|
|
7
8
|
return Number(amount || 0).toLocaleString("ko-KR", {
|
|
@@ -10,146 +11,59 @@ export function formatKrwCredit(amount) {
|
|
|
10
11
|
});
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
// 개인 지갑 잔액만 조회한다. 공유 공간 목록·선택은 selectAndBindWorkspace가 맡는다.
|
|
15
|
+
export async function fetchCreditSummary({ auth = authFetch } = {}) {
|
|
16
|
+
const res = await auth("/credits");
|
|
15
17
|
if (!res.ok) {
|
|
16
18
|
const body = await res.text().catch(() => "");
|
|
17
19
|
throw new Error(t("Failed to fetch balance ({status})", { status: res.status }) + (body ? `: ${body}` : ""));
|
|
18
20
|
}
|
|
19
21
|
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
|
-
let teamWallets = [];
|
|
30
|
-
if (Array.isArray(orgs) && orgs.length > 0) {
|
|
31
|
-
const results = 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
|
-
return {
|
|
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
|
-
return null;
|
|
46
|
-
})
|
|
47
|
-
);
|
|
48
|
-
teamWallets = results.filter(Boolean);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
22
|
return {
|
|
52
23
|
personal: {
|
|
53
24
|
balance_krw: personal.balance_krw ?? 0,
|
|
54
25
|
balance_usd: personal.balance_usd,
|
|
55
26
|
},
|
|
56
|
-
teams: teamWallets,
|
|
57
27
|
};
|
|
58
28
|
}
|
|
59
29
|
|
|
60
|
-
export function buildCreditOptions({ personal,
|
|
61
|
-
const options = [];
|
|
30
|
+
export function buildCreditOptions({ personal, activeContext = { type: "personal" } }) {
|
|
62
31
|
const isPersonalActive = activeContext.type === "personal";
|
|
63
32
|
const personalBalanceStr = `${formatKrwCredit(personal.balance_krw)} ${t("credits")}${
|
|
64
33
|
personal.balance_usd != null ? ` / $${personal.balance_usd.toFixed(4)}` : ""
|
|
65
34
|
}`;
|
|
66
35
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const isTeamActive = activeContext.type === "team" && String(activeContext.org_id) === String(team.id);
|
|
79
|
-
const teamBalanceStr = `${formatKrwCredit(team.balance_krw)} ${t("credits")}`;
|
|
80
|
-
const roleStr = team.role ? ` (${team.role})` : "";
|
|
81
|
-
const teamTitle = `${t("Team")}: ${team.name}${roleStr}`;
|
|
82
|
-
options.push({
|
|
83
|
-
id: String(team.id),
|
|
84
|
-
type: "team",
|
|
85
|
-
name: team.name,
|
|
86
|
-
title: teamTitle,
|
|
87
|
-
balance: teamBalanceStr,
|
|
88
|
-
isActive: isTeamActive,
|
|
89
|
-
label: `${isTeamActive ? "✓ " : " "}${teamTitle}: ${teamBalanceStr}`,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return options;
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
id: "personal",
|
|
39
|
+
type: "personal",
|
|
40
|
+
name: t("Personal"),
|
|
41
|
+
title: t("Personal Credit"),
|
|
42
|
+
balance: personalBalanceStr,
|
|
43
|
+
isActive: isPersonalActive,
|
|
44
|
+
label: `${isPersonalActive ? "✓ " : " "}${t("Personal Credit")}: ${personalBalanceStr}`,
|
|
45
|
+
},
|
|
46
|
+
];
|
|
94
47
|
}
|
|
95
48
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
} catch (e) {
|
|
101
|
-
console.error(`✗ ${e.message}`);
|
|
102
|
-
return getActiveContext();
|
|
103
|
-
}
|
|
104
|
-
const active = getActiveContext();
|
|
105
|
-
const options = buildCreditOptions({
|
|
106
|
-
personal: data.personal,
|
|
107
|
-
teams: data.teams,
|
|
108
|
-
activeContext: active,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
if (options.length <= 1) {
|
|
112
|
-
console.log(t("You are not a member of any team. Using personal credit."));
|
|
113
|
-
return active;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const picked = await selectFn(options, {
|
|
117
|
-
message: t("Select credit workspace"),
|
|
118
|
-
formatItem: (x) => x.label,
|
|
119
|
-
limit: 10,
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
if (!picked) return active;
|
|
123
|
-
|
|
124
|
-
const targetOrgId = picked.type === "team" ? picked.id : null;
|
|
49
|
+
// 과금 공간 전환. 후보 선택·키 재바인딩·저장은 selectAndBindWorkspace가 한다.
|
|
50
|
+
export async function switchCreditWorkspace({ selectFn = select } = {}) {
|
|
51
|
+
return selectAndBindWorkspace({ selectFn });
|
|
52
|
+
}
|
|
125
53
|
|
|
54
|
+
// 전환 안내는 공유 공간 후보가 있을 때만 띄운다. 조회가 실패하면 조용히 생략한다.
|
|
55
|
+
async function hasWorkspaceChoices() {
|
|
126
56
|
try {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
});
|
|
132
|
-
if (!res.ok) {
|
|
133
|
-
const err = await res.text().catch(() => "");
|
|
134
|
-
console.error(`✗ ${t("Failed to update credit context")}: ${err}`);
|
|
135
|
-
return active;
|
|
136
|
-
}
|
|
137
|
-
} catch (e) {
|
|
138
|
-
console.error(`✗ ${t("Failed to update credit context")}: ${e.message}`);
|
|
139
|
-
return active;
|
|
57
|
+
const data = await fetchWorkspaceContext();
|
|
58
|
+
return (data?.workspace_candidates ?? []).length > 0;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
140
61
|
}
|
|
141
|
-
|
|
142
|
-
const newContext = setActiveContext({
|
|
143
|
-
type: picked.type,
|
|
144
|
-
org_id: targetOrgId,
|
|
145
|
-
org_name: picked.type === "team" ? picked.name : null,
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
console.log(`✓ ${t("Active credit set to: {name}", { name: picked.title })}`);
|
|
149
|
-
return newContext;
|
|
150
62
|
}
|
|
151
63
|
|
|
152
64
|
export async function balance({ interactive = false, switchOnly = false, exitOnError = true } = {}) {
|
|
65
|
+
if (switchOnly) return await switchCreditWorkspace();
|
|
66
|
+
|
|
153
67
|
let summary;
|
|
154
68
|
try {
|
|
155
69
|
summary = await fetchCreditSummary();
|
|
@@ -159,28 +73,15 @@ export async function balance({ interactive = false, switchOnly = false, exitOnE
|
|
|
159
73
|
return;
|
|
160
74
|
}
|
|
161
75
|
|
|
162
|
-
if (switchOnly) {
|
|
163
|
-
return await switchCreditWorkspace({ summary });
|
|
164
|
-
}
|
|
165
|
-
|
|
166
76
|
const active = getActiveContext();
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const mark = opt.isActive ? style.bold(style.plum("▸ [Active] ")) : " [ ] ";
|
|
177
|
-
console.log(`${mark}${opt.title}: ${opt.balance}`);
|
|
178
|
-
}
|
|
179
|
-
if (options.length > 1 && !interactive) {
|
|
180
|
-
console.log(`\n${style.dim(t("Run `pleum balance --switch` to switch credit workspace."))}`);
|
|
181
|
-
}
|
|
182
|
-
return;
|
|
77
|
+
const [personal] = buildCreditOptions({ personal: summary.personal, activeContext: active });
|
|
78
|
+
const mark = personal.isActive ? style.bold(style.plum("▸ [Active] ")) : " [ ] ";
|
|
79
|
+
console.log(t("Credit Balance:"));
|
|
80
|
+
console.log(`${mark}${personal.title}: ${personal.balance}`);
|
|
81
|
+
console.log(` ${t("Credit workspace")}: ${workspaceLabel(active)}`);
|
|
82
|
+
|
|
83
|
+
if (interactive) return await switchCreditWorkspace();
|
|
84
|
+
if (await hasWorkspaceChoices()) {
|
|
85
|
+
console.log(`\n${style.dim(t("Run `pleum balance --switch` to switch credit workspace."))}`);
|
|
183
86
|
}
|
|
184
|
-
|
|
185
|
-
return await switchCreditWorkspace({ summary });
|
|
186
87
|
}
|
package/src/config.mjs
CHANGED
|
@@ -215,9 +215,7 @@ export function getActiveContext(cfg = readConfig()) {
|
|
|
215
215
|
workspace_name: ctx.workspace_name || ctx.workspace_id,
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
|
-
|
|
219
|
-
return { type: "team", org_id: ctx.org_id, org_name: ctx.org_name || ctx.org_id };
|
|
220
|
-
}
|
|
218
|
+
// 팀 컨텍스트는 제거됐다. 예전에 저장된 team 값도 개인으로 본다.
|
|
221
219
|
return { type: "personal", org_id: null, org_name: null };
|
|
222
220
|
}
|
|
223
221
|
|
|
@@ -230,29 +228,18 @@ export function setActiveContext(context, cfg = readConfig()) {
|
|
|
230
228
|
workspace_id: context.workspace_id,
|
|
231
229
|
workspace_name: context.workspace_name || null,
|
|
232
230
|
};
|
|
233
|
-
} else if (!context || context.type === "personal" || !context.org_id) {
|
|
234
|
-
updated.active_context = { type: "personal" };
|
|
235
231
|
} else {
|
|
236
|
-
updated.active_context = {
|
|
237
|
-
type: "team",
|
|
238
|
-
org_id: context.org_id,
|
|
239
|
-
org_name: context.org_name || null,
|
|
240
|
-
};
|
|
232
|
+
updated.active_context = { type: "personal" };
|
|
241
233
|
}
|
|
242
234
|
writeConfig(updated);
|
|
243
235
|
return updated.active_context;
|
|
244
236
|
}
|
|
245
237
|
|
|
246
|
-
/** Bearer 요청 — 401이면 1회 refresh 후 재시도.
|
|
238
|
+
/** Bearer 요청 — 401이면 1회 refresh 후 재시도. 과금 귀속은 키의 workspace_id가 정하므로 헤더를 붙이지 않는다. */
|
|
247
239
|
export async function authFetch(path, init = {}) {
|
|
248
240
|
const token = await requireAuth();
|
|
249
241
|
const url = path.startsWith("http") ? path : `${V1}${path.startsWith("/") ? path : `/${path}`}`;
|
|
250
|
-
const
|
|
251
|
-
const customHeaders = {};
|
|
252
|
-
if (ctx.type === "team" && ctx.org_id) {
|
|
253
|
-
customHeaders["X-Pleum-Org"] = ctx.org_id;
|
|
254
|
-
}
|
|
255
|
-
const headers = { ...customHeaders, ...(init.headers || {}), Authorization: `Bearer ${token}` };
|
|
242
|
+
const headers = { ...(init.headers || {}), Authorization: `Bearer ${token}` };
|
|
256
243
|
let res = await fetch(url, { ...init, headers });
|
|
257
244
|
if (res.status !== 401) return res;
|
|
258
245
|
|
|
@@ -261,7 +248,7 @@ export async function authFetch(path, init = {}) {
|
|
|
261
248
|
if (!refreshed) return res;
|
|
262
249
|
return fetch(url, {
|
|
263
250
|
...init,
|
|
264
|
-
headers: { ...
|
|
251
|
+
headers: { ...(init.headers || {}), Authorization: `Bearer ${refreshed}` },
|
|
265
252
|
});
|
|
266
253
|
}
|
|
267
254
|
|
package/src/launch.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { join, delimiter, dirname, resolve } from "node:path";
|
|
|
5
5
|
import { readConfig, isLoggedIn, getActiveContext, ROOT } from "./config.mjs";
|
|
6
6
|
import { login } from "./login.mjs";
|
|
7
7
|
import { ensureWireKey } from "./wire.mjs";
|
|
8
|
+
import { workspaceLabel } from "./workspace.mjs";
|
|
8
9
|
import {
|
|
9
10
|
RECIPES,
|
|
10
11
|
CORE_MODELS,
|
|
@@ -316,12 +317,7 @@ export async function launch(tool, model, passthrough, {
|
|
|
316
317
|
process.removeListener("unhandledRejection", onException);
|
|
317
318
|
};
|
|
318
319
|
|
|
319
|
-
const
|
|
320
|
-
const creditMsg = activeCtx.type === "workspace"
|
|
321
|
-
? `${t("Workspace")}: ${sanitizeTerminalText(activeCtx.workspace_name)}`
|
|
322
|
-
: activeCtx.type === "team"
|
|
323
|
-
? `${t("Team")}: ${activeCtx.org_name}`
|
|
324
|
-
: t("Personal");
|
|
320
|
+
const creditMsg = workspaceLabel(getActiveContext());
|
|
325
321
|
console.log(`ℹ ${t("Credit:")} ${creditMsg}`);
|
|
326
322
|
|
|
327
323
|
const args = [...r.args(model), ...passthrough];
|
package/src/locales/es.mjs
CHANGED
|
@@ -169,6 +169,8 @@ export const ES = {
|
|
|
169
169
|
"Unsupported tool: {tool}. Use claude or codex.": "Herramienta no compatible: {tool}. Usa claude o codex.",
|
|
170
170
|
"Wrote {path}": "Escrito en {path}",
|
|
171
171
|
"Wired {tool}.": "Cableado {tool}.",
|
|
172
|
+
"The wire key is shared by all wired tools and launch, so a billing workspace change applies to all of them.": "La clave de conexión la comparten todas las herramientas conectadas y launch, así que cambiar el espacio de facturación se aplica a todas.",
|
|
173
|
+
"{path}: not valid JSON — fix or remove it, nothing was written": "{path}: no es JSON válido — corríjalo o elimínelo; no se escribió nada",
|
|
172
174
|
"Desktop key: {prefix}…": "Clave de escritorio: {prefix}…",
|
|
173
175
|
"Claude Code Desktop: Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference. Gateway base URL {root} (no /v1), Bearer, paste the plm_ key.":
|
|
174
176
|
"Claude Code Desktop: Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference. Gateway base URL {root} (sin /v1), Bearer, pega la clave plm_.",
|
|
@@ -244,6 +246,7 @@ export const ES = {
|
|
|
244
246
|
"↑/↓ move • enter set model • esc back": "↑/↓ mover • enter definir modelo • esc volver",
|
|
245
247
|
"⏹ aborted": "⏹ abortado",
|
|
246
248
|
"⏹ generation aborted — the partial reply stays in context": "⏹ generación abortada — la respuesta parcial permanece en el contexto",
|
|
249
|
+
"partial reply kept in context": "la respuesta parcial se mantuvo en el contexto",
|
|
247
250
|
"✓ installed": "✓ instalado",
|
|
248
251
|
"✗ not installed —": "✗ no instalado —",
|
|
249
252
|
"Log out": "Cerrar sesión",
|
package/src/locales/ja.mjs
CHANGED
|
@@ -169,6 +169,8 @@ export const JA = {
|
|
|
169
169
|
"Example: pleum wire": "例: pleum wire",
|
|
170
170
|
"Wrote {path}": "{path} に書きました",
|
|
171
171
|
"Wired {tool}.": "{tool} を配線しました。",
|
|
172
|
+
"The wire key is shared by all wired tools and launch, so a billing workspace change applies to all of them.": "配線キーは配線済みのすべてのツールと launch で共有されるため、課金ワークスペースを変更するとすべてに適用されます。",
|
|
173
|
+
"{path}: not valid JSON — fix or remove it, nothing was written": "{path}: 有効な JSON ではありません — 修正するか削除してください。何も書き込んでいません",
|
|
172
174
|
"Desktop key: {prefix}…": "デスクトップキー: {prefix}…",
|
|
173
175
|
"Claude Code Desktop: Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference. Gateway base URL {root} (no /v1), Bearer, paste the plm_ key.":
|
|
174
176
|
"Claude Code Desktop: Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference. Gateway base URL {root}(/v1 なし)、Bearer、plm_ キーを貼り付けます。",
|
|
@@ -244,6 +246,7 @@ export const JA = {
|
|
|
244
246
|
"↑/↓ move • enter set model • esc back": "↑/↓ 移動 • enter モデル設定 • esc 戻る",
|
|
245
247
|
"⏹ aborted": "⏹ 中断しました",
|
|
246
248
|
"⏹ generation aborted — the partial reply stays in context": "⏹ 生成を中断しました — 途中までの応答はコンテキストに残ります",
|
|
249
|
+
"partial reply kept in context": "途中まで受信した応答をコンテキストに残しました",
|
|
247
250
|
"✓ installed": "✓ インストール済み",
|
|
248
251
|
"✗ not installed —": "✗ 未インストール —",
|
|
249
252
|
"Log out": "ログアウト",
|
package/src/locales/ko.mjs
CHANGED
|
@@ -233,6 +233,7 @@ export const KO = {
|
|
|
233
233
|
"{model} thinking…": "{model} 생각 중…",
|
|
234
234
|
"⏹ aborted": "⏹ 중단됨",
|
|
235
235
|
"⏹ generation aborted — the partial reply stays in context": "⏹ 생성 중단됨 — 부분 응답은 컨텍스트에 유지",
|
|
236
|
+
"partial reply kept in context": "중간까지 받은 응답을 컨텍스트에 남겼습니다",
|
|
236
237
|
"Select a model": "모델 선택",
|
|
237
238
|
"… {n} more — narrow with a search term": "… {n}개 더 — 검색어로 좁히기",
|
|
238
239
|
"Choose a number, or search (Enter to cancel): ": "번호 선택 또는 검색어 (Enter=취소): ",
|
|
@@ -275,6 +276,8 @@ export const KO = {
|
|
|
275
276
|
"Unsupported tool: {tool}. Use claude or codex.": "지원하지 않는 도구: {tool}. claude 또는 codex를 쓰세요.",
|
|
276
277
|
"Wrote {path}": "{path}에 썼습니다",
|
|
277
278
|
"Wired {tool}.": "{tool}을 배선했습니다.",
|
|
279
|
+
"The wire key is shared by all wired tools and launch, so a billing workspace change applies to all of them.": "배선 키는 배선된 모든 도구와 launch가 공유하므로, 과금 공간을 바꾸면 전부에 적용됩니다.",
|
|
280
|
+
"{path}: not valid JSON — fix or remove it, nothing was written": "{path}: JSON 형식이 아닙니다 — 고치거나 삭제하세요. 아무것도 쓰지 않았습니다",
|
|
278
281
|
"Desktop key: {prefix}…": "데스크톱 키: {prefix}…",
|
|
279
282
|
"Claude Code Desktop: Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference. Gateway base URL {root} (no /v1), Bearer, paste the plm_ key.":
|
|
280
283
|
"Claude Code Desktop: Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference. Gateway base URL {root} (/v1 없음), Bearer, plm_ 키를 붙여넣으세요.",
|
package/src/locales/zh.mjs
CHANGED
|
@@ -169,6 +169,8 @@ export const ZH = {
|
|
|
169
169
|
"Unsupported tool: {tool}. Use claude or codex.": "不支持的工具:{tool}。请使用 claude 或 codex。",
|
|
170
170
|
"Wrote {path}": "已写入 {path}",
|
|
171
171
|
"Wired {tool}.": "已接线 {tool}。",
|
|
172
|
+
"The wire key is shared by all wired tools and launch, so a billing workspace change applies to all of them.": "接入密钥由所有已接入的工具和 launch 共享,因此更改计费工作区会对全部生效。",
|
|
173
|
+
"{path}: not valid JSON — fix or remove it, nothing was written": "{path}: 不是有效的 JSON — 请修复或删除,未写入任何内容",
|
|
172
174
|
"Desktop key: {prefix}…": "桌面密钥:{prefix}…",
|
|
173
175
|
"Claude Code Desktop: Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference. Gateway base URL {root} (no /v1), Bearer, paste the plm_ key.":
|
|
174
176
|
"Claude Code Desktop:Help → Troubleshooting → Enable Developer Mode → Developer → Configure Third-Party Inference。Gateway base URL {root}(不要 /v1),Bearer,粘贴 plm_ 密钥。",
|
|
@@ -244,6 +246,7 @@ export const ZH = {
|
|
|
244
246
|
"↑/↓ move • enter set model • esc back": "↑/↓ 移动 • enter 设置模型 • esc 返回",
|
|
245
247
|
"⏹ aborted": "⏹ 已中止",
|
|
246
248
|
"⏹ generation aborted — the partial reply stays in context": "⏹ 生成已中止 — 部分回复保留在上下文中",
|
|
249
|
+
"partial reply kept in context": "已接收的部分回复已保留在上下文中",
|
|
247
250
|
"✓ installed": "✓ 已安装",
|
|
248
251
|
"✗ not installed —": "✗ 未安装 —",
|
|
249
252
|
"Log out": "登出",
|
package/src/menu.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { runSettings } from "./settings.mjs";
|
|
|
16
16
|
import { t } from "./i18n.mjs";
|
|
17
17
|
import { parseInstallCommand, runInstallCommand, printManualInstall } from "./install.mjs";
|
|
18
18
|
import { getWireStatus, wire, unwire, WIRE_WARNINGS } from "./wire.mjs";
|
|
19
|
-
import { selectAndBindWorkspace } from "./workspace.mjs";
|
|
19
|
+
import { selectAndBindWorkspace, workspaceLabel } from "./workspace.mjs";
|
|
20
20
|
|
|
21
21
|
// 값은 t() 키(영어 원문)만 담는다 — 여기서 t()를 호출하면 모듈 로드 시점(import 1회)에
|
|
22
22
|
// 번역이 확정돼, 같은 프로세스 안에서 언어를 바꿔도(Settings) 이 설명들만 예전 언어로 남는다.
|
|
@@ -74,9 +74,7 @@ export function buildItems({ savedModels = {}, activeContext = { type: "personal
|
|
|
74
74
|
title: t("Chat with a model"),
|
|
75
75
|
desc: `${t("Start an interactive chat")}${savedModels.chat ? ` · ${t("model")}: ${savedModels.chat}` : ""}`,
|
|
76
76
|
});
|
|
77
|
-
const contextDesc = activeContext
|
|
78
|
-
? `${t("Team")}: ${activeContext.org_name}`
|
|
79
|
-
: t("Personal");
|
|
77
|
+
const contextDesc = workspaceLabel(activeContext);
|
|
80
78
|
items.push(
|
|
81
79
|
{ id: "models", title: t("Model list"), desc: t("All models — price and context included") },
|
|
82
80
|
{ id: "balance", title: t("Balance"), desc: `${t("Check your credit balance")} · ${contextDesc}` },
|
|
@@ -624,7 +622,7 @@ export async function runWireMenu({ loop = true } = {}) {
|
|
|
624
622
|
if (selectedModels.length > 1) {
|
|
625
623
|
const defaultChoice = await select(selectedModels.map((id) => ({ id })), {
|
|
626
624
|
message: `${r.label} — ${t("Select default startup model")}`,
|
|
627
|
-
formatItem: (x) => x.id,
|
|
625
|
+
formatItem: (x) => sanitizeTerminalText(x.id),
|
|
628
626
|
});
|
|
629
627
|
if (defaultChoice) selectedModel = defaultChoice.id;
|
|
630
628
|
} else if (selectedModels.length === 1) {
|
package/src/run.mjs
CHANGED
|
@@ -254,6 +254,7 @@ async function repl(initialModel, initialVerbose, {
|
|
|
254
254
|
let lastSigint = 0;
|
|
255
255
|
let pickerActive = false; // /model 셀렉터가 rl을 잠시 닫는 동안 close 핸들러 무시
|
|
256
256
|
let rl;
|
|
257
|
+
let queue = Promise.resolve(); // 입력 직렬화 큐 — /model 피커가 rl을 다시 만들어도 유지된다
|
|
257
258
|
|
|
258
259
|
const send = async (text) => {
|
|
259
260
|
let added = false;
|
|
@@ -277,7 +278,11 @@ async function repl(initialModel, initialVerbose, {
|
|
|
277
278
|
else messages.pop(); // 응답 없음(즉시 중단 등) — user 턴 되돌림
|
|
278
279
|
} catch (e) {
|
|
279
280
|
console.error(`\n${style.red("✗")} ${sanitizeTerminalText(e.message)}`);
|
|
280
|
-
if (
|
|
281
|
+
if (e.partialText) {
|
|
282
|
+
// 중간에 끊긴 답도 맥락에 남겨 다음 질문이 이어받게 한다.
|
|
283
|
+
messages.push({ role: "assistant", content: e.partialText });
|
|
284
|
+
console.error(style.dim(t("partial reply kept in context")));
|
|
285
|
+
} else if (added) messages.pop(); // 실패한 user 턴 제거
|
|
281
286
|
} finally {
|
|
282
287
|
currentAbort = null;
|
|
283
288
|
}
|
|
@@ -388,7 +393,10 @@ async function repl(initialModel, initialVerbose, {
|
|
|
388
393
|
history,
|
|
389
394
|
historySize: 1000,
|
|
390
395
|
});
|
|
391
|
-
|
|
396
|
+
// 생성 중 입력은 순서대로 대기한다. 여러 줄을 한 질문으로 보내려면 """ 로 감싼다.
|
|
397
|
+
rl.on("line", (l) => {
|
|
398
|
+
queue = queue.then(() => onLine(l)).catch((e) => console.error(`✗ ${sanitizeTerminalText(e.message)}`));
|
|
399
|
+
});
|
|
392
400
|
rl.on("close", onClose);
|
|
393
401
|
rl.on("SIGINT", onSigint);
|
|
394
402
|
return rl;
|
|
@@ -542,6 +550,7 @@ export async function streamChat(model, messages, {
|
|
|
542
550
|
let cost = null;
|
|
543
551
|
let firstAt = 0;
|
|
544
552
|
let aborted = false;
|
|
553
|
+
let streamError = null; // 서버가 스트림 중간에 보낸 에러 프레임
|
|
545
554
|
const decoder = new TextDecoder();
|
|
546
555
|
try {
|
|
547
556
|
for await (const chunk of res.body) {
|
|
@@ -555,6 +564,8 @@ export async function streamChat(model, messages, {
|
|
|
555
564
|
if (data === "[DONE]") continue;
|
|
556
565
|
try {
|
|
557
566
|
const j = JSON.parse(data);
|
|
567
|
+
// 여기서 throw하면 아래 catch가 삼키므로 기록만 해 두고 루프 뒤에 던진다.
|
|
568
|
+
if (j.error) streamError = { message: j.error.message || "stream error", partial: j.error.partial === true };
|
|
558
569
|
if (j.usage) usage = j.usage;
|
|
559
570
|
if (j.cost) cost = j.cost; // 마지막 청크 — {usd, krw, fx_rate, markup_rate}
|
|
560
571
|
const delta = j.choices?.[0]?.delta?.content;
|
|
@@ -583,6 +594,12 @@ export async function streamChat(model, messages, {
|
|
|
583
594
|
sp.stop();
|
|
584
595
|
md.finish();
|
|
585
596
|
if (aborted) console.error(style.dim(t("⏹ generation aborted — the partial reply stays in context")));
|
|
597
|
+
if (streamError) {
|
|
598
|
+
// 받은 부분 응답을 함께 실어, REPL이 끊긴 답도 맥락에 남길 수 있게 한다.
|
|
599
|
+
const err = new Error(sanitizeTerminalText(streamError.message));
|
|
600
|
+
err.partialText = full;
|
|
601
|
+
throw err;
|
|
602
|
+
}
|
|
586
603
|
|
|
587
604
|
const ms = Date.now() - t0;
|
|
588
605
|
const genSec = (Date.now() - (firstAt || t0)) / 1000;
|
package/src/settings.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// 즉시 참조하면 순환의 평가 순서에 따라 깨질 수 있다).
|
|
7
7
|
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
|
-
import { style
|
|
9
|
+
import { style } from "./ansi.mjs";
|
|
10
10
|
import { select, confirm } from "./interactive.mjs";
|
|
11
11
|
import { readConfig, writeConfig, isLoggedIn, getActiveContext } from "./config.mjs";
|
|
12
12
|
import { getLocale, LANGS, t } from "./i18n.mjs";
|
|
@@ -16,6 +16,7 @@ import { login, logout } from "./login.mjs";
|
|
|
16
16
|
import { completion } from "./completion.mjs";
|
|
17
17
|
import { lang } from "./lang.mjs";
|
|
18
18
|
import { switchCreditWorkspace } from "./balance.mjs";
|
|
19
|
+
import { workspaceLabel } from "./workspace.mjs";
|
|
19
20
|
import { runList, saveModel } from "./menu.mjs";
|
|
20
21
|
import { checkLatestVersion, isNewerVersion, runUpdateCommand } from "./update.mjs";
|
|
21
22
|
|
|
@@ -28,11 +29,7 @@ export function buildSettingsItems({
|
|
|
28
29
|
activeContext = { type: "personal" },
|
|
29
30
|
latestVersion = null,
|
|
30
31
|
}) {
|
|
31
|
-
const contextDesc = activeContext?.type === "workspace"
|
|
32
|
-
? `${t("Workspace")}: ${sanitizeTerminalText(activeContext.workspace_name || activeContext.workspace_id)}`
|
|
33
|
-
: activeContext?.type === "team"
|
|
34
|
-
? `${t("Team")}: ${activeContext.org_name || activeContext.org_id}`
|
|
35
|
-
: t("Personal Credit");
|
|
32
|
+
const contextDesc = activeContext?.type === "workspace" ? workspaceLabel(activeContext) : t("Personal Credit");
|
|
36
33
|
const updateDesc = latestVersion && isNewerVersion(version, latestVersion)
|
|
37
34
|
? `${t("Current version: {version}", { version })} → v${latestVersion} (${t("Update available")})`
|
|
38
35
|
: t("Current version: {version}", { version });
|
package/src/wire.mjs
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import {
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
lstatSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
readlinkSync,
|
|
10
|
+
realpathSync,
|
|
11
|
+
renameSync,
|
|
12
|
+
statSync,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "node:fs";
|
|
4
16
|
import { ROOT, V1, authFetch, getActiveContext } from "./config.mjs";
|
|
5
17
|
import { tomlBasicString, RECIPES } from "./recipes.mjs";
|
|
6
18
|
import { t } from "./i18n.mjs";
|
|
19
|
+
import { selectAndBindWorkspace, workspaceLabel } from "./workspace.mjs";
|
|
7
20
|
|
|
8
21
|
export const WIRE_DEFAULT_TOOLS = ["claude", "codex"];
|
|
9
22
|
export const WIRE_TOOLS = [
|
|
@@ -99,6 +112,35 @@ function readText(path) {
|
|
|
99
112
|
}
|
|
100
113
|
}
|
|
101
114
|
|
|
115
|
+
// 파싱이 깨진 설정 파일은 덮어쓰지 않고 멈춘다(사용자 설정을 {}로 날리지 않게).
|
|
116
|
+
class InvalidConfigError extends Error {}
|
|
117
|
+
|
|
118
|
+
function parseJsonObject(text) {
|
|
119
|
+
if (!text || !text.trim()) return {};
|
|
120
|
+
let obj;
|
|
121
|
+
try {
|
|
122
|
+
obj = JSON.parse(text);
|
|
123
|
+
} catch {
|
|
124
|
+
throw new InvalidConfigError("invalid JSON");
|
|
125
|
+
}
|
|
126
|
+
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
127
|
+
throw new InvalidConfigError("not a JSON object");
|
|
128
|
+
}
|
|
129
|
+
return obj;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 설정 파일을 읽어 변환한다. 깨졌으면 경로를 붙여 던지고, 호출자는 백업·쓰기 전에 이것을 쓴다.
|
|
133
|
+
function transformConfig(path, transform) {
|
|
134
|
+
try {
|
|
135
|
+
return transform(readText(path));
|
|
136
|
+
} catch (e) {
|
|
137
|
+
if (e instanceof InvalidConfigError) {
|
|
138
|
+
throw new Error(t("{path}: not valid JSON — fix or remove it, nothing was written", { path }));
|
|
139
|
+
}
|
|
140
|
+
throw e;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
102
144
|
export function writeSecretFile(path, contents, mode = 0o600) {
|
|
103
145
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
104
146
|
const tmp = `${path}.pleum-tmp-${process.pid}`;
|
|
@@ -156,13 +198,7 @@ function removeRootValue(toml, name) {
|
|
|
156
198
|
|
|
157
199
|
// ── Claude settings.json ──────────────────────────────────────────
|
|
158
200
|
export function wireClaudeSettings(existingJson, { root, key, model } = {}) {
|
|
159
|
-
|
|
160
|
-
if (existingJson && existingJson.trim()) {
|
|
161
|
-
try {
|
|
162
|
-
obj = JSON.parse(existingJson);
|
|
163
|
-
} catch {}
|
|
164
|
-
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) obj = {};
|
|
165
|
-
}
|
|
201
|
+
const obj = parseJsonObject(existingJson);
|
|
166
202
|
obj.env = {
|
|
167
203
|
...(obj.env && typeof obj.env === "object" && !Array.isArray(obj.env) ? obj.env : {}),
|
|
168
204
|
ANTHROPIC_BASE_URL: root,
|
|
@@ -193,14 +229,7 @@ export function wireClaudeSettings(existingJson, { root, key, model } = {}) {
|
|
|
193
229
|
}
|
|
194
230
|
|
|
195
231
|
export function unwireClaudeSettings(existingJson) {
|
|
196
|
-
|
|
197
|
-
let obj = {};
|
|
198
|
-
try {
|
|
199
|
-
obj = JSON.parse(existingJson);
|
|
200
|
-
} catch {}
|
|
201
|
-
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
202
|
-
return `${JSON.stringify({}, null, 2)}\n`;
|
|
203
|
-
}
|
|
232
|
+
const obj = parseJsonObject(existingJson);
|
|
204
233
|
if (obj.env && typeof obj.env === "object" && !Array.isArray(obj.env)) {
|
|
205
234
|
for (const envKey of CLAUDE_ENV_KEYS) delete obj.env[envKey];
|
|
206
235
|
if (Object.keys(obj.env).length === 0) delete obj.env;
|
|
@@ -216,18 +245,21 @@ export function wireCodexToml(existingToml, { v1, key, model = "gpt-6.1-sol", bi
|
|
|
216
245
|
const src = existingToml ?? "";
|
|
217
246
|
const previousProvider = previousValue(src, "model_provider");
|
|
218
247
|
const previousModel = previousValue(src, "model");
|
|
219
|
-
const targetModel = billingOnly && previousModel ? previousModel : model;
|
|
220
248
|
|
|
221
249
|
let body = stripWireBlock(src);
|
|
222
250
|
body = removeTomlTable(body, "model_providers.pleum.http_headers");
|
|
223
251
|
body = removeTomlTable(body, "model_providers.pleum");
|
|
224
252
|
body = removeRootValue(removeRootValue(body, "model_provider"), "model");
|
|
225
253
|
|
|
254
|
+
// billing-only는 사용자 모델을 유지한다. previousModel은 이미 TOML 값("...")이라 따옴표를 다시 씌우지 않고,
|
|
255
|
+
// 원래 모델이 없었으면 model 줄을 생략해 Codex 기본값을 쓴다.
|
|
256
|
+
const modelLine = billingOnly
|
|
257
|
+
? (previousModel ? `model = ${previousModel}\n` : "")
|
|
258
|
+
: `model = ${tomlBasicString(model)}\n`;
|
|
226
259
|
const rootBlock = `${WIRE_MARKER_START}
|
|
227
260
|
# previous_model_provider = ${previousProvider}
|
|
228
261
|
# previous_model = ${previousModel}
|
|
229
|
-
|
|
230
|
-
model_provider = "pleum"
|
|
262
|
+
${modelLine}model_provider = "pleum"
|
|
231
263
|
${WIRE_MARKER_END}
|
|
232
264
|
`;
|
|
233
265
|
const providerBlock = `[model_providers.pleum]
|
|
@@ -260,13 +292,7 @@ export function unwireCodexToml(existingToml) {
|
|
|
260
292
|
|
|
261
293
|
// ── OpenCode (opencode.json & auth.json) ──────────────────────────
|
|
262
294
|
export function wireOpenCodeJson(existingJson, { v1, models = [], defaultModel } = {}) {
|
|
263
|
-
|
|
264
|
-
if (existingJson && existingJson.trim()) {
|
|
265
|
-
try {
|
|
266
|
-
obj = JSON.parse(existingJson);
|
|
267
|
-
} catch {}
|
|
268
|
-
}
|
|
269
|
-
if (!obj || typeof obj !== "object" || Array.isArray(obj)) obj = {};
|
|
295
|
+
const obj = parseJsonObject(existingJson);
|
|
270
296
|
|
|
271
297
|
obj.provider = obj.provider || {};
|
|
272
298
|
const modelEntries = Object.fromEntries(models.map((id) => [id, {}]));
|
|
@@ -292,12 +318,7 @@ export function wireOpenCodeJson(existingJson, { v1, models = [], defaultModel }
|
|
|
292
318
|
}
|
|
293
319
|
|
|
294
320
|
export function unwireOpenCodeJson(existingJson) {
|
|
295
|
-
|
|
296
|
-
let obj = {};
|
|
297
|
-
try {
|
|
298
|
-
obj = JSON.parse(existingJson);
|
|
299
|
-
} catch {}
|
|
300
|
-
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return `${JSON.stringify({}, null, 2)}\n`;
|
|
321
|
+
const obj = parseJsonObject(existingJson);
|
|
301
322
|
|
|
302
323
|
if (obj.provider && obj.provider.pleum) {
|
|
303
324
|
delete obj.provider.pleum;
|
|
@@ -315,24 +336,13 @@ export function unwireOpenCodeJson(existingJson) {
|
|
|
315
336
|
}
|
|
316
337
|
|
|
317
338
|
export function wireOpenCodeAuth(existingJson, key) {
|
|
318
|
-
|
|
319
|
-
if (existingJson && existingJson.trim()) {
|
|
320
|
-
try {
|
|
321
|
-
obj = JSON.parse(existingJson);
|
|
322
|
-
} catch {}
|
|
323
|
-
}
|
|
324
|
-
if (!obj || typeof obj !== "object" || Array.isArray(obj)) obj = {};
|
|
339
|
+
const obj = parseJsonObject(existingJson);
|
|
325
340
|
obj.pleum = { type: "api", key };
|
|
326
341
|
return `${JSON.stringify(obj, null, 2)}\n`;
|
|
327
342
|
}
|
|
328
343
|
|
|
329
344
|
export function unwireOpenCodeAuth(existingJson) {
|
|
330
|
-
|
|
331
|
-
let obj = {};
|
|
332
|
-
try {
|
|
333
|
-
obj = JSON.parse(existingJson);
|
|
334
|
-
} catch {}
|
|
335
|
-
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return `${JSON.stringify({}, null, 2)}\n`;
|
|
345
|
+
const obj = parseJsonObject(existingJson);
|
|
336
346
|
delete obj.pleum;
|
|
337
347
|
return `${JSON.stringify(obj, null, 2)}\n`;
|
|
338
348
|
}
|
|
@@ -366,13 +376,7 @@ export function unwireAiderYaml(existingYaml = "") {
|
|
|
366
376
|
|
|
367
377
|
// ── Crush ~/.config/crush/crush.json ──────────────────────────────
|
|
368
378
|
export function wireCrushConfig(existingJson, { v1, key, models = [] } = {}) {
|
|
369
|
-
|
|
370
|
-
if (existingJson && existingJson.trim()) {
|
|
371
|
-
try {
|
|
372
|
-
obj = JSON.parse(existingJson);
|
|
373
|
-
} catch {}
|
|
374
|
-
}
|
|
375
|
-
if (!obj || typeof obj !== "object" || Array.isArray(obj)) obj = {};
|
|
379
|
+
const obj = parseJsonObject(existingJson);
|
|
376
380
|
obj.providers = obj.providers || {};
|
|
377
381
|
obj.providers.pleum = {
|
|
378
382
|
type: "openai-compat",
|
|
@@ -384,12 +388,7 @@ export function wireCrushConfig(existingJson, { v1, key, models = [] } = {}) {
|
|
|
384
388
|
}
|
|
385
389
|
|
|
386
390
|
export function unwireCrushConfig(existingJson) {
|
|
387
|
-
|
|
388
|
-
let obj = {};
|
|
389
|
-
try {
|
|
390
|
-
obj = JSON.parse(existingJson);
|
|
391
|
-
} catch {}
|
|
392
|
-
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return `${JSON.stringify({}, null, 2)}\n`;
|
|
391
|
+
const obj = parseJsonObject(existingJson);
|
|
393
392
|
if (obj.providers && obj.providers.pleum) {
|
|
394
393
|
delete obj.providers.pleum;
|
|
395
394
|
if (Object.keys(obj.providers).length === 0) delete obj.providers;
|
|
@@ -399,13 +398,7 @@ export function unwireCrushConfig(existingJson) {
|
|
|
399
398
|
|
|
400
399
|
// ── Pi ~/.pi/agent/models.json ────────────────────────────────────
|
|
401
400
|
export function wirePiConfig(existingJson, { v1, key, models = [] } = {}) {
|
|
402
|
-
|
|
403
|
-
if (existingJson && existingJson.trim()) {
|
|
404
|
-
try {
|
|
405
|
-
obj = JSON.parse(existingJson);
|
|
406
|
-
} catch {}
|
|
407
|
-
}
|
|
408
|
-
if (!obj || typeof obj !== "object" || Array.isArray(obj)) obj = {};
|
|
401
|
+
const obj = parseJsonObject(existingJson);
|
|
409
402
|
obj.providers = obj.providers || {};
|
|
410
403
|
obj.providers.pleum = {
|
|
411
404
|
baseUrl: v1,
|
|
@@ -417,12 +410,7 @@ export function wirePiConfig(existingJson, { v1, key, models = [] } = {}) {
|
|
|
417
410
|
}
|
|
418
411
|
|
|
419
412
|
export function unwirePiConfig(existingJson) {
|
|
420
|
-
|
|
421
|
-
let obj = {};
|
|
422
|
-
try {
|
|
423
|
-
obj = JSON.parse(existingJson);
|
|
424
|
-
} catch {}
|
|
425
|
-
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return `${JSON.stringify({}, null, 2)}\n`;
|
|
413
|
+
const obj = parseJsonObject(existingJson);
|
|
426
414
|
if (obj.providers && obj.providers.pleum) {
|
|
427
415
|
delete obj.providers.pleum;
|
|
428
416
|
if (Object.keys(obj.providers).length === 0) delete obj.providers;
|
|
@@ -432,45 +420,106 @@ export function unwirePiConfig(existingJson) {
|
|
|
432
420
|
|
|
433
421
|
// ── Antigravity ~/.gemini/antigravity-cli/settings.json ────────────
|
|
434
422
|
export function wireAgySettings(existingJson, { model } = {}) {
|
|
435
|
-
|
|
436
|
-
if (existingJson && existingJson.trim()) {
|
|
437
|
-
try {
|
|
438
|
-
obj = JSON.parse(existingJson);
|
|
439
|
-
} catch {}
|
|
440
|
-
}
|
|
441
|
-
if (!obj || typeof obj !== "object" || Array.isArray(obj)) obj = {};
|
|
423
|
+
const obj = parseJsonObject(existingJson);
|
|
442
424
|
obj.modelProvider = "gemini";
|
|
443
425
|
if (model) obj.model = model;
|
|
444
426
|
return `${JSON.stringify(obj, null, 2)}\n`;
|
|
445
427
|
}
|
|
446
428
|
|
|
447
429
|
export function unwireAgySettings(existingJson) {
|
|
448
|
-
|
|
449
|
-
let obj = {};
|
|
450
|
-
try {
|
|
451
|
-
obj = JSON.parse(existingJson);
|
|
452
|
-
} catch {}
|
|
453
|
-
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return `${JSON.stringify({}, null, 2)}\n`;
|
|
430
|
+
const obj = parseJsonObject(existingJson);
|
|
454
431
|
if (obj.modelProvider === "gemini") delete obj.modelProvider;
|
|
455
432
|
delete obj.model;
|
|
456
433
|
return `${JSON.stringify(obj, null, 2)}\n`;
|
|
457
434
|
}
|
|
458
435
|
|
|
459
436
|
// ── Shell function wrapper (rc files) ─────────────────────────────
|
|
460
|
-
|
|
437
|
+
// POSIX single quotes. `'\''` ends the quote, inserts a literal quote, and reopens it.
|
|
438
|
+
export function posixSingleQuote(value) {
|
|
439
|
+
return "'" + String(value).replace(/'/g, "'\\''") + "'";
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function formatShellEnv(envMap = {}) {
|
|
443
|
+
const lines = Object.entries(envMap).map(
|
|
444
|
+
([key, value]) => `export ${key}=${posixSingleQuote(value)}`,
|
|
445
|
+
);
|
|
446
|
+
return lines.length ? `${lines.join("\n")}\n` : "";
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function shellEnvFile(paths, tool) {
|
|
450
|
+
const safe = String(tool).replace(/[^A-Za-z0-9._-]/g, "_");
|
|
451
|
+
return join(paths.home, ".pleum", "wire-env", `${safe}.env`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function shellRcTarget(path) {
|
|
455
|
+
let st;
|
|
456
|
+
try {
|
|
457
|
+
st = lstatSync(path);
|
|
458
|
+
} catch {
|
|
459
|
+
return { dest: path, mode: 0o600, present: false };
|
|
460
|
+
}
|
|
461
|
+
if (!st.isSymbolicLink()) {
|
|
462
|
+
return { dest: path, mode: st.mode & 0o777, present: true };
|
|
463
|
+
}
|
|
464
|
+
let dest;
|
|
465
|
+
try {
|
|
466
|
+
dest = realpathSync(path);
|
|
467
|
+
} catch {
|
|
468
|
+
const link = readlinkSync(path);
|
|
469
|
+
dest = isAbsolute(link) ? link : resolve(dirname(path), link);
|
|
470
|
+
}
|
|
471
|
+
let mode = 0o600;
|
|
472
|
+
try {
|
|
473
|
+
mode = statSync(dest).mode & 0o777;
|
|
474
|
+
} catch {
|
|
475
|
+
// Dangling link: the file we create is new, so it gets the new-file mode.
|
|
476
|
+
}
|
|
477
|
+
return { dest, mode, present: true };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function readShellRc(path) {
|
|
481
|
+
const { dest, present } = shellRcTarget(path);
|
|
482
|
+
if (!present) return "";
|
|
483
|
+
return readText(dest) || "";
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function writeShellRc(path, contents) {
|
|
487
|
+
// Rename onto the symlink path would replace the link. Write the target.
|
|
488
|
+
const { dest, mode } = shellRcTarget(path);
|
|
489
|
+
writeSecretFile(dest, contents, mode);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function wireShellTool(paths, tool, { bin, envMap }) {
|
|
493
|
+
const envFile = shellEnvFile(paths, tool);
|
|
494
|
+
writeSecretFile(envFile, formatShellEnv(envMap));
|
|
495
|
+
const nextRc = wireShellWrapper(readShellRc(paths.shellRc), { tool, bin, envFile });
|
|
496
|
+
writeShellRc(paths.shellRc, nextRc);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function unwireShellTool(paths, tool) {
|
|
500
|
+
if (shellRcTarget(paths.shellRc).present) {
|
|
501
|
+
writeShellRc(paths.shellRc, unwireShellWrapper(readShellRc(paths.shellRc), tool));
|
|
502
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
unlinkSync(shellEnvFile(paths, tool));
|
|
505
|
+
} catch {
|
|
506
|
+
// Already removed.
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function wireShellWrapper(existingRc = "", { tool, bin, envFile } = {}) {
|
|
461
511
|
const startMarker = `# pleum-wire:${tool}-start`;
|
|
462
512
|
const endMarker = `# pleum-wire:${tool}-end`;
|
|
463
513
|
const clean = (existingRc || "")
|
|
464
514
|
.replace(new RegExp(`${startMarker}[\\s\\S]*?${endMarker}\\n?`, "g"), "")
|
|
465
515
|
.trim();
|
|
466
516
|
|
|
467
|
-
const
|
|
468
|
-
.map(([k, v]) => `${k}=${JSON.stringify(String(v))}`)
|
|
469
|
-
.join(" ");
|
|
470
|
-
|
|
517
|
+
const source = envFile ? `. ${posixSingleQuote(envFile)}\n ` : "";
|
|
471
518
|
const block = `${startMarker}
|
|
472
519
|
${tool}() {
|
|
473
|
-
|
|
520
|
+
(
|
|
521
|
+
${source}command ${bin || tool} "$@"
|
|
522
|
+
)
|
|
474
523
|
}
|
|
475
524
|
${endMarker}`;
|
|
476
525
|
|
|
@@ -612,13 +661,34 @@ function parseJsonBody(res) {
|
|
|
612
661
|
return res.json().catch(() => ({}));
|
|
613
662
|
}
|
|
614
663
|
|
|
664
|
+
// 개인 과금은 소유자의 기본 공간이다. 조회가 실패하거나 없으면 null.
|
|
665
|
+
async function findDefaultWorkspaceId(auth) {
|
|
666
|
+
try {
|
|
667
|
+
const res = await auth("/workspaces");
|
|
668
|
+
if (!res.ok) return null;
|
|
669
|
+
const data = await parseJsonBody(res);
|
|
670
|
+
const items = Array.isArray(data) ? data : [];
|
|
671
|
+
return items.find((w) => w.is_default === true && w.role === "owner")?.id ?? null;
|
|
672
|
+
} catch {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// 개인 키 레코드에는 workspace_id 필드를 두지 않는다(공유 공간일 때만 남긴다).
|
|
678
|
+
function withWorkspace(record, workspaceId) {
|
|
679
|
+
const next = { ...record };
|
|
680
|
+
delete next.workspace_id;
|
|
681
|
+
if (workspaceId) next.workspace_id = workspaceId;
|
|
682
|
+
return next;
|
|
683
|
+
}
|
|
684
|
+
|
|
615
685
|
export async function ensureWireKey({
|
|
616
686
|
auth = authFetch,
|
|
617
687
|
paths = wirePaths(),
|
|
618
688
|
activeContext = getActiveContext(),
|
|
619
689
|
} = {}) {
|
|
620
|
-
|
|
621
|
-
|
|
690
|
+
// 과금 귀속은 키의 workspace_id가 정한다. 공유 공간이면 그 공간, 개인이면 소유자의 기본 공간.
|
|
691
|
+
const shared = activeContext?.type === "workspace" ? activeContext.workspace_id : null;
|
|
622
692
|
const stored = (() => {
|
|
623
693
|
try {
|
|
624
694
|
return JSON.parse(readText(paths.wire) ?? "");
|
|
@@ -633,23 +703,23 @@ export async function ensureWireKey({
|
|
|
633
703
|
const items = Array.isArray(data.items) ? data.items : [];
|
|
634
704
|
const found = items.find((item) => String(item.id) === String(stored.id));
|
|
635
705
|
if (found && found.is_active !== false) {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
) {
|
|
706
|
+
const target = shared ?? (await findDefaultWorkspaceId(auth));
|
|
707
|
+
const current = String(found.workspace_id ?? "");
|
|
708
|
+
if (target && current !== String(target)) {
|
|
640
709
|
const moved = await auth(`/workspaces/keys/${stored.id}/move`, {
|
|
641
710
|
method: "POST",
|
|
642
711
|
headers: { "Content-Type": "application/json" },
|
|
643
|
-
body: JSON.stringify({ target_workspace_id:
|
|
712
|
+
body: JSON.stringify({ target_workspace_id: target }),
|
|
644
713
|
});
|
|
645
714
|
if (moved.ok) {
|
|
646
|
-
const movedRecord =
|
|
715
|
+
const movedRecord = withWorkspace(stored, shared);
|
|
647
716
|
writeSecretFile(paths.wire, `${JSON.stringify(movedRecord, null, 2)}\n`);
|
|
648
717
|
return movedRecord;
|
|
649
718
|
}
|
|
650
|
-
} else {
|
|
719
|
+
} else if (target || !current) {
|
|
651
720
|
return stored;
|
|
652
721
|
}
|
|
722
|
+
// 그 밖(개인인데 기본 공간을 못 찾았고 키가 공유 공간에 묶인 경우)은 아래에서 새로 발급한다.
|
|
653
723
|
}
|
|
654
724
|
} else if (listed.status !== 404) {
|
|
655
725
|
return stored;
|
|
@@ -659,9 +729,10 @@ export async function ensureWireKey({
|
|
|
659
729
|
const created = await auth("/keys", {
|
|
660
730
|
method: "POST",
|
|
661
731
|
headers: { "Content-Type": "application/json" },
|
|
732
|
+
// workspace_id를 생략하면 서버가 소유자의 기본 공간에 키를 만든다.
|
|
662
733
|
body: JSON.stringify({
|
|
663
734
|
name: WIRE_KEY_NAME,
|
|
664
|
-
...(
|
|
735
|
+
...(shared ? { workspace_id: shared } : {}),
|
|
665
736
|
}),
|
|
666
737
|
});
|
|
667
738
|
if (!created.ok) {
|
|
@@ -675,7 +746,7 @@ export async function ensureWireKey({
|
|
|
675
746
|
id: data.id,
|
|
676
747
|
key: data.key,
|
|
677
748
|
name: data.name ?? WIRE_KEY_NAME,
|
|
678
|
-
...(
|
|
749
|
+
...(shared ? { workspace_id: shared } : {}),
|
|
679
750
|
};
|
|
680
751
|
writeSecretFile(paths.wire, `${JSON.stringify(record, null, 2)}\n`);
|
|
681
752
|
return record;
|
|
@@ -742,8 +813,9 @@ export function applyToolWiring(tool, paths, { key, model, models = [], billingO
|
|
|
742
813
|
|
|
743
814
|
switch (tool) {
|
|
744
815
|
case "claude": {
|
|
816
|
+
// 파싱이 끝나야 백업·쓰기로 간다 — 깨진 파일이면 .pleum.bak도 남기지 않는다.
|
|
817
|
+
const next = transformConfig(paths.claude, (text) => wireClaudeSettings(text, { root: ROOT, key, model: billingOnly ? undefined : model }));
|
|
745
818
|
backupOnce(paths.claude, paths.claudeBak);
|
|
746
|
-
const next = wireClaudeSettings(readText(paths.claude), { root: ROOT, key, model: billingOnly ? undefined : model });
|
|
747
819
|
writeSecretFile(paths.claude, next);
|
|
748
820
|
console.log(t("Wrote {path}", { path: paths.claude }));
|
|
749
821
|
break;
|
|
@@ -756,10 +828,10 @@ export function applyToolWiring(tool, paths, { key, model, models = [], billingO
|
|
|
756
828
|
break;
|
|
757
829
|
}
|
|
758
830
|
case "opencode": {
|
|
831
|
+
const nextJson = transformConfig(paths.opencode, (text) => wireOpenCodeJson(text, { v1: V1, models, defaultModel: model }));
|
|
832
|
+
const nextAuth = transformConfig(paths.opencodeAuth, (text) => wireOpenCodeAuth(text, key));
|
|
759
833
|
backupOnce(paths.opencode, paths.opencodeBak);
|
|
760
|
-
const nextJson = wireOpenCodeJson(readText(paths.opencode), { v1: V1, models, defaultModel: model });
|
|
761
834
|
writeSecretFile(paths.opencode, nextJson);
|
|
762
|
-
const nextAuth = wireOpenCodeAuth(readText(paths.opencodeAuth), key);
|
|
763
835
|
writeSecretFile(paths.opencodeAuth, nextAuth);
|
|
764
836
|
console.log(t("Wrote {path}", { path: paths.opencode }));
|
|
765
837
|
break;
|
|
@@ -772,26 +844,24 @@ export function applyToolWiring(tool, paths, { key, model, models = [], billingO
|
|
|
772
844
|
break;
|
|
773
845
|
}
|
|
774
846
|
case "crush": {
|
|
847
|
+
const next = transformConfig(paths.crush, (text) => wireCrushConfig(text, { v1: V1, key, models }));
|
|
775
848
|
backupOnce(paths.crush, paths.crushBak);
|
|
776
|
-
const next = wireCrushConfig(readText(paths.crush), { v1: V1, key, models });
|
|
777
849
|
writeSecretFile(paths.crush, next);
|
|
778
850
|
console.log(t("Wrote {path}", { path: paths.crush }));
|
|
779
851
|
break;
|
|
780
852
|
}
|
|
781
853
|
case "pi": {
|
|
854
|
+
const next = transformConfig(paths.pi, (text) => wirePiConfig(text, { v1: V1, key, models }));
|
|
782
855
|
backupOnce(paths.pi, paths.piBak);
|
|
783
|
-
const next = wirePiConfig(readText(paths.pi), { v1: V1, key, models });
|
|
784
856
|
writeSecretFile(paths.pi, next);
|
|
785
857
|
console.log(t("Wrote {path}", { path: paths.pi }));
|
|
786
858
|
break;
|
|
787
859
|
}
|
|
788
860
|
case "agy": {
|
|
861
|
+
const nextAgy = transformConfig(paths.agy, (text) => wireAgySettings(text, { model: billingOnly ? undefined : model }));
|
|
789
862
|
backupOnce(paths.agy, paths.agyBak);
|
|
790
|
-
const nextAgy = wireAgySettings(readText(paths.agy), { model: billingOnly ? undefined : model });
|
|
791
863
|
writeSecretFile(paths.agy, nextAgy);
|
|
792
|
-
|
|
793
|
-
const nextRc = wireShellWrapper(rcContent, {
|
|
794
|
-
tool: "agy",
|
|
864
|
+
wireShellTool(paths, "agy", {
|
|
795
865
|
bin: "agy",
|
|
796
866
|
envMap: {
|
|
797
867
|
GOOGLE_GEMINI_BASE_URL: ROOT,
|
|
@@ -799,17 +869,13 @@ export function applyToolWiring(tool, paths, { key, model, models = [], billingO
|
|
|
799
869
|
GOOGLE_API_KEY: key,
|
|
800
870
|
},
|
|
801
871
|
});
|
|
802
|
-
writeSecretFile(paths.shellRc, nextRc, 0o644);
|
|
803
872
|
console.log(t("Wrote {path}", { path: paths.agy }));
|
|
804
873
|
break;
|
|
805
874
|
}
|
|
806
875
|
default: {
|
|
807
876
|
const r = RECIPES[tool];
|
|
808
877
|
if (r) {
|
|
809
|
-
|
|
810
|
-
const envMap = r.env(key, model);
|
|
811
|
-
const nextRc = wireShellWrapper(rcContent, { tool, bin: r.bin, envMap });
|
|
812
|
-
writeSecretFile(paths.shellRc, nextRc, 0o644);
|
|
878
|
+
wireShellTool(paths, tool, { bin: r.bin, envMap: r.env(key, model) });
|
|
813
879
|
console.log(t("Wrote {path}", { path: paths.shellRc }));
|
|
814
880
|
}
|
|
815
881
|
break;
|
|
@@ -833,7 +899,7 @@ export function revertToolWiring(tool, paths) {
|
|
|
833
899
|
switch (tool) {
|
|
834
900
|
case "claude": {
|
|
835
901
|
if (existsSync(paths.claude)) {
|
|
836
|
-
writeSecretFile(paths.claude,
|
|
902
|
+
writeSecretFile(paths.claude, transformConfig(paths.claude, unwireClaudeSettings));
|
|
837
903
|
console.log(t("Removed PleumRouter settings from {path}.", { path: paths.claude }));
|
|
838
904
|
}
|
|
839
905
|
break;
|
|
@@ -846,12 +912,11 @@ export function revertToolWiring(tool, paths) {
|
|
|
846
912
|
break;
|
|
847
913
|
}
|
|
848
914
|
case "opencode": {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
if (
|
|
853
|
-
|
|
854
|
-
}
|
|
915
|
+
// 두 파일 모두 파싱이 끝난 뒤에 쓴다(하나만 깨졌을 때 반쪽만 바뀌지 않게).
|
|
916
|
+
const nextJson = existsSync(paths.opencode) ? transformConfig(paths.opencode, unwireOpenCodeJson) : null;
|
|
917
|
+
const nextAuth = existsSync(paths.opencodeAuth) ? transformConfig(paths.opencodeAuth, unwireOpenCodeAuth) : null;
|
|
918
|
+
if (nextJson !== null) writeSecretFile(paths.opencode, nextJson);
|
|
919
|
+
if (nextAuth !== null) writeSecretFile(paths.opencodeAuth, nextAuth);
|
|
855
920
|
console.log(t("Removed PleumRouter settings from {path}.", { path: paths.opencode }));
|
|
856
921
|
break;
|
|
857
922
|
}
|
|
@@ -864,31 +929,30 @@ export function revertToolWiring(tool, paths) {
|
|
|
864
929
|
}
|
|
865
930
|
case "crush": {
|
|
866
931
|
if (existsSync(paths.crush)) {
|
|
867
|
-
writeSecretFile(paths.crush,
|
|
932
|
+
writeSecretFile(paths.crush, transformConfig(paths.crush, unwireCrushConfig));
|
|
868
933
|
console.log(t("Removed PleumRouter settings from {path}.", { path: paths.crush }));
|
|
869
934
|
}
|
|
870
935
|
break;
|
|
871
936
|
}
|
|
872
937
|
case "pi": {
|
|
873
938
|
if (existsSync(paths.pi)) {
|
|
874
|
-
writeSecretFile(paths.pi,
|
|
939
|
+
writeSecretFile(paths.pi, transformConfig(paths.pi, unwirePiConfig));
|
|
875
940
|
console.log(t("Removed PleumRouter settings from {path}.", { path: paths.pi }));
|
|
876
941
|
}
|
|
877
942
|
break;
|
|
878
943
|
}
|
|
879
944
|
case "agy": {
|
|
880
945
|
if (existsSync(paths.agy)) {
|
|
881
|
-
writeSecretFile(paths.agy,
|
|
882
|
-
}
|
|
883
|
-
if (existsSync(paths.shellRc)) {
|
|
884
|
-
writeSecretFile(paths.shellRc, unwireShellWrapper(readText(paths.shellRc), "agy"), 0o644);
|
|
946
|
+
writeSecretFile(paths.agy, transformConfig(paths.agy, unwireAgySettings));
|
|
885
947
|
}
|
|
948
|
+
unwireShellTool(paths, "agy");
|
|
886
949
|
console.log(t("Removed PleumRouter settings from {path}.", { path: paths.agy }));
|
|
887
950
|
break;
|
|
888
951
|
}
|
|
889
952
|
default: {
|
|
890
|
-
|
|
891
|
-
|
|
953
|
+
const hadRc = shellRcTarget(paths.shellRc).present;
|
|
954
|
+
unwireShellTool(paths, tool);
|
|
955
|
+
if (hadRc) {
|
|
892
956
|
console.log(t("Removed PleumRouter settings from {path}.", { path: paths.shellRc }));
|
|
893
957
|
}
|
|
894
958
|
break;
|
|
@@ -899,42 +963,66 @@ export function revertToolWiring(tool, paths) {
|
|
|
899
963
|
writeWiredState(paths, state);
|
|
900
964
|
}
|
|
901
965
|
|
|
966
|
+
// 과금 공간은 wire 때 고른다. 대화형 터미널이면 묻고, 파이프·CI에서는 저장된 값을 쓴다.
|
|
967
|
+
async function pickBillingContext(auth) {
|
|
968
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && typeof process.stdin.setRawMode === "function");
|
|
969
|
+
return interactive ? selectAndBindWorkspace({ auth }) : getActiveContext();
|
|
970
|
+
}
|
|
971
|
+
|
|
902
972
|
export async function wire(args = [], {
|
|
903
973
|
auth = authFetch,
|
|
904
974
|
home = homedir(),
|
|
905
975
|
model,
|
|
906
976
|
models,
|
|
907
977
|
billingOnly = false,
|
|
908
|
-
activeContext
|
|
978
|
+
activeContext,
|
|
909
979
|
} = {}) {
|
|
910
980
|
const parsed = parseWireArgs(args);
|
|
911
981
|
const tools = parsed.tools;
|
|
912
982
|
const isBilling = billingOnly || parsed.billingOnly;
|
|
913
983
|
const targetModel = model || parsed.model;
|
|
914
984
|
const paths = wirePaths(home);
|
|
915
|
-
|
|
985
|
+
// 메뉴 경로는 호출자가 이미 고른 값을 넘긴다(중복 질문 방지).
|
|
986
|
+
const billing = activeContext ?? (await pickBillingContext(auth));
|
|
987
|
+
const record = await ensureWireKey({ auth, paths, activeContext: billing });
|
|
916
988
|
|
|
989
|
+
// 한 도구가 깨져도 나머지는 배선한다. 실패 목록은 마지막에 한 번에 던져 exit 1이 되게 한다.
|
|
990
|
+
const failures = [];
|
|
917
991
|
for (const tool of tools) {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
992
|
+
try {
|
|
993
|
+
applyToolWiring(tool, paths, {
|
|
994
|
+
key: record.key,
|
|
995
|
+
model: targetModel,
|
|
996
|
+
models,
|
|
997
|
+
billingOnly: isBilling,
|
|
998
|
+
activeContext: billing,
|
|
999
|
+
});
|
|
1000
|
+
console.log(t("Wired {tool}.", { tool }));
|
|
1001
|
+
} catch (e) {
|
|
1002
|
+
failures.push(e.message);
|
|
1003
|
+
}
|
|
926
1004
|
}
|
|
927
1005
|
|
|
1006
|
+
console.log(`ℹ ${t("Credit workspace")}: ${workspaceLabel(billing)}`);
|
|
1007
|
+
console.log(t("The wire key is shared by all wired tools and launch, so a billing workspace change applies to all of them."));
|
|
928
1008
|
printDesktopHints(record);
|
|
1009
|
+
if (failures.length) throw new Error(failures.join("\n"));
|
|
929
1010
|
}
|
|
930
1011
|
|
|
931
1012
|
export async function unwire(args = [], { auth = authFetch, home = homedir() } = {}) {
|
|
932
1013
|
const { tools, revoke } = parseWireArgs(args);
|
|
933
1014
|
const paths = wirePaths(home);
|
|
934
1015
|
|
|
1016
|
+
const failures = [];
|
|
935
1017
|
for (const tool of tools) {
|
|
936
|
-
|
|
1018
|
+
try {
|
|
1019
|
+
revertToolWiring(tool, paths);
|
|
1020
|
+
} catch (e) {
|
|
1021
|
+
failures.push(e.message);
|
|
1022
|
+
}
|
|
937
1023
|
}
|
|
1024
|
+
// 실패가 있으면 키는 폐기하지 않는다(고친 뒤 다시 unwire할 수 있게).
|
|
1025
|
+
if (failures.length) throw new Error(failures.join("\n"));
|
|
938
1026
|
|
|
939
1027
|
if (!revoke) return;
|
|
940
1028
|
|
package/src/workspace.mjs
CHANGED
|
@@ -62,7 +62,7 @@ export function formatWorkspaceList(context, candidates = []) {
|
|
|
62
62
|
for (const candidate of candidates) {
|
|
63
63
|
const mark = String(candidate.workspace_id) === activeId ? style.bold(style.plum("▸ ")) : " ";
|
|
64
64
|
lines.push(
|
|
65
|
-
`${mark}${candidate.workspace_id} ${sanitizeTerminalText(candidate.name)} (${candidate.role})`,
|
|
65
|
+
`${mark}${candidate.workspace_id} ${sanitizeTerminalText(candidate.name)} (${sanitizeTerminalText(candidate.role)})`,
|
|
66
66
|
);
|
|
67
67
|
}
|
|
68
68
|
return lines.join("\n");
|
|
@@ -130,8 +130,8 @@ export async function selectAndBindWorkspace({
|
|
|
130
130
|
type: "workspace",
|
|
131
131
|
workspace_id: c.workspace_id,
|
|
132
132
|
name: c.name,
|
|
133
|
-
title: `${c.name} (${c.role})`,
|
|
134
|
-
label: `${activeId === String(c.workspace_id) ? "✓ " : " "}${sanitizeTerminalText(c.name)} (${c.role})`,
|
|
133
|
+
title: `${sanitizeTerminalText(c.name)} (${sanitizeTerminalText(c.role)})`,
|
|
134
|
+
label: `${activeId === String(c.workspace_id) ? "✓ " : " "}${sanitizeTerminalText(c.name)} (${sanitizeTerminalText(c.role)})`,
|
|
135
135
|
})),
|
|
136
136
|
];
|
|
137
137
|
|