pi-connect 0.1.0 → 0.1.2
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 +4 -2
- package/assets/screenshot.png +0 -0
- package/index.ts +122 -106
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -1,8 +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
|
+
|
|
7
|
+
Connect 15+ providers with one `/connect` command.
|
|
6
8
|
|
|
7
9
|
Official pi providers list:
|
|
8
10
|
- https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/providers.md
|
|
Binary file
|
package/index.ts
CHANGED
|
@@ -3,26 +3,59 @@ import { getEnvApiKey } from "@mariozechner/pi-ai";
|
|
|
3
3
|
import { Container, 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,27 +87,35 @@ 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));
|
|
69
|
-
|
|
70
|
-
// Subtitle if provided (muted, smaller hint)
|
|
71
|
-
if (subtitle) {
|
|
72
|
-
container.addChild(new Text(theme.fg("dim", subtitle), 1, 0));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Spacing before list
|
|
115
|
+
if (subtitle) container.addChild(new Text(theme.fg("dim", subtitle), 1, 0));
|
|
76
116
|
container.addChild(new Text("", 0, 0));
|
|
77
117
|
|
|
78
|
-
// Calculate visible items based on terminal height
|
|
79
118
|
const maxVisible = Math.max(6, Math.min(items.length, Math.floor((tui.terminal.rows - 12) / 2)));
|
|
80
|
-
|
|
81
119
|
const list = new SelectList(items, maxVisible, {
|
|
82
120
|
selectedPrefix: (t) => theme.fg("accent", t),
|
|
83
121
|
selectedText: (t) => theme.fg("accent", theme.bold(t)),
|
|
@@ -89,19 +127,9 @@ async function pickItem(ctx: any, title: string, subtitle: string | undefined, i
|
|
|
89
127
|
list.onCancel = () => done(null);
|
|
90
128
|
container.addChild(list);
|
|
91
129
|
|
|
92
|
-
// Spacing before legend
|
|
93
130
|
container.addChild(new Text("", 0, 0));
|
|
94
|
-
|
|
95
|
-
// Legend for status icons - on ONE clean line
|
|
96
|
-
container.addChild(new Text(
|
|
97
|
-
`${theme.fg("success", "●")} connected ${theme.fg("warning", "◌")} env ${theme.fg("muted", "○")} new`,
|
|
98
|
-
1, 0
|
|
99
|
-
));
|
|
100
|
-
|
|
101
|
-
// Keyboard hints
|
|
131
|
+
container.addChild(new Text(`${theme.fg("success", "●")} connected ${theme.fg("warning", "◌")} env ${theme.fg("muted", "○")} new`, 1, 0));
|
|
102
132
|
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • Enter select • Esc cancel"), 1, 0));
|
|
103
|
-
|
|
104
|
-
// Bottom border
|
|
105
133
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
106
134
|
|
|
107
135
|
return {
|
|
@@ -119,11 +147,7 @@ export default function piConnectExtension(pi: ExtensionAPI) {
|
|
|
119
147
|
async function chooseProvider(ctx: any) {
|
|
120
148
|
const authStorage = ctx.modelRegistry.authStorage;
|
|
121
149
|
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)));
|
|
150
|
+
const apiProviderIds = getApiCapableProviderIds(ctx);
|
|
127
151
|
|
|
128
152
|
const statusIcon = (providerId: string) => {
|
|
129
153
|
if (authStorage.has(providerId)) return ctx.ui.theme.fg("success", "●");
|
|
@@ -133,40 +157,37 @@ export default function piConnectExtension(pi: ExtensionAPI) {
|
|
|
133
157
|
|
|
134
158
|
const items: SelectItem[] = [];
|
|
135
159
|
|
|
136
|
-
// OAuth section
|
|
137
160
|
if (oauthProviders.length > 0) {
|
|
138
161
|
items.push({
|
|
139
162
|
value: "__section_oauth",
|
|
140
163
|
label: ctx.ui.theme.bold("OAuth providers"),
|
|
141
|
-
description: "login via browser
|
|
164
|
+
description: "login via browser"
|
|
142
165
|
});
|
|
143
166
|
for (const provider of oauthProviders) {
|
|
144
167
|
items.push({
|
|
145
168
|
value: `oauth:${provider.id}`,
|
|
146
169
|
label: `${statusIcon(provider.id)} ${provider.name}`,
|
|
147
|
-
description: "
|
|
170
|
+
description: "OAuth"
|
|
148
171
|
});
|
|
149
172
|
}
|
|
150
173
|
}
|
|
151
174
|
|
|
152
|
-
|
|
153
|
-
if (apiProviders.length > 0) {
|
|
175
|
+
if (apiProviderIds.length > 0) {
|
|
154
176
|
items.push({
|
|
155
177
|
value: "__section_api",
|
|
156
178
|
label: ctx.ui.theme.bold("API key providers"),
|
|
157
|
-
description: "paste
|
|
179
|
+
description: "paste and save key"
|
|
158
180
|
});
|
|
159
|
-
for (const providerId of
|
|
160
|
-
const envName = API_KEY_PROVIDERS[providerId]?.env;
|
|
181
|
+
for (const providerId of apiProviderIds) {
|
|
161
182
|
items.push({
|
|
162
183
|
value: `api:${providerId}`,
|
|
163
184
|
label: `${statusIcon(providerId)} ${prettyProviderName(providerId)}`,
|
|
164
|
-
description:
|
|
185
|
+
description: ENV_VAR_OVERRIDES[providerId] ?? "API key"
|
|
165
186
|
});
|
|
166
187
|
}
|
|
167
188
|
}
|
|
168
189
|
|
|
169
|
-
const selected = await pickItem(ctx, "Connect provider", "OAuth
|
|
190
|
+
const selected = await pickItem(ctx, "Connect provider", "Unified OAuth and API key login", items);
|
|
170
191
|
if (!selected || selected.value.startsWith("__section_")) return;
|
|
171
192
|
|
|
172
193
|
const [kind, providerId] = selected.value.split(":", 2);
|
|
@@ -182,11 +203,9 @@ export default function piConnectExtension(pi: ExtensionAPI) {
|
|
|
182
203
|
|
|
183
204
|
async function promptApiKey(providerId: string, ctx: any) {
|
|
184
205
|
const authStorage = ctx.modelRegistry.authStorage;
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
? `${label} API key (${provider.env})`
|
|
189
|
-
: `${label} API key`;
|
|
206
|
+
const prompt = ENV_VAR_OVERRIDES[providerId]
|
|
207
|
+
? `${prettyProviderName(providerId)} API key (${ENV_VAR_OVERRIDES[providerId]})`
|
|
208
|
+
: `${prettyProviderName(providerId)} API key`;
|
|
190
209
|
const value = await ctx.ui.input(prompt, "Paste API key");
|
|
191
210
|
if (!value) {
|
|
192
211
|
ctx.ui.notify("Cancelled", "info");
|
|
@@ -203,56 +222,53 @@ export default function piConnectExtension(pi: ExtensionAPI) {
|
|
|
203
222
|
openUrl(url);
|
|
204
223
|
ctx.ui.notify(instructions ? `${instructions}\n${url}` : url, "info");
|
|
205
224
|
},
|
|
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
|
-
},
|
|
225
|
+
onPrompt: async ({ message, placeholder }) => (await ctx.ui.input(message, placeholder)) ?? "",
|
|
226
|
+
onManualCodeInput: async () => (await ctx.ui.input("Paste the callback URL or code", "code or redirect URL")) ?? "",
|
|
227
|
+
onProgress: (message) => ctx.ui.notify(message, "info"),
|
|
215
228
|
});
|
|
216
229
|
ctx.ui.notify(`Connected ${prettyProviderName(providerId)}`, "info");
|
|
217
230
|
}
|
|
218
231
|
|
|
219
232
|
pi.registerCommand("connect", {
|
|
220
|
-
description: "Connect
|
|
233
|
+
description: "Connect any OAuth or API key provider from one unified UI",
|
|
221
234
|
handler: async (args, ctx) => {
|
|
222
|
-
const providerId = args.trim();
|
|
235
|
+
const providerId = args.trim().toLowerCase();
|
|
223
236
|
if (!providerId) {
|
|
224
237
|
await chooseProvider(ctx);
|
|
225
238
|
return;
|
|
226
239
|
}
|
|
227
240
|
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
} else {
|
|
242
|
-
await loginWithOAuth(normalized, ctx);
|
|
241
|
+
const oauthIds = new Set(ctx.modelRegistry.authStorage.getOAuthProviders().map((provider: any) => provider.id));
|
|
242
|
+
const apiIds = new Set(getApiCapableProviderIds(ctx));
|
|
243
|
+
|
|
244
|
+
if (oauthIds.has(providerId) && apiIds.has(providerId)) {
|
|
245
|
+
const method = await pickItem(ctx, prettyProviderName(providerId), "Choose how to connect", [
|
|
246
|
+
{ value: "oauth", label: ctx.ui.theme.bold("OAuth"), description: "browser login" },
|
|
247
|
+
{ value: "api", label: ctx.ui.theme.bold("API key"), description: "paste and save key" },
|
|
248
|
+
]);
|
|
249
|
+
if (!method) return;
|
|
250
|
+
if (method.value === "oauth") {
|
|
251
|
+
await loginWithOAuth(providerId, ctx);
|
|
243
252
|
return;
|
|
244
253
|
}
|
|
254
|
+
await promptApiKey(providerId, ctx);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (oauthIds.has(providerId)) {
|
|
259
|
+
await loginWithOAuth(providerId, ctx);
|
|
260
|
+
return;
|
|
245
261
|
}
|
|
246
262
|
|
|
247
|
-
await promptApiKey(
|
|
263
|
+
await promptApiKey(providerId, ctx);
|
|
248
264
|
},
|
|
249
265
|
});
|
|
250
266
|
|
|
251
267
|
pi.registerCommand("disconnect", {
|
|
252
|
-
description: "Remove saved provider
|
|
268
|
+
description: "Remove a saved provider credential",
|
|
253
269
|
handler: async (_args, ctx) => {
|
|
254
270
|
const authStorage = ctx.modelRegistry.authStorage;
|
|
255
|
-
const providers = authStorage.list()
|
|
271
|
+
const providers = sortProviderIds(authStorage.list());
|
|
256
272
|
if (providers.length === 0) {
|
|
257
273
|
ctx.ui.notify("No saved credentials", "info");
|
|
258
274
|
return;
|
|
@@ -260,13 +276,13 @@ export default function piConnectExtension(pi: ExtensionAPI) {
|
|
|
260
276
|
const items: SelectItem[] = providers.map((providerId) => ({
|
|
261
277
|
value: providerId,
|
|
262
278
|
label: `${ctx.ui.theme.fg("success", "●")} ${ctx.ui.theme.bold(prettyProviderName(providerId))}`,
|
|
263
|
-
description: "Connected"
|
|
279
|
+
description: "Connected"
|
|
264
280
|
}));
|
|
265
281
|
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(
|
|
282
|
+
const selectedProviderId = selected?.value;
|
|
283
|
+
if (!selectedProviderId) return;
|
|
284
|
+
authStorage.remove(selectedProviderId);
|
|
285
|
+
ctx.ui.notify(`Removed ${prettyProviderName(selectedProviderId)}`, "info");
|
|
270
286
|
},
|
|
271
287
|
});
|
|
272
288
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-connect",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Unified OAuth
|
|
3
|
+
"version": "0.1.2",
|
|
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": [
|
|
8
8
|
"pi-package",
|
|
9
9
|
"pi-extension",
|
|
10
|
+
"pi",
|
|
10
11
|
"oauth",
|
|
11
12
|
"api-key",
|
|
12
13
|
"provider-login",
|
|
@@ -26,7 +27,8 @@
|
|
|
26
27
|
"files": [
|
|
27
28
|
"index.ts",
|
|
28
29
|
"README.md",
|
|
29
|
-
"LICENSE"
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"assets"
|
|
30
32
|
],
|
|
31
33
|
"publishConfig": {
|
|
32
34
|
"access": "public"
|
|
@@ -34,7 +36,8 @@
|
|
|
34
36
|
"pi": {
|
|
35
37
|
"extensions": [
|
|
36
38
|
"./index.ts"
|
|
37
|
-
]
|
|
39
|
+
],
|
|
40
|
+
"image": "https://raw.githubusercontent.com/hk-vk/pi-connect/main/assets/screenshot.png"
|
|
38
41
|
},
|
|
39
42
|
"peerDependencies": {
|
|
40
43
|
"@mariozechner/pi-coding-agent": "*",
|