proxium 0.0.0-stage → 0.1.0

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.
Files changed (3) hide show
  1. package/README.md +13 -2
  2. package/package.json +11 -6
  3. package/proxium-cli.mjs +1301 -0
package/README.md CHANGED
@@ -1,3 +1,14 @@
1
- # Temporary Holding Version
1
+ # proxium
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Sign in to [Proxium](https://proxium.tech) once, then run your AI tools through it with your login.
4
+
5
+ ```sh
6
+ npx proxium login
7
+ proxium launch claude
8
+ ```
9
+
10
+ - `proxium login` signs you in with the browser and sets up Claude Code, OpenCode, Pi and Hermes on this machine.
11
+ - `proxium launch claude` starts Claude Code with Proxium as its model provider.
12
+ - `proxium token` prints a fresh token of your login, for a tool or a script.
13
+
14
+ The tools keep no key: each one runs `proxium token` when it needs one. The guide: https://proxium.tech/docs/guides/login
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
- {
2
- "name": "proxium",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "proxium",
3
+ "version": "0.1.0",
4
+ "description": "Sign in to Proxium once, then run Claude Code, OpenCode, Pi and Hermes through it with your login.",
5
+ "license": "Elastic-2.0",
6
+ "homepage": "https://proxium.tech",
7
+ "type": "module",
8
+ "bin": { "proxium": "proxium-cli.mjs" },
9
+ "files": ["proxium-cli.mjs", "README.md"],
10
+ "engines": { "node": ">=20" }
11
+ }
@@ -0,0 +1,1301 @@
1
+ #!/usr/bin/env node
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { homedir, hostname } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { parseArgs } from "node:util";
7
+ //#region ../node_modules/@munhq/mcp-kit/dist/cli/device.js
8
+ /**
9
+ * The CLI half of the sign-in: the device login of RFC 8628 against a product whose identity runs
10
+ * better-auth's `deviceAuthorization` plugin (`fleetDevice()` in `@munhq/auth`).
11
+ *
12
+ * The product shows a code, the person approves it in the browser where they are signed in, and the
13
+ * CLI gets a better-auth session token. What the CLI does with that token is the product's own
14
+ * step: desk exchanges it for a desk token, chat-recall for a sync device token, proxium for a
15
+ * project key.
16
+ */
17
+ var LoginError = class extends Error {};
18
+ const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
19
+ /** Opens `url` with the desktop's opener. False on a machine with no display. */
20
+ function openInBrowser(url) {
21
+ const [bin, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", [
22
+ "/c",
23
+ "start",
24
+ "",
25
+ url
26
+ ]] : ["xdg-open", [url]];
27
+ if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false;
28
+ try {
29
+ const child = spawn(bin, args, {
30
+ stdio: "ignore",
31
+ detached: true
32
+ });
33
+ child.on("error", () => void 0);
34
+ child.unref();
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+ async function postJson(f, url, body) {
41
+ let res;
42
+ try {
43
+ res = await f(url, {
44
+ method: "POST",
45
+ headers: { "content-type": "application/json" },
46
+ body: JSON.stringify(body)
47
+ });
48
+ } catch (e) {
49
+ throw new LoginError(`${new URL(url).origin} did not answer: ${e.message}`);
50
+ }
51
+ const text = await res.text();
52
+ try {
53
+ return {
54
+ status: res.status,
55
+ json: text ? JSON.parse(text) : {}
56
+ };
57
+ } catch {
58
+ return {
59
+ status: res.status,
60
+ json: { error: text.slice(0, 200) }
61
+ };
62
+ }
63
+ }
64
+ const said = (j) => String(j.error_description ?? j.error ?? "no reason given");
65
+ /** Runs the device login and answers the session token of the person who approved the code. */
66
+ async function deviceLogin(o) {
67
+ const server = o.server.replace(/\/+$/, "");
68
+ const f = o.fetch ?? fetch;
69
+ const say = o.write ?? ((l) => process.stdout.write(`${l}\n`));
70
+ const sleep = o.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
71
+ const open = o.openBrowser ?? openInBrowser;
72
+ const start = await postJson(f, `${server}/api/auth/device/code`, { client_id: o.clientId });
73
+ if (start.status === 404) throw new LoginError(`${server} has no device login.`);
74
+ const j = start.json;
75
+ const deviceCode = j.device_code;
76
+ const userCode = j.user_code;
77
+ const uri = j.verification_uri;
78
+ if (start.status !== 200 || !deviceCode || !userCode || !uri) throw new LoginError(`${server} refused the device login (${start.status}): ${said(j)}`);
79
+ const page = j.verification_uri_complete ?? uri;
80
+ say(`Your code: ${userCode}`);
81
+ say(!o.noBrowser && open(page) ? `The browser opens ${page}. Sign in there and approve the code.` : `Open ${page} in a browser, sign in and approve the code.`);
82
+ let interval = Math.max(1, Number(j.interval ?? 5)) * 1e3;
83
+ const end = Date.now() + Math.max(30, Number(j.expires_in ?? 600)) * 1e3;
84
+ for (;;) {
85
+ if (Date.now() > end) throw new LoginError("the code expired before it was approved. Run the login again.");
86
+ await sleep(interval);
87
+ const r = await postJson(f, `${server}/api/auth/device/token`, {
88
+ grant_type: DEVICE_GRANT,
89
+ device_code: deviceCode,
90
+ client_id: o.clientId
91
+ });
92
+ if (r.status === 200 && typeof r.json.access_token === "string") return r.json.access_token;
93
+ const error = r.json.error;
94
+ if (error === "authorization_pending") continue;
95
+ if (error === "slow_down") interval += 5e3;
96
+ else if (error === "access_denied") throw new LoginError("the code was denied in the browser. Nothing was stored.");
97
+ else if (error === "expired_token") throw new LoginError("the code expired before it was approved. Run the login again.");
98
+ else throw new LoginError(`the device login failed (${r.status}): ${said(r.json)}`);
99
+ }
100
+ }
101
+ //#endregion
102
+ //#region ../node_modules/@munhq/mcp-kit/dist/cli/host.js
103
+ function systemHost(env = process.env) {
104
+ return {
105
+ home: env.HOME ?? homedir(),
106
+ env,
107
+ run(bin, args, opts = {}) {
108
+ const r = spawnSync(bin, args, {
109
+ input: opts.input,
110
+ cwd: opts.cwd,
111
+ env: opts.env ?? env,
112
+ encoding: "utf8"
113
+ });
114
+ return {
115
+ code: r.status ?? 1,
116
+ stdout: r.stdout ?? "",
117
+ stderr: r.stderr ?? (r.error ? String(r.error) : "")
118
+ };
119
+ },
120
+ read(path) {
121
+ try {
122
+ return readFileSync(path, "utf8");
123
+ } catch {
124
+ return;
125
+ }
126
+ },
127
+ write(path, text, opts = {}) {
128
+ mkdirSync(dirname(path), {
129
+ recursive: true,
130
+ ...opts.dirMode === void 0 ? {} : { mode: opts.dirMode }
131
+ });
132
+ if (opts.mode === void 0) {
133
+ if (existsSync(path)) copyFileSync(path, `${path}.munhq-bak`);
134
+ writeFileSync(path, text);
135
+ } else {
136
+ writeFileSync(path, text, { mode: opts.mode });
137
+ chmodSync(path, opts.mode);
138
+ }
139
+ }
140
+ };
141
+ }
142
+ //#endregion
143
+ //#region ../node_modules/@munhq/mcp-kit/dist/cli/store.js
144
+ const SECRET_NAME = /^[a-z][a-z0-9-]*(\/[A-Za-z0-9_.-]+){2,3}$/;
145
+ /** Keeps `value` and answers where it went. */
146
+ function saveSecret(value, o) {
147
+ const host = o.host ?? systemHost();
148
+ const run = o.run ?? host.run;
149
+ if (!o.forceFile && SECRET_NAME.test(o.name)) {
150
+ if (run("sealkeep", [
151
+ "set",
152
+ "--stdin",
153
+ ...o.description ? ["-d", o.description] : [],
154
+ o.name
155
+ ], { input: value }).code === 0) return {
156
+ kind: "sealkeep",
157
+ name: o.name
158
+ };
159
+ }
160
+ host.write(o.file, `${value}\n`, {
161
+ mode: 384,
162
+ dirMode: 448
163
+ });
164
+ return {
165
+ kind: "file",
166
+ path: o.file
167
+ };
168
+ }
169
+ const quote$1 = (w) => /^[A-Za-z0-9_./:=@%+-]+$/.test(w) ? w : `'${w.replace(/'/g, `'\\''`)}'`;
170
+ /** A shell script that starts `command` with the secret in `envVar`; its own arguments follow the command. */
171
+ function launchScript(where, envVar, command, comment = "") {
172
+ const lines = ["#!/bin/sh", ...comment ? comment.split("\n").map((l) => `# ${l}`) : []];
173
+ if (where.kind === "sealkeep") lines.push(`exec sealkeep run -e ${envVar}=${where.name} -- ${command.map(quote$1).join(" ")} "$@"`);
174
+ else lines.push(`${envVar}="$(cat ${quote$1(where.path)})"`, `export ${envVar}`, `exec ${command.map(quote$1).join(" ")} "$@"`);
175
+ return `${lines.join("\n")}\n`;
176
+ }
177
+ //#endregion
178
+ //#region ../node_modules/@munhq/mcp-kit/dist/cli/clients.js
179
+ const done = (client, changed, message) => ({
180
+ client,
181
+ ok: true,
182
+ changed,
183
+ message
184
+ });
185
+ const failed = (client, message) => ({
186
+ client,
187
+ ok: false,
188
+ changed: false,
189
+ message
190
+ });
191
+ const last = (text) => text.trim().split("\n").filter(Boolean).slice(-2).join(" / ");
192
+ const xdg = (h) => h.env.XDG_CONFIG_HOME || join(h.home, ".config");
193
+ /** Reads a JSON file, lets `edit` change it, and writes it when `edit` answers true. */
194
+ function editJson(h, path, edit) {
195
+ const text = h.read(path);
196
+ let doc = {};
197
+ if (text !== void 0 && text.trim() !== "") try {
198
+ doc = JSON.parse(text);
199
+ } catch {
200
+ return { error: `${path} is not JSON, so it was not changed` };
201
+ }
202
+ if (!edit(doc)) return { changed: false };
203
+ h.write(path, `${JSON.stringify(doc, null, 2)}\n`);
204
+ return { changed: true };
205
+ }
206
+ const obj = (d, key) => d[key] ??= {};
207
+ /** Adds `value` under `d[section][name]` unless the entry is there already. */
208
+ function putEntry(h, client, label, path, section, name, value) {
209
+ const r = editJson(h, path, (d) => {
210
+ const entries = obj(d, section);
211
+ if (JSON.stringify(entries[name]) === JSON.stringify(value)) return false;
212
+ entries[name] = value;
213
+ return true;
214
+ });
215
+ if ("error" in r) return failed(client, r.error);
216
+ return done(client, r.changed, r.changed ? `${label}: added ${name} to ${path}` : `${label}: ${name} is already in ${path}`);
217
+ }
218
+ /** The major version of OpenCode, or 0 when it does not answer. */
219
+ function openCodeMajor(h) {
220
+ const r = h.run("opencode", ["--version"]);
221
+ const m = /(\d+)\.\d+\.\d+/.exec(`${r.stdout} ${r.stderr}`);
222
+ return m ? Number(m[1]) : 0;
223
+ }
224
+ /**
225
+ * The OpenCode 2 plugin that gives the provider `name` a fresh key on each request: its `model.request` hook runs
226
+ * `command` (which prints `{"access_token", "expires_in"}`) and keeps the token until two minutes before its end.
227
+ * OpenCode takes no key command in its config, and reads `{env:…}` only at start.
228
+ */
229
+ function openCodeKeyPlugin(name, command) {
230
+ return `// Written by the login of ${name}: each request of the provider ${name} carries a fresh key.
231
+ import { execFile } from "node:child_process";
232
+ const COMMAND = ${JSON.stringify(command)};
233
+ let kept;
234
+ function mint() {
235
+ return new Promise((resolve, reject) =>
236
+ execFile("/bin/sh", ["-c", COMMAND], { timeout: 15000 }, (e, out, err) =>
237
+ e ? reject(new Error(String(err).trim() || e.message)) : resolve(String(out))),
238
+ );
239
+ }
240
+ async function key() {
241
+ const now = Date.now() / 1000;
242
+ if (kept && kept.exp - now > 120) return kept.token;
243
+ const j = JSON.parse(await mint());
244
+ kept = { token: j.access_token, exp: now + Number(j.expires_in) };
245
+ return kept.token;
246
+ }
247
+ export default {
248
+ id: ${JSON.stringify(`${name}-key`)},
249
+ async setup(ctx) {
250
+ await ctx.session.hook("model.request", async (event) => {
251
+ event.headers.authorization = "Bearer " + (await key());
252
+ }, { providerID: ${JSON.stringify(name)} });
253
+ },
254
+ };
255
+ `;
256
+ }
257
+ /**
258
+ * OpenCode 2 with a key command: the plugin in the product's own folder (`~/.config/<name>/opencode-plugin`), listed
259
+ * in `plugins`, and the provider with no key. OpenCode loads a plugin from any folder that `plugins` lists.
260
+ */
261
+ function openCodeWithPlugin(h, p, command) {
262
+ const dir = join(xdg(h), p.name, "opencode-plugin");
263
+ const config = join(xdg(h), "opencode", "opencode.json");
264
+ const source = openCodeKeyPlugin(p.name, command);
265
+ const pluginChanged = h.read(join(dir, "index.js")) !== source;
266
+ if (pluginChanged) h.write(join(dir, "index.js"), source);
267
+ const r = editJson(h, config, (d) => {
268
+ let changed = false;
269
+ const plugins = Array.isArray(d.plugins) ? d.plugins : d.plugins = [];
270
+ if (!plugins.includes(dir)) {
271
+ plugins.push(dir);
272
+ changed = true;
273
+ }
274
+ const providers = obj(d, "provider");
275
+ const value = {
276
+ npm: "@ai-sdk/openai-compatible",
277
+ name: p.label,
278
+ options: {
279
+ baseURL: p.baseUrl,
280
+ ...p.headers ? { headers: p.headers } : {}
281
+ },
282
+ models: Object.fromEntries(p.models.map((m) => [m.id, { name: m.label ?? m.id }]))
283
+ };
284
+ if (JSON.stringify(providers[p.name]) !== JSON.stringify(value)) {
285
+ providers[p.name] = value;
286
+ changed = true;
287
+ }
288
+ return changed;
289
+ });
290
+ if ("error" in r) return failed("opencode", r.error);
291
+ const changed = r.changed || pluginChanged;
292
+ return done("opencode", changed, changed ? `OpenCode: added ${p.name} to ${config}, with the plugin ${dir} that sets its key` : `OpenCode: ${p.name} is already in ${config}`);
293
+ }
294
+ /** `check` succeeds when the server is registered already. */
295
+ function byCommand(h, client, label, name, check, add) {
296
+ if (h.run(check[0], check[1]).code === 0) return done(client, false, `${label}: ${name} is already registered`);
297
+ const r = h.run(add[0], add[1]);
298
+ if (r.code !== 0) return failed(client, `${label}: ${add[0]} ${add[1].slice(0, 2).join(" ")} failed: ${last(r.stderr || r.stdout)}`);
299
+ return done(client, true, `${label}: registered ${name}`);
300
+ }
301
+ const CLIENTS = {
302
+ claude: {
303
+ id: "claude",
304
+ name: "Claude Code",
305
+ bin: "claude",
306
+ addMcp: (h, s) => byCommand(h, "claude", "Claude Code", s.name, ["claude", [
307
+ "mcp",
308
+ "get",
309
+ s.name
310
+ ]], ["claude", [
311
+ "mcp",
312
+ "add",
313
+ "--scope",
314
+ "user",
315
+ "--transport",
316
+ "http",
317
+ s.name,
318
+ s.url
319
+ ]]),
320
+ mcpLogin: (_h, s) => ({
321
+ client: "claude",
322
+ instruction: `Start a new session, run /mcp, select ${s.name}, and sign in.`
323
+ })
324
+ },
325
+ codex: {
326
+ id: "codex",
327
+ untested: true,
328
+ name: "Codex",
329
+ bin: "codex",
330
+ addMcp: (h, s) => byCommand(h, "codex", "Codex", s.name, ["codex", [
331
+ "mcp",
332
+ "get",
333
+ s.name
334
+ ]], ["codex", [
335
+ "mcp",
336
+ "add",
337
+ s.name,
338
+ "--url",
339
+ s.url
340
+ ]]),
341
+ mcpLogin: (_h, s) => ({
342
+ client: "codex",
343
+ command: [
344
+ "codex",
345
+ "mcp",
346
+ "login",
347
+ s.name
348
+ ],
349
+ instruction: `Run codex mcp login ${s.name}.`
350
+ })
351
+ },
352
+ opencode: {
353
+ id: "opencode",
354
+ name: "OpenCode",
355
+ bin: "opencode",
356
+ addMcp: (h, s) => putEntry(h, "opencode", "OpenCode", join(xdg(h), "opencode", "opencode.json"), "mcp", s.name, {
357
+ type: "remote",
358
+ url: s.url,
359
+ enabled: true
360
+ }),
361
+ mcpLogin: (h, s) => ({
362
+ client: "opencode",
363
+ command: [
364
+ "opencode",
365
+ "mcp",
366
+ "auth",
367
+ s.name
368
+ ],
369
+ cwd: h.home,
370
+ instruction: `Run opencode mcp auth ${s.name} in your home folder.`
371
+ }),
372
+ takesKeyCommand: (h) => openCodeMajor(h) >= 2,
373
+ addProvider: (h, p) => p.keyCommandJson && openCodeMajor(h) >= 2 ? openCodeWithPlugin(h, p, p.keyCommandJson) : putEntry(h, "opencode", "OpenCode", join(xdg(h), "opencode", "opencode.json"), "provider", p.name, {
374
+ npm: "@ai-sdk/openai-compatible",
375
+ name: p.label,
376
+ env: [p.keyEnv],
377
+ options: {
378
+ baseURL: p.baseUrl,
379
+ apiKey: `{env:${p.keyEnv}}`,
380
+ ...p.headers ? { headers: p.headers } : {}
381
+ },
382
+ models: Object.fromEntries(p.models.map((m) => [m.id, { name: m.label ?? m.id }]))
383
+ })
384
+ },
385
+ pi: {
386
+ id: "pi",
387
+ name: "Pi",
388
+ bin: "pi",
389
+ addMcp: (h, s) => {
390
+ if (h.run("pi", [
391
+ "mcp",
392
+ "list",
393
+ "--json"
394
+ ]).stdout.includes(`"${s.name}"`)) return done("pi", false, `Pi: ${s.name} is already registered`);
395
+ const r = h.run("pi", [
396
+ "mcp",
397
+ "add",
398
+ s.name,
399
+ "--url",
400
+ s.url
401
+ ]);
402
+ return r.code === 0 ? done("pi", true, `Pi: registered ${s.name}`) : failed("pi", `Pi: pi mcp add failed: ${last(r.stderr || r.stdout)}`);
403
+ },
404
+ mcpLogin: (_h, s) => ({
405
+ client: "pi",
406
+ command: [
407
+ "pi",
408
+ "mcp",
409
+ "login",
410
+ s.name
411
+ ],
412
+ instruction: `Run pi mcp login ${s.name}.`
413
+ }),
414
+ takesKeyCommand: () => true,
415
+ addProvider: (h, p) => putEntry(h, "pi", "Pi", join(h.home, ".pi", "agent", "models.json"), "providers", p.name, {
416
+ baseUrl: p.baseUrl,
417
+ api: "openai-completions",
418
+ apiKey: p.keyCommand ? `!${p.keyCommand}` : `$${p.keyEnv}`,
419
+ ...p.headers ? { headers: p.headers } : {},
420
+ models: p.models.map((m) => ({ id: m.id }))
421
+ })
422
+ },
423
+ hermes: {
424
+ id: "hermes",
425
+ name: "Hermes",
426
+ bin: "hermes",
427
+ addMcp: (h, s) => hermesSet(h, "mcp_servers", s.name, {
428
+ url: s.url,
429
+ auth: "oauth",
430
+ ...s.registerClient ? { oauth: { cimd: false } } : {}
431
+ }),
432
+ mcpLogin: (_h, s) => ({
433
+ client: "hermes",
434
+ command: [
435
+ "hermes",
436
+ "mcp",
437
+ "login",
438
+ s.name
439
+ ],
440
+ instruction: `Run hermes mcp login ${s.name}.`
441
+ }),
442
+ takesKeyCommand: () => true,
443
+ addProvider: (h, p) => hermesSet(h, "providers", p.name, {
444
+ api: p.baseUrl,
445
+ ...p.keyCommandJson ? { key_cmd: p.keyCommandJson } : { key_env: p.keyEnv },
446
+ default_model: p.defaultModel,
447
+ ...p.headers ? { extra_headers: p.headers } : {}
448
+ })
449
+ },
450
+ gemini: {
451
+ id: "gemini",
452
+ untested: true,
453
+ name: "Gemini CLI",
454
+ bin: "gemini",
455
+ addMcp: (h, s) => putEntry(h, "gemini", "Gemini CLI", join(h.home, ".gemini", "settings.json"), "mcpServers", s.name, { httpUrl: s.url }),
456
+ mcpLogin: (_h, s) => ({
457
+ client: "gemini",
458
+ instruction: `Start gemini, run /mcp auth ${s.name}, and sign in.`
459
+ })
460
+ },
461
+ cursor: {
462
+ id: "cursor",
463
+ untested: true,
464
+ name: "Cursor",
465
+ bin: "cursor",
466
+ addMcp: (h, s) => putEntry(h, "cursor", "Cursor", join(h.home, ".cursor", "mcp.json"), "mcpServers", s.name, { url: s.url }),
467
+ mcpLogin: (_h, s) => ({
468
+ client: "cursor",
469
+ instruction: `Open Cursor, and sign in to ${s.name} when it asks.`
470
+ })
471
+ }
472
+ };
473
+ /** Hermes keeps its configuration in YAML. Its own CLI reads and writes the entry, so the kit has no YAML parser. */
474
+ function hermesSet(h, section, name, value) {
475
+ const key = `${section}.${name}`;
476
+ const have = h.run("hermes", [
477
+ "config",
478
+ "get",
479
+ "--json",
480
+ key
481
+ ]);
482
+ if (have.code === 0) try {
483
+ if (JSON.stringify(JSON.parse(have.stdout)) === JSON.stringify(value)) return done("hermes", false, `Hermes: ${name} is already in ${section}`);
484
+ } catch {}
485
+ const r = h.run("hermes", [
486
+ "config",
487
+ "set",
488
+ key,
489
+ JSON.stringify(value)
490
+ ]);
491
+ if (r.code !== 0) return failed("hermes", `Hermes: hermes config set ${key} failed: ${last(r.stderr || r.stdout)}`);
492
+ return done("hermes", true, `Hermes: set ${key}`);
493
+ }
494
+ /** The clients of `only` (all when omitted) whose program is on the PATH. */
495
+ function installedClients(h, only) {
496
+ return Object.values(CLIENTS).filter((c) => (!only || only.includes(c.id)) && h.run("sh", ["-c", `command -v ${c.bin}`]).code === 0);
497
+ }
498
+ /** Adds a remote MCP server to each client. A failure of one client does not stop the others. */
499
+ function addMcpServer(h, clients, server) {
500
+ const outcomes = [];
501
+ const logins = [];
502
+ for (const c of clients) {
503
+ const o = c.addMcp(h, server);
504
+ outcomes.push(o);
505
+ if (o.ok) logins.push(c.mcpLogin(h, server));
506
+ }
507
+ return {
508
+ outcomes,
509
+ logins
510
+ };
511
+ }
512
+ const upper = (name) => name.toUpperCase().replace(/-/g, "_");
513
+ /** The folder of the files of a product's CLI. */
514
+ function configDir(product, host) {
515
+ return join(host.home, ".config", product.name);
516
+ }
517
+ /** The environment variable that carries the session into a `token` run under sealkeep. */
518
+ function sessionEnv(product) {
519
+ return `${upper(product.name)}_SESSION`;
520
+ }
521
+ /** Keeps the session of a login, and the server it belongs to. A token of an earlier login is dropped. */
522
+ function keepSession(product, server, session, host) {
523
+ const dir = configDir(product, host);
524
+ const record = {
525
+ server,
526
+ session: saveSecret(session, {
527
+ name: `personal/${product.name}/prod/${upper(product.name)}_SESSION`,
528
+ file: join(dir, "session"),
529
+ description: `The ${product.name} login session; ${product.name} token trades it for a short-lived token`,
530
+ host
531
+ })
532
+ };
533
+ host.write(join(dir, "login.json"), `${JSON.stringify(record, null, 2)}\n`, {
534
+ mode: 384,
535
+ dirMode: 448
536
+ });
537
+ host.write(join(dir, "token.json"), "{}\n", {
538
+ mode: 384,
539
+ dirMode: 448
540
+ });
541
+ return record;
542
+ }
543
+ /** The `exp` of a JWT, in seconds, or 0 when the token has none. The signature is the server's to check. */
544
+ function expiryOf(token) {
545
+ const payload = token.split(".")[1];
546
+ if (!payload) return 0;
547
+ try {
548
+ const exp = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")).exp;
549
+ return typeof exp === "number" && Number.isFinite(exp) ? exp : 0;
550
+ } catch {
551
+ return 0;
552
+ }
553
+ }
554
+ function parse(text) {
555
+ try {
556
+ return text ? JSON.parse(text) : {};
557
+ } catch {
558
+ return {};
559
+ }
560
+ }
561
+ /** One line of output in `format`. */
562
+ function formatToken(token, format, now) {
563
+ if (format === "json") return `${JSON.stringify({
564
+ access_token: token,
565
+ expires_in: Math.max(0, expiryOf(token) - now)
566
+ })}\n`;
567
+ if (format === "header") return `${JSON.stringify({ Authorization: `Bearer ${token}` })}\n`;
568
+ return `${token}\n`;
569
+ }
570
+ /** Runs `<product> token`. Answers the exit code; the token alone goes to the output. */
571
+ async function runToken(product, deps = {}) {
572
+ const host = deps.host ?? systemHost();
573
+ const f = deps.fetch ?? fetch;
574
+ const now = deps.now ?? (() => Math.floor(Date.now() / 1e3));
575
+ const out = deps.out ?? ((t) => process.stdout.write(t));
576
+ const err = deps.err ?? ((l) => process.stderr.write(`${l}\n`));
577
+ const format = deps.format ?? "token";
578
+ const dir = configDir(product, host);
579
+ const login = parse(host.read(join(dir, "login.json")));
580
+ if (typeof login.server !== "string" || !login.session) {
581
+ err(`${product.name}: no login on this machine. Run ${product.name} login`);
582
+ return 1;
583
+ }
584
+ const cached = parse(host.read(join(dir, "token.json")));
585
+ if (cached.server === login.server && typeof cached.token === "string" && (cached.exp ?? 0) - now() > 120) {
586
+ out(formatToken(cached.token, format, now()));
587
+ return 0;
588
+ }
589
+ const session = readSession(product, login.session, host);
590
+ if (session.kind === "sealkeep") {
591
+ const self = deps.self ?? [
592
+ process.execPath,
593
+ process.argv[1],
594
+ "token"
595
+ ];
596
+ const r = host.run("sealkeep", [
597
+ "run",
598
+ "-e",
599
+ `${sessionEnv(product)}=${session.name}`,
600
+ "--",
601
+ ...self
602
+ ]);
603
+ if (r.code !== 0) {
604
+ err(r.stderr.trim() || `${product.name}: sealkeep did not give the session (exit ${r.code})`);
605
+ return r.code || 1;
606
+ }
607
+ const token = r.stdout.trim();
608
+ if (!token) {
609
+ err(`${product.name}: the run under sealkeep printed no token`);
610
+ return 1;
611
+ }
612
+ out(formatToken(token, format, now()));
613
+ return 0;
614
+ }
615
+ if (session.kind === "missing") {
616
+ err(`${product.name}: the login session is missing. Run ${product.name} login`);
617
+ return 1;
618
+ }
619
+ let r;
620
+ try {
621
+ r = await f(`${login.server}${product.tokenPath ?? "/api/auth/token"}`, { headers: {
622
+ authorization: `Bearer ${session.value}`,
623
+ "user-agent": `${product.name}-cli`
624
+ } });
625
+ } catch (e) {
626
+ err(`${product.name}: ${login.server} did not answer (${e.message})`);
627
+ return 1;
628
+ }
629
+ if (r.status === 401 || r.status === 403) {
630
+ err(`${product.name}: the login expired. Run ${product.name} login`);
631
+ return 1;
632
+ }
633
+ const token = r.ok ? (await r.json().catch(() => ({}))).token : void 0;
634
+ if (typeof token !== "string" || !token) {
635
+ err(`${product.name}: the server gave no token (${r.status})`);
636
+ return 1;
637
+ }
638
+ const record = {
639
+ server: login.server,
640
+ token,
641
+ exp: expiryOf(token)
642
+ };
643
+ host.write(join(dir, "token.json"), `${JSON.stringify(record)}\n`, {
644
+ mode: 384,
645
+ dirMode: 448
646
+ });
647
+ out(formatToken(token, format, now()));
648
+ return 0;
649
+ }
650
+ /** The session: its value, or the sealkeep name when this run is not the one inside sealkeep. */
651
+ function readSession(product, where, host) {
652
+ const value = where.kind === "file" ? host.read(where.path)?.trim() : host.env[sessionEnv(product)]?.trim();
653
+ if (value) return {
654
+ kind: "value",
655
+ value
656
+ };
657
+ return where.kind === "sealkeep" ? {
658
+ kind: "sealkeep",
659
+ name: where.name
660
+ } : { kind: "missing" };
661
+ }
662
+ //#endregion
663
+ //#region ../node_modules/@munhq/mcp-kit/dist/cli/login.js
664
+ const CLIENT_IDS = [
665
+ "claude",
666
+ "codex",
667
+ "opencode",
668
+ "pi",
669
+ "hermes",
670
+ "gemini",
671
+ "cursor"
672
+ ];
673
+ const HELP$1 = (p) => `${p.name} login [--server URL] [--only claude,pi,…] [--no-browser] [--no-signin]${Object.keys(p.options ?? {}).map((k) => ` [--${k}${p.options?.[k]?.type === "string" ? " VALUE" : ""}]`).join("")}
674
+
675
+ Signs you in, adds the ${p.name} MCP server to each AI client on this machine, and signs each one in.
676
+ --server URL the server (default ${p.server}, or the variable ${p.name.toUpperCase().replace(/-/g, "_")}_URL)
677
+ --only LIST only these clients: ${CLIENT_IDS.join(", ")}
678
+ --no-browser print the code and the page, and open no browser
679
+ --no-signin add the server to the clients, and do not start their sign-in
680
+ ${Object.entries(p.options ?? {}).map(([k, o]) => ` --${(k + (o.type === "string" ? " VALUE" : "")).padEnd(14)} ${o.help}`).join("\n")}
681
+ `;
682
+ /** The MCP URL: the discovery document of the server when it has one, else the origin and the path. */
683
+ async function discoverMcp(server, path, f) {
684
+ const fallback = `${server}${path}`;
685
+ try {
686
+ const r = await f(`${server}/.well-known/oauth-protected-resource${path}`);
687
+ if (!r.ok) return fallback;
688
+ const j = await r.json();
689
+ return typeof j.resource === "string" && /^https?:\/\//.test(j.resource) ? j.resource : fallback;
690
+ } catch {
691
+ return fallback;
692
+ }
693
+ }
694
+ function terminalSignIn(step) {
695
+ const [bin, ...args] = step.command;
696
+ return spawnSync(bin, args, {
697
+ stdio: "inherit",
698
+ cwd: step.cwd
699
+ }).status ?? 1;
700
+ }
701
+ /** Runs `<product> login`. Answers the exit code. */
702
+ async function runLogin(product, argv, deps = {}) {
703
+ const say = deps.say ?? ((l) => process.stdout.write(`${l}\n`));
704
+ const host = deps.host ?? systemHost();
705
+ const f = deps.fetch ?? fetch;
706
+ let values;
707
+ try {
708
+ ({values} = parseArgs({
709
+ args: argv,
710
+ options: {
711
+ ...Object.fromEntries(Object.entries(product.options ?? {}).map(([k, o]) => [k, { type: o.type }])),
712
+ server: { type: "string" },
713
+ only: { type: "string" },
714
+ "no-browser": { type: "boolean" },
715
+ "no-signin": { type: "boolean" },
716
+ help: { type: "boolean" }
717
+ }
718
+ }));
719
+ } catch (e) {
720
+ say(`${e.message}\n\n${HELP$1(product)}`);
721
+ return 2;
722
+ }
723
+ if (values.help) {
724
+ say(HELP$1(product));
725
+ return 0;
726
+ }
727
+ const only = values.only?.split(",").map((s) => s.trim()).filter(Boolean);
728
+ const unknown = (only ?? []).filter((c) => !CLIENT_IDS.includes(c));
729
+ if (unknown.length) {
730
+ say(`unknown client: ${unknown.join(", ")}. The clients are ${CLIENT_IDS.join(", ")}.`);
731
+ return 2;
732
+ }
733
+ const envName = `${product.name.toUpperCase().replace(/-/g, "_")}_URL`;
734
+ const server = (values.server ?? host.env[envName] ?? product.server).replace(/\/+$/, "");
735
+ let session;
736
+ try {
737
+ session = await (deps.device ?? deviceLogin)({
738
+ server,
739
+ clientId: product.clientId,
740
+ noBrowser: values["no-browser"],
741
+ fetch: f,
742
+ write: say
743
+ });
744
+ } catch (e) {
745
+ if (e instanceof LoginError) {
746
+ say(`Login failed: ${e.message}`);
747
+ return 1;
748
+ }
749
+ throw e;
750
+ }
751
+ say("Signed in.");
752
+ const kept = keepSession(product, server, session, host);
753
+ say(`The login is kept in ${kept.session.kind === "sealkeep" ? `sealkeep (${kept.session.name})` : `${kept.session.path} (only you can read it)`}. ${product.name} token prints a token from it.`);
754
+ const clients = installedClients(host, only);
755
+ const ctx = {
756
+ server,
757
+ flags: Object.fromEntries(Object.keys(product.options ?? {}).map((k) => [k, values[k]])),
758
+ session,
759
+ clients,
760
+ host,
761
+ fetch: f,
762
+ say
763
+ };
764
+ let failed = false;
765
+ const summary = [];
766
+ try {
767
+ summary.push(...await product.afterApproval?.(ctx) ?? []);
768
+ } catch (e) {
769
+ failed = true;
770
+ summary.push(`${product.name}: ${e.message}`);
771
+ }
772
+ if (clients.length === 0) summary.push("No AI client was found on this machine, so no MCP server was added.");
773
+ else {
774
+ const url = await discoverMcp(server, product.mcpPath ?? "/mcp", f);
775
+ const { outcomes, logins } = addMcpServer(host, clients, {
776
+ name: product.name,
777
+ url,
778
+ registerClient: product.registerClient
779
+ });
780
+ for (const o of outcomes) {
781
+ summary.push(o.message);
782
+ if (!o.ok) failed = true;
783
+ }
784
+ for (const step of logins) if (step.command && !values["no-signin"]) {
785
+ say(`Signing in ${step.client}…`);
786
+ const code = (deps.signIn ?? terminalSignIn)({
787
+ ...step,
788
+ command: step.command
789
+ });
790
+ summary.push(code === 0 ? `${step.client}: signed in` : `${step.client}: the sign-in did not finish (exit ${code}). ${step.instruction}`);
791
+ if (code !== 0) failed = true;
792
+ } else summary.push(`${step.client}: ${step.instruction}`);
793
+ }
794
+ for (const l of summary) say(l);
795
+ return failed ? 1 : 0;
796
+ }
797
+ //#endregion
798
+ //#region src/cli/launch.ts
799
+ const PRODUCT = { name: "proxium" };
800
+ function settingsPath(host) {
801
+ return join(configDir(PRODUCT, host), "settings.json");
802
+ }
803
+ function readSettings(host) {
804
+ try {
805
+ return JSON.parse(host.read(settingsPath(host)) ?? "{}");
806
+ } catch {
807
+ return {};
808
+ }
809
+ }
810
+ function writeSettings(host, s) {
811
+ host.write(settingsPath(host), `${JSON.stringify(s, null, 2)}\n`, {
812
+ mode: 384,
813
+ dirMode: 448
814
+ });
815
+ }
816
+ const quote = (w) => `'${w.replace(/'/g, `'\\''`)}'`;
817
+ /**
818
+ * The command that runs this program, for the tools that run `proxium token` later. `npx proxium` runs the file
819
+ * from the cache of npm (`…/_npx/<hash>/…`), which npm may delete, so that file is copied to `~/.local/bin/proxium`
820
+ * first, as `install.sh` does, and the tools name the copy.
821
+ */
822
+ function stableSelf(host, execPath, script) {
823
+ if (!/[\\/]_npx[\\/]/.test(script)) return [execPath, script];
824
+ const text = host.read(script);
825
+ if (text === void 0) return [execPath, script];
826
+ const dest = join(host.home, ".local", "bin", "proxium");
827
+ if (host.read(dest) !== text) host.write(dest, text, { mode: 493 });
828
+ return [execPath, dest];
829
+ }
830
+ /** The command line of `proxium token`, for a tool that runs it through a shell. */
831
+ function tokenCommand(self, ...flags) {
832
+ return [
833
+ ...self,
834
+ "token",
835
+ ...flags
836
+ ].map(quote).join(" ");
837
+ }
838
+ /** The Claude Code config folder of Proxium. A folder of its own keeps the subscription login of `~/.claude` out of it. */
839
+ const CLAUDE_DIR = ".claude-proxium";
840
+ /**
841
+ * Claude Code as an API client of Proxium. Its key comes from `apiKeyHelper`, so the two key variables are removed:
842
+ * Claude Code prefers them to the helper. `auto` is the model unless the person set one. The Proxium MCP comes with
843
+ * the same login: `headersHelper` runs `proxium token --header` (the MCP of `id/` takes the login token).
844
+ */
845
+ function claudeLaunch(o) {
846
+ const env = { ...o.env };
847
+ delete env.ANTHROPIC_API_KEY;
848
+ delete env.ANTHROPIC_AUTH_TOKEN;
849
+ env.ANTHROPIC_BASE_URL = o.server;
850
+ env.ANTHROPIC_CUSTOM_HEADERS = ["x-proxium-source: claude-code", ...o.project ? [`x-proxium-project: ${o.project}`] : []].join("\n");
851
+ env.ANTHROPIC_MODEL ??= "auto";
852
+ env.ANTHROPIC_SMALL_FAST_MODEL ??= "auto";
853
+ env.ANTHROPIC_DEFAULT_HAIKU_MODEL ??= "auto";
854
+ env.CLAUDE_CONFIG_DIR = join(o.home, CLAUDE_DIR);
855
+ env.CLAUDE_CODE_API_KEY_HELPER_TTL_MS ??= "600000";
856
+ return {
857
+ command: [
858
+ "claude",
859
+ "--settings",
860
+ JSON.stringify({ apiKeyHelper: tokenCommand(o.self) }),
861
+ "--mcp-config",
862
+ JSON.stringify({ mcpServers: { proxium: {
863
+ type: "http",
864
+ url: `${o.server}/mcp`,
865
+ headersHelper: tokenCommand(o.self, "--header")
866
+ } } }),
867
+ ...o.args
868
+ ],
869
+ env
870
+ };
871
+ }
872
+ const TOOLS = ["claude"];
873
+ const HELP = `proxium launch <tool> [--project SLUG] [-- tool arguments]
874
+
875
+ Starts the tool with Proxium as its model provider and your login as its key. The tools: ${TOOLS.join(", ")}.
876
+ --project SLUG the project of the calls (default: PROXIUM_PROJECT, else the project of proxium login)
877
+ Example: proxium launch claude -- -p "Reply with one word: ok"
878
+ `;
879
+ function runInherit(l) {
880
+ const [bin, ...args] = l.command;
881
+ const r = spawnSync(bin, args, {
882
+ stdio: "inherit",
883
+ env: l.env
884
+ });
885
+ if (r.error) throw r.error;
886
+ return r.status ?? 1;
887
+ }
888
+ /** Runs `proxium launch`. Answers the exit code: the tool's own, or 1 or 2 for a fault before it starts. */
889
+ function runLaunch(argv, deps = {}) {
890
+ const host = deps.host ?? systemHost();
891
+ const err = deps.err ?? ((l) => process.stderr.write(`${l}\n`));
892
+ let parsed;
893
+ try {
894
+ parsed = parseArgs({
895
+ args: argv,
896
+ allowPositionals: true,
897
+ options: {
898
+ project: { type: "string" },
899
+ help: { type: "boolean" }
900
+ }
901
+ });
902
+ } catch (e) {
903
+ err(`${e.message}\n\n${HELP}`);
904
+ return 2;
905
+ }
906
+ const [tool, ...rest] = parsed.positionals;
907
+ if (parsed.values.help || !tool) {
908
+ err(HELP);
909
+ return parsed.values.help ? 0 : 2;
910
+ }
911
+ if (!TOOLS.includes(tool)) {
912
+ err(`proxium launch: ${tool} is not a tool it can start. The tools: ${TOOLS.join(", ")}.`);
913
+ return 2;
914
+ }
915
+ let login = {};
916
+ try {
917
+ login = JSON.parse(host.read(join(configDir(PRODUCT, host), "login.json")) ?? "{}");
918
+ } catch {
919
+ login = {};
920
+ }
921
+ if (typeof login.server !== "string") {
922
+ err("proxium: no login on this machine. Run proxium login");
923
+ return 1;
924
+ }
925
+ const project = parsed.values.project ?? host.env.PROXIUM_PROJECT ?? readSettings(host).project;
926
+ const self = deps.self ?? stableSelf(host, process.execPath, process.argv[1]);
927
+ const l = claudeLaunch({
928
+ server: login.server,
929
+ project,
930
+ self,
931
+ home: host.home,
932
+ env: host.env,
933
+ args: rest
934
+ });
935
+ if (!deps.run) prepareClaudeDir(host.home);
936
+ return (deps.run ?? runInherit)(l);
937
+ }
938
+ /** The config folder of Proxium's Claude Code, with the person's `CLAUDE.md` linked into it. */
939
+ function prepareClaudeDir(home) {
940
+ const dir = join(home, CLAUDE_DIR);
941
+ mkdirSync(dir, {
942
+ recursive: true,
943
+ mode: 448
944
+ });
945
+ const own = join(home, ".claude", "CLAUDE.md");
946
+ const linked = join(dir, "CLAUDE.md");
947
+ if (existsSync(own) && !present(linked)) symlinkSync(own, linked);
948
+ }
949
+ /** Whether a path exists, a link that points nowhere included. */
950
+ function present(path) {
951
+ try {
952
+ lstatSync(path);
953
+ return true;
954
+ } catch {
955
+ return false;
956
+ }
957
+ }
958
+ //#endregion
959
+ //#region src/cli/launchers.ts
960
+ /** `opencode run` talks to a shared background service that has no key in its environment, so it starts a server of its own. */
961
+ const OPENCODE = `if [ "$1" = run ]; then shift; set -- run --standalone "$@"; elif [ $# -eq 0 ] || [ "\${1#-}" != "$1" ]; then set -- --standalone "$@"; fi; exec opencode "$@"`;
962
+ /** The launcher of a tool, or undefined for a tool that has none (it still gets the MCP server). */
963
+ function launcherFor(id, server, project, app) {
964
+ if (!/^https?:\/\/[\w.:-]+$/.test(server)) throw new Error(`${server} is not a server address`);
965
+ const comment = `${app} through Proxium, project ${project}. Written by proxium login.`;
966
+ switch (id) {
967
+ case "opencode": return {
968
+ file: "opencode-proxium",
969
+ envVar: "PROXIUM_API_KEY",
970
+ command: [
971
+ "sh",
972
+ "-c",
973
+ OPENCODE,
974
+ "opencode-proxium"
975
+ ],
976
+ comment: `${comment}\nUse the model proxium/auto, or proxium/code for the code tier.`
977
+ };
978
+ case "hermes": return {
979
+ file: "hermes-proxium",
980
+ envVar: "PROXIUM_API_KEY",
981
+ command: [
982
+ "hermes",
983
+ "--provider",
984
+ "proxium",
985
+ "-m",
986
+ "auto"
987
+ ],
988
+ comment
989
+ };
990
+ case "pi": return {
991
+ file: "pi-proxium",
992
+ envVar: "PROXIUM_API_KEY",
993
+ command: [
994
+ "pi",
995
+ "--provider",
996
+ "proxium",
997
+ "--model",
998
+ "auto"
999
+ ],
1000
+ comment
1001
+ };
1002
+ default: return;
1003
+ }
1004
+ }
1005
+ /** The app name in the log of Proxium for each tool (the header x-proxium-source). */
1006
+ const APP_OF = {
1007
+ claude: "claude-code",
1008
+ opencode: "opencode",
1009
+ pi: "pi",
1010
+ hermes: "hermes"
1011
+ };
1012
+ const shq = (w) => `'${w.replace(/'/g, `'\\''`)}'`;
1013
+ /** `claude-proxium`: Claude Code through Proxium with the login of `proxium login` (`proxium launch claude`). */
1014
+ function claudeLauncher(self, project) {
1015
+ return [
1016
+ "#!/bin/sh",
1017
+ `# claude-code through Proxium, project ${project}. Written by proxium login.`,
1018
+ "# Claude Code runs proxium token for its key, so this file holds no key.",
1019
+ "# Any Proxium model works: ANTHROPIC_MODEL=openrouter/anthropic/claude-sonnet-5.5 claude-proxium",
1020
+ `exec ${self.map(shq).join(" ")} launch claude -- "$@"`,
1021
+ ""
1022
+ ].join("\n");
1023
+ }
1024
+ //#endregion
1025
+ //#region src/cli/provision.ts
1026
+ var Api = class Api {
1027
+ server;
1028
+ jwt;
1029
+ f;
1030
+ constructor(server, jwt, f) {
1031
+ this.server = server;
1032
+ this.jwt = jwt;
1033
+ this.f = f;
1034
+ }
1035
+ /** Trades the session token of the device login for the token that the gateway checks. */
1036
+ static async open(server, session, f) {
1037
+ const r = await f(`${server}/api/auth/token`, { headers: {
1038
+ authorization: `Bearer ${session}`,
1039
+ "user-agent": "proxium-cli"
1040
+ } });
1041
+ const j = await r.json().catch(() => ({}));
1042
+ if (!r.ok || !j.token) throw new Error(`the server gave no token for this login (${r.status})`);
1043
+ return new Api(server, j.token, f);
1044
+ }
1045
+ async call(method, path, body) {
1046
+ const r = await this.f(`${this.server}${path}`, {
1047
+ method,
1048
+ headers: {
1049
+ authorization: `Bearer ${this.jwt}`,
1050
+ "user-agent": "proxium-cli",
1051
+ ...body === void 0 ? {} : { "content-type": "application/json" }
1052
+ },
1053
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
1054
+ });
1055
+ return {
1056
+ status: r.status,
1057
+ json: await r.json().catch(() => ({}))
1058
+ };
1059
+ }
1060
+ };
1061
+ const slugOf = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "host";
1062
+ /** The project to use: the one named, a new one, or the only one the person has. */
1063
+ async function chooseProject(api, me, named) {
1064
+ const projects = me.projects ?? [];
1065
+ const want = named?.trim();
1066
+ if (want) {
1067
+ const found = projects.find((p) => p.tenant_slug === want || p.display_name.toLowerCase() === want.toLowerCase());
1068
+ if (found) return {
1069
+ project: found,
1070
+ created: false
1071
+ };
1072
+ return {
1073
+ project: await createProject(api, want),
1074
+ created: true
1075
+ };
1076
+ }
1077
+ if (projects.length === 1 && projects[0]) return {
1078
+ project: projects[0],
1079
+ created: false
1080
+ };
1081
+ if (projects.length === 0) return {
1082
+ project: await createProject(api, "development"),
1083
+ created: true
1084
+ };
1085
+ throw new Error(`you have ${projects.length} projects (${projects.map((p) => p.tenant_slug).join(", ")}). Run proxium login --project NAME`);
1086
+ }
1087
+ async function createProject(api, name) {
1088
+ const r = await api.call("POST", "/api/teams", { name });
1089
+ if (r.status >= 300 || !r.json.tenant_slug) throw new Error(`the project ${name} was not created (${r.status}): ${r.json.error?.message ?? "no reason given"}`);
1090
+ return {
1091
+ tenant_slug: r.json.tenant_slug,
1092
+ display_name: r.json.display_name ?? name
1093
+ };
1094
+ }
1095
+ function secretIn(value) {
1096
+ if (typeof value === "string") return void 0;
1097
+ if (Array.isArray(value)) return value.map(secretIn).find(Boolean);
1098
+ if (value && typeof value === "object") for (const [k, v] of Object.entries(value)) {
1099
+ if (typeof v === "string" && [
1100
+ "key",
1101
+ "secret",
1102
+ "api_key",
1103
+ "token"
1104
+ ].includes(k) && v.length > 20) return v;
1105
+ const inner = secretIn(v);
1106
+ if (inner) return inner;
1107
+ }
1108
+ }
1109
+ async function provision(ctx, deps = {}) {
1110
+ const out = [];
1111
+ const { host, say } = ctx;
1112
+ const api = await Api.open(ctx.server, ctx.session, ctx.fetch);
1113
+ const me = (await api.call("GET", "/api/me")).json;
1114
+ const { project, created } = await chooseProject(api, me, ctx.flags.project);
1115
+ const slug = project.tenant_slug;
1116
+ out.push(`proxium: ${created ? "created" : "using"} the project ${project.display_name} (${slug})`);
1117
+ writeSettings(host, { project: slug });
1118
+ const readiness = async () => (await api.call("GET", `/api/teams/${slug}/readiness`)).json.readiness?.can_route === true;
1119
+ if (!await readiness()) {
1120
+ if (me.is_admin && !ctx.flags["no-grant"]) {
1121
+ for (const what of ["platform-pool", "platform-classifier"]) await api.call("PUT", `/provision/teams/${slug}/${what}`, { granted: true });
1122
+ out.push("proxium: you are the operator, so the project may use the platform keys and the platform classifier");
1123
+ }
1124
+ if (!await readiness()) out.push(`proxium: ${slug} cannot route yet. Add a vendor and a key of your own, or ask the operator to allow the platform pool.`);
1125
+ }
1126
+ const existing = new Set((await api.call("GET", `/api/teams/${slug}/api-endpoints`)).json.endpoints?.map((e) => e.name) ?? []);
1127
+ const tag = slugOf((deps.hostname ?? hostname)());
1128
+ const binDir = deps.binDir ?? join(host.home, ".local", "bin");
1129
+ const made = [];
1130
+ const provided = [];
1131
+ const self = deps.self ?? stableSelf(host, process.execPath, process.argv[1]);
1132
+ const providerOf = (app, key) => ({
1133
+ name: "proxium",
1134
+ label: "Proxium",
1135
+ baseUrl: `${ctx.server}/v1`,
1136
+ keyEnv: "PROXIUM_API_KEY",
1137
+ ...key ? {
1138
+ keyCommand: key.command,
1139
+ keyCommandJson: key.json
1140
+ } : {},
1141
+ headers: {
1142
+ "x-proxium-source": app,
1143
+ "x-proxium-project": slug
1144
+ },
1145
+ models: [{
1146
+ id: "auto",
1147
+ label: "Proxium auto (the classifier decides)"
1148
+ }, {
1149
+ id: "code",
1150
+ label: "Proxium code tier"
1151
+ }],
1152
+ defaultModel: "auto"
1153
+ });
1154
+ for (const client of ctx.clients) {
1155
+ const app = APP_OF[client.id];
1156
+ if (!app) continue;
1157
+ if (client.id === "claude") {
1158
+ host.write(join(binDir, "claude-proxium"), claudeLauncher(self, slug), { mode: 493 });
1159
+ out.push("claude-code: start it with claude-proxium. It uses your login, and keeps no key");
1160
+ made.push(app);
1161
+ continue;
1162
+ }
1163
+ const launcher = launcherFor(client.id, ctx.server, slug, app);
1164
+ if (!launcher) continue;
1165
+ if (client.takesKeyCommand?.(host)) {
1166
+ if (client.addProvider) {
1167
+ const o = client.addProvider(host, providerOf(app, {
1168
+ command: tokenCommand(self),
1169
+ json: tokenCommand(self, "--json")
1170
+ }));
1171
+ out.push(o.message);
1172
+ if (o.ok) provided.push(client);
1173
+ }
1174
+ host.write(join(binDir, launcher.file), loginLauncher(launcher), { mode: 493 });
1175
+ out.push(`${app}: start it with ${launcher.file}. It uses your login, and keeps no key`);
1176
+ made.push(app);
1177
+ continue;
1178
+ }
1179
+ const secretName = `personal/${slug}/prod/PROXIUM_API_KEY_${app.toUpperCase().replace(/-/g, "_")}`;
1180
+ const file = join(host.home, ".config", "proxium", slug, `${app}.key`);
1181
+ let where = keptBefore(host, secretName, file);
1182
+ let fresh;
1183
+ if (!where) {
1184
+ let name = `${app}-${tag}`;
1185
+ for (let n = 2; existing.has(name); n++) name = `${app}-${tag}-${n}`;
1186
+ const r = await api.call("POST", `/api/teams/${slug}/api-endpoints`, { name });
1187
+ fresh = secretIn(r.json);
1188
+ if (r.status >= 300 || !fresh) {
1189
+ out.push(`${app}: no key was made (${r.status})`);
1190
+ continue;
1191
+ }
1192
+ existing.add(name);
1193
+ where = saveSecret(fresh, {
1194
+ name: secretName,
1195
+ file,
1196
+ description: `Proxium virtual key of the app ${app} in the project ${slug}`,
1197
+ host
1198
+ });
1199
+ out.push(`${app}: made the key ${name}, kept in ${where.kind === "sealkeep" ? `sealkeep (${where.name})` : `${where.path} (only you can read it)`}`);
1200
+ } else out.push(`${app}: the key is kept already (${where.kind === "sealkeep" ? where.name : where.path})`);
1201
+ if (client.addProvider) {
1202
+ const o = client.addProvider(host, providerOf(app));
1203
+ out.push(o.message);
1204
+ if (o.ok) provided.push(client);
1205
+ }
1206
+ const path = join(binDir, launcher.file);
1207
+ host.write(path, launchScript(where, launcher.envVar, launcher.command, launcher.comment), { mode: 493 });
1208
+ out.push(`${app}: start it with ${launcher.file}`);
1209
+ if (fresh) {
1210
+ const r = await ctx.fetch(`${ctx.server}/v1/chat/completions`, {
1211
+ method: "POST",
1212
+ headers: {
1213
+ authorization: `Bearer ${fresh}`,
1214
+ "content-type": "application/json",
1215
+ "x-proxium-source": app,
1216
+ "user-agent": "proxium-cli"
1217
+ },
1218
+ body: JSON.stringify({
1219
+ model: "auto",
1220
+ messages: [{
1221
+ role: "user",
1222
+ content: "Reply with one word: ok"
1223
+ }],
1224
+ max_tokens: 8
1225
+ })
1226
+ }).catch(() => void 0);
1227
+ out.push(r?.ok ? `${app}: a call through Proxium answered` : `${app}: a test call did not answer (${r?.status ?? "no answer"})`);
1228
+ }
1229
+ made.push(app);
1230
+ }
1231
+ if (made.length) say("");
1232
+ return out;
1233
+ }
1234
+ /** The launcher of a tool that runs `proxium token` itself: its command, with no key in its environment. */
1235
+ function loginLauncher(l) {
1236
+ const q = (w) => /^[A-Za-z0-9_./:=@%+-]+$/.test(w) ? w : `'${w.replace(/'/g, `'\\''`)}'`;
1237
+ return [
1238
+ "#!/bin/sh",
1239
+ ...l.comment.split("\n").map((c) => `# ${c}`),
1240
+ "# It runs proxium token for its key, so this file holds no key.",
1241
+ `exec ${l.command.map(q).join(" ")} "$@"`,
1242
+ ""
1243
+ ].join("\n");
1244
+ }
1245
+ /** The place where an earlier login kept the key of an app, or undefined. */
1246
+ function keptBefore(host, name, file) {
1247
+ if (host.read(file)?.trim()) return {
1248
+ kind: "file",
1249
+ path: file
1250
+ };
1251
+ const r = host.run("sealkeep", ["list", name]);
1252
+ return r.code === 0 && r.stdout.includes(name) ? {
1253
+ kind: "sealkeep",
1254
+ name
1255
+ } : void 0;
1256
+ }
1257
+ //#endregion
1258
+ //#region src/cli/proxium.ts
1259
+ /** What `proxium login` is: the shared login of `@munhq/mcp-kit/cli`, with Proxium's own step. */
1260
+ const PROXIUM = {
1261
+ name: "proxium",
1262
+ server: "https://proxium.tech",
1263
+ clientId: "proxium-cli",
1264
+ registerClient: true,
1265
+ options: {
1266
+ project: {
1267
+ type: "string",
1268
+ help: "the project to set up: its name or slug, or a new name to create it"
1269
+ },
1270
+ "no-grant": {
1271
+ type: "boolean",
1272
+ help: "do not allow the platform keys and classifier, even for the operator"
1273
+ }
1274
+ },
1275
+ afterApproval: (ctx) => provision(ctx)
1276
+ };
1277
+ //#endregion
1278
+ //#region src/cli/main.ts
1279
+ const USAGE = `The commands:
1280
+ proxium login sign in, and set up the AI tools on this machine
1281
+ proxium token print a fresh token of your login, for a tool's key
1282
+ (--json: for Hermes; --header: as {"Authorization": "Bearer ..."})
1283
+ proxium launch <tool> start a tool through Proxium with your login (proxium launch --help)
1284
+ `;
1285
+ /** `proxium token`: the bare token, or with `--json` or `--header` the form that a tool reads. */
1286
+ function token(args) {
1287
+ const format = args[0] === "--json" ? "json" : args[0] === "--header" ? "header" : "token";
1288
+ return runToken(PROXIUM, { format });
1289
+ }
1290
+ const [command, ...rest] = process.argv.slice(2);
1291
+ if (command === "login" || command === void 0) process.exit(await runLogin(PROXIUM, command === "login" ? rest : []));
1292
+ if (command === "token") process.exit(await token(rest));
1293
+ if (command === "launch") process.exit(runLaunch(rest));
1294
+ if (command === "--help" || command === "help") {
1295
+ process.stdout.write(USAGE);
1296
+ process.exit(0);
1297
+ }
1298
+ process.stderr.write(`proxium: unknown command ${command}.\n${USAGE}`);
1299
+ process.exit(2);
1300
+ //#endregion
1301
+ export {};