pi-connect 0.1.1 → 0.1.3
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/README.md +3 -3
- package/assets/screenshot.png +0 -0
- package/index.ts +198 -122
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# pi-connect
|
|
2
2
|
|
|
3
|
-

|
|
3
|
+

|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Unified OAuth and API key login for pi with an OpenCode-inspired UI.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Connect 15+ providers with one `/connect` command.
|
|
8
8
|
|
|
9
9
|
Official pi providers list:
|
|
10
10
|
- https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/providers.md
|
package/assets/screenshot.png
CHANGED
|
Binary file
|
package/index.ts
CHANGED
|
@@ -1,28 +1,61 @@
|
|
|
1
1
|
import { DynamicBorder, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { getEnvApiKey } from "@mariozechner/pi-ai";
|
|
3
|
-
import { Container, SelectList, Text, type SelectItem } from "@mariozechner/pi-tui";
|
|
3
|
+
import { Container, fuzzyFilter, Input, Key, matchesKey, SelectList, Text, type SelectItem } from "@mariozechner/pi-tui";
|
|
4
4
|
import { exec as execCb } from "node:child_process";
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
anthropic:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
zai:
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
"
|
|
6
|
+
const DISPLAY_NAME_OVERRIDES: Record<string, string> = {
|
|
7
|
+
anthropic: "Anthropic",
|
|
8
|
+
openai: "OpenAI",
|
|
9
|
+
google: "Google Gemini",
|
|
10
|
+
openrouter: "OpenRouter",
|
|
11
|
+
opencode: "OpenCode",
|
|
12
|
+
"opencode-go": "OpenCode Go",
|
|
13
|
+
groq: "Groq",
|
|
14
|
+
mistral: "Mistral",
|
|
15
|
+
cerebras: "Cerebras",
|
|
16
|
+
xai: "xAI",
|
|
17
|
+
zai: "ZAI",
|
|
18
|
+
huggingface: "Hugging Face",
|
|
19
|
+
"kimi-coding": "Kimi",
|
|
20
|
+
minimax: "MiniMax",
|
|
21
|
+
"minimax-cn": "MiniMax China",
|
|
22
|
+
"azure-openai-responses": "Azure OpenAI",
|
|
23
|
+
"vercel-ai-gateway": "Vercel AI Gateway",
|
|
24
|
+
"openai-codex": "ChatGPT",
|
|
25
|
+
"github-copilot": "Copilot",
|
|
26
|
+
"google-gemini-cli": "Gemini CLI",
|
|
27
|
+
"google-antigravity": "Antigravity",
|
|
28
|
+
"google-vertex": "Google Vertex",
|
|
29
|
+
"amazon-bedrock": "Amazon Bedrock"
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const ENV_VAR_OVERRIDES: Record<string, string> = {
|
|
33
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
34
|
+
openai: "OPENAI_API_KEY",
|
|
35
|
+
google: "GEMINI_API_KEY",
|
|
36
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
37
|
+
opencode: "OPENCODE_API_KEY",
|
|
38
|
+
"opencode-go": "OPENCODE_API_KEY",
|
|
39
|
+
groq: "GROQ_API_KEY",
|
|
40
|
+
mistral: "MISTRAL_API_KEY",
|
|
41
|
+
cerebras: "CEREBRAS_API_KEY",
|
|
42
|
+
xai: "XAI_API_KEY",
|
|
43
|
+
zai: "ZAI_API_KEY",
|
|
44
|
+
huggingface: "HF_TOKEN",
|
|
45
|
+
"kimi-coding": "KIMI_API_KEY",
|
|
46
|
+
minimax: "MINIMAX_API_KEY",
|
|
47
|
+
"minimax-cn": "MINIMAX_CN_API_KEY",
|
|
48
|
+
"azure-openai-responses": "AZURE_OPENAI_API_KEY",
|
|
49
|
+
"vercel-ai-gateway": "AI_GATEWAY_API_KEY"
|
|
24
50
|
};
|
|
25
51
|
|
|
52
|
+
const OAUTH_ONLY_PROVIDERS = new Set([
|
|
53
|
+
"openai-codex",
|
|
54
|
+
"github-copilot",
|
|
55
|
+
"google-gemini-cli",
|
|
56
|
+
"google-antigravity"
|
|
57
|
+
]);
|
|
58
|
+
|
|
26
59
|
const PRIORITY: Record<string, number> = {
|
|
27
60
|
anthropic: 0,
|
|
28
61
|
openai: 1,
|
|
@@ -33,19 +66,16 @@ const PRIORITY: Record<string, number> = {
|
|
|
33
66
|
openrouter: 6,
|
|
34
67
|
opencode: 7,
|
|
35
68
|
"opencode-go": 8,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const LABELS: Record<string, string> = {
|
|
39
|
-
...Object.fromEntries(Object.entries(API_KEY_PROVIDERS).map(([id, value]) => [id, value.label])),
|
|
40
|
-
anthropic: "Anthropic",
|
|
41
|
-
"openai-codex": "ChatGPT Plus/Pro (Codex)",
|
|
42
|
-
"github-copilot": "GitHub Copilot",
|
|
43
|
-
"google-gemini-cli": "Google Gemini CLI",
|
|
44
|
-
"google-antigravity": "Google Antigravity",
|
|
69
|
+
groq: 9,
|
|
70
|
+
mistral: 10
|
|
45
71
|
};
|
|
46
72
|
|
|
47
73
|
function prettyProviderName(providerId: string): string {
|
|
48
|
-
return
|
|
74
|
+
return DISPLAY_NAME_OVERRIDES[providerId]
|
|
75
|
+
?? providerId
|
|
76
|
+
.split("-")
|
|
77
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
78
|
+
.join(" ");
|
|
49
79
|
}
|
|
50
80
|
|
|
51
81
|
function openUrl(url: string): void {
|
|
@@ -57,58 +87,116 @@ function openUrl(url: string): void {
|
|
|
57
87
|
execCb(command, () => {});
|
|
58
88
|
}
|
|
59
89
|
|
|
90
|
+
function sortProviderIds(providerIds: string[]): string[] {
|
|
91
|
+
return [...providerIds].sort((a, b) => {
|
|
92
|
+
return (PRIORITY[a] ?? 99) - (PRIORITY[b] ?? 99)
|
|
93
|
+
|| prettyProviderName(a).localeCompare(prettyProviderName(b));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getRuntimeProviderIds(ctx: any): string[] {
|
|
98
|
+
const fromModels = ctx.modelRegistry.getAll().map((model: any) => model.provider);
|
|
99
|
+
const fromSavedAuth = ctx.modelRegistry.authStorage.list();
|
|
100
|
+
const fromOauth = ctx.modelRegistry.authStorage.getOAuthProviders().map((provider: any) => provider.id);
|
|
101
|
+
return [...new Set([...fromModels, ...fromSavedAuth, ...fromOauth])];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getApiCapableProviderIds(ctx: any): string[] {
|
|
105
|
+
return sortProviderIds(
|
|
106
|
+
getRuntimeProviderIds(ctx).filter((providerId) => !OAUTH_ONLY_PROVIDERS.has(providerId))
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
60
110
|
async function pickItem(ctx: any, title: string, subtitle: string | undefined, items: SelectItem[]): Promise<SelectItem | null> {
|
|
61
111
|
return ctx.ui.custom<SelectItem | null>((tui, theme, _kb, done) => {
|
|
62
112
|
const container = new Container();
|
|
63
|
-
|
|
64
|
-
// Top border
|
|
65
113
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
66
|
-
|
|
67
|
-
// Title (bold)
|
|
68
114
|
container.addChild(new Text(theme.fg("text", theme.bold(title)), 1, 0));
|
|
115
|
+
if (subtitle) container.addChild(new Text(theme.fg("dim", subtitle), 1, 0));
|
|
69
116
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// Spacing before list
|
|
117
|
+
container.addChild(new Text(theme.fg("muted", "Search"), 1, 0));
|
|
118
|
+
const searchInput = new Input();
|
|
119
|
+
searchInput.focused = true;
|
|
120
|
+
container.addChild(searchInput);
|
|
76
121
|
container.addChild(new Text("", 0, 0));
|
|
77
122
|
|
|
78
|
-
|
|
79
|
-
const
|
|
123
|
+
const maxVisible = Math.max(6, Math.min(items.length, Math.floor((tui.terminal.rows - 14) / 2)));
|
|
124
|
+
const listContainer = new Container();
|
|
125
|
+
container.addChild(listContainer);
|
|
80
126
|
|
|
81
|
-
|
|
82
|
-
selectedPrefix: (t) => theme.fg("accent", t),
|
|
83
|
-
selectedText: (t) => theme.fg("accent", theme.bold(t)),
|
|
84
|
-
description: (t) => theme.fg("muted", t),
|
|
85
|
-
scrollInfo: (t) => theme.fg("dim", t),
|
|
86
|
-
noMatch: (t) => theme.fg("warning", t),
|
|
87
|
-
});
|
|
88
|
-
list.onSelect = (item) => done(item);
|
|
89
|
-
list.onCancel = () => done(null);
|
|
90
|
-
container.addChild(list);
|
|
127
|
+
let list: SelectList;
|
|
91
128
|
|
|
92
|
-
|
|
93
|
-
|
|
129
|
+
const sectionOauth = items.find((item) => item.value === "__section_oauth");
|
|
130
|
+
const sectionApi = items.find((item) => item.value === "__section_api");
|
|
131
|
+
const itemTheme = {
|
|
132
|
+
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
133
|
+
selectedText: (t: string) => theme.fg("accent", theme.bold(t)),
|
|
134
|
+
description: (t: string) => theme.fg("muted", t),
|
|
135
|
+
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
136
|
+
noMatch: (t: string) => theme.fg("warning", t),
|
|
137
|
+
};
|
|
94
138
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
139
|
+
const rebuildList = () => {
|
|
140
|
+
const query = searchInput.getValue().trim();
|
|
141
|
+
const normalItems = items.filter((item) => !item.value.startsWith("__section_"));
|
|
142
|
+
const filtered = query
|
|
143
|
+
? fuzzyFilter(normalItems, query, (item) => `${item.label} ${item.description ?? ""}`)
|
|
144
|
+
: normalItems;
|
|
145
|
+
|
|
146
|
+
const oauthItems = filtered.filter((item) => item.value.startsWith("oauth:"));
|
|
147
|
+
const apiItems = filtered.filter((item) => item.value.startsWith("api:"));
|
|
148
|
+
|
|
149
|
+
const displayItems: SelectItem[] = [];
|
|
150
|
+
if (oauthItems.length > 0 && sectionOauth) displayItems.push(sectionOauth);
|
|
151
|
+
displayItems.push(...oauthItems);
|
|
152
|
+
if (apiItems.length > 0 && sectionApi) displayItems.push(sectionApi);
|
|
153
|
+
displayItems.push(...apiItems);
|
|
154
|
+
|
|
155
|
+
const finalItems = displayItems.length > 0
|
|
156
|
+
? displayItems
|
|
157
|
+
: [{ value: "__empty", label: "No matching providers", description: "Try a different search" }];
|
|
158
|
+
|
|
159
|
+
list = new SelectList(finalItems, maxVisible, itemTheme);
|
|
160
|
+
list.onSelect = (item) => {
|
|
161
|
+
if (item.value.startsWith("__section_") || item.value === "__empty") return;
|
|
162
|
+
done(item);
|
|
163
|
+
};
|
|
164
|
+
list.onCancel = () => done(null);
|
|
165
|
+
|
|
166
|
+
listContainer.clear();
|
|
167
|
+
listContainer.addChild(list);
|
|
168
|
+
};
|
|
100
169
|
|
|
101
|
-
|
|
102
|
-
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • Enter select • Esc cancel"), 1, 0));
|
|
170
|
+
rebuildList();
|
|
103
171
|
|
|
104
|
-
|
|
172
|
+
container.addChild(new Text("", 0, 0));
|
|
173
|
+
container.addChild(new Text(`${theme.fg("success", "●")} connected ${theme.fg("warning", "◌")} env ${theme.fg("muted", "○")} new`, 1, 0));
|
|
174
|
+
container.addChild(new Text(theme.fg("dim", "type to search • ↑↓ navigate • Enter select • Esc cancel/clear"), 1, 0));
|
|
105
175
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
106
176
|
|
|
107
177
|
return {
|
|
108
178
|
render: (w) => container.render(w),
|
|
109
179
|
invalidate: () => container.invalidate(),
|
|
110
180
|
handleInput: (data) => {
|
|
111
|
-
|
|
181
|
+
if (matchesKey(data, Key.escape)) {
|
|
182
|
+
if (searchInput.getValue()) {
|
|
183
|
+
searchInput.setValue("");
|
|
184
|
+
rebuildList();
|
|
185
|
+
tui.requestRender();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
done(null);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (matchesKey(data, Key.up) || matchesKey(data, Key.down) || matchesKey(data, Key.enter) || matchesKey(data, Key.return) || matchesKey(data, Key.pageUp) || matchesKey(data, Key.pageDown)) {
|
|
193
|
+
list.handleInput(data);
|
|
194
|
+
tui.requestRender();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
searchInput.handleInput(data);
|
|
199
|
+
rebuildList();
|
|
112
200
|
tui.requestRender();
|
|
113
201
|
},
|
|
114
202
|
};
|
|
@@ -119,54 +207,47 @@ export default function piConnectExtension(pi: ExtensionAPI) {
|
|
|
119
207
|
async function chooseProvider(ctx: any) {
|
|
120
208
|
const authStorage = ctx.modelRegistry.authStorage;
|
|
121
209
|
const oauthProviders = authStorage.getOAuthProviders();
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
const apiProviders = Object.keys(API_KEY_PROVIDERS)
|
|
125
|
-
.filter((providerId) => modelProviderIds.has(providerId) || authStorage.has(providerId) || !!getEnvApiKey(providerId))
|
|
126
|
-
.sort((a, b) => (PRIORITY[a] ?? 99) - (PRIORITY[b] ?? 99) || prettyProviderName(a).localeCompare(prettyProviderName(b)));
|
|
210
|
+
const apiProviderIds = getApiCapableProviderIds(ctx);
|
|
127
211
|
|
|
128
212
|
const statusIcon = (providerId: string) => {
|
|
129
|
-
if (authStorage.has(providerId)) return
|
|
130
|
-
if (getEnvApiKey(providerId)) return
|
|
131
|
-
return
|
|
213
|
+
if (authStorage.has(providerId)) return "●";
|
|
214
|
+
if (getEnvApiKey(providerId)) return "◌";
|
|
215
|
+
return "○";
|
|
132
216
|
};
|
|
133
217
|
|
|
134
218
|
const items: SelectItem[] = [];
|
|
135
219
|
|
|
136
|
-
// OAuth section
|
|
137
220
|
if (oauthProviders.length > 0) {
|
|
138
221
|
items.push({
|
|
139
222
|
value: "__section_oauth",
|
|
140
|
-
label:
|
|
141
|
-
description: "login via browser
|
|
223
|
+
label: "OAuth providers",
|
|
224
|
+
description: "login via browser"
|
|
142
225
|
});
|
|
143
226
|
for (const provider of oauthProviders) {
|
|
144
227
|
items.push({
|
|
145
228
|
value: `oauth:${provider.id}`,
|
|
146
229
|
label: `${statusIcon(provider.id)} ${provider.name}`,
|
|
147
|
-
description: "
|
|
230
|
+
description: "OAuth"
|
|
148
231
|
});
|
|
149
232
|
}
|
|
150
233
|
}
|
|
151
234
|
|
|
152
|
-
|
|
153
|
-
if (apiProviders.length > 0) {
|
|
235
|
+
if (apiProviderIds.length > 0) {
|
|
154
236
|
items.push({
|
|
155
237
|
value: "__section_api",
|
|
156
|
-
label:
|
|
157
|
-
description: "paste
|
|
238
|
+
label: "API key providers",
|
|
239
|
+
description: "paste and save key"
|
|
158
240
|
});
|
|
159
|
-
for (const providerId of
|
|
160
|
-
const envName = API_KEY_PROVIDERS[providerId]?.env;
|
|
241
|
+
for (const providerId of apiProviderIds) {
|
|
161
242
|
items.push({
|
|
162
243
|
value: `api:${providerId}`,
|
|
163
244
|
label: `${statusIcon(providerId)} ${prettyProviderName(providerId)}`,
|
|
164
|
-
description:
|
|
245
|
+
description: ENV_VAR_OVERRIDES[providerId] ?? "API key"
|
|
165
246
|
});
|
|
166
247
|
}
|
|
167
248
|
}
|
|
168
249
|
|
|
169
|
-
const selected = await pickItem(ctx, "Connect provider", "OAuth
|
|
250
|
+
const selected = await pickItem(ctx, "Connect provider", "Unified OAuth and API key login", items);
|
|
170
251
|
if (!selected || selected.value.startsWith("__section_")) return;
|
|
171
252
|
|
|
172
253
|
const [kind, providerId] = selected.value.split(":", 2);
|
|
@@ -182,11 +263,9 @@ export default function piConnectExtension(pi: ExtensionAPI) {
|
|
|
182
263
|
|
|
183
264
|
async function promptApiKey(providerId: string, ctx: any) {
|
|
184
265
|
const authStorage = ctx.modelRegistry.authStorage;
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
? `${label} API key (${provider.env})`
|
|
189
|
-
: `${label} API key`;
|
|
266
|
+
const prompt = ENV_VAR_OVERRIDES[providerId]
|
|
267
|
+
? `${prettyProviderName(providerId)} API key (${ENV_VAR_OVERRIDES[providerId]})`
|
|
268
|
+
: `${prettyProviderName(providerId)} API key`;
|
|
190
269
|
const value = await ctx.ui.input(prompt, "Paste API key");
|
|
191
270
|
if (!value) {
|
|
192
271
|
ctx.ui.notify("Cancelled", "info");
|
|
@@ -203,70 +282,67 @@ export default function piConnectExtension(pi: ExtensionAPI) {
|
|
|
203
282
|
openUrl(url);
|
|
204
283
|
ctx.ui.notify(instructions ? `${instructions}\n${url}` : url, "info");
|
|
205
284
|
},
|
|
206
|
-
onPrompt: async ({ message, placeholder }) =>
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
onManualCodeInput: async () => {
|
|
210
|
-
return (await ctx.ui.input("Paste the callback URL or code", "code or redirect URL")) ?? "";
|
|
211
|
-
},
|
|
212
|
-
onProgress: (message) => {
|
|
213
|
-
ctx.ui.notify(message, "info");
|
|
214
|
-
},
|
|
285
|
+
onPrompt: async ({ message, placeholder }) => (await ctx.ui.input(message, placeholder)) ?? "",
|
|
286
|
+
onManualCodeInput: async () => (await ctx.ui.input("Paste the callback URL or code", "code or redirect URL")) ?? "",
|
|
287
|
+
onProgress: (message) => ctx.ui.notify(message, "info"),
|
|
215
288
|
});
|
|
216
289
|
ctx.ui.notify(`Connected ${prettyProviderName(providerId)}`, "info");
|
|
217
290
|
}
|
|
218
291
|
|
|
219
292
|
pi.registerCommand("connect", {
|
|
220
|
-
description: "Connect
|
|
293
|
+
description: "Connect any OAuth or API key provider from one unified UI",
|
|
221
294
|
handler: async (args, ctx) => {
|
|
222
|
-
const providerId = args.trim();
|
|
295
|
+
const providerId = args.trim().toLowerCase();
|
|
223
296
|
if (!providerId) {
|
|
224
297
|
await chooseProvider(ctx);
|
|
225
298
|
return;
|
|
226
299
|
}
|
|
227
300
|
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
} else {
|
|
242
|
-
await loginWithOAuth(normalized, ctx);
|
|
301
|
+
const oauthIds = new Set(ctx.modelRegistry.authStorage.getOAuthProviders().map((provider: any) => provider.id));
|
|
302
|
+
const apiIds = new Set(getApiCapableProviderIds(ctx));
|
|
303
|
+
|
|
304
|
+
if (oauthIds.has(providerId) && apiIds.has(providerId)) {
|
|
305
|
+
const method = await pickItem(ctx, prettyProviderName(providerId), "Choose how to connect", [
|
|
306
|
+
{ value: "oauth", label: "OAuth", description: "browser login" },
|
|
307
|
+
{ value: "api", label: "API key", description: "paste and save key" },
|
|
308
|
+
]);
|
|
309
|
+
if (!method) return;
|
|
310
|
+
if (method.value === "oauth") {
|
|
311
|
+
await loginWithOAuth(providerId, ctx);
|
|
243
312
|
return;
|
|
244
313
|
}
|
|
314
|
+
await promptApiKey(providerId, ctx);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (oauthIds.has(providerId)) {
|
|
319
|
+
await loginWithOAuth(providerId, ctx);
|
|
320
|
+
return;
|
|
245
321
|
}
|
|
246
322
|
|
|
247
|
-
await promptApiKey(
|
|
323
|
+
await promptApiKey(providerId, ctx);
|
|
248
324
|
},
|
|
249
325
|
});
|
|
250
326
|
|
|
251
327
|
pi.registerCommand("disconnect", {
|
|
252
|
-
description: "Remove saved provider
|
|
328
|
+
description: "Remove a saved provider credential",
|
|
253
329
|
handler: async (_args, ctx) => {
|
|
254
330
|
const authStorage = ctx.modelRegistry.authStorage;
|
|
255
|
-
const providers = authStorage.list()
|
|
331
|
+
const providers = sortProviderIds(authStorage.list());
|
|
256
332
|
if (providers.length === 0) {
|
|
257
333
|
ctx.ui.notify("No saved credentials", "info");
|
|
258
334
|
return;
|
|
259
335
|
}
|
|
260
336
|
const items: SelectItem[] = providers.map((providerId) => ({
|
|
261
337
|
value: providerId,
|
|
262
|
-
label:
|
|
263
|
-
description: "Connected"
|
|
338
|
+
label: `● ${prettyProviderName(providerId)}`,
|
|
339
|
+
description: "Connected"
|
|
264
340
|
}));
|
|
265
341
|
const selected = await pickItem(ctx, "Disconnect provider", "Remove a saved credential", items);
|
|
266
|
-
const
|
|
267
|
-
if (!
|
|
268
|
-
authStorage.remove(
|
|
269
|
-
ctx.ui.notify(`Removed ${prettyProviderName(
|
|
342
|
+
const selectedProviderId = selected?.value;
|
|
343
|
+
if (!selectedProviderId) return;
|
|
344
|
+
authStorage.remove(selectedProviderId);
|
|
345
|
+
ctx.ui.notify(`Removed ${prettyProviderName(selectedProviderId)}`, "info");
|
|
270
346
|
},
|
|
271
347
|
});
|
|
272
348
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-connect",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Unified OAuth
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Unified OAuth and API key login for pi with an OpenCode-inspired UI. Connect 15+ providers with one /connect command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|