@wrongstack/cli 0.302.0 → 0.302.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/data/providers.json +41 -155
- package/dist/{acp-DATHHPNT.js → acp-XCE4JJF3.js} +4 -4
- package/dist/{audit-QRCLEHHW.js → audit-BBR22QH3.js} +2 -2
- package/dist/{auth-PJD3JRH2.js → auth-K7CYVVKH.js} +121 -78
- package/dist/auth-menu/index.d.ts +5 -5
- package/dist/auth-menu/local.d.ts +4 -5
- package/dist/auth-menu/openai-codex-oauth.d.ts +1 -1
- package/dist/{auto-discover-providers-DRW4OZHG.js → auto-discover-providers-MHFLDSD3.js} +2 -2
- package/dist/{bench-7ZDWSNEF.js → bench-XNDUJOSZ.js} +10 -11
- package/dist/boot/auto-discover-providers.d.ts +1 -1
- package/dist/boot/container-wiring.d.ts +3 -3
- package/dist/boot/dispatch-webui.d.ts +1 -1
- package/dist/boot/system-prompt.d.ts +1 -2
- package/dist/boot/tool-registry.d.ts +1 -1
- package/dist/boot/tui-coordinator-setup.d.ts +1 -1
- package/dist/boot/tui-project-picker-callback.d.ts +1 -1
- package/dist/boot-config.d.ts +2 -2
- package/dist/boot.d.ts +1 -1
- package/dist/{chunk-ETOEGL3V.js → chunk-3IC4IEZC.js} +7 -2
- package/dist/{chunk-PFVVUH5B.js → chunk-3JPTNADJ.js} +3 -2
- package/dist/{chunk-4GVH7V3S.js → chunk-52C7PD2H.js} +1 -1
- package/dist/{chunk-OP47B3IX.js → chunk-7YLN7YUA.js} +7 -2
- package/dist/{chunk-263G3FMB.js → chunk-B4JLSXZB.js} +11 -14
- package/dist/{chunk-I5MEUFDI.js → chunk-BXQG3H2Y.js} +8 -3
- package/dist/{chunk-MT6FGXO3.js → chunk-C5NXM2SI.js} +19 -6
- package/dist/{chunk-XTIXVJXP.js → chunk-DGXNL7OT.js} +1 -1
- package/dist/{chunk-3MOUBRKZ.js → chunk-FJVCVZIV.js} +974 -935
- package/dist/{chunk-MRLYDHQ7.js → chunk-JQURRYU4.js} +4 -6
- package/dist/{chunk-ZECLGUTF.js → chunk-KV6DNQT6.js} +2 -2
- package/dist/{chunk-F4PEGAJZ.js → chunk-NUIHRGPY.js} +1 -1
- package/dist/{chunk-GUHQQG63.js → chunk-RK7E3IXT.js} +2 -4
- package/dist/{chunk-PYTFS4MC.js → chunk-SSSJQVUY.js} +27 -16
- package/dist/{chunk-PJHET45A.js → chunk-SZ42FYPT.js} +3 -3
- package/dist/{chunk-2KDOFTTM.js → chunk-WOXUE2CN.js} +1067 -900
- package/dist/{chunk-A6Q5KGLV.js → chunk-YKBRFSGL.js} +5 -5
- package/dist/cli-context.d.ts +2 -2
- package/dist/cli-eternal-flag.d.ts +1 -2
- package/dist/{cli-main-EOJ74CI4.js → cli-main-PJMDZHDK.js} +2680 -2133
- package/dist/context-limit.d.ts +11 -5
- package/dist/{diag-doctor-OQ3AA63O.js → diag-doctor-ZF5BF2MK.js} +2 -3
- package/dist/execute-deps.d.ts +1 -1
- package/dist/{execution-CQ772VPS.js → execution-LZRYRGDD.js} +234 -143
- package/dist/{export-DOBGPY23.js → export-CCUXRJOU.js} +3 -11
- package/dist/fleet/host-supervisor.d.ts +1 -1
- package/dist/fleet/host.d.ts +1 -1
- package/dist/fleet/routing.d.ts +1 -2
- package/dist/goal-host.d.ts +2 -2
- package/dist/hooks-wiring.d.ts +1 -1
- package/dist/{hq-LN5CJHCM.js → hq-AHPAE7EN.js} +104 -55
- package/dist/hq-command-controller.d.ts +1 -1
- package/dist/hq-server/routes/system-handlers.d.ts +1 -2
- package/dist/hq-server/routes.d.ts +7 -7
- package/dist/{hq-server-S7HZG3PA.js → hq-server-KU2Z54QU.js} +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2635 -2642
- package/dist/mailbox-bridge-bootstrap.d.ts +2 -2
- package/dist/{mcp-OHPJH73U.js → mcp-E5EBGOHF.js} +6 -6
- package/dist/mcp-serve.d.ts +1 -1
- package/dist/{modeldiag-3DOK4BWW.js → modeldiag-SIVUOX2Y.js} +18 -13
- package/dist/{permissions-FNYLZIL4.js → permissions-2PWBL4YU.js} +5 -3
- package/dist/picker.d.ts +1 -1
- package/dist/plugin-management.d.ts +1 -1
- package/dist/{plugin-usage-YEVLOR33.js → plugin-usage-PTEETL3J.js} +2 -2
- package/dist/{plugins-K3BAUP4D.js → plugins-XRGWC3NJ.js} +3 -3
- package/dist/pre-launch.d.ts +2 -2
- package/dist/{providers-models-WEZ4EGLL.js → providers-models-VV74TRQ2.js} +86 -37
- package/dist/repl.d.ts +1 -1
- package/dist/{replay-FCBNHW3K.js → replay-WBRMEWQ5.js} +2 -2
- package/dist/{rewind-AOD3JIYX.js → rewind-7RE6ONIC.js} +17 -17
- package/dist/{sdd-runtime-AIJCJGWC.js → sdd-runtime-GFZ3NJKS.js} +2 -2
- package/dist/{sessions-config-XXN7267N.js → sessions-config-YX2ZPAFM.js} +36 -32
- package/dist/{simpleui-dist-JIFGUJO7.js → simpleui-dist-33VO3QHF.js} +9 -2
- package/dist/slash-commands/agents.d.ts +14 -0
- package/dist/slash-commands/command-context.d.ts +2 -3
- package/dist/slash-commands/helpers.d.ts +1 -1
- package/dist/slash-commands/index.d.ts +1 -1
- package/dist/slash-commands/intake.d.ts +10 -0
- package/dist/slash-commands/memory-triage.d.ts +1 -1
- package/dist/slash-commands/provider-status.d.ts +1 -1
- package/dist/subcommands/handlers/hq.d.ts +20 -0
- package/dist/subcommands/handlers/per-subcommand-help-data.d.ts +1 -1
- package/dist/{update-MV6TUACD.js → update-LTDJGAFU.js} +3 -3
- package/dist/{update-check-UMYTOH7Z.js → update-check-WARHSVZA.js} +2 -2
- package/dist/webui-server/connection-handler.d.ts +1 -1
- package/dist/webui-server/context-breakdown.d.ts +1 -1
- package/dist/webui-server/cost-helpers.d.ts +1 -1
- package/dist/webui-server/lifecycle.d.ts +1 -1
- package/dist/webui-server/prefs-seeding.d.ts +1 -1
- package/dist/webui-server/setup-events.d.ts +1 -1
- package/dist/webui-server/static-serve.d.ts +1 -1
- package/dist/{webui-server-NFNRGNNT.js → webui-server-E3OPEKU5.js} +18 -10
- package/dist/webui-server-options.d.ts +1 -1
- package/dist/wiring/command-host-state.d.ts +2 -2
- package/dist/wiring/controllers.d.ts +1 -2
- package/dist/wiring/dep-watcher.d.ts +1 -1
- package/dist/wiring/director-setup.d.ts +3 -3
- package/dist/wiring/eternal-command-handlers.d.ts +2 -2
- package/dist/wiring/management-tools.d.ts +2 -2
- package/dist/wiring/provider-runtime.d.ts +1 -1
- package/dist/wiring/provider-status.d.ts +1 -1
- package/dist/wiring/provider-utility-tools.d.ts +3 -3
- package/dist/wiring/provider.d.ts +1 -1
- package/dist/wiring/replay.d.ts +1 -1
- package/dist/wiring/runtime-controller-deps.d.ts +1 -1
- package/dist/wiring/runtime-dispatch-state.d.ts +1 -2
- package/dist/wiring/runtime-lifecycle-deps.d.ts +2 -2
- package/dist/wiring/runtime-picker-deps.d.ts +1 -2
- package/dist/wiring/sage.d.ts +1 -1
- package/dist/wiring/session-command-handlers.d.ts +1 -2
- package/dist/wiring/session.d.ts +6 -1
- package/dist/wiring/slash-commands.d.ts +2 -3
- package/package.json +23 -23
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
LOCAL_LLM_PRESETS,
|
|
3
|
-
runLiveProviderPicker
|
|
4
|
-
} from "./chunk-MT6FGXO3.js";
|
|
5
1
|
import {
|
|
6
2
|
openBrowser,
|
|
7
3
|
runCodexOAuthLogin,
|
|
8
4
|
startLoopbackServer
|
|
9
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-SSSJQVUY.js";
|
|
6
|
+
import {
|
|
7
|
+
LOCAL_LLM_PRESETS,
|
|
8
|
+
runLiveProviderPicker
|
|
9
|
+
} from "./chunk-C5NXM2SI.js";
|
|
10
10
|
import {
|
|
11
11
|
activeLabel,
|
|
12
12
|
loadConfigProviders,
|
|
@@ -15,432 +15,717 @@ import {
|
|
|
15
15
|
normalizeKeys,
|
|
16
16
|
nowIso,
|
|
17
17
|
writeKeysBack
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-SZ42FYPT.js";
|
|
19
19
|
|
|
20
|
-
// src/auth-menu/
|
|
21
|
-
import {
|
|
20
|
+
// src/auth-menu/local.ts
|
|
21
|
+
import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
|
|
22
|
+
import { color as color2 } from "@wrongstack/core/utils";
|
|
23
|
+
import { probeLocalLlm } from "@wrongstack/runtime/probe";
|
|
22
24
|
|
|
23
|
-
// src/auth-menu/
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
28
|
-
var AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
29
|
-
var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
30
|
-
var REDIRECT_PORT = 53692;
|
|
31
|
-
var REDIRECT_HOST = "127.0.0.1";
|
|
32
|
-
var REDIRECT_PATH = "/callback";
|
|
33
|
-
var REDIRECT_URI = `http://localhost:${REDIRECT_PORT}${REDIRECT_PATH}`;
|
|
34
|
-
var SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
|
|
35
|
-
var CLAUDE_PROVIDER_ID = "anthropic-oauth";
|
|
36
|
-
var CLAUDE_BASE_URL = "https://api.anthropic.com";
|
|
37
|
-
function base64url(buf) {
|
|
38
|
-
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
39
|
-
}
|
|
40
|
-
function generatePkce() {
|
|
41
|
-
const verifier = base64url(randomBytes(32));
|
|
42
|
-
const challenge = base64url(createHash("sha256").update(verifier).digest());
|
|
43
|
-
return { verifier, challenge };
|
|
44
|
-
}
|
|
45
|
-
function buildAuthorizeUrl(challenge, verifier) {
|
|
46
|
-
const params = new URLSearchParams({
|
|
47
|
-
code: "true",
|
|
48
|
-
client_id: CLIENT_ID,
|
|
49
|
-
response_type: "code",
|
|
50
|
-
redirect_uri: REDIRECT_URI,
|
|
51
|
-
scope: SCOPES,
|
|
52
|
-
code_challenge: challenge,
|
|
53
|
-
code_challenge_method: "S256",
|
|
54
|
-
state: verifier
|
|
55
|
-
});
|
|
56
|
-
return `${AUTHORIZE_URL}?${params.toString()}`;
|
|
57
|
-
}
|
|
58
|
-
function parseAuthorizationInput(input) {
|
|
59
|
-
const value = input.trim();
|
|
60
|
-
if (!value) return {};
|
|
61
|
-
try {
|
|
62
|
-
const url = new URL(value);
|
|
63
|
-
return {
|
|
64
|
-
code: url.searchParams.get("code") ?? void 0,
|
|
65
|
-
state: url.searchParams.get("state") ?? void 0
|
|
66
|
-
};
|
|
67
|
-
} catch {
|
|
25
|
+
// src/auth-menu/auth-menu-audit.ts
|
|
26
|
+
import { DefaultSecretScrubber } from "@wrongstack/core/security";
|
|
27
|
+
var NOOP_AUDIT_LOGGER = {
|
|
28
|
+
emit: () => {
|
|
68
29
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
30
|
+
};
|
|
31
|
+
function decideAuthLocalEvents(opts) {
|
|
32
|
+
const events = [];
|
|
33
|
+
if (opts.probeSkip) {
|
|
34
|
+
events.push({
|
|
35
|
+
type: "auth.local.probe_skip",
|
|
36
|
+
providerId: opts.providerId,
|
|
37
|
+
baseUrl: opts.baseUrl,
|
|
38
|
+
requestedShape: opts.probeSkip.shape
|
|
39
|
+
});
|
|
72
40
|
}
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
41
|
+
if (opts.probeFailedSave) {
|
|
42
|
+
events.push({
|
|
43
|
+
type: "auth.local.probe_failed_save",
|
|
44
|
+
providerId: opts.providerId,
|
|
45
|
+
baseUrl: opts.baseUrl,
|
|
46
|
+
probeStatus: opts.probeFailedSave.status,
|
|
47
|
+
...opts.probeFailedSave.detail !== void 0 ? { probeDetail: opts.probeFailedSave.detail } : {}
|
|
48
|
+
});
|
|
79
49
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
context: { provider: "anthropic-oauth", op, url }
|
|
50
|
+
if (opts.newModels === null) return events;
|
|
51
|
+
const hadPriorAllowlist = opts.previousModels !== void 0 && opts.previousModels.length > 0;
|
|
52
|
+
if (hadPriorAllowlist && opts.newModels.length === 0) {
|
|
53
|
+
events.push({
|
|
54
|
+
type: "auth.local.clear",
|
|
55
|
+
providerId: opts.providerId,
|
|
56
|
+
baseUrl: opts.baseUrl,
|
|
57
|
+
previousModels: opts.previousModels
|
|
89
58
|
});
|
|
59
|
+
return events;
|
|
90
60
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
61
|
+
if (!hadPriorAllowlist && opts.newModels.length > 0) {
|
|
62
|
+
if (opts.previousModels !== void 0) {
|
|
63
|
+
events.push({
|
|
64
|
+
type: "auth.local.undo",
|
|
65
|
+
providerId: opts.providerId,
|
|
66
|
+
baseUrl: opts.baseUrl,
|
|
67
|
+
restoredModels: [...opts.newModels]
|
|
68
|
+
});
|
|
69
|
+
} else {
|
|
70
|
+
events.push({
|
|
71
|
+
type: "auth.local.add",
|
|
72
|
+
providerId: opts.providerId,
|
|
73
|
+
baseUrl: opts.baseUrl,
|
|
74
|
+
models: [...opts.newModels],
|
|
75
|
+
...opts.keyLabel !== void 0 ? { keyLabel: opts.keyLabel } : {}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return events;
|
|
79
|
+
}
|
|
80
|
+
if (hadPriorAllowlist && opts.newModels.length > 0) {
|
|
81
|
+
events.push({
|
|
82
|
+
type: "auth.local.add",
|
|
83
|
+
providerId: opts.providerId,
|
|
84
|
+
baseUrl: opts.baseUrl,
|
|
85
|
+
models: [...opts.newModels],
|
|
86
|
+
...opts.keyLabel !== void 0 ? { keyLabel: opts.keyLabel } : {}
|
|
97
87
|
});
|
|
88
|
+
return events;
|
|
98
89
|
}
|
|
99
|
-
return
|
|
100
|
-
access: json.access_token,
|
|
101
|
-
refresh: json.refresh_token,
|
|
102
|
-
expires: Date.now() + json.expires_in * 1e3
|
|
103
|
-
};
|
|
90
|
+
return events;
|
|
104
91
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
client_id: CLIENT_ID,
|
|
112
|
-
code,
|
|
113
|
-
state,
|
|
114
|
-
redirect_uri: REDIRECT_URI,
|
|
115
|
-
code_verifier: verifier
|
|
116
|
-
}),
|
|
117
|
-
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4)
|
|
92
|
+
|
|
93
|
+
// src/auth-menu/helpers.ts
|
|
94
|
+
async function loadProviders(deps) {
|
|
95
|
+
return loadConfigProviders(deps.profileConfigPath, deps.vault, {
|
|
96
|
+
warn: (msg) => deps.renderer.writeWarning(msg),
|
|
97
|
+
profileConfigPath: deps.profileConfigPath
|
|
118
98
|
});
|
|
119
|
-
return readTokens(res, "exchange", TOKEN_URL);
|
|
120
99
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
} catch {
|
|
137
|
-
return [];
|
|
100
|
+
|
|
101
|
+
// src/auth-menu/shared.ts
|
|
102
|
+
import { color } from "@wrongstack/core/utils";
|
|
103
|
+
function renderProviderLine(renderer, id, cfg, idx) {
|
|
104
|
+
const keys = normalizeKeys(cfg);
|
|
105
|
+
const active = activeLabel(cfg, keys);
|
|
106
|
+
const firstKey = keys[0];
|
|
107
|
+
let summary;
|
|
108
|
+
if (keys.length === 0) {
|
|
109
|
+
summary = color.dim("(no keys)");
|
|
110
|
+
} else if (keys.length === 1) {
|
|
111
|
+
summary = maskedKey(firstKey?.apiKey ?? "");
|
|
112
|
+
} else {
|
|
113
|
+
const activeKeyObj = active != null ? keys.find((k) => k.label === active) : void 0;
|
|
114
|
+
summary = `${color.dim(`${keys.length} keys`)} ${color.dim("active:")} ${color.bold(active ?? "?")} ` + maskedKey(activeKeyObj?.apiKey ?? firstKey?.apiKey ?? "");
|
|
138
115
|
}
|
|
116
|
+
const fam = cfg.family ? color.dim(`[${cfg.family}]`) : "";
|
|
117
|
+
const aliasHint = cfg.type && cfg.type !== id ? color.dim(`\u2192 ${cfg.type}`) : "";
|
|
118
|
+
renderer.write(
|
|
119
|
+
` ${color.dim(`${idx}.`.padStart(4))} ${id.padEnd(22)} ${fam} ${aliasHint} ${summary}
|
|
120
|
+
`
|
|
121
|
+
);
|
|
139
122
|
}
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
123
|
+
function renderProviderHeader(renderer, providerId, cfg) {
|
|
124
|
+
const keys = normalizeKeys(cfg);
|
|
125
|
+
const active = activeLabel(cfg, keys);
|
|
126
|
+
renderer.write(
|
|
127
|
+
`
|
|
128
|
+
${color.bold(providerId)} ${cfg.family ? color.dim(`[${cfg.family}]`) : color.amber("[no family]")}
|
|
129
|
+
`
|
|
130
|
+
);
|
|
131
|
+
const details = [
|
|
132
|
+
color.dim(` type: ${cfg.type ?? providerId}`),
|
|
133
|
+
color.dim(` family: ${cfg.family ?? "(unset \u2192 resolved from models.dev when type matches)"}`),
|
|
134
|
+
color.dim(` baseUrl: ${cfg.baseUrl ?? "(unset \u2192 catalog default)"}`)
|
|
135
|
+
];
|
|
136
|
+
if (cfg.envVars && cfg.envVars.length > 0) {
|
|
137
|
+
details.push(color.dim(` envVars: ${cfg.envVars.join(", ")}`));
|
|
138
|
+
}
|
|
139
|
+
if (cfg.models && cfg.models.length > 0) {
|
|
140
|
+
details.push(color.dim(` models: ${cfg.models.join(", ")}`));
|
|
141
|
+
}
|
|
142
|
+
renderer.write(details.join("\n") + "\n");
|
|
143
|
+
if (keys.length === 0) {
|
|
144
|
+
renderer.write(color.dim(" (no keys saved)\n"));
|
|
153
145
|
} else {
|
|
154
|
-
|
|
146
|
+
for (let i = 0; i < keys.length; i++) {
|
|
147
|
+
const key = keys[i];
|
|
148
|
+
if (key) renderKeyLine(renderer, key, i + 1, active);
|
|
149
|
+
}
|
|
155
150
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
`
|
|
161
|
-
`
|
|
162
|
-
`) + color.bold(` ${"\u2500".repeat(56)}
|
|
163
|
-
|
|
164
|
-
`)
|
|
151
|
+
}
|
|
152
|
+
function renderKeyLine(renderer, key, idx, active) {
|
|
153
|
+
const marker = key.label === active ? color.green("\u25CF") : color.dim("\u25CB");
|
|
154
|
+
renderer.write(
|
|
155
|
+
` ${color.dim(`${idx}.`.padStart(4))} ${marker} ${key.label.padEnd(20)} ${maskedKey(key.apiKey)} ${color.dim(key.createdAt)}
|
|
156
|
+
`
|
|
165
157
|
);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
)
|
|
158
|
+
}
|
|
159
|
+
function renderActions(renderer, keysLength) {
|
|
160
|
+
renderer.write(`
|
|
161
|
+
${color.dim("Actions:")}
|
|
162
|
+
`);
|
|
163
|
+
renderer.write(` ${color.bold("a")} Add another key
|
|
164
|
+
`);
|
|
165
|
+
if (keysLength > 0) {
|
|
166
|
+
renderer.write(` ${color.bold("u")} <n> Update key <n>
|
|
167
|
+
`);
|
|
168
|
+
renderer.write(` ${color.bold("d")} <n> Delete key <n>
|
|
169
|
+
`);
|
|
170
|
+
renderer.write(` ${color.bold("s")} <n> Set key <n> as active
|
|
171
|
+
`);
|
|
175
172
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
173
|
+
renderer.write(` ${color.bold("f")} Edit family
|
|
174
|
+
`);
|
|
175
|
+
renderer.write(
|
|
176
|
+
` ${color.bold("B")} Edit baseUrl ${color.dim("(capital B \u2014 b is Back)")}
|
|
177
|
+
`
|
|
178
|
+
);
|
|
179
|
+
renderer.write(` ${color.bold("m")} Edit visible model list
|
|
180
|
+
`);
|
|
181
|
+
renderer.write(` ${color.bold("x")} Remove this provider entirely
|
|
182
|
+
`);
|
|
183
|
+
renderer.write(` ${color.bold("b")} Back
|
|
184
|
+
`);
|
|
185
|
+
renderer.write(` ${color.bold("q")} Quit
|
|
186
|
+
`);
|
|
187
|
+
}
|
|
188
|
+
function renderTopMenu(renderer, providers) {
|
|
189
|
+
renderer.write(`
|
|
190
|
+
${color.bold("WrongStack")} ${color.dim("\u2014 API key manager")}
|
|
191
|
+
|
|
192
|
+
`);
|
|
193
|
+
const ids = Object.keys(providers).sort();
|
|
194
|
+
if (ids.length === 0) {
|
|
195
|
+
renderer.write(color.dim(" No providers configured yet.\n"));
|
|
196
|
+
renderer.write(
|
|
197
|
+
color.dim(
|
|
198
|
+
" Use (a) to add one from the models.dev catalog, (l) for a local server, or (c) for a custom provider.\n"
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
} else {
|
|
202
|
+
renderer.write(` ${color.dim("Saved providers:")}
|
|
203
|
+
`);
|
|
204
|
+
let idx = 1;
|
|
205
|
+
for (const id of ids) {
|
|
206
|
+
const cfg = providers[id];
|
|
207
|
+
if (!cfg) continue;
|
|
208
|
+
renderProviderLine(renderer, id, cfg, idx);
|
|
209
|
+
idx++;
|
|
185
210
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
211
|
+
}
|
|
212
|
+
renderer.write(`
|
|
213
|
+
${color.dim("Actions:")}
|
|
214
|
+
`);
|
|
215
|
+
renderer.write(` ${color.bold("a")} Add a provider (from catalog)
|
|
216
|
+
`);
|
|
217
|
+
renderer.write(
|
|
218
|
+
` ${color.bold("l")} Add a local server ${color.dim("(OmniRoute / Ollama / vLLM / LM Studio)")}
|
|
219
|
+
`
|
|
220
|
+
);
|
|
221
|
+
renderer.write(` ${color.bold("c")} Add a custom provider
|
|
222
|
+
`);
|
|
223
|
+
renderer.write(
|
|
224
|
+
` ${color.bold("s")} Login with OAuth ${color.dim("(ChatGPT / Claude / Copilot)")}
|
|
225
|
+
`
|
|
226
|
+
);
|
|
227
|
+
if (ids.length > 0) {
|
|
228
|
+
renderer.write(
|
|
229
|
+
` ${color.dim("1-")}${color.dim(String(ids.length))} ${color.bold("Manage a provider")}
|
|
230
|
+
`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
renderer.write(` ${color.bold("q")} Quit
|
|
234
|
+
`);
|
|
235
|
+
}
|
|
236
|
+
async function readKeyInput(deps, intent) {
|
|
237
|
+
const key = (await deps.reader.readSecret(
|
|
238
|
+
` ${color.amber("?")} ${intent} ${color.dim("(hidden, paste OK)")}: `
|
|
239
|
+
)).trim();
|
|
240
|
+
if (!key) {
|
|
241
|
+
deps.renderer.writeError("No key entered.");
|
|
242
|
+
return void 0;
|
|
243
|
+
}
|
|
244
|
+
return key;
|
|
245
|
+
}
|
|
246
|
+
async function confirm(deps, question) {
|
|
247
|
+
const answer = (await deps.reader.readLine(` ${color.amber("?")} ${question} ${color.dim("[y/N/q]")} `)).trim().toLowerCase();
|
|
248
|
+
if (answer === "q" || answer === "quit") return null;
|
|
249
|
+
return answer === "y" || answer === "yes";
|
|
250
|
+
}
|
|
251
|
+
function suggestLabel(usedLabels) {
|
|
252
|
+
const candidate = "default";
|
|
253
|
+
if (!usedLabels.has(candidate)) return candidate;
|
|
254
|
+
let n = 2;
|
|
255
|
+
while (usedLabels.has(`key${n}`)) n++;
|
|
256
|
+
return `key${n}`;
|
|
257
|
+
}
|
|
258
|
+
function validateFamily(raw) {
|
|
259
|
+
const valid = ["anthropic", "openai", "openai-compatible", "google"];
|
|
260
|
+
return valid.includes(raw) ? raw : null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/auth-menu/local.ts
|
|
264
|
+
var PRESET_BY_ID = new Map(LOCAL_LLM_PRESETS.map((p) => [p.id, p]));
|
|
265
|
+
async function runAuthLocal(deps, opts = {}) {
|
|
266
|
+
const scrubber = deps.secretScrubber ?? new DefaultSecretScrubber2();
|
|
267
|
+
const audit = opts.audit ?? NOOP_AUDIT_LOGGER;
|
|
268
|
+
const preset = opts.name ? PRESET_BY_ID.get(opts.name.toLowerCase()) : void 0;
|
|
269
|
+
if (opts.name && !preset) {
|
|
270
|
+
deps.renderer.writeError(
|
|
271
|
+
`Unknown local server "${opts.name}". Valid: ${LOCAL_LLM_PRESETS.map((p) => p.id).join(", ")}.`
|
|
272
|
+
);
|
|
273
|
+
return 1;
|
|
274
|
+
}
|
|
275
|
+
const chosen = preset ?? await pickLocalPreset(deps);
|
|
276
|
+
if (!chosen) {
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
const baseUrl = opts.baseUrl?.trim() || chosen.defaultBaseUrl;
|
|
280
|
+
let apiKey;
|
|
281
|
+
if (opts.apiKey !== void 0) {
|
|
282
|
+
apiKey = opts.apiKey.trim() || void 0;
|
|
283
|
+
} else if (chosen.noAuth) {
|
|
284
|
+
apiKey = void 0;
|
|
285
|
+
} else if (opts.skipKey) {
|
|
286
|
+
apiKey = void 0;
|
|
287
|
+
} else {
|
|
288
|
+
apiKey = await promptOptionalKey(deps, chosen);
|
|
289
|
+
}
|
|
290
|
+
let probe;
|
|
291
|
+
let probeFailedAndSaved = false;
|
|
292
|
+
if (!opts.noProbe) {
|
|
293
|
+
probe = await probeLocalLlm({
|
|
294
|
+
baseUrl,
|
|
295
|
+
apiKey,
|
|
296
|
+
noAuth: chosen.noAuth,
|
|
297
|
+
presetLabel: chosen.label,
|
|
298
|
+
scrubber,
|
|
299
|
+
fetchImpl: opts.fetchImpl,
|
|
300
|
+
timeoutMs: opts.probeTimeoutMs
|
|
301
|
+
});
|
|
302
|
+
renderProbeResult(deps, probe);
|
|
303
|
+
if (opts.probeOnly) {
|
|
304
|
+
return 0;
|
|
201
305
|
}
|
|
202
|
-
if (!
|
|
203
|
-
|
|
204
|
-
|
|
306
|
+
if (!probe.ok) {
|
|
307
|
+
if (chosen.noAuth) {
|
|
308
|
+
deps.renderer.write(
|
|
309
|
+
color2.dim(
|
|
310
|
+
` Saving ${chosen.label} anyway \u2014 keyless local gateway needs no key and discovers models when it comes up.
|
|
311
|
+
`
|
|
312
|
+
)
|
|
313
|
+
);
|
|
314
|
+
probeFailedAndSaved = true;
|
|
315
|
+
} else {
|
|
316
|
+
const saveAnyway = await promptSaveAnyway(deps);
|
|
317
|
+
if (!saveAnyway) {
|
|
318
|
+
deps.renderer.write(color2.dim(" Cancelled. Nothing saved.\n"));
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
probeFailedAndSaved = true;
|
|
322
|
+
}
|
|
205
323
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
324
|
+
}
|
|
325
|
+
const resolvedModels = resolveModelList(opts.models, probe, scrubber);
|
|
326
|
+
const label = opts.label?.trim() || "default";
|
|
327
|
+
const providers = await loadProviders(deps);
|
|
328
|
+
const existing = providers[chosen.id];
|
|
329
|
+
const previousModels = existing?.models ? [...existing.models] : existing?.models === void 0 ? void 0 : [...existing.models];
|
|
330
|
+
const usedLabels = new Set(existing ? normalizeKeys(existing).map((k) => k.label) : []);
|
|
331
|
+
let finalLabel = label;
|
|
332
|
+
if (usedLabels.has(finalLabel)) {
|
|
333
|
+
let n = 2;
|
|
334
|
+
while (usedLabels.has(`${label}-${n}`)) n++;
|
|
335
|
+
finalLabel = `${label}-${n}`;
|
|
336
|
+
deps.renderer.writeInfo(`Label collided; saving as "${finalLabel}".`);
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
await mutateConfigProviders(
|
|
340
|
+
deps.profileConfigPath,
|
|
341
|
+
deps.vault,
|
|
342
|
+
(all) => {
|
|
343
|
+
const p = all[chosen.id] ?? { type: chosen.id };
|
|
344
|
+
if (!p.type) p.type = chosen.id;
|
|
345
|
+
if (!p.family) p.family = "openai-compatible";
|
|
346
|
+
if (!p.baseUrl) p.baseUrl = baseUrl;
|
|
347
|
+
if (!p.envVars) p.envVars = [];
|
|
348
|
+
if (resolvedModels !== null) {
|
|
349
|
+
p.models = [...resolvedModels];
|
|
350
|
+
}
|
|
351
|
+
if (apiKey) {
|
|
352
|
+
const list = normalizeKeys(p);
|
|
353
|
+
list.push({ label: finalLabel, apiKey, createdAt: nowIso() });
|
|
354
|
+
writeKeysBack(p, list);
|
|
355
|
+
if (!p.activeKey) p.activeKey = finalLabel;
|
|
356
|
+
}
|
|
357
|
+
all[chosen.id] = p;
|
|
358
|
+
},
|
|
359
|
+
deps.profileConfigPath
|
|
218
360
|
);
|
|
219
|
-
return 0;
|
|
220
361
|
} catch (err) {
|
|
221
|
-
|
|
222
|
-
deps.renderer.writeError(` Login failed: ${msg}`);
|
|
362
|
+
deps.renderer.writeError(`Failed to save ${chosen.id}: ${err.message}`);
|
|
223
363
|
return 1;
|
|
224
|
-
} finally {
|
|
225
|
-
server.close();
|
|
226
|
-
if (external) external.removeEventListener("abort", onExternalAbort);
|
|
227
|
-
else process.off("SIGINT", onSig);
|
|
228
364
|
}
|
|
365
|
+
const events = decideAuthLocalEvents({
|
|
366
|
+
providerId: chosen.id,
|
|
367
|
+
baseUrl,
|
|
368
|
+
previousModels,
|
|
369
|
+
newModels: resolvedModels,
|
|
370
|
+
keyLabel: apiKey ? finalLabel : void 0,
|
|
371
|
+
probeFailedSave: probeFailedAndSaved && probe ? {
|
|
372
|
+
status: probe.status,
|
|
373
|
+
...probe.detail !== void 0 ? { detail: probe.detail } : {}
|
|
374
|
+
} : void 0
|
|
375
|
+
});
|
|
376
|
+
for (const event of events) {
|
|
377
|
+
audit.emit(event);
|
|
378
|
+
}
|
|
379
|
+
if (apiKey) {
|
|
380
|
+
deps.renderer.write(
|
|
381
|
+
` ${color2.green("\u2713")} Saved ${color2.bold(chosen.id)}/${color2.bold(finalLabel)} \u2192 ${color2.cyan(baseUrl)}
|
|
382
|
+
`
|
|
383
|
+
);
|
|
384
|
+
} else {
|
|
385
|
+
deps.renderer.write(
|
|
386
|
+
` ${color2.green("\u2713")} Saved ${color2.bold(chosen.id)} (no key) \u2192 ${color2.cyan(baseUrl)}
|
|
387
|
+
`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
const firstModel = resolvedModels && resolvedModels.length > 0 ? resolvedModels[0] : null;
|
|
391
|
+
const modelHint = firstModel ? color2.cyan(firstModel) : color2.dim("<model-id>");
|
|
392
|
+
deps.renderer.write(
|
|
393
|
+
color2.dim(` Launch: wstack --provider ${chosen.id} --model ${modelHint} "<task>"
|
|
394
|
+
`)
|
|
395
|
+
);
|
|
396
|
+
return 0;
|
|
229
397
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
scope: SCOPES
|
|
240
|
-
};
|
|
241
|
-
try {
|
|
242
|
-
await mutateConfigProviders(deps.profileConfigPath, deps.vault, (all) => {
|
|
243
|
-
const existing = all[providerId];
|
|
244
|
-
const p = existing ? { ...existing } : { type: providerId };
|
|
245
|
-
p.family = "anthropic-oauth";
|
|
246
|
-
if (!p.baseUrl) p.baseUrl = CLAUDE_BASE_URL;
|
|
247
|
-
if (models.length > 0) p.models = models;
|
|
248
|
-
const keys = normalizeKeys(p).filter((k) => k.label !== entry.label);
|
|
249
|
-
keys.push(entry);
|
|
250
|
-
writeKeysBack(p, keys);
|
|
251
|
-
p.activeKey = entry.label;
|
|
252
|
-
all[providerId] = p;
|
|
253
|
-
}, deps.profileConfigPath);
|
|
254
|
-
return true;
|
|
255
|
-
} catch (err) {
|
|
256
|
-
deps.renderer.writeError(` Failed to save tokens: ${err.message}`);
|
|
257
|
-
return false;
|
|
398
|
+
function resolveModelList(models, probe, scrubber) {
|
|
399
|
+
if (models === void 0) return null;
|
|
400
|
+
const trimmed = models.trim();
|
|
401
|
+
if (trimmed.length === 0) return [];
|
|
402
|
+
const hasProbeIds = probe?.status === "ok" && (probe.modelIds?.length ?? 0) > 0;
|
|
403
|
+
const probeIds = hasProbeIds ? probe?.modelIds ?? [] : [];
|
|
404
|
+
if (trimmed === "first") {
|
|
405
|
+
if (probeIds.length === 0) return null;
|
|
406
|
+
return [scrubber.scrub(probeIds[0])];
|
|
258
407
|
}
|
|
408
|
+
if (/^\d+$/.test(trimmed)) {
|
|
409
|
+
if (probeIds.length === 0) return null;
|
|
410
|
+
const n = Math.min(Number.parseInt(trimmed, 10), probeIds.length);
|
|
411
|
+
return probeIds.slice(0, n).map((id) => scrubber.scrub(id));
|
|
412
|
+
}
|
|
413
|
+
const seen = /* @__PURE__ */ new Set();
|
|
414
|
+
const out = [];
|
|
415
|
+
for (const raw of trimmed.split(",")) {
|
|
416
|
+
const id = scrubber.scrub(raw.trim());
|
|
417
|
+
if (id.length === 0) continue;
|
|
418
|
+
if (seen.has(id)) continue;
|
|
419
|
+
seen.add(id);
|
|
420
|
+
out.push(id);
|
|
421
|
+
}
|
|
422
|
+
return out;
|
|
259
423
|
}
|
|
424
|
+
function renderProbeResult(deps, probe, opts = {}) {
|
|
425
|
+
const label = opts.presetLabel ? ` ${color2.bold(opts.presetLabel)}` : "";
|
|
426
|
+
const elapsed = probe.elapsedMs !== void 0 ? ` ${color2.dim(`(${probe.elapsedMs}ms)`)}` : "";
|
|
427
|
+
switch (probe.status) {
|
|
428
|
+
case "ok": {
|
|
429
|
+
const models = probe.modelCount !== void 0 ? color2.dim(`, ${probe.modelCount} model${probe.modelCount === 1 ? "" : "s"}`) : "";
|
|
430
|
+
deps.renderer.write(` ${color2.green("\u25CF")} ${label} health probe ok${models}${elapsed}
|
|
431
|
+
`);
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
case "timeout": {
|
|
435
|
+
deps.renderer.write(
|
|
436
|
+
` ${color2.red("\u25CF")} ${label} health probe timed out${elapsed}
|
|
437
|
+
${color2.dim("The server did not respond within the timeout.")}
|
|
438
|
+
${color2.dim("Is the local server running? Check the port and try again.")}
|
|
439
|
+
`
|
|
440
|
+
);
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
case "unreachable": {
|
|
444
|
+
const detail = probe.detail ? ` ${color2.dim(`(${probe.detail})`)}` : "";
|
|
445
|
+
deps.renderer.write(
|
|
446
|
+
` ${color2.red("\u25CF")} ${label} server unreachable${detail}
|
|
447
|
+
${color2.dim("Could not connect. Common causes:")}
|
|
448
|
+
${color2.dim(" - The local server is not running")}
|
|
449
|
+
${color2.dim(" - The port is blocked by a firewall")}
|
|
450
|
+
${color2.dim(" - The base URL is wrong")}
|
|
451
|
+
`
|
|
452
|
+
);
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
case "http_error": {
|
|
456
|
+
const status = probe.httpStatus !== void 0 ? ` HTTP ${probe.httpStatus}` : "";
|
|
457
|
+
const detail = probe.detail ? ` ${color2.dim(`\u2014 ${probe.detail}`)}` : "";
|
|
458
|
+
deps.renderer.write(` ${color2.yellow("\u25CF")} ${label} health probe got${status}${detail}
|
|
459
|
+
`);
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
case "invalid_response": {
|
|
463
|
+
deps.renderer.write(
|
|
464
|
+
` ${color2.yellow("\u25CF")} ${label} health probe returned an unexpected shape
|
|
465
|
+
` + (probe.detail ? ` ${color2.dim(probe.detail)}
|
|
466
|
+
` : "")
|
|
467
|
+
);
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
case "skipped": {
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async function promptSaveAnyway(deps) {
|
|
476
|
+
const answer = (await deps.reader.readLine(
|
|
477
|
+
`
|
|
478
|
+
${color2.amber("?")} ${color2.bold("Save anyway?")} ${color2.dim("[y/N]")}: `
|
|
479
|
+
)).trim().toLowerCase();
|
|
480
|
+
return answer === "y" || answer === "yes";
|
|
481
|
+
}
|
|
482
|
+
async function pickLocalPreset(deps) {
|
|
483
|
+
deps.renderer.write(
|
|
484
|
+
`
|
|
485
|
+
${color2.bold("Local LLM servers")} ${color2.dim("\u2014 pick one to pre-fill the base URL")}
|
|
260
486
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
270
|
-
"Editor-Version": "vscode/1.107.0",
|
|
271
|
-
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
272
|
-
"Copilot-Integration-Id": "vscode-chat"
|
|
273
|
-
};
|
|
274
|
-
var COPILOT_API_VERSION = "2026-06-01";
|
|
275
|
-
var COPILOT_PROVIDER_ID = "github-copilot";
|
|
276
|
-
function sleep(ms, signal) {
|
|
277
|
-
return new Promise((resolve, reject) => {
|
|
278
|
-
if (signal.aborted) return reject(new DOMException("Aborted", "AbortError"));
|
|
279
|
-
const t = setTimeout(resolve, ms);
|
|
280
|
-
signal.addEventListener(
|
|
281
|
-
"abort",
|
|
282
|
-
() => {
|
|
283
|
-
clearTimeout(t);
|
|
284
|
-
reject(new DOMException("Aborted", "AbortError"));
|
|
285
|
-
},
|
|
286
|
-
{ once: true }
|
|
487
|
+
`
|
|
488
|
+
);
|
|
489
|
+
let idx = 1;
|
|
490
|
+
for (const entry of LOCAL_LLM_PRESETS) {
|
|
491
|
+
const auth = entry.noAuth ? color2.dim("(no auth)") : color2.dim("(optional auth)");
|
|
492
|
+
deps.renderer.write(
|
|
493
|
+
` ${color2.dim(`${idx}.`.padStart(4))} ${color2.bold(entry.label.padEnd(12))} ${color2.cyan(entry.defaultBaseUrl.padEnd(34))} ${auth}
|
|
494
|
+
`
|
|
287
495
|
);
|
|
288
|
-
|
|
496
|
+
deps.renderer.write(` ${color2.dim(entry.hint)}
|
|
497
|
+
`);
|
|
498
|
+
idx++;
|
|
499
|
+
}
|
|
500
|
+
const answer = (await deps.reader.readLine(
|
|
501
|
+
`
|
|
502
|
+
${color2.amber("?")} Pick ${color2.dim(`(1-${LOCAL_LLM_PRESETS.length}, q to quit)`)}: `
|
|
503
|
+
)).trim().toLowerCase();
|
|
504
|
+
if (!answer || answer === "q" || answer === "quit") return void 0;
|
|
505
|
+
const num = Number.parseInt(answer, 10);
|
|
506
|
+
if (!Number.isNaN(num) && num >= 1 && num <= LOCAL_LLM_PRESETS.length) {
|
|
507
|
+
return LOCAL_LLM_PRESETS[num - 1];
|
|
508
|
+
}
|
|
509
|
+
const byId = LOCAL_LLM_PRESETS.find((p) => p.id === answer);
|
|
510
|
+
if (byId) return byId;
|
|
511
|
+
deps.renderer.writeError(`Unknown selection: "${answer}"`);
|
|
512
|
+
return void 0;
|
|
289
513
|
}
|
|
290
|
-
async function
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
514
|
+
async function promptOptionalKey(deps, preset) {
|
|
515
|
+
const answer = (await deps.reader.readSecret(
|
|
516
|
+
` ${color2.amber("?")} ${color2.bold(preset.label)} API key ${color2.dim("(hidden \u2014 Enter to skip if auth is disabled on the server)")}: `
|
|
517
|
+
)).trim();
|
|
518
|
+
if (!answer) return void 0;
|
|
519
|
+
return answer;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/auth-menu/oauth-menu.ts
|
|
523
|
+
import { color as color5 } from "@wrongstack/core/utils";
|
|
524
|
+
|
|
525
|
+
// src/auth-menu/anthropic-oauth.ts
|
|
526
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
527
|
+
import {
|
|
528
|
+
FetchError,
|
|
529
|
+
ParseError
|
|
530
|
+
} from "@wrongstack/core/types";
|
|
531
|
+
import { color as color3 } from "@wrongstack/core/utils";
|
|
532
|
+
var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
533
|
+
var AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
534
|
+
var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
535
|
+
var REDIRECT_PORT = 53692;
|
|
536
|
+
var REDIRECT_HOST = "127.0.0.1";
|
|
537
|
+
var REDIRECT_PATH = "/callback";
|
|
538
|
+
var REDIRECT_URI = `http://localhost:${REDIRECT_PORT}${REDIRECT_PATH}`;
|
|
539
|
+
var SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
|
|
540
|
+
var CLAUDE_PROVIDER_ID = "anthropic-oauth";
|
|
541
|
+
var CLAUDE_BASE_URL = "https://api.anthropic.com";
|
|
542
|
+
function base64url(buf) {
|
|
543
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
544
|
+
}
|
|
545
|
+
function generatePkce() {
|
|
546
|
+
const verifier = base64url(randomBytes(32));
|
|
547
|
+
const challenge = base64url(createHash("sha256").update(verifier).digest());
|
|
548
|
+
return { verifier, challenge };
|
|
549
|
+
}
|
|
550
|
+
function buildAuthorizeUrl(challenge, verifier) {
|
|
551
|
+
const params = new URLSearchParams({
|
|
552
|
+
code: "true",
|
|
553
|
+
client_id: CLIENT_ID,
|
|
554
|
+
response_type: "code",
|
|
555
|
+
redirect_uri: REDIRECT_URI,
|
|
556
|
+
scope: SCOPES,
|
|
557
|
+
code_challenge: challenge,
|
|
558
|
+
code_challenge_method: "S256",
|
|
559
|
+
state: verifier
|
|
300
560
|
});
|
|
561
|
+
return `${AUTHORIZE_URL}?${params.toString()}`;
|
|
562
|
+
}
|
|
563
|
+
function parseAuthorizationInput(input) {
|
|
564
|
+
const value = input.trim();
|
|
565
|
+
if (!value) return {};
|
|
566
|
+
try {
|
|
567
|
+
const url = new URL(value);
|
|
568
|
+
return {
|
|
569
|
+
code: url.searchParams.get("code") ?? void 0,
|
|
570
|
+
state: url.searchParams.get("state") ?? void 0
|
|
571
|
+
};
|
|
572
|
+
} catch {
|
|
573
|
+
}
|
|
574
|
+
if (value.includes("#")) {
|
|
575
|
+
const [code, state] = value.split("#", 2);
|
|
576
|
+
return { code, state };
|
|
577
|
+
}
|
|
578
|
+
if (value.includes("code=")) {
|
|
579
|
+
const params = new URLSearchParams(value);
|
|
580
|
+
return {
|
|
581
|
+
code: params.get("code") ?? void 0,
|
|
582
|
+
state: params.get("state") ?? void 0
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
return { code: value };
|
|
586
|
+
}
|
|
587
|
+
async function readTokens(res, op, url) {
|
|
301
588
|
if (!res.ok) {
|
|
302
|
-
|
|
303
|
-
|
|
589
|
+
const text = await res.text().catch(() => "");
|
|
590
|
+
throw new FetchError({
|
|
591
|
+
message: `Claude token ${op} failed (${res.status}): ${text || res.statusText}`,
|
|
304
592
|
status: res.status,
|
|
305
|
-
context: { provider: "
|
|
593
|
+
context: { provider: "anthropic-oauth", op, url }
|
|
306
594
|
});
|
|
307
595
|
}
|
|
308
596
|
const json = await res.json();
|
|
309
|
-
if (!json?.
|
|
310
|
-
throw new
|
|
311
|
-
message:
|
|
312
|
-
source: "
|
|
597
|
+
if (!json?.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
|
|
598
|
+
throw new ParseError({
|
|
599
|
+
message: `Claude token ${op} response missing fields`,
|
|
600
|
+
source: "anthropic-oauth-token-response",
|
|
601
|
+
context: { op }
|
|
313
602
|
});
|
|
314
603
|
}
|
|
315
604
|
return {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
interval: json.interval ?? 5,
|
|
320
|
-
expires_in: json.expires_in
|
|
605
|
+
access: json.access_token,
|
|
606
|
+
refresh: json.refresh_token,
|
|
607
|
+
expires: Date.now() + json.expires_in * 1e3
|
|
321
608
|
};
|
|
322
609
|
}
|
|
323
|
-
async function
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
client_id: CLIENT_ID2,
|
|
337
|
-
device_code: device.device_code,
|
|
338
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
339
|
-
}).toString(),
|
|
340
|
-
signal: AbortSignal.any([signal, AbortSignal.timeout(15e3)])
|
|
341
|
-
});
|
|
342
|
-
const json = await res.json().catch(() => ({}));
|
|
343
|
-
if (json.access_token) return json.access_token;
|
|
344
|
-
if (json.error === "authorization_pending") continue;
|
|
345
|
-
if (json.error === "slow_down") {
|
|
346
|
-
intervalMs += 5e3;
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
throw new Error(`Device flow failed: ${json.error ?? "unknown error"}`);
|
|
350
|
-
}
|
|
351
|
-
throw new FetchError2({
|
|
352
|
-
message: "Device code expired \u2014 please restart the login.",
|
|
353
|
-
status: 408,
|
|
354
|
-
// Request Timeout
|
|
355
|
-
context: { provider: "github-copilot", op: "device-code-poll", reason: "expired" }
|
|
610
|
+
async function exchangeAuthorizationCode(code, state, verifier, signal) {
|
|
611
|
+
const res = await fetch(TOKEN_URL, {
|
|
612
|
+
method: "POST",
|
|
613
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
614
|
+
body: JSON.stringify({
|
|
615
|
+
grant_type: "authorization_code",
|
|
616
|
+
client_id: CLIENT_ID,
|
|
617
|
+
code,
|
|
618
|
+
state,
|
|
619
|
+
redirect_uri: REDIRECT_URI,
|
|
620
|
+
code_verifier: verifier
|
|
621
|
+
}),
|
|
622
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4)
|
|
356
623
|
});
|
|
624
|
+
return readTokens(res, "exchange", TOKEN_URL);
|
|
357
625
|
}
|
|
358
|
-
function
|
|
359
|
-
if (typeof item.id !== "string" || item.id.length === 0) return false;
|
|
360
|
-
const cap = item.capabilities;
|
|
361
|
-
if (cap?.type !== "chat") return false;
|
|
362
|
-
if (cap.supports?.tool_calls !== true) return false;
|
|
363
|
-
const eps = item.supported_endpoints;
|
|
364
|
-
if (Array.isArray(eps) && !eps.includes("/chat/completions")) return false;
|
|
365
|
-
if (item.policy?.state === "disabled") return false;
|
|
366
|
-
if (item.vendor === "Experimental") return false;
|
|
367
|
-
return true;
|
|
368
|
-
}
|
|
369
|
-
async function fetchCopilotModels(copilotToken, signal) {
|
|
626
|
+
async function fetchClaudeModels(accessToken, signal) {
|
|
370
627
|
try {
|
|
371
|
-
const
|
|
372
|
-
const res = await fetch(`${base}/models`, {
|
|
628
|
+
const res = await fetch(`${CLAUDE_BASE_URL}/v1/models?limit=100`, {
|
|
373
629
|
headers: {
|
|
374
630
|
accept: "application/json",
|
|
375
|
-
authorization: `Bearer ${
|
|
376
|
-
"
|
|
377
|
-
|
|
631
|
+
authorization: `Bearer ${accessToken}`,
|
|
632
|
+
"anthropic-version": "2023-06-01",
|
|
633
|
+
"anthropic-beta": "claude-code-20250219,oauth-2025-04-20"
|
|
378
634
|
},
|
|
379
635
|
signal: AbortSignal.any([signal, AbortSignal.timeout(8e3)])
|
|
380
636
|
});
|
|
381
637
|
if (!res.ok) return [];
|
|
382
638
|
const json = await res.json();
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
const usable = data.filter(isUsableCopilotChatModel);
|
|
386
|
-
usable.sort((a, b) => copilotModelRank(a) - copilotModelRank(b));
|
|
387
|
-
return usable.map((m) => m.id);
|
|
639
|
+
const ids = (json?.data ?? []).map((m) => m.id).filter((id) => typeof id === "string" && id.startsWith("claude-"));
|
|
640
|
+
return ids;
|
|
388
641
|
} catch {
|
|
389
642
|
return [];
|
|
390
643
|
}
|
|
391
644
|
}
|
|
392
|
-
function
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const providerId = opts.providerId ?? COPILOT_PROVIDER_ID;
|
|
645
|
+
async function runClaudeOAuthLogin(deps, opts = {}) {
|
|
646
|
+
const providerId = opts.providerId ?? CLAUDE_PROVIDER_ID;
|
|
647
|
+
const { verifier, challenge } = generatePkce();
|
|
648
|
+
const state = verifier;
|
|
649
|
+
const authorizeUrl = buildAuthorizeUrl(challenge, verifier);
|
|
650
|
+
const osc8 = (url) => `\x1B]8;;${url}\x1B\\${url}\x1B]8;;\x1B\\`;
|
|
399
651
|
const ac = new AbortController();
|
|
400
652
|
const onSig = () => ac.abort();
|
|
401
|
-
const onExternalAbort = () => ac.abort();
|
|
402
653
|
const external = opts.signal;
|
|
654
|
+
const onExternalAbort = () => ac.abort();
|
|
403
655
|
if (external) {
|
|
404
656
|
if (external.aborted) ac.abort();
|
|
405
657
|
else external.addEventListener("abort", onExternalAbort, { once: true });
|
|
406
658
|
} else {
|
|
407
659
|
process.on("SIGINT", onSig);
|
|
408
660
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
`) +
|
|
661
|
+
const server = await startLoopbackServer(
|
|
662
|
+
state,
|
|
663
|
+
[REDIRECT_PORT],
|
|
664
|
+
ac.signal,
|
|
665
|
+
REDIRECT_PATH,
|
|
666
|
+
REDIRECT_HOST
|
|
667
|
+
);
|
|
668
|
+
deps.renderer.write(
|
|
669
|
+
color3.bold(`
|
|
670
|
+
Sign in with Claude \u2014 ${color3.cyan(providerId)}
|
|
671
|
+
`) + color3.dim(" Uses your Claude Pro/Max subscription (not an API key).\n") + color3.amber(" \u26A0 Using a subscription outside the official Claude Code client is against\n") + color3.amber(" Anthropic\u2019s Terms \u2014 your account could be rate-limited or banned.\n") + color3.dim(" Sanctioned programmatic use = an API key: ") + color3.bold("wstack auth anthropic") + color3.dim("\n\n") + color3.bold(` ${"\u2500".repeat(56)}
|
|
672
|
+
`) + color3.bold(" Open this URL in your browser to sign in:\n") + color3.cyan(` ${osc8(authorizeUrl)}
|
|
673
|
+
`) + color3.bold(` ${"\u2500".repeat(56)}
|
|
420
674
|
|
|
421
675
|
`)
|
|
676
|
+
);
|
|
677
|
+
if (server.bound) {
|
|
678
|
+
openBrowser(authorizeUrl);
|
|
679
|
+
deps.renderer.write(
|
|
680
|
+
color3.dim(" A browser window should open. Waiting for you to finish signing in...\n") + color3.dim(" (Listening on http://localhost:53692 \u2014 press Ctrl+C to cancel.)\n")
|
|
422
681
|
);
|
|
423
|
-
|
|
424
|
-
deps.renderer.write(
|
|
425
|
-
|
|
426
|
-
deps.renderer.write(color2.dim(" Authorized. Fetching your Copilot token...\n"));
|
|
427
|
-
const copilot = await refreshCopilotToken(githubToken, ac.signal);
|
|
428
|
-
const models = await fetchCopilotModels(copilot.token, ac.signal);
|
|
429
|
-
const saved = await saveCopilotTokens(
|
|
430
|
-
deps,
|
|
431
|
-
providerId,
|
|
432
|
-
copilot.token,
|
|
433
|
-
githubToken,
|
|
434
|
-
copilot.expires,
|
|
435
|
-
models
|
|
682
|
+
} else {
|
|
683
|
+
deps.renderer.write(
|
|
684
|
+
color3.amber(" \u26A0 Could not start the local callback listener (port 53692 in use).\n") + color3.dim(" After signing in, paste the full redirect URL (or the code) below.\n")
|
|
436
685
|
);
|
|
686
|
+
}
|
|
687
|
+
let code;
|
|
688
|
+
try {
|
|
689
|
+
if (server.bound) {
|
|
690
|
+
const got = await server.waitForCode();
|
|
691
|
+
if (ac.signal.aborted) {
|
|
692
|
+
deps.renderer.write(color3.dim(" Cancelled.\n"));
|
|
693
|
+
return 1;
|
|
694
|
+
}
|
|
695
|
+
if (got) code = got.code;
|
|
696
|
+
}
|
|
697
|
+
if (!code) {
|
|
698
|
+
const input = (await deps.reader.readLine(
|
|
699
|
+
`
|
|
700
|
+
${color3.amber("?")} Paste the redirect URL or code ${color3.dim("(or q to cancel)")}: `
|
|
701
|
+
)).trim();
|
|
702
|
+
if (input.toLowerCase() === "q" || input === "") {
|
|
703
|
+
deps.renderer.write(color3.dim(" Cancelled.\n"));
|
|
704
|
+
return 1;
|
|
705
|
+
}
|
|
706
|
+
const parsed = parseAuthorizationInput(input);
|
|
707
|
+
if (parsed.state && parsed.state !== state) {
|
|
708
|
+
deps.renderer.writeError(" State mismatch \u2014 please restart the login flow.");
|
|
709
|
+
return 1;
|
|
710
|
+
}
|
|
711
|
+
code = parsed.code;
|
|
712
|
+
}
|
|
713
|
+
if (!code) {
|
|
714
|
+
deps.renderer.writeError(" No authorization code received.");
|
|
715
|
+
return 1;
|
|
716
|
+
}
|
|
717
|
+
deps.renderer.write(color3.dim("\n Exchanging authorization code for tokens...\n"));
|
|
718
|
+
const tokens = await exchangeAuthorizationCode(code, state, verifier, ac.signal);
|
|
719
|
+
const models = await fetchClaudeModels(tokens.access, ac.signal);
|
|
720
|
+
const saved = await saveClaudeTokens(deps, providerId, tokens, models);
|
|
437
721
|
if (!saved) return 1;
|
|
438
|
-
deps.renderer.write(
|
|
439
|
-
const modelHint = models[0]
|
|
722
|
+
deps.renderer.write(color3.green("\n \u2713 Signed in with Claude!\n"));
|
|
723
|
+
const modelHint = models[0];
|
|
440
724
|
deps.renderer.writeInfo(
|
|
441
|
-
` Saved as provider ${
|
|
442
|
-
Use: ${
|
|
443
|
-
`
|
|
725
|
+
` Saved as provider ${color3.bold(providerId)}${models.length ? ` (${models.length} models)` : ""}.
|
|
726
|
+
` + (modelHint ? ` Use: ${color3.bold(`wstack --provider ${providerId} --model ${modelHint}`)} "<task>"
|
|
727
|
+
` : ` No model list was discovered; choose a model before starting a run.
|
|
728
|
+
`) + color3.dim(" Tokens refresh automatically before they expire.\n")
|
|
444
729
|
);
|
|
445
730
|
return 0;
|
|
446
731
|
} catch (err) {
|
|
@@ -448,602 +733,351 @@ async function runCopilotOAuthLogin(deps, opts = {}) {
|
|
|
448
733
|
deps.renderer.writeError(` Login failed: ${msg}`);
|
|
449
734
|
return 1;
|
|
450
735
|
} finally {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
process.off("SIGINT", onSig);
|
|
455
|
-
}
|
|
736
|
+
server.close();
|
|
737
|
+
if (external) external.removeEventListener("abort", onExternalAbort);
|
|
738
|
+
else process.off("SIGINT", onSig);
|
|
456
739
|
}
|
|
457
740
|
}
|
|
458
|
-
async function
|
|
741
|
+
async function saveClaudeTokens(deps, providerId, tokens, models) {
|
|
459
742
|
const entry = {
|
|
460
743
|
label: "oauth-default",
|
|
461
|
-
apiKey:
|
|
744
|
+
apiKey: tokens.access,
|
|
462
745
|
createdAt: nowIso(),
|
|
463
746
|
authMethod: "oauth",
|
|
464
|
-
expiresAt: new Date(expires).toISOString(),
|
|
465
|
-
refreshToken:
|
|
466
|
-
tokenType: "bearer"
|
|
747
|
+
expiresAt: new Date(tokens.expires).toISOString(),
|
|
748
|
+
refreshToken: tokens.refresh,
|
|
749
|
+
tokenType: "bearer",
|
|
750
|
+
scope: SCOPES
|
|
467
751
|
};
|
|
468
752
|
try {
|
|
469
|
-
await mutateConfigProviders(
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return false;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// src/auth-menu/oauth-menu.ts
|
|
490
|
-
function renderOAuthLoginOptions(deps, indent = " ") {
|
|
491
|
-
deps.renderer.write(
|
|
492
|
-
`${indent}${color3.bold("OAuth login options")} ${color3.dim("(subscription sign-in)")}
|
|
493
|
-
${indent}${color3.bold("chatgpt")} ChatGPT Plus/Pro ${color3.dim("(\u2192 openai-codex)")}
|
|
494
|
-
${indent}${color3.bold("claude")} Claude Pro/Max ${color3.dim("(\u2192 anthropic-oauth)")}
|
|
495
|
-
${indent}${color3.bold("copilot")} GitHub Copilot ${color3.dim("(\u2192 github-copilot)")}
|
|
496
|
-
`
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
var OAUTH_ALIASES = {
|
|
500
|
-
chatgpt: ["chatgpt", "openai", "codex", "codex-cli", "openai-codex", "chatgpt-plus", "plus", "pro"],
|
|
501
|
-
claude: ["claude", "anthropic", "claude-pro", "claude-max", "anthropic-oauth", "max"],
|
|
502
|
-
copilot: ["copilot", "github", "gh", "github-copilot"]
|
|
503
|
-
};
|
|
504
|
-
var OAUTH_NUMERIC = { "1": "chatgpt", "2": "claude", "3": "copilot" };
|
|
505
|
-
function resolveOAuthKind(choice, opts = {}) {
|
|
506
|
-
const pick = choice.trim().toLowerCase();
|
|
507
|
-
if (!pick) return void 0;
|
|
508
|
-
if (opts.allowNumeric !== false && OAUTH_NUMERIC[pick]) return OAUTH_NUMERIC[pick];
|
|
509
|
-
for (const [kind, aliases] of Object.entries(OAUTH_ALIASES)) {
|
|
510
|
-
if (aliases.includes(pick)) return kind;
|
|
511
|
-
}
|
|
512
|
-
return void 0;
|
|
513
|
-
}
|
|
514
|
-
async function runOAuthLoginChoice(deps, choice, opts = {}) {
|
|
515
|
-
const kind = resolveOAuthKind(choice, opts);
|
|
516
|
-
if (!kind) return false;
|
|
517
|
-
await runOAuthLoginKind(deps, kind);
|
|
518
|
-
return true;
|
|
519
|
-
}
|
|
520
|
-
async function runOAuthLoginKind(deps, kind) {
|
|
521
|
-
if (kind === "claude") return runClaudeOAuthLogin(deps);
|
|
522
|
-
if (kind === "copilot") return runCopilotOAuthLogin(deps);
|
|
523
|
-
return runCodexOAuthLogin(deps);
|
|
524
|
-
}
|
|
525
|
-
async function runOAuthLoginMenu(deps) {
|
|
526
|
-
deps.renderer.write(
|
|
527
|
-
`
|
|
528
|
-
${color3.bold("Login with OAuth:")}
|
|
529
|
-
` + color3.amber(" \u26A0 Subscription tokens used outside official clients may violate provider\n") + color3.amber(" Terms \u2014 your account could be rate-limited or banned. An API key is the\n") + color3.dim(" sanctioned path for programmatic use.\n") + ` ${color3.bold("1")} ChatGPT Plus/Pro ${color3.dim("(\u2192 openai-codex)")}
|
|
530
|
-
${color3.bold("2")} Claude Pro/Max ${color3.dim("(\u2192 anthropic-oauth)")}
|
|
531
|
-
${color3.bold("3")} GitHub Copilot ${color3.dim("(\u2192 github-copilot)")}
|
|
532
|
-
`
|
|
533
|
-
);
|
|
534
|
-
const pick = await deps.reader.readLine(` ${color3.amber("?")} Pick ${color3.dim("(or b to go back)")}: `);
|
|
535
|
-
await runOAuthLoginChoice(deps, pick);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// src/auth-menu/local.ts
|
|
539
|
-
import { color as color5 } from "@wrongstack/core/utils";
|
|
540
|
-
import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
|
|
541
|
-
|
|
542
|
-
// src/auth-menu/helpers.ts
|
|
543
|
-
async function loadProviders(deps) {
|
|
544
|
-
return loadConfigProviders(deps.profileConfigPath, deps.vault, {
|
|
545
|
-
warn: (msg) => deps.renderer.writeWarning(msg),
|
|
546
|
-
profileConfigPath: deps.profileConfigPath
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// src/auth-menu/shared.ts
|
|
551
|
-
import { color as color4 } from "@wrongstack/core/utils";
|
|
552
|
-
function renderProviderLine(renderer, id, cfg, idx) {
|
|
553
|
-
const keys = normalizeKeys(cfg);
|
|
554
|
-
const active = activeLabel(cfg, keys);
|
|
555
|
-
const firstKey = keys[0];
|
|
556
|
-
let summary;
|
|
557
|
-
if (keys.length === 0) {
|
|
558
|
-
summary = color4.dim("(no keys)");
|
|
559
|
-
} else if (keys.length === 1) {
|
|
560
|
-
summary = maskedKey(firstKey?.apiKey ?? "");
|
|
561
|
-
} else {
|
|
562
|
-
const activeKeyObj = active != null ? keys.find((k) => k.label === active) : void 0;
|
|
563
|
-
summary = `${color4.dim(`${keys.length} keys`)} ${color4.dim("active:")} ${color4.bold(active ?? "?")} ` + maskedKey(activeKeyObj?.apiKey ?? firstKey?.apiKey ?? "");
|
|
564
|
-
}
|
|
565
|
-
const fam = cfg.family ? color4.dim(`[${cfg.family}]`) : "";
|
|
566
|
-
const aliasHint = cfg.type && cfg.type !== id ? color4.dim(`\u2192 ${cfg.type}`) : "";
|
|
567
|
-
renderer.write(
|
|
568
|
-
` ${color4.dim(`${idx}.`.padStart(4))} ${id.padEnd(22)} ${fam} ${aliasHint} ${summary}
|
|
569
|
-
`
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
function renderProviderHeader(renderer, providerId, cfg) {
|
|
573
|
-
const keys = normalizeKeys(cfg);
|
|
574
|
-
const active = activeLabel(cfg, keys);
|
|
575
|
-
renderer.write(
|
|
576
|
-
`
|
|
577
|
-
${color4.bold(providerId)} ${cfg.family ? color4.dim(`[${cfg.family}]`) : color4.amber("[no family]")}
|
|
578
|
-
`
|
|
579
|
-
);
|
|
580
|
-
const details = [
|
|
581
|
-
color4.dim(` type: ${cfg.type ?? providerId}`),
|
|
582
|
-
color4.dim(` family: ${cfg.family ?? "(unset \u2192 resolved from models.dev when type matches)"}`),
|
|
583
|
-
color4.dim(` baseUrl: ${cfg.baseUrl ?? "(unset \u2192 catalog default)"}`)
|
|
584
|
-
];
|
|
585
|
-
if (cfg.envVars && cfg.envVars.length > 0) {
|
|
586
|
-
details.push(color4.dim(` envVars: ${cfg.envVars.join(", ")}`));
|
|
587
|
-
}
|
|
588
|
-
if (cfg.models && cfg.models.length > 0) {
|
|
589
|
-
details.push(color4.dim(` models: ${cfg.models.join(", ")}`));
|
|
590
|
-
}
|
|
591
|
-
renderer.write(details.join("\n") + "\n");
|
|
592
|
-
if (keys.length === 0) {
|
|
593
|
-
renderer.write(color4.dim(" (no keys saved)\n"));
|
|
594
|
-
} else {
|
|
595
|
-
for (let i = 0; i < keys.length; i++) {
|
|
596
|
-
const key = keys[i];
|
|
597
|
-
if (key) renderKeyLine(renderer, key, i + 1, active);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
function renderKeyLine(renderer, key, idx, active) {
|
|
602
|
-
const marker = key.label === active ? color4.green("\u25CF") : color4.dim("\u25CB");
|
|
603
|
-
renderer.write(
|
|
604
|
-
` ${color4.dim(`${idx}.`.padStart(4))} ${marker} ${key.label.padEnd(20)} ${maskedKey(key.apiKey)} ${color4.dim(key.createdAt)}
|
|
605
|
-
`
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
function renderActions(renderer, keysLength) {
|
|
609
|
-
renderer.write(`
|
|
610
|
-
${color4.dim("Actions:")}
|
|
611
|
-
`);
|
|
612
|
-
renderer.write(` ${color4.bold("a")} Add another key
|
|
613
|
-
`);
|
|
614
|
-
if (keysLength > 0) {
|
|
615
|
-
renderer.write(` ${color4.bold("u")} <n> Update key <n>
|
|
616
|
-
`);
|
|
617
|
-
renderer.write(` ${color4.bold("d")} <n> Delete key <n>
|
|
618
|
-
`);
|
|
619
|
-
renderer.write(` ${color4.bold("s")} <n> Set key <n> as active
|
|
620
|
-
`);
|
|
621
|
-
}
|
|
622
|
-
renderer.write(` ${color4.bold("f")} Edit family
|
|
623
|
-
`);
|
|
624
|
-
renderer.write(` ${color4.bold("B")} Edit baseUrl ${color4.dim("(capital B \u2014 b is Back)")}
|
|
625
|
-
`);
|
|
626
|
-
renderer.write(` ${color4.bold("m")} Edit visible model list
|
|
627
|
-
`);
|
|
628
|
-
renderer.write(` ${color4.bold("x")} Remove this provider entirely
|
|
629
|
-
`);
|
|
630
|
-
renderer.write(` ${color4.bold("b")} Back
|
|
631
|
-
`);
|
|
632
|
-
renderer.write(` ${color4.bold("q")} Quit
|
|
633
|
-
`);
|
|
634
|
-
}
|
|
635
|
-
function renderTopMenu(renderer, providers) {
|
|
636
|
-
renderer.write(`
|
|
637
|
-
${color4.bold("WrongStack")} ${color4.dim("\u2014 API key manager")}
|
|
638
|
-
|
|
639
|
-
`);
|
|
640
|
-
const ids = Object.keys(providers).sort();
|
|
641
|
-
if (ids.length === 0) {
|
|
642
|
-
renderer.write(color4.dim(" No providers configured yet.\n"));
|
|
643
|
-
renderer.write(
|
|
644
|
-
color4.dim(
|
|
645
|
-
" Use (a) to add one from the models.dev catalog, (l) for a local server, or (c) for a custom provider.\n"
|
|
646
|
-
)
|
|
647
|
-
);
|
|
648
|
-
} else {
|
|
649
|
-
renderer.write(` ${color4.dim("Saved providers:")}
|
|
650
|
-
`);
|
|
651
|
-
let idx = 1;
|
|
652
|
-
for (const id of ids) {
|
|
653
|
-
const cfg = providers[id];
|
|
654
|
-
if (!cfg) continue;
|
|
655
|
-
renderProviderLine(renderer, id, cfg, idx);
|
|
656
|
-
idx++;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
renderer.write(`
|
|
660
|
-
${color4.dim("Actions:")}
|
|
661
|
-
`);
|
|
662
|
-
renderer.write(` ${color4.bold("a")} Add a provider (from catalog)
|
|
663
|
-
`);
|
|
664
|
-
renderer.write(
|
|
665
|
-
` ${color4.bold("l")} Add a local server ${color4.dim("(OmniRoute / Ollama / vLLM / LM Studio)")}
|
|
666
|
-
`
|
|
667
|
-
);
|
|
668
|
-
renderer.write(` ${color4.bold("c")} Add a custom provider
|
|
669
|
-
`);
|
|
670
|
-
renderer.write(
|
|
671
|
-
` ${color4.bold("s")} Login with OAuth ${color4.dim("(ChatGPT / Claude / Copilot)")}
|
|
672
|
-
`
|
|
673
|
-
);
|
|
674
|
-
if (ids.length > 0) {
|
|
675
|
-
renderer.write(
|
|
676
|
-
` ${color4.dim("1-")}${color4.dim(String(ids.length))} ${color4.bold("Manage a provider")}
|
|
677
|
-
`
|
|
753
|
+
await mutateConfigProviders(
|
|
754
|
+
deps.profileConfigPath,
|
|
755
|
+
deps.vault,
|
|
756
|
+
(all) => {
|
|
757
|
+
const existing = all[providerId];
|
|
758
|
+
const p = existing ? { ...existing } : { type: providerId };
|
|
759
|
+
p.family = "anthropic-oauth";
|
|
760
|
+
if (!p.baseUrl) p.baseUrl = CLAUDE_BASE_URL;
|
|
761
|
+
if (models.length > 0) p.models = models;
|
|
762
|
+
const keys = normalizeKeys(p).filter((k) => k.label !== entry.label);
|
|
763
|
+
keys.push(entry);
|
|
764
|
+
writeKeysBack(p, keys);
|
|
765
|
+
p.activeKey = entry.label;
|
|
766
|
+
all[providerId] = p;
|
|
767
|
+
},
|
|
768
|
+
deps.profileConfigPath
|
|
678
769
|
);
|
|
770
|
+
return true;
|
|
771
|
+
} catch (err) {
|
|
772
|
+
deps.renderer.writeError(` Failed to save tokens: ${err.message}`);
|
|
773
|
+
return false;
|
|
679
774
|
}
|
|
680
|
-
renderer.write(` ${color4.bold("q")} Quit
|
|
681
|
-
`);
|
|
682
|
-
}
|
|
683
|
-
async function readKeyInput(deps, intent) {
|
|
684
|
-
const key = (await deps.reader.readSecret(
|
|
685
|
-
` ${color4.amber("?")} ${intent} ${color4.dim("(hidden, paste OK)")}: `
|
|
686
|
-
)).trim();
|
|
687
|
-
if (!key) {
|
|
688
|
-
deps.renderer.writeError("No key entered.");
|
|
689
|
-
return void 0;
|
|
690
|
-
}
|
|
691
|
-
return key;
|
|
692
|
-
}
|
|
693
|
-
async function confirm(deps, question) {
|
|
694
|
-
const answer = (await deps.reader.readLine(` ${color4.amber("?")} ${question} ${color4.dim("[y/N/q]")} `)).trim().toLowerCase();
|
|
695
|
-
if (answer === "q" || answer === "quit") return null;
|
|
696
|
-
return answer === "y" || answer === "yes";
|
|
697
|
-
}
|
|
698
|
-
function suggestLabel(usedLabels) {
|
|
699
|
-
const candidate = "default";
|
|
700
|
-
if (!usedLabels.has(candidate)) return candidate;
|
|
701
|
-
let n = 2;
|
|
702
|
-
while (usedLabels.has(`key${n}`)) n++;
|
|
703
|
-
return `key${n}`;
|
|
704
|
-
}
|
|
705
|
-
function validateFamily(raw) {
|
|
706
|
-
const valid = ["anthropic", "openai", "openai-compatible", "google"];
|
|
707
|
-
return valid.includes(raw) ? raw : null;
|
|
708
775
|
}
|
|
709
776
|
|
|
710
|
-
// src/auth-menu/
|
|
711
|
-
import {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
777
|
+
// src/auth-menu/github-copilot-oauth.ts
|
|
778
|
+
import {
|
|
779
|
+
FetchError as FetchError2,
|
|
780
|
+
ParseError as ParseError2
|
|
781
|
+
} from "@wrongstack/core/types";
|
|
782
|
+
import { color as color4 } from "@wrongstack/core/utils";
|
|
783
|
+
import { copilotBaseUrlFromToken, refreshCopilotToken } from "@wrongstack/providers";
|
|
784
|
+
var CLIENT_ID2 = "Iv1.b507a08c87ecfe98";
|
|
785
|
+
var DEVICE_CODE_URL = "https://github.com/login/device/code";
|
|
786
|
+
var ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
787
|
+
var COPILOT_HEADERS = {
|
|
788
|
+
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
789
|
+
"Editor-Version": "vscode/1.107.0",
|
|
790
|
+
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
791
|
+
"Copilot-Integration-Id": "vscode-chat"
|
|
718
792
|
};
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
793
|
+
var COPILOT_API_VERSION = "2026-06-01";
|
|
794
|
+
var COPILOT_PROVIDER_ID = "github-copilot";
|
|
795
|
+
function sleep(ms, signal) {
|
|
796
|
+
return new Promise((resolve, reject) => {
|
|
797
|
+
if (signal.aborted) return reject(new DOMException("Aborted", "AbortError"));
|
|
798
|
+
const t = setTimeout(resolve, ms);
|
|
799
|
+
signal.addEventListener(
|
|
800
|
+
"abort",
|
|
801
|
+
() => {
|
|
802
|
+
clearTimeout(t);
|
|
803
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
804
|
+
},
|
|
805
|
+
{ once: true }
|
|
806
|
+
);
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
async function startDeviceFlow(signal) {
|
|
810
|
+
const res = await fetch(DEVICE_CODE_URL, {
|
|
811
|
+
method: "POST",
|
|
812
|
+
headers: {
|
|
813
|
+
accept: "application/json",
|
|
814
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
815
|
+
"user-agent": COPILOT_HEADERS["User-Agent"]
|
|
816
|
+
},
|
|
817
|
+
body: new URLSearchParams({ client_id: CLIENT_ID2, scope: "read:user" }).toString(),
|
|
818
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(15e3)]) : AbortSignal.timeout(15e3)
|
|
819
|
+
});
|
|
820
|
+
if (!res.ok) {
|
|
821
|
+
throw new FetchError2({
|
|
822
|
+
message: `GitHub device-code request failed (${res.status})`,
|
|
823
|
+
status: res.status,
|
|
824
|
+
context: { provider: "github-copilot", op: "device-code", url: DEVICE_CODE_URL }
|
|
746
825
|
});
|
|
747
|
-
return events;
|
|
748
|
-
}
|
|
749
|
-
if (!hadPriorAllowlist && opts.newModels.length > 0) {
|
|
750
|
-
if (opts.previousModels !== void 0) {
|
|
751
|
-
events.push({
|
|
752
|
-
type: "auth.local.undo",
|
|
753
|
-
providerId: opts.providerId,
|
|
754
|
-
baseUrl: opts.baseUrl,
|
|
755
|
-
restoredModels: [...opts.newModels]
|
|
756
|
-
});
|
|
757
|
-
} else {
|
|
758
|
-
events.push({
|
|
759
|
-
type: "auth.local.add",
|
|
760
|
-
providerId: opts.providerId,
|
|
761
|
-
baseUrl: opts.baseUrl,
|
|
762
|
-
models: [...opts.newModels],
|
|
763
|
-
...opts.keyLabel !== void 0 ? { keyLabel: opts.keyLabel } : {}
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
return events;
|
|
767
826
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
models: [...opts.newModels],
|
|
774
|
-
...opts.keyLabel !== void 0 ? { keyLabel: opts.keyLabel } : {}
|
|
827
|
+
const json = await res.json();
|
|
828
|
+
if (!json?.device_code || !json.user_code || !json.verification_uri || typeof json.expires_in !== "number") {
|
|
829
|
+
throw new ParseError2({
|
|
830
|
+
message: "Invalid device-code response",
|
|
831
|
+
source: "github-copilot-device-code-response"
|
|
775
832
|
});
|
|
776
|
-
return events;
|
|
777
833
|
}
|
|
778
|
-
return
|
|
834
|
+
return {
|
|
835
|
+
device_code: json.device_code,
|
|
836
|
+
user_code: json.user_code,
|
|
837
|
+
verification_uri: json.verification_uri,
|
|
838
|
+
interval: json.interval ?? 5,
|
|
839
|
+
expires_in: json.expires_in
|
|
840
|
+
};
|
|
779
841
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
let apiKey;
|
|
799
|
-
if (opts.apiKey !== void 0) {
|
|
800
|
-
apiKey = opts.apiKey.trim() || void 0;
|
|
801
|
-
} else if (chosen.noAuth) {
|
|
802
|
-
apiKey = void 0;
|
|
803
|
-
} else if (opts.skipKey) {
|
|
804
|
-
apiKey = void 0;
|
|
805
|
-
} else {
|
|
806
|
-
apiKey = await promptOptionalKey(deps, chosen);
|
|
807
|
-
}
|
|
808
|
-
let probe;
|
|
809
|
-
let probeFailedAndSaved = false;
|
|
810
|
-
if (!opts.noProbe) {
|
|
811
|
-
probe = await probeLocalLlm({
|
|
812
|
-
baseUrl,
|
|
813
|
-
apiKey,
|
|
814
|
-
noAuth: chosen.noAuth,
|
|
815
|
-
presetLabel: chosen.label,
|
|
816
|
-
scrubber,
|
|
817
|
-
fetchImpl: opts.fetchImpl,
|
|
818
|
-
timeoutMs: opts.probeTimeoutMs
|
|
842
|
+
async function pollForGitHubToken(device, signal) {
|
|
843
|
+
let intervalMs = device.interval * 1e3;
|
|
844
|
+
const expiresAt = Date.now() + device.expires_in * 1e3;
|
|
845
|
+
while (Date.now() < expiresAt) {
|
|
846
|
+
await sleep(intervalMs, signal);
|
|
847
|
+
const res = await fetch(ACCESS_TOKEN_URL, {
|
|
848
|
+
method: "POST",
|
|
849
|
+
headers: {
|
|
850
|
+
accept: "application/json",
|
|
851
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
852
|
+
"user-agent": COPILOT_HEADERS["User-Agent"]
|
|
853
|
+
},
|
|
854
|
+
body: new URLSearchParams({
|
|
855
|
+
client_id: CLIENT_ID2,
|
|
856
|
+
device_code: device.device_code,
|
|
857
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
858
|
+
}).toString(),
|
|
859
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(15e3)])
|
|
819
860
|
});
|
|
820
|
-
|
|
821
|
-
if (
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
deps.renderer.write(
|
|
827
|
-
color5.dim(
|
|
828
|
-
` Saving ${chosen.label} anyway \u2014 keyless local gateway needs no key and discovers models when it comes up.
|
|
829
|
-
`
|
|
830
|
-
)
|
|
831
|
-
);
|
|
832
|
-
probeFailedAndSaved = true;
|
|
833
|
-
} else {
|
|
834
|
-
const saveAnyway = await promptSaveAnyway(deps);
|
|
835
|
-
if (!saveAnyway) {
|
|
836
|
-
deps.renderer.write(color5.dim(" Cancelled. Nothing saved.\n"));
|
|
837
|
-
return 0;
|
|
838
|
-
}
|
|
839
|
-
probeFailedAndSaved = true;
|
|
840
|
-
}
|
|
861
|
+
const json = await res.json().catch(() => ({}));
|
|
862
|
+
if (json.access_token) return json.access_token;
|
|
863
|
+
if (json.error === "authorization_pending") continue;
|
|
864
|
+
if (json.error === "slow_down") {
|
|
865
|
+
intervalMs += 5e3;
|
|
866
|
+
continue;
|
|
841
867
|
}
|
|
868
|
+
throw new Error(`Device flow failed: ${json.error ?? "unknown error"}`);
|
|
842
869
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
870
|
+
throw new FetchError2({
|
|
871
|
+
message: "Device code expired \u2014 please restart the login.",
|
|
872
|
+
status: 408,
|
|
873
|
+
// Request Timeout
|
|
874
|
+
context: { provider: "github-copilot", op: "device-code-poll", reason: "expired" }
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
function isUsableCopilotChatModel(item) {
|
|
878
|
+
if (typeof item.id !== "string" || item.id.length === 0) return false;
|
|
879
|
+
const cap = item.capabilities;
|
|
880
|
+
if (cap?.type !== "chat") return false;
|
|
881
|
+
if (cap.supports?.tool_calls !== true) return false;
|
|
882
|
+
const eps = item.supported_endpoints;
|
|
883
|
+
if (Array.isArray(eps) && !eps.includes("/chat/completions")) return false;
|
|
884
|
+
if (item.policy?.state === "disabled") return false;
|
|
885
|
+
if (item.vendor === "Experimental") return false;
|
|
886
|
+
return true;
|
|
887
|
+
}
|
|
888
|
+
async function fetchCopilotModels(copilotToken, signal) {
|
|
856
889
|
try {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
} catch
|
|
875
|
-
|
|
876
|
-
return 1;
|
|
890
|
+
const base = copilotBaseUrlFromToken(copilotToken);
|
|
891
|
+
const res = await fetch(`${base}/models`, {
|
|
892
|
+
headers: {
|
|
893
|
+
accept: "application/json",
|
|
894
|
+
authorization: `Bearer ${copilotToken}`,
|
|
895
|
+
"X-GitHub-Api-Version": COPILOT_API_VERSION,
|
|
896
|
+
...COPILOT_HEADERS
|
|
897
|
+
},
|
|
898
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(8e3)])
|
|
899
|
+
});
|
|
900
|
+
if (!res.ok) return [];
|
|
901
|
+
const json = await res.json();
|
|
902
|
+
const data = json?.data;
|
|
903
|
+
if (!Array.isArray(data)) return [];
|
|
904
|
+
const usable = data.filter(isUsableCopilotChatModel);
|
|
905
|
+
usable.sort((a, b) => copilotModelRank(a) - copilotModelRank(b));
|
|
906
|
+
return usable.map((m) => m.id);
|
|
907
|
+
} catch {
|
|
908
|
+
return [];
|
|
877
909
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
910
|
+
}
|
|
911
|
+
function copilotModelRank(item) {
|
|
912
|
+
if (item.is_chat_default === true) return 0;
|
|
913
|
+
if (item.is_chat_fallback === true) return 1;
|
|
914
|
+
return 2;
|
|
915
|
+
}
|
|
916
|
+
async function runCopilotOAuthLogin(deps, opts = {}) {
|
|
917
|
+
const providerId = opts.providerId ?? COPILOT_PROVIDER_ID;
|
|
918
|
+
const ac = new AbortController();
|
|
919
|
+
const onSig = () => ac.abort();
|
|
920
|
+
const onExternalAbort = () => ac.abort();
|
|
921
|
+
const external = opts.signal;
|
|
922
|
+
if (external) {
|
|
923
|
+
if (external.aborted) ac.abort();
|
|
924
|
+
else external.addEventListener("abort", onExternalAbort, { once: true });
|
|
925
|
+
} else {
|
|
926
|
+
process.on("SIGINT", onSig);
|
|
891
927
|
}
|
|
892
|
-
|
|
928
|
+
try {
|
|
893
929
|
deps.renderer.write(
|
|
894
|
-
|
|
895
|
-
|
|
930
|
+
color4.bold(`
|
|
931
|
+
Sign in with GitHub Copilot \u2014 ${color4.cyan(providerId)}
|
|
932
|
+
`) + color4.dim(" Uses your GitHub Copilot subscription (not an API key).\n") + color4.amber(" \u26A0 Using Copilot outside its official editor integrations may violate\n") + color4.amber(" GitHub\u2019s Terms \u2014 your account could be rate-limited or banned.\n\n")
|
|
896
933
|
);
|
|
897
|
-
|
|
934
|
+
const device = await startDeviceFlow(ac.signal);
|
|
898
935
|
deps.renderer.write(
|
|
899
|
-
` ${
|
|
900
|
-
`
|
|
936
|
+
color4.bold(` ${"\u2500".repeat(56)}
|
|
937
|
+
`) + color4.bold(" Open this URL and enter the code:\n") + color4.cyan(` ${device.verification_uri}
|
|
938
|
+
`) + color4.bold(" Code: ") + color4.green(color4.bold(device.user_code)) + "\n" + color4.bold(` ${"\u2500".repeat(56)}
|
|
939
|
+
|
|
940
|
+
`)
|
|
941
|
+
);
|
|
942
|
+
openBrowser(device.verification_uri);
|
|
943
|
+
deps.renderer.write(color4.dim(" Waiting for you to authorize in the browser...\n"));
|
|
944
|
+
const githubToken = await pollForGitHubToken(device, ac.signal);
|
|
945
|
+
deps.renderer.write(color4.dim(" Authorized. Fetching your Copilot token...\n"));
|
|
946
|
+
const copilot = await refreshCopilotToken(githubToken, ac.signal);
|
|
947
|
+
const models = await fetchCopilotModels(copilot.token, ac.signal);
|
|
948
|
+
const saved = await saveCopilotTokens(
|
|
949
|
+
deps,
|
|
950
|
+
providerId,
|
|
951
|
+
copilot.token,
|
|
952
|
+
githubToken,
|
|
953
|
+
copilot.expires,
|
|
954
|
+
models
|
|
955
|
+
);
|
|
956
|
+
if (!saved) return 1;
|
|
957
|
+
deps.renderer.write(color4.green("\n \u2713 Signed in with GitHub Copilot!\n"));
|
|
958
|
+
const modelHint = models[0] ?? "gpt-4o";
|
|
959
|
+
deps.renderer.writeInfo(
|
|
960
|
+
` Saved as provider ${color4.bold(providerId)}${models.length ? ` (${models.length} models)` : ""}.
|
|
961
|
+
Use: ${color4.bold(`wstack --provider ${providerId} --model ${modelHint}`)} "<task>"
|
|
962
|
+
` + color4.dim(" The Copilot token refreshes automatically.\n")
|
|
963
|
+
);
|
|
964
|
+
return 0;
|
|
965
|
+
} catch (err) {
|
|
966
|
+
const msg = err instanceof DOMException && err.name === "AbortError" ? "Login cancelled." : err.message;
|
|
967
|
+
deps.renderer.writeError(` Login failed: ${msg}`);
|
|
968
|
+
return 1;
|
|
969
|
+
} finally {
|
|
970
|
+
if (external) {
|
|
971
|
+
external.removeEventListener("abort", onExternalAbort);
|
|
972
|
+
} else {
|
|
973
|
+
process.off("SIGINT", onSig);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
async function saveCopilotTokens(deps, providerId, copilotToken, githubToken, expires, models) {
|
|
978
|
+
const entry = {
|
|
979
|
+
label: "oauth-default",
|
|
980
|
+
apiKey: copilotToken,
|
|
981
|
+
createdAt: nowIso(),
|
|
982
|
+
authMethod: "oauth",
|
|
983
|
+
expiresAt: new Date(expires).toISOString(),
|
|
984
|
+
refreshToken: githubToken,
|
|
985
|
+
tokenType: "bearer"
|
|
986
|
+
};
|
|
987
|
+
try {
|
|
988
|
+
await mutateConfigProviders(
|
|
989
|
+
deps.profileConfigPath,
|
|
990
|
+
deps.vault,
|
|
991
|
+
(all) => {
|
|
992
|
+
const existing = all[providerId];
|
|
993
|
+
const p = existing ? { ...existing } : { type: providerId };
|
|
994
|
+
p.family = "github-copilot";
|
|
995
|
+
if (!p.baseUrl) p.baseUrl = copilotBaseUrlFromToken(copilotToken);
|
|
996
|
+
if (models.length > 0) p.models = models;
|
|
997
|
+
else if (!p.models || p.models.length === 0) p.models = ["gpt-4o"];
|
|
998
|
+
const keys = normalizeKeys(p).filter((k) => k.label !== entry.label);
|
|
999
|
+
keys.push(entry);
|
|
1000
|
+
writeKeysBack(p, keys);
|
|
1001
|
+
p.activeKey = entry.label;
|
|
1002
|
+
all[providerId] = p;
|
|
1003
|
+
},
|
|
1004
|
+
deps.profileConfigPath
|
|
901
1005
|
);
|
|
1006
|
+
return true;
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
deps.renderer.writeError(` Failed to save tokens: ${err.message}`);
|
|
1009
|
+
return false;
|
|
902
1010
|
}
|
|
903
|
-
|
|
904
|
-
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// src/auth-menu/oauth-menu.ts
|
|
1014
|
+
function renderOAuthLoginOptions(deps, indent = " ") {
|
|
905
1015
|
deps.renderer.write(
|
|
906
|
-
color5.dim(
|
|
907
|
-
|
|
1016
|
+
`${indent}${color5.bold("OAuth login options")} ${color5.dim("(subscription sign-in)")}
|
|
1017
|
+
${indent}${color5.bold("chatgpt")} ChatGPT Plus/Pro ${color5.dim("(\u2192 openai-codex)")}
|
|
1018
|
+
${indent}${color5.bold("claude")} Claude Pro/Max ${color5.dim("(\u2192 anthropic-oauth)")}
|
|
1019
|
+
${indent}${color5.bold("copilot")} GitHub Copilot ${color5.dim("(\u2192 github-copilot)")}
|
|
908
1020
|
`
|
|
909
|
-
)
|
|
910
1021
|
);
|
|
911
|
-
return 0;
|
|
912
1022
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1023
|
+
var OAUTH_ALIASES = {
|
|
1024
|
+
chatgpt: [
|
|
1025
|
+
"chatgpt",
|
|
1026
|
+
"openai",
|
|
1027
|
+
"codex",
|
|
1028
|
+
"codex-cli",
|
|
1029
|
+
"openai-codex",
|
|
1030
|
+
"chatgpt-plus",
|
|
1031
|
+
"plus",
|
|
1032
|
+
"pro"
|
|
1033
|
+
],
|
|
1034
|
+
claude: ["claude", "anthropic", "claude-pro", "claude-max", "anthropic-oauth", "max"],
|
|
1035
|
+
copilot: ["copilot", "github", "gh", "github-copilot"]
|
|
1036
|
+
};
|
|
1037
|
+
var OAUTH_NUMERIC = {
|
|
1038
|
+
"1": "chatgpt",
|
|
1039
|
+
"2": "claude",
|
|
1040
|
+
"3": "copilot"
|
|
1041
|
+
};
|
|
1042
|
+
function resolveOAuthKind(choice, opts = {}) {
|
|
1043
|
+
const pick = choice.trim().toLowerCase();
|
|
1044
|
+
if (!pick) return void 0;
|
|
1045
|
+
if (opts.allowNumeric !== false && OAUTH_NUMERIC[pick]) return OAUTH_NUMERIC[pick];
|
|
1046
|
+
for (const [kind, aliases] of Object.entries(OAUTH_ALIASES)) {
|
|
1047
|
+
if (aliases.includes(pick)) return kind;
|
|
936
1048
|
}
|
|
937
|
-
return
|
|
1049
|
+
return void 0;
|
|
938
1050
|
}
|
|
939
|
-
function
|
|
940
|
-
const
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
const models = probe.modelCount !== void 0 ? color5.dim(`, ${probe.modelCount} model${probe.modelCount === 1 ? "" : "s"}`) : "";
|
|
945
|
-
deps.renderer.write(
|
|
946
|
-
` ${color5.green("\u25CF")} ${label} health probe ok${models}${elapsed}
|
|
947
|
-
`
|
|
948
|
-
);
|
|
949
|
-
break;
|
|
950
|
-
}
|
|
951
|
-
case "timeout": {
|
|
952
|
-
deps.renderer.write(
|
|
953
|
-
` ${color5.red("\u25CF")} ${label} health probe timed out${elapsed}
|
|
954
|
-
${color5.dim("The server did not respond within the timeout.")}
|
|
955
|
-
${color5.dim("Is the local server running? Check the port and try again.")}
|
|
956
|
-
`
|
|
957
|
-
);
|
|
958
|
-
break;
|
|
959
|
-
}
|
|
960
|
-
case "unreachable": {
|
|
961
|
-
const detail = probe.detail ? ` ${color5.dim(`(${probe.detail})`)}` : "";
|
|
962
|
-
deps.renderer.write(
|
|
963
|
-
` ${color5.red("\u25CF")} ${label} server unreachable${detail}
|
|
964
|
-
${color5.dim("Could not connect. Common causes:")}
|
|
965
|
-
${color5.dim(" - The local server is not running")}
|
|
966
|
-
${color5.dim(" - The port is blocked by a firewall")}
|
|
967
|
-
${color5.dim(" - The base URL is wrong")}
|
|
968
|
-
`
|
|
969
|
-
);
|
|
970
|
-
break;
|
|
971
|
-
}
|
|
972
|
-
case "http_error": {
|
|
973
|
-
const status = probe.httpStatus !== void 0 ? ` HTTP ${probe.httpStatus}` : "";
|
|
974
|
-
const detail = probe.detail ? ` ${color5.dim(`\u2014 ${probe.detail}`)}` : "";
|
|
975
|
-
deps.renderer.write(
|
|
976
|
-
` ${color5.yellow("\u25CF")} ${label} health probe got${status}${detail}
|
|
977
|
-
`
|
|
978
|
-
);
|
|
979
|
-
break;
|
|
980
|
-
}
|
|
981
|
-
case "invalid_response": {
|
|
982
|
-
deps.renderer.write(
|
|
983
|
-
` ${color5.yellow("\u25CF")} ${label} health probe returned an unexpected shape
|
|
984
|
-
` + (probe.detail ? ` ${color5.dim(probe.detail)}
|
|
985
|
-
` : "")
|
|
986
|
-
);
|
|
987
|
-
break;
|
|
988
|
-
}
|
|
989
|
-
case "skipped": {
|
|
990
|
-
break;
|
|
991
|
-
}
|
|
992
|
-
}
|
|
1051
|
+
async function runOAuthLoginChoice(deps, choice, opts = {}) {
|
|
1052
|
+
const kind = resolveOAuthKind(choice, opts);
|
|
1053
|
+
if (!kind) return false;
|
|
1054
|
+
await runOAuthLoginKind(deps, kind);
|
|
1055
|
+
return true;
|
|
993
1056
|
}
|
|
994
|
-
async function
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
)).trim().toLowerCase();
|
|
999
|
-
return answer === "y" || answer === "yes";
|
|
1057
|
+
async function runOAuthLoginKind(deps, kind) {
|
|
1058
|
+
if (kind === "claude") return runClaudeOAuthLogin(deps);
|
|
1059
|
+
if (kind === "copilot") return runCopilotOAuthLogin(deps);
|
|
1060
|
+
return runCodexOAuthLogin(deps);
|
|
1000
1061
|
}
|
|
1001
|
-
async function
|
|
1062
|
+
async function runOAuthLoginMenu(deps) {
|
|
1002
1063
|
deps.renderer.write(
|
|
1003
1064
|
`
|
|
1004
|
-
${color5.bold("
|
|
1005
|
-
|
|
1065
|
+
${color5.bold("Login with OAuth:")}
|
|
1066
|
+
` + color5.amber(" \u26A0 Subscription tokens used outside official clients may violate provider\n") + color5.amber(" Terms \u2014 your account could be rate-limited or banned. An API key is the\n") + color5.dim(" sanctioned path for programmatic use.\n") + ` ${color5.bold("1")} ChatGPT Plus/Pro ${color5.dim("(\u2192 openai-codex)")}
|
|
1067
|
+
${color5.bold("2")} Claude Pro/Max ${color5.dim("(\u2192 anthropic-oauth)")}
|
|
1068
|
+
${color5.bold("3")} GitHub Copilot ${color5.dim("(\u2192 github-copilot)")}
|
|
1006
1069
|
`
|
|
1007
1070
|
);
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
` ${color5.dim(`${idx}.`.padStart(4))} ${color5.bold(entry.label.padEnd(12))} ${color5.cyan(entry.defaultBaseUrl.padEnd(34))} ${auth}
|
|
1013
|
-
`
|
|
1014
|
-
);
|
|
1015
|
-
deps.renderer.write(
|
|
1016
|
-
` ${color5.dim(entry.hint)}
|
|
1017
|
-
`
|
|
1018
|
-
);
|
|
1019
|
-
idx++;
|
|
1020
|
-
}
|
|
1021
|
-
const answer = (await deps.reader.readLine(
|
|
1022
|
-
`
|
|
1023
|
-
${color5.amber("?")} Pick ${color5.dim(`(1-${LOCAL_LLM_PRESETS.length}, q to quit)`)}: `
|
|
1024
|
-
)).trim().toLowerCase();
|
|
1025
|
-
if (!answer || answer === "q" || answer === "quit") return void 0;
|
|
1026
|
-
const num = Number.parseInt(answer, 10);
|
|
1027
|
-
if (!Number.isNaN(num) && num >= 1 && num <= LOCAL_LLM_PRESETS.length) {
|
|
1028
|
-
return LOCAL_LLM_PRESETS[num - 1];
|
|
1029
|
-
}
|
|
1030
|
-
const byId = LOCAL_LLM_PRESETS.find((p) => p.id === answer);
|
|
1031
|
-
if (byId) return byId;
|
|
1032
|
-
deps.renderer.writeError(`Unknown selection: "${answer}"`);
|
|
1033
|
-
return void 0;
|
|
1034
|
-
}
|
|
1035
|
-
async function promptOptionalKey(deps, preset) {
|
|
1036
|
-
const answer = (await deps.reader.readSecret(
|
|
1037
|
-
` ${color5.amber("?")} ${color5.bold(preset.label)} API key ${color5.dim("(hidden \u2014 Enter to skip if auth is disabled on the server)")}: `
|
|
1038
|
-
)).trim();
|
|
1039
|
-
if (!answer) return void 0;
|
|
1040
|
-
return answer;
|
|
1071
|
+
const pick = await deps.reader.readLine(
|
|
1072
|
+
` ${color5.amber("?")} Pick ${color5.dim("(or b to go back)")}: `
|
|
1073
|
+
);
|
|
1074
|
+
await runOAuthLoginChoice(deps, pick);
|
|
1041
1075
|
}
|
|
1042
1076
|
|
|
1043
1077
|
// src/auth-menu/add-provider.ts
|
|
1078
|
+
import * as fs from "node:fs/promises";
|
|
1044
1079
|
import { atomicWrite, color as color6 } from "@wrongstack/core/utils";
|
|
1045
1080
|
import { catalogProviderIdFor, PROVIDER_DEFINITIONS } from "@wrongstack/providers";
|
|
1046
|
-
import * as fs from "node:fs/promises";
|
|
1047
1081
|
|
|
1048
1082
|
// src/provider-id.ts
|
|
1049
1083
|
function nextCustomProviderId(existing) {
|
|
@@ -1245,27 +1279,32 @@ async function addKeyForProvider(providerId, deps, template) {
|
|
|
1245
1279
|
if (!label) return false;
|
|
1246
1280
|
const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
|
|
1247
1281
|
if (!apiKey) return false;
|
|
1248
|
-
await mutateConfigProviders(
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1282
|
+
await mutateConfigProviders(
|
|
1283
|
+
deps.profileConfigPath,
|
|
1284
|
+
deps.vault,
|
|
1285
|
+
(all) => {
|
|
1286
|
+
const existingProv = all[providerId] ?? {
|
|
1287
|
+
type: providerId,
|
|
1288
|
+
...template
|
|
1289
|
+
};
|
|
1290
|
+
if (!existingProv.type) existingProv.type = providerId;
|
|
1291
|
+
if (!existingProv.family && template.family) {
|
|
1292
|
+
existingProv.family = template.family;
|
|
1293
|
+
}
|
|
1294
|
+
if (!existingProv.baseUrl && template.baseUrl) {
|
|
1295
|
+
existingProv.baseUrl = template.baseUrl;
|
|
1296
|
+
}
|
|
1297
|
+
if (!existingProv.envVars && template.envVars) {
|
|
1298
|
+
existingProv.envVars = template.envVars;
|
|
1299
|
+
}
|
|
1300
|
+
const list = normalizeKeys(existingProv);
|
|
1301
|
+
list.push({ label, apiKey, createdAt: nowIso() });
|
|
1302
|
+
writeKeysBack(existingProv, list);
|
|
1303
|
+
if (!existingProv.activeKey) existingProv.activeKey = label;
|
|
1304
|
+
all[providerId] = existingProv;
|
|
1305
|
+
},
|
|
1306
|
+
deps.profileConfigPath
|
|
1307
|
+
);
|
|
1269
1308
|
deps.renderer.write(
|
|
1270
1309
|
` ${color6.green("\u2713")} Saved ${color6.bold(providerId)}/${color6.bold(label)}.
|
|
1271
1310
|
`
|
|
@@ -1316,6 +1355,7 @@ async function promptForLabel(deps, usedLabels) {
|
|
|
1316
1355
|
}
|
|
1317
1356
|
|
|
1318
1357
|
export {
|
|
1358
|
+
runClaudeOAuthLogin,
|
|
1319
1359
|
loadProviders,
|
|
1320
1360
|
renderProviderHeader,
|
|
1321
1361
|
renderActions,
|
|
@@ -1323,8 +1363,8 @@ export {
|
|
|
1323
1363
|
readKeyInput,
|
|
1324
1364
|
confirm,
|
|
1325
1365
|
validateFamily,
|
|
1326
|
-
runClaudeOAuthLogin,
|
|
1327
1366
|
runCopilotOAuthLogin,
|
|
1367
|
+
runAuthLocal,
|
|
1328
1368
|
resolveOAuthKind,
|
|
1329
1369
|
runOAuthLoginKind,
|
|
1330
1370
|
runOAuthLoginMenu,
|
|
@@ -1332,7 +1372,6 @@ export {
|
|
|
1332
1372
|
addFromCatalog,
|
|
1333
1373
|
addKeyForCatalogProvider,
|
|
1334
1374
|
addCustomProvider,
|
|
1335
|
-
addKeyForProvider
|
|
1336
|
-
runAuthLocal
|
|
1375
|
+
addKeyForProvider
|
|
1337
1376
|
};
|
|
1338
|
-
//# sourceMappingURL=chunk-
|
|
1377
|
+
//# sourceMappingURL=chunk-FJVCVZIV.js.map
|