@xopcai/xopc 0.0.102 → 0.0.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/extensions/feishu/src/transport/webhook/monitor.js +2 -2
- package/dist/extensions/feishu/src/transport/webhook/monitor.js.map +1 -1
- package/dist/extensions/telegram/src/send-options.js +8 -5
- package/dist/extensions/telegram/src/send-options.js.map +1 -1
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/{agents-DQ0mQxIA.js → agents-BwXkia_W.js} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-Uv4kOmDE.js → apps-page-C5RmwmTT.js} +1 -1
- package/dist/gateway/static/root/assets/{channels-settings-BEtP53V6.js → channels-settings-tlOTdaXw.js} +1 -1
- package/dist/gateway/static/root/assets/{channels-status-swr-CxTtS1hI.js → channels-status-swr-D4XjqwqC.js} +1 -1
- package/dist/gateway/static/root/assets/{cron-api-CznlPIZT.js → cron-api-DcmfGuj0.js} +1 -1
- package/dist/gateway/static/root/assets/{cron-page-Dx6dE3eD.js → cron-page-CijJ4G6F.js} +1 -1
- package/dist/gateway/static/root/assets/{dist-DH4IuRV0.js → dist-CWm8hTXZ.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-BNkhvRQW.js → extension-debug-page-BzYgTkS9.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-CcIGQah2.js → extension-page-BmTZH6gL.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-DuDCSb3J.js → extension-settings-page-CyJk_G64.js} +1 -1
- package/dist/gateway/static/root/assets/{fetch-drJUxxKP.js → fetch-CMoWY2Si.js} +1 -1
- package/dist/gateway/static/root/assets/{field-primitives-BUwUiDf5.js → field-primitives-C4GpxsDo.js} +1 -1
- package/dist/gateway/static/root/assets/{heartbeat-config-api-BjFW1cl-.js → heartbeat-config-api-CLD0Gpi3.js} +1 -1
- package/dist/gateway/static/root/assets/index-0HmunJW9.js +4845 -0
- package/dist/gateway/static/root/assets/index-Bk75Phrd.css +1 -0
- package/dist/gateway/static/root/assets/{logs-page-CiG0m81R.js → logs-page-BjdQjHwk.js} +1 -1
- package/dist/gateway/static/root/assets/{note-detail-page-DG3DVFTf.js → note-detail-page-8Er4197b.js} +1 -1
- package/dist/gateway/static/root/assets/{note-time-DjlllCF3.js → note-time-CF3vf1jm.js} +1 -1
- package/dist/gateway/static/root/assets/{notes-page-D40ZpS0Y.js → notes-page-BBnTh0uI.js} +1 -1
- package/dist/gateway/static/root/assets/{sessions-page-BvtdbmW8.js → sessions-page-B0riYK7j.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-advanced-gate-BQamRjkV.js → settings-advanced-gate-DiP_WHu_.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-form-section-C3_sEFfD.js → settings-form-section-C7qA7y0J.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-page-D972_jaS.js → settings-page-1ZPhcCZ9.js} +2 -2
- package/dist/gateway/static/root/assets/share-preview-page-Tvr5Mtq5.js +2 -0
- package/dist/gateway/static/root/assets/{skills-page-QwFCB4yV.js → skills-page-BQ1hllKD.js} +1 -1
- package/dist/gateway/static/root/assets/{theme-store-B6KXPHgK.js → theme-store-HTvJlFLL.js} +1 -1
- package/dist/gateway/static/root/assets/url-F3_D_6zZ.js +3 -0
- package/dist/gateway/static/root/assets/{utils-DCm11_KW.js → utils-CPr3FBh2.js} +1 -1
- package/dist/gateway/static/root/assets/{voice-api-key-field-CywiB7Dw.js → voice-api-key-field-BkbDl6dH.js} +1 -1
- package/dist/gateway/static/root/assets/{workflow-page.utils-g57BRfEd.js → workflow-page.utils-CeBkAjP7.js} +1 -1
- package/dist/gateway/static/root/assets/{workflows-page-Be7l_SWw.js → workflows-page-D059ysoF.js} +1 -1
- package/dist/gateway/static/root/index.html +5 -5
- package/dist/package.js +1 -1
- package/dist/src/agent/inbound/attachment-pipeline.js +2 -2
- package/dist/src/agent/inbound/attachment-pipeline.js.map +1 -1
- package/dist/src/agent/tools/send-media.js +1 -2
- package/dist/src/agent/tools/send-media.js.map +1 -1
- package/dist/src/auth/credentials.d.ts +7 -0
- package/dist/src/auth/credentials.js +35 -2
- package/dist/src/auth/credentials.js.map +1 -1
- package/dist/src/auth/index.d.ts +1 -1
- package/dist/src/auth/index.js.map +1 -1
- package/dist/src/auth/oauth/github-copilot.d.ts +1 -1
- package/dist/src/auth/oauth/google-antigravity.d.ts +1 -3
- package/dist/src/auth/oauth/google-antigravity.js +1 -1
- package/dist/src/auth/oauth/google-gemini-cli.d.ts +1 -3
- package/dist/src/auth/oauth/google-gemini-cli.js +1 -1
- package/dist/src/auth/oauth/{vendor-pi-google → google-oauth}/google-antigravity.js +3 -5
- package/dist/src/auth/oauth/google-oauth/google-antigravity.js.map +1 -0
- package/dist/src/auth/oauth/{vendor-pi-google → google-oauth}/google-gemini-cli.js +4 -5
- package/dist/src/auth/oauth/google-oauth/google-gemini-cli.js.map +1 -0
- package/dist/src/auth/oauth/{vendor-pi-google → google-oauth}/oauth-page.js +12 -4
- package/dist/src/auth/oauth/google-oauth/oauth-page.js.map +1 -0
- package/dist/src/auth/oauth/{vendor-pi-google → google-oauth}/pkce.d.ts +1 -1
- package/dist/src/auth/oauth/{vendor-pi-google → google-oauth}/pkce.js +2 -2
- package/dist/src/auth/oauth/google-oauth/pkce.js.map +1 -0
- package/dist/src/auth/oauth/index.js +2 -2
- package/dist/src/auth/oauth/kimi-coding.d.ts +1 -1
- package/dist/src/auth/oauth/kimi-coding.js +1 -1
- package/dist/src/auth/oauth/kimi-coding.js.map +1 -1
- package/dist/src/auth/oauth/minimax-cn.js +1 -1
- package/dist/src/auth/oauth/minimax-cn.js.map +1 -1
- package/dist/src/auth/oauth/minimax.js +1 -1
- package/dist/src/auth/oauth/minimax.js.map +1 -1
- package/dist/src/auth/oauth/openai-codex.d.ts +7 -2
- package/dist/src/auth/oauth/openai-codex.js +318 -1
- package/dist/src/auth/oauth/openai-codex.js.map +1 -0
- package/dist/src/auth/profiles/store.js +3 -2
- package/dist/src/auth/profiles/store.js.map +1 -1
- package/dist/src/channels/attachments/attachment-classifiers.d.ts +1 -0
- package/dist/src/channels/attachments/attachment-classifiers.js +13 -2
- package/dist/src/channels/attachments/attachment-classifiers.js.map +1 -1
- package/dist/src/chat-commands/builtins/model.js +2 -2
- package/dist/src/chat-commands/builtins/model.js.map +1 -1
- package/dist/src/cli/command-manifest.js +1 -1
- package/dist/src/cli/command-manifest.js.map +1 -1
- package/dist/src/cli/commands/auth.js +39 -51
- package/dist/src/cli/commands/auth.js.map +1 -1
- package/dist/src/cli/commands/models.js +189 -54
- package/dist/src/cli/commands/models.js.map +1 -1
- package/dist/src/cli/commands/onboard/model.d.ts +1 -4
- package/dist/src/cli/commands/onboard/model.js +253 -183
- package/dist/src/cli/commands/onboard/model.js.map +1 -1
- package/dist/src/cli/utils/oauth-login.d.ts +17 -0
- package/dist/src/cli/utils/oauth-login.js +93 -0
- package/dist/src/cli/utils/oauth-login.js.map +1 -0
- package/dist/src/cli/utils/oauth-providers.js +2 -2
- package/dist/src/gateway/heartbeat/service.js +1 -1
- package/dist/src/gateway/hono/oauth-async.js +2 -2
- package/dist/src/gateway/hono/oauth.js +2 -2
- package/dist/src/gateway/hono/routes/models.js +25 -5
- package/dist/src/gateway/hono/routes/models.js.map +1 -1
- package/dist/src/heartbeat/index.js +1 -1
- package/dist/src/media/store.js +3 -0
- package/dist/src/media/store.js.map +1 -1
- package/dist/src/notes/service.js +1 -1
- package/dist/src/notes/service.js.map +1 -1
- package/dist/src/providers/index.d.ts +3 -3
- package/dist/src/providers/index.js +23 -15
- package/dist/src/providers/index.js.map +1 -1
- package/dist/src/providers/presentation.d.ts +34 -0
- package/dist/src/providers/presentation.js +162 -0
- package/dist/src/providers/presentation.js.map +1 -0
- package/package.json +1 -1
- package/dist/gateway/static/root/assets/index-DKgHC1Ta.js +0 -4840
- package/dist/gateway/static/root/assets/index-DjNDHzwG.css +0 -1
- package/dist/gateway/static/root/assets/share-preview-page-CKCTMo0_.js +0 -2
- package/dist/gateway/static/root/assets/url-q28QhI7l.js +0 -3
- package/dist/src/auth/oauth/vendor-pi-google/google-antigravity.js.map +0 -1
- package/dist/src/auth/oauth/vendor-pi-google/google-gemini-cli.js.map +0 -1
- package/dist/src/auth/oauth/vendor-pi-google/oauth-page.js.map +0 -1
- package/dist/src/auth/oauth/vendor-pi-google/pkce.js.map +0 -1
- /package/dist/src/auth/oauth/{vendor-pi-google → google-oauth}/google-antigravity.d.ts +0 -0
- /package/dist/src/auth/oauth/{vendor-pi-google → google-oauth}/google-gemini-cli.d.ts +0 -0
- /package/dist/src/auth/oauth/{vendor-pi-google → google-oauth}/oauth-page.d.ts +0 -0
|
@@ -1,2 +1,319 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generatePKCE } from "./pkce.js";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
//#region src/auth/oauth/openai-codex.ts
|
|
5
|
+
/**
|
|
6
|
+
* OpenAI Codex (ChatGPT) OAuth.
|
|
7
|
+
*
|
|
8
|
+
* Owns callback validation, manual prompt handling, and success/error pages.
|
|
9
|
+
*/
|
|
10
|
+
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
11
|
+
const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
|
12
|
+
const TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
13
|
+
const CALLBACK_PORT = 1455;
|
|
14
|
+
const CALLBACK_PATH = "/auth/callback";
|
|
15
|
+
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
16
|
+
const SCOPE = "openid profile email offline_access";
|
|
17
|
+
const MANUAL_PROMPT_DELAY_MS = 15e3;
|
|
18
|
+
const TOKEN_REQUEST_TIMEOUT_MS = 3e4;
|
|
19
|
+
function xopcOAuthHtml(params) {
|
|
20
|
+
const color = params.tone === "success" ? "#2563eb" : "#dc2626";
|
|
21
|
+
const escapedTitle = escapeHtml(params.title);
|
|
22
|
+
return `<!doctype html>
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="utf-8" />
|
|
26
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
27
|
+
<title>${escapedTitle}</title>
|
|
28
|
+
<style>
|
|
29
|
+
:root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
30
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #f8fafc; color: #0f172a; }
|
|
31
|
+
@media (prefers-color-scheme: dark) { body { background: #020617; color: #e2e8f0; } .card { background: #0f172a !important; border-color: #1e293b !important; } .muted { color: #94a3b8 !important; } }
|
|
32
|
+
.card { width: min(520px, calc(100vw - 32px)); background: white; border: 1px solid #e2e8f0; border-radius: 20px; padding: 28px; box-sizing: border-box; }
|
|
33
|
+
.brand { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; font-weight: 700; }
|
|
34
|
+
.logo { width: 36px; height: 36px; border-radius: 12px; display: grid; place-items: center; color: white; background: ${color}; font-weight: 800; letter-spacing: -0.04em; }
|
|
35
|
+
h1 { font-size: 22px; margin: 0 0 10px; }
|
|
36
|
+
p { margin: 0; line-height: 1.6; }
|
|
37
|
+
.muted { color: #64748b; margin-top: 14px; font-size: 14px; }
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<main class="card">
|
|
42
|
+
<div class="brand"><div class="logo">x</div><div>xopc</div></div>
|
|
43
|
+
<h1>${escapedTitle}</h1>
|
|
44
|
+
<p>${escapeHtml(params.message)}</p>
|
|
45
|
+
<p class="muted">You can close this window and return to the terminal.</p>
|
|
46
|
+
</main>
|
|
47
|
+
</body>
|
|
48
|
+
</html>`;
|
|
49
|
+
}
|
|
50
|
+
function escapeHtml(value) {
|
|
51
|
+
return value.replace(/[&<>"]/g, (char) => ({
|
|
52
|
+
"&": "&",
|
|
53
|
+
"<": "<",
|
|
54
|
+
">": ">",
|
|
55
|
+
"\"": """
|
|
56
|
+
})[char] ?? char);
|
|
57
|
+
}
|
|
58
|
+
function createState() {
|
|
59
|
+
return randomBytes(16).toString("hex");
|
|
60
|
+
}
|
|
61
|
+
function waitForManualPromptDelay(signal) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
if (signal?.aborted) {
|
|
64
|
+
reject(/* @__PURE__ */ new Error("Login cancelled"));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const timeout = setTimeout(() => resolve({ type: "unavailable" }), MANUAL_PROMPT_DELAY_MS);
|
|
68
|
+
timeout.unref?.();
|
|
69
|
+
const abort = () => {
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
reject(/* @__PURE__ */ new Error("Login cancelled"));
|
|
72
|
+
};
|
|
73
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function parseOAuthAuthorizationInput(input) {
|
|
77
|
+
const trimmed = input.trim();
|
|
78
|
+
if (!trimmed) return {};
|
|
79
|
+
try {
|
|
80
|
+
const url = new URL(trimmed);
|
|
81
|
+
return {
|
|
82
|
+
code: url.searchParams.get("code") ?? void 0,
|
|
83
|
+
state: url.searchParams.get("state") ?? void 0
|
|
84
|
+
};
|
|
85
|
+
} catch {}
|
|
86
|
+
if (trimmed.includes("=")) {
|
|
87
|
+
const params = new URLSearchParams(trimmed.startsWith("?") ? trimmed.slice(1) : trimmed);
|
|
88
|
+
return {
|
|
89
|
+
code: params.get("code") ?? void 0,
|
|
90
|
+
state: params.get("state") ?? void 0
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { code: trimmed };
|
|
94
|
+
}
|
|
95
|
+
async function parsePromptedAuthorizationCode(inputPromise, state) {
|
|
96
|
+
const parsed = parseOAuthAuthorizationInput(await inputPromise);
|
|
97
|
+
if (parsed.state && parsed.state !== state) throw new Error("State mismatch. Paste the redirect URL from the most recent xopc OAuth login.");
|
|
98
|
+
if (!parsed.code) throw new Error("Missing authorization code");
|
|
99
|
+
return parsed.code;
|
|
100
|
+
}
|
|
101
|
+
async function createAuthorizationFlow() {
|
|
102
|
+
const { verifier, challenge } = await generatePKCE();
|
|
103
|
+
const state = createState();
|
|
104
|
+
const url = new URL(AUTHORIZE_URL);
|
|
105
|
+
url.searchParams.set("response_type", "code");
|
|
106
|
+
url.searchParams.set("client_id", CLIENT_ID);
|
|
107
|
+
url.searchParams.set("redirect_uri", REDIRECT_URI);
|
|
108
|
+
url.searchParams.set("scope", SCOPE);
|
|
109
|
+
url.searchParams.set("code_challenge", challenge);
|
|
110
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
111
|
+
url.searchParams.set("state", state);
|
|
112
|
+
url.searchParams.set("id_token_add_organizations", "true");
|
|
113
|
+
url.searchParams.set("codex_cli_simplified_flow", "true");
|
|
114
|
+
url.searchParams.set("originator", "xopc");
|
|
115
|
+
return {
|
|
116
|
+
verifier,
|
|
117
|
+
state,
|
|
118
|
+
url: url.toString()
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function startLocalOAuthServer(state) {
|
|
122
|
+
let settleWait;
|
|
123
|
+
const waitForCodePromise = new Promise((resolve) => {
|
|
124
|
+
let settled = false;
|
|
125
|
+
settleWait = (value) => {
|
|
126
|
+
if (settled) return;
|
|
127
|
+
settled = true;
|
|
128
|
+
resolve(value);
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
const server = http.createServer((req, res) => {
|
|
132
|
+
try {
|
|
133
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
134
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
135
|
+
if (url.pathname !== CALLBACK_PATH) {
|
|
136
|
+
res.statusCode = 404;
|
|
137
|
+
res.end(xopcOAuthHtml({
|
|
138
|
+
title: "Callback route not found",
|
|
139
|
+
message: "This xopc OAuth callback URL is not valid.",
|
|
140
|
+
tone: "error"
|
|
141
|
+
}));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const code = url.searchParams.get("code");
|
|
145
|
+
if (!code) {
|
|
146
|
+
res.statusCode = 400;
|
|
147
|
+
res.end(xopcOAuthHtml({
|
|
148
|
+
title: "Authentication failed",
|
|
149
|
+
message: "OpenAI did not return an authorization code.",
|
|
150
|
+
tone: "error"
|
|
151
|
+
}));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (url.searchParams.get("state") !== state) {
|
|
155
|
+
res.statusCode = 400;
|
|
156
|
+
res.end(xopcOAuthHtml({
|
|
157
|
+
title: "Authentication needs manual confirmation",
|
|
158
|
+
message: "This callback belongs to a different xopc OAuth attempt. Copy the full URL from the address bar and paste it in the terminal prompt for the latest login.",
|
|
159
|
+
tone: "error"
|
|
160
|
+
}));
|
|
161
|
+
settleWait?.({ type: "unavailable" });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
res.statusCode = 200;
|
|
165
|
+
res.end(xopcOAuthHtml({
|
|
166
|
+
title: "OpenAI authentication complete",
|
|
167
|
+
message: "xopc received the authorization callback successfully.",
|
|
168
|
+
tone: "success"
|
|
169
|
+
}));
|
|
170
|
+
settleWait?.({
|
|
171
|
+
type: "code",
|
|
172
|
+
code
|
|
173
|
+
});
|
|
174
|
+
} catch {
|
|
175
|
+
res.statusCode = 500;
|
|
176
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
177
|
+
res.end(xopcOAuthHtml({
|
|
178
|
+
title: "Authentication failed",
|
|
179
|
+
message: "xopc could not process the OAuth callback.",
|
|
180
|
+
tone: "error"
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
return new Promise((resolve) => {
|
|
185
|
+
server.listen(CALLBACK_PORT, () => {
|
|
186
|
+
resolve({
|
|
187
|
+
available: true,
|
|
188
|
+
close: () => server.close(),
|
|
189
|
+
cancelWait: () => settleWait?.({ type: "unavailable" }),
|
|
190
|
+
waitForCode: () => waitForCodePromise
|
|
191
|
+
});
|
|
192
|
+
}).on("error", () => {
|
|
193
|
+
resolve({
|
|
194
|
+
available: false,
|
|
195
|
+
close: () => {},
|
|
196
|
+
cancelWait: () => settleWait?.({ type: "unavailable" }),
|
|
197
|
+
waitForCode: async () => ({ type: "unavailable" })
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
async function postTokenForm(body, signal) {
|
|
203
|
+
const response = await fetch(TOKEN_URL, {
|
|
204
|
+
method: "POST",
|
|
205
|
+
headers: {
|
|
206
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
207
|
+
Accept: "application/json"
|
|
208
|
+
},
|
|
209
|
+
body,
|
|
210
|
+
signal: signal ?? AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS)
|
|
211
|
+
});
|
|
212
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
213
|
+
const text = await response.text();
|
|
214
|
+
if (!response.ok) throw new Error(`OpenAI Codex token request failed (${response.status}): ${text || response.statusText}`);
|
|
215
|
+
if (!contentType.toLowerCase().includes("application/json")) throw new Error(`OpenAI Codex token response was not JSON: ${contentType || "missing content-type"}`);
|
|
216
|
+
return JSON.parse(text);
|
|
217
|
+
}
|
|
218
|
+
async function exchangeAuthorizationCode(code, verifier, signal) {
|
|
219
|
+
return normalizeTokenResponse(await postTokenForm(new URLSearchParams({
|
|
220
|
+
grant_type: "authorization_code",
|
|
221
|
+
client_id: CLIENT_ID,
|
|
222
|
+
code,
|
|
223
|
+
code_verifier: verifier,
|
|
224
|
+
redirect_uri: REDIRECT_URI
|
|
225
|
+
}), signal));
|
|
226
|
+
}
|
|
227
|
+
async function refreshAccessToken(refreshToken) {
|
|
228
|
+
return normalizeTokenResponse(await postTokenForm(new URLSearchParams({
|
|
229
|
+
grant_type: "refresh_token",
|
|
230
|
+
client_id: CLIENT_ID,
|
|
231
|
+
refresh_token: refreshToken
|
|
232
|
+
})));
|
|
233
|
+
}
|
|
234
|
+
function normalizeTokenResponse(json) {
|
|
235
|
+
if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
|
|
236
|
+
const missing = [
|
|
237
|
+
!json.access_token ? "access_token" : "",
|
|
238
|
+
!json.refresh_token ? "refresh_token" : "",
|
|
239
|
+
typeof json.expires_in !== "number" ? "expires_in" : ""
|
|
240
|
+
].filter(Boolean).join(", ");
|
|
241
|
+
throw new Error(`OpenAI Codex token response missing fields: ${missing}`);
|
|
242
|
+
}
|
|
243
|
+
const accountId = getAccountId(json.access_token);
|
|
244
|
+
if (!accountId) throw new Error("Failed to extract accountId from token");
|
|
245
|
+
return {
|
|
246
|
+
access: json.access_token,
|
|
247
|
+
refresh: json.refresh_token,
|
|
248
|
+
expires: Date.now() + json.expires_in * 1e3,
|
|
249
|
+
accountId
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function getAccountId(accessToken) {
|
|
253
|
+
const [, payload] = accessToken.split(".");
|
|
254
|
+
if (!payload) return null;
|
|
255
|
+
try {
|
|
256
|
+
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
257
|
+
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
|
|
258
|
+
const json = JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
|
259
|
+
const authClaim = json["https://api.openai.com/auth"];
|
|
260
|
+
if (authClaim && typeof authClaim === "object") {
|
|
261
|
+
const accountId = authClaim.chatgpt_account_id;
|
|
262
|
+
if (typeof accountId === "string" && accountId.length > 0) return accountId;
|
|
263
|
+
}
|
|
264
|
+
return typeof json.sub === "string" && json.sub.length > 0 ? json.sub : null;
|
|
265
|
+
} catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function loginOpenAICodex(callbacks) {
|
|
270
|
+
const { verifier, state, url } = await createAuthorizationFlow();
|
|
271
|
+
const server = await startLocalOAuthServer(state);
|
|
272
|
+
try {
|
|
273
|
+
callbacks.onAuth({
|
|
274
|
+
url,
|
|
275
|
+
instructions: server.available ? "A browser window should open. Complete login to finish. If it does not complete automatically, paste the final redirect URL in the terminal." : "Open this URL and paste the final redirect URL in the terminal. xopc could not bind localhost:1455 for the automatic callback."
|
|
276
|
+
});
|
|
277
|
+
let code;
|
|
278
|
+
if (server.available) {
|
|
279
|
+
const callback = await Promise.race([server.waitForCode(), waitForManualPromptDelay(callbacks.signal)]);
|
|
280
|
+
if (callback.type === "code") code = callback.code;
|
|
281
|
+
}
|
|
282
|
+
if (!code) {
|
|
283
|
+
const manualCode = parsePromptedAuthorizationCode(callbacks.onManualCodeInput ? callbacks.onManualCodeInput() : callbacks.onPrompt({ message: "Paste the authorization code (or full redirect URL):" }), state).then((value) => {
|
|
284
|
+
server.cancelWait();
|
|
285
|
+
return {
|
|
286
|
+
type: "code",
|
|
287
|
+
code: value
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
const callback = await Promise.race([server.waitForCode(), manualCode]);
|
|
291
|
+
if (callback.type === "code") code = callback.code;
|
|
292
|
+
}
|
|
293
|
+
if (!code) throw new Error("Missing authorization code");
|
|
294
|
+
return await exchangeAuthorizationCode(code, verifier, callbacks.signal);
|
|
295
|
+
} finally {
|
|
296
|
+
server.close();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function refreshOpenAICodexToken(refreshToken) {
|
|
300
|
+
return refreshAccessToken(refreshToken);
|
|
301
|
+
}
|
|
302
|
+
const openaiCodexOAuthProvider = {
|
|
303
|
+
id: "openai-codex",
|
|
304
|
+
name: "OpenAI Codex",
|
|
305
|
+
usesCallbackServer: true,
|
|
306
|
+
async login(callbacks) {
|
|
307
|
+
return loginOpenAICodex(callbacks);
|
|
308
|
+
},
|
|
309
|
+
async refreshToken(credentials) {
|
|
310
|
+
return refreshOpenAICodexToken(credentials.refresh);
|
|
311
|
+
},
|
|
312
|
+
getApiKey(credentials) {
|
|
313
|
+
return credentials.access;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
//#endregion
|
|
2
317
|
export { loginOpenAICodex, openaiCodexOAuthProvider, refreshOpenAICodexToken };
|
|
318
|
+
|
|
319
|
+
//# sourceMappingURL=openai-codex.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai-codex.js","names":[],"sources":["../../../../src/auth/oauth/openai-codex.ts"],"sourcesContent":["/**\n * OpenAI Codex (ChatGPT) OAuth.\n *\n * Owns callback validation, manual prompt handling, and success/error pages.\n */\n\nimport { randomBytes } from 'node:crypto';\nimport http from 'node:http';\n\nimport { generatePKCE } from './pkce.js';\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from './types.js';\n\nconst CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';\nconst AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize';\nconst TOKEN_URL = 'https://auth.openai.com/oauth/token';\nconst CALLBACK_PORT = 1455;\nconst CALLBACK_PATH = '/auth/callback';\nconst REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;\nconst SCOPE = 'openid profile email offline_access';\nconst MANUAL_PROMPT_DELAY_MS = 15_000;\nconst TOKEN_REQUEST_TIMEOUT_MS = 30_000;\n\ntype TokenResponseJson = {\n access_token?: string;\n refresh_token?: string;\n expires_in?: number;\n error?: string;\n error_description?: string;\n};\n\ntype CallbackResult =\n | { type: 'code'; code: string }\n | { type: 'unavailable' };\n\ntype LocalOAuthServer = {\n close: () => void;\n cancelWait: () => void;\n waitForCode: () => Promise<CallbackResult>;\n available: boolean;\n};\n\nfunction xopcOAuthHtml(params: { title: string; message: string; tone: 'success' | 'error' }): string {\n const color = params.tone === 'success' ? '#2563eb' : '#dc2626';\n const escapedTitle = escapeHtml(params.title);\n const escapedMessage = escapeHtml(params.message);\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${escapedTitle}</title>\n <style>\n :root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; }\n body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #f8fafc; color: #0f172a; }\n @media (prefers-color-scheme: dark) { body { background: #020617; color: #e2e8f0; } .card { background: #0f172a !important; border-color: #1e293b !important; } .muted { color: #94a3b8 !important; } }\n .card { width: min(520px, calc(100vw - 32px)); background: white; border: 1px solid #e2e8f0; border-radius: 20px; padding: 28px; box-sizing: border-box; }\n .brand { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; font-weight: 700; }\n .logo { width: 36px; height: 36px; border-radius: 12px; display: grid; place-items: center; color: white; background: ${color}; font-weight: 800; letter-spacing: -0.04em; }\n h1 { font-size: 22px; margin: 0 0 10px; }\n p { margin: 0; line-height: 1.6; }\n .muted { color: #64748b; margin-top: 14px; font-size: 14px; }\n </style>\n</head>\n<body>\n <main class=\"card\">\n <div class=\"brand\"><div class=\"logo\">x</div><div>xopc</div></div>\n <h1>${escapedTitle}</h1>\n <p>${escapedMessage}</p>\n <p class=\"muted\">You can close this window and return to the terminal.</p>\n </main>\n</body>\n</html>`;\n}\n\nfunction escapeHtml(value: string): string {\n return value.replace(/[&<>\"]/g, (char) => ({ '&': '&', '<': '<', '>': '>', '\"': '"' })[char] ?? char);\n}\n\nfunction createState(): string {\n return randomBytes(16).toString('hex');\n}\n\nfunction waitForManualPromptDelay(signal?: AbortSignal): Promise<CallbackResult> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(new Error('Login cancelled'));\n return;\n }\n const timeout = setTimeout(() => resolve({ type: 'unavailable' }), MANUAL_PROMPT_DELAY_MS);\n timeout.unref?.();\n const abort = () => {\n clearTimeout(timeout);\n reject(new Error('Login cancelled'));\n };\n signal?.addEventListener('abort', abort, { once: true });\n });\n}\n\nfunction parseOAuthAuthorizationInput(input: string): { code?: string; state?: string } {\n const trimmed = input.trim();\n if (!trimmed) return {};\n\n try {\n const url = new URL(trimmed);\n return {\n code: url.searchParams.get('code') ?? undefined,\n state: url.searchParams.get('state') ?? undefined,\n };\n } catch {\n // Continue: raw code or query-string paste.\n }\n\n if (trimmed.includes('=')) {\n const params = new URLSearchParams(trimmed.startsWith('?') ? trimmed.slice(1) : trimmed);\n return {\n code: params.get('code') ?? undefined,\n state: params.get('state') ?? undefined,\n };\n }\n\n return { code: trimmed };\n}\n\nasync function parsePromptedAuthorizationCode(inputPromise: Promise<string>, state: string): Promise<string> {\n const input = await inputPromise;\n const parsed = parseOAuthAuthorizationInput(input);\n if (parsed.state && parsed.state !== state) {\n throw new Error('State mismatch. Paste the redirect URL from the most recent xopc OAuth login.');\n }\n if (!parsed.code) {\n throw new Error('Missing authorization code');\n }\n return parsed.code;\n}\n\nasync function createAuthorizationFlow(): Promise<{ verifier: string; state: string; url: string }> {\n const { verifier, challenge } = await generatePKCE();\n const state = createState();\n const url = new URL(AUTHORIZE_URL);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('client_id', CLIENT_ID);\n url.searchParams.set('redirect_uri', REDIRECT_URI);\n url.searchParams.set('scope', SCOPE);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n url.searchParams.set('state', state);\n url.searchParams.set('id_token_add_organizations', 'true');\n url.searchParams.set('codex_cli_simplified_flow', 'true');\n url.searchParams.set('originator', 'xopc');\n return { verifier, state, url: url.toString() };\n}\n\nasync function startLocalOAuthServer(state: string): Promise<LocalOAuthServer> {\n let settleWait: ((value: CallbackResult) => void) | undefined;\n const waitForCodePromise = new Promise<CallbackResult>((resolve) => {\n let settled = false;\n settleWait = (value) => {\n if (settled) return;\n settled = true;\n resolve(value);\n };\n });\n\n const server = http.createServer((req, res) => {\n try {\n const url = new URL(req.url || '', 'http://localhost');\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\n if (url.pathname !== CALLBACK_PATH) {\n res.statusCode = 404;\n res.end(xopcOAuthHtml({ title: 'Callback route not found', message: 'This xopc OAuth callback URL is not valid.', tone: 'error' }));\n return;\n }\n\n const code = url.searchParams.get('code');\n if (!code) {\n res.statusCode = 400;\n res.end(xopcOAuthHtml({ title: 'Authentication failed', message: 'OpenAI did not return an authorization code.', tone: 'error' }));\n return;\n }\n\n if (url.searchParams.get('state') !== state) {\n res.statusCode = 400;\n res.end(\n xopcOAuthHtml({\n title: 'Authentication needs manual confirmation',\n message: 'This callback belongs to a different xopc OAuth attempt. Copy the full URL from the address bar and paste it in the terminal prompt for the latest login.',\n tone: 'error',\n }),\n );\n settleWait?.({ type: 'unavailable' });\n return;\n }\n\n res.statusCode = 200;\n res.end(xopcOAuthHtml({ title: 'OpenAI authentication complete', message: 'xopc received the authorization callback successfully.', tone: 'success' }));\n settleWait?.({ type: 'code', code });\n } catch {\n res.statusCode = 500;\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\n res.end(xopcOAuthHtml({ title: 'Authentication failed', message: 'xopc could not process the OAuth callback.', tone: 'error' }));\n }\n });\n\n return new Promise((resolve) => {\n server\n .listen(CALLBACK_PORT, () => {\n resolve({\n available: true,\n close: () => server.close(),\n cancelWait: () => settleWait?.({ type: 'unavailable' }),\n waitForCode: () => waitForCodePromise,\n });\n })\n .on('error', () => {\n resolve({\n available: false,\n close: () => {},\n cancelWait: () => settleWait?.({ type: 'unavailable' }),\n waitForCode: async () => ({ type: 'unavailable' }),\n });\n });\n });\n}\n\nasync function postTokenForm(body: URLSearchParams, signal?: AbortSignal): Promise<TokenResponseJson> {\n const response = await fetch(TOKEN_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n },\n body,\n signal: signal ?? AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS),\n });\n\n const contentType = response.headers.get('content-type') ?? '';\n const text = await response.text();\n if (!response.ok) {\n throw new Error(`OpenAI Codex token request failed (${response.status}): ${text || response.statusText}`);\n }\n if (!contentType.toLowerCase().includes('application/json')) {\n throw new Error(`OpenAI Codex token response was not JSON: ${contentType || 'missing content-type'}`);\n }\n return JSON.parse(text) as TokenResponseJson;\n}\n\nasync function exchangeAuthorizationCode(code: string, verifier: string, signal?: AbortSignal): Promise<OAuthCredentials> {\n const json = await postTokenForm(\n new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: CLIENT_ID,\n code,\n code_verifier: verifier,\n redirect_uri: REDIRECT_URI,\n }),\n signal,\n );\n return normalizeTokenResponse(json);\n}\n\nasync function refreshAccessToken(refreshToken: string): Promise<OAuthCredentials> {\n const json = await postTokenForm(\n new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: CLIENT_ID,\n refresh_token: refreshToken,\n }),\n );\n return normalizeTokenResponse(json);\n}\n\nfunction normalizeTokenResponse(json: TokenResponseJson): OAuthCredentials {\n if (!json.access_token || !json.refresh_token || typeof json.expires_in !== 'number') {\n const missing = [\n !json.access_token ? 'access_token' : '',\n !json.refresh_token ? 'refresh_token' : '',\n typeof json.expires_in !== 'number' ? 'expires_in' : '',\n ].filter(Boolean).join(', ');\n throw new Error(`OpenAI Codex token response missing fields: ${missing}`);\n }\n const accountId = getAccountId(json.access_token);\n if (!accountId) {\n throw new Error('Failed to extract accountId from token');\n }\n return {\n access: json.access_token,\n refresh: json.refresh_token,\n expires: Date.now() + json.expires_in * 1000,\n accountId,\n };\n}\n\nfunction getAccountId(accessToken: string): string | null {\n const [, payload] = accessToken.split('.');\n if (!payload) return null;\n try {\n const normalized = payload.replace(/-/g, '+').replace(/_/g, '/');\n const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');\n const json = JSON.parse(Buffer.from(padded, 'base64').toString('utf8')) as Record<string, unknown>;\n const authClaim = json['https://api.openai.com/auth'];\n if (authClaim && typeof authClaim === 'object') {\n const accountId = (authClaim as Record<string, unknown>).chatgpt_account_id;\n if (typeof accountId === 'string' && accountId.length > 0) return accountId;\n }\n return typeof json.sub === 'string' && json.sub.length > 0 ? json.sub : null;\n } catch {\n return null;\n }\n}\n\nexport async function loginOpenAICodex(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n const { verifier, state, url } = await createAuthorizationFlow();\n const server = await startLocalOAuthServer(state);\n\n try {\n callbacks.onAuth({\n url,\n instructions: server.available\n ? 'A browser window should open. Complete login to finish. If it does not complete automatically, paste the final redirect URL in the terminal.'\n : 'Open this URL and paste the final redirect URL in the terminal. xopc could not bind localhost:1455 for the automatic callback.',\n });\n\n let code: string | undefined;\n if (server.available) {\n const callback = await Promise.race([server.waitForCode(), waitForManualPromptDelay(callbacks.signal)]);\n if (callback.type === 'code') {\n code = callback.code;\n }\n }\n\n if (!code) {\n const manualInput = callbacks.onManualCodeInput\n ? callbacks.onManualCodeInput()\n : callbacks.onPrompt({ message: 'Paste the authorization code (or full redirect URL):' });\n const manualCode = parsePromptedAuthorizationCode(manualInput, state).then((value) => {\n server.cancelWait();\n return { type: 'code' as const, code: value };\n });\n const callback = await Promise.race([server.waitForCode(), manualCode]);\n if (callback.type === 'code') {\n code = callback.code;\n }\n }\n\n if (!code) {\n throw new Error('Missing authorization code');\n }\n\n return await exchangeAuthorizationCode(code, verifier, callbacks.signal);\n } finally {\n server.close();\n }\n}\n\nexport async function refreshOpenAICodexToken(refreshToken: string): Promise<OAuthCredentials> {\n return refreshAccessToken(refreshToken);\n}\n\nexport const openaiCodexOAuthProvider: OAuthProviderInterface = {\n id: 'openai-codex',\n name: 'OpenAI Codex',\n usesCallbackServer: true,\n\n async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n return loginOpenAICodex(callbacks);\n },\n\n async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n return refreshOpenAICodexToken(credentials.refresh);\n },\n\n getApiKey(credentials: OAuthCredentials): string {\n return credentials.access;\n },\n};\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { init_write_file_atomic, writeTextAtomicSync } from "../../infra/write-file-atomic.js";
|
|
2
|
+
import { init_paths, resolveCredentialsDir } from "../../config/paths.js";
|
|
2
3
|
import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
import { homedir } from "os";
|
|
5
5
|
//#region src/auth/profiles/store.ts
|
|
6
6
|
/**
|
|
7
7
|
* Auth Profile Store
|
|
@@ -10,11 +10,12 @@ import { homedir } from "os";
|
|
|
10
10
|
* Handles loading and saving auth credentials.
|
|
11
11
|
*/
|
|
12
12
|
init_write_file_atomic();
|
|
13
|
+
init_paths();
|
|
13
14
|
const AUTH_STORE_VERSION = 1;
|
|
14
15
|
const AUTH_STORE_FILENAME = "auth-profiles.json";
|
|
15
16
|
/** Get the auth profiles file path */
|
|
16
17
|
function resolveAuthStorePath(dataDir) {
|
|
17
|
-
const dir = dataDir ??
|
|
18
|
+
const dir = dataDir ?? resolveCredentialsDir();
|
|
18
19
|
return path.join(dir, AUTH_STORE_FILENAME);
|
|
19
20
|
}
|
|
20
21
|
/** Ensure auth store file exists */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","names":[],"sources":["../../../../src/auth/profiles/store.ts"],"sourcesContent":["/**\n * Auth Profile Store\n * \n * Persistent storage for auth profiles.\n * Handles loading and saving auth credentials.\n */\n\nimport { chmodSync, existsSync, mkdirSync, readFileSync } from 'node:fs';\nimport path from 'node:path';\nimport { writeTextAtomicSync } from '../../infra/write-file-atomic.js';\nimport {
|
|
1
|
+
{"version":3,"file":"store.js","names":[],"sources":["../../../../src/auth/profiles/store.ts"],"sourcesContent":["/**\n * Auth Profile Store\n * \n * Persistent storage for auth profiles.\n * Handles loading and saving auth credentials.\n */\n\nimport { chmodSync, existsSync, mkdirSync, readFileSync } from 'node:fs';\nimport path from 'node:path';\nimport { writeTextAtomicSync } from '../../infra/write-file-atomic.js';\nimport { resolveCredentialsDir } from '../../config/paths.js';\nimport type { AuthProfileCredential, AuthProfileStore, ProfileUsageStats } from './types.js';\n\nconst AUTH_STORE_VERSION = 1;\nconst AUTH_STORE_FILENAME = 'auth-profiles.json';\n\n/** Get the auth profiles file path */\nexport function resolveAuthStorePath(dataDir?: string): string {\n\tconst dir = dataDir ?? resolveCredentialsDir();\n\treturn path.join(dir, AUTH_STORE_FILENAME);\n}\n\n/** Ensure auth store file exists */\nexport function ensureAuthStoreFile(authPath: string): void {\n\tconst dir = path.dirname(authPath);\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t}\n\tif (!existsSync(authPath)) {\n\t\twriteTextAtomicSync(\n\t\t\tauthPath,\n\t\t\tJSON.stringify({ version: AUTH_STORE_VERSION, profiles: {} }, null, 2),\n\t\t);\n\t\tchmodSync(authPath, 0o600);\n\t}\n}\n\nfunction loadJsonFile(filePath: string): unknown {\n\tif (!existsSync(filePath)) {\n\t\treturn null;\n\t}\n\ttry {\n\t\tconst content = readFileSync(filePath, 'utf-8');\n\t\treturn JSON.parse(content);\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction saveJsonFile(filePath: string, data: unknown): void {\n\tconst dir = path.dirname(filePath);\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t}\n\twriteTextAtomicSync(filePath, JSON.stringify(data, null, 2));\n\tchmodSync(filePath, 0o600);\n}\n\nfunction coerceAuthStore(raw: unknown): AuthProfileStore | null {\n\tif (!raw || typeof raw !== 'object') {\n\t\treturn null;\n\t}\n\tconst record = raw as Record<string, unknown>;\n\tif (!record.profiles || typeof record.profiles !== 'object') {\n\t\treturn null;\n\t}\n\tconst profiles = record.profiles as Record<string, unknown>;\n\tconst normalized: Record<string, AuthProfileCredential> = {};\n\tfor (const [key, value] of Object.entries(profiles)) {\n\t\tif (!value || typeof value !== 'object') {\n\t\t\tcontinue;\n\t\t}\n\t\tconst typed = value as Partial<AuthProfileCredential>;\n\t\tif (typed.type !== 'api_key' && typed.type !== 'oauth' && typed.type !== 'token') {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!typed.provider) {\n\t\t\tcontinue;\n\t\t}\n\t\tnormalized[key] = typed as AuthProfileCredential;\n\t}\n\tconst order =\n\t\trecord.order && typeof record.order === 'object'\n\t\t\t? Object.entries(record.order as Record<string, unknown>).reduce(\n\t\t\t\t(acc, [provider, value]) => {\n\t\t\t\t\tif (!Array.isArray(value)) {\n\t\t\t\t\t\treturn acc;\n\t\t\t\t\t}\n\t\t\t\t\tconst list = value\n\t\t\t\t\t\t.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))\n\t\t\t\t\t\t.filter(Boolean);\n\t\t\t\t\tif (list.length === 0) {\n\t\t\t\t\t\treturn acc;\n\t\t\t\t\t}\n\t\t\t\t\tacc[provider] = list;\n\t\t\t\t\treturn acc;\n\t\t\t\t},\n\t\t\t\t{} as Record<string, string[]>,\n\t\t\t)\n\t\t\t: undefined;\n\treturn {\n\t\tversion: Number(record.version ?? AUTH_STORE_VERSION),\n\t\tprofiles: normalized,\n\t\torder,\n\t\tlastGood:\n\t\t\trecord.lastGood && typeof record.lastGood === 'object'\n\t\t\t\t? (record.lastGood as Record<string, string>)\n\t\t\t\t: undefined,\n\t\tusageStats:\n\t\t\trecord.usageStats && typeof record.usageStats === 'object'\n\t\t\t\t? (record.usageStats as Record<string, ProfileUsageStats>)\n\t\t\t\t: undefined,\n\t};\n}\n\nfunction mergeRecord<T>(\n\tbase?: Record<string, T>,\n\toverride?: Record<string, T>,\n): Record<string, T> | undefined {\n\tif (!base && !override) {\n\t\treturn undefined;\n\t}\n\tif (!base) {\n\t\treturn { ...override };\n\t}\n\tif (!override) {\n\t\treturn { ...base };\n\t}\n\treturn { ...base, ...override };\n}\n\nfunction _mergeAuthProfileStores(\n\tbase: AuthProfileStore,\n\toverride: AuthProfileStore,\n): AuthProfileStore {\n\tif (\n\t\tObject.keys(override.profiles).length === 0 &&\n\t\t!override.order &&\n\t\t!override.lastGood &&\n\t\t!override.usageStats\n\t) {\n\t\treturn base;\n\t}\n\treturn {\n\t\tversion: Math.max(base.version, override.version ?? base.version),\n\t\tprofiles: { ...base.profiles, ...override.profiles },\n\t\torder: mergeRecord(base.order, override.order),\n\t\tlastGood: mergeRecord(base.lastGood, override.lastGood),\n\t\tusageStats: mergeRecord(base.usageStats, override.usageStats),\n\t};\n}\n\n/** Load auth profile store from disk */\nexport function loadAuthProfileStore(dataDir?: string): AuthProfileStore {\n\tconst authPath = resolveAuthStorePath(dataDir);\n\tconst raw = loadJsonFile(authPath);\n\tconst asStore = coerceAuthStore(raw);\n\tif (asStore) {\n\t\treturn asStore;\n\t}\n\n\treturn { version: AUTH_STORE_VERSION, profiles: {} };\n}\n\n/** Ensure auth profile store exists */\nexport function ensureAuthProfileStore(dataDir?: string): AuthProfileStore {\n\tconst authPath = resolveAuthStorePath(dataDir);\n\tensureAuthStoreFile(authPath);\n\treturn loadAuthProfileStore(dataDir);\n}\n\n/** Save auth profile store to disk */\nexport function saveAuthProfileStore(store: AuthProfileStore, dataDir?: string): void {\n\tconst authPath = resolveAuthStorePath(dataDir);\n\tconst payload = {\n\t\tversion: AUTH_STORE_VERSION,\n\t\tprofiles: store.profiles,\n\t\torder: store.order ?? undefined,\n\t\tlastGood: store.lastGood ?? undefined,\n\t\tusageStats: store.usageStats ?? undefined,\n\t} satisfies AuthProfileStore;\n\tsaveJsonFile(authPath, payload);\n}\n"],"mappings":";;;;;;;;;;;wBASuE;YACT;AAG9D,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;;AAG5B,SAAgB,qBAAqB,SAA0B;CAC9D,MAAM,MAAM,WAAW,uBAAuB;AAC9C,QAAO,KAAK,KAAK,KAAK,oBAAoB;;;AAI3C,SAAgB,oBAAoB,UAAwB;CAC3D,MAAM,MAAM,KAAK,QAAQ,SAAS;AAClC,KAAI,CAAC,WAAW,IAAI,CACnB,WAAU,KAAK;EAAE,WAAW;EAAM,MAAM;EAAO,CAAC;AAEjD,KAAI,CAAC,WAAW,SAAS,EAAE;AAC1B,sBACC,UACA,KAAK,UAAU;GAAE,SAAS;GAAoB,UAAU,EAAE;GAAE,EAAE,MAAM,EAAE,CACtE;AACD,YAAU,UAAU,IAAM;;;AAI5B,SAAS,aAAa,UAA2B;AAChD,KAAI,CAAC,WAAW,SAAS,CACxB,QAAO;AAER,KAAI;EACH,MAAM,UAAU,aAAa,UAAU,QAAQ;AAC/C,SAAO,KAAK,MAAM,QAAQ;SACnB;AACP,SAAO;;;AAIT,SAAS,aAAa,UAAkB,MAAqB;CAC5D,MAAM,MAAM,KAAK,QAAQ,SAAS;AAClC,KAAI,CAAC,WAAW,IAAI,CACnB,WAAU,KAAK;EAAE,WAAW;EAAM,MAAM;EAAO,CAAC;AAEjD,qBAAoB,UAAU,KAAK,UAAU,MAAM,MAAM,EAAE,CAAC;AAC5D,WAAU,UAAU,IAAM;;AAG3B,SAAS,gBAAgB,KAAuC;AAC/D,KAAI,CAAC,OAAO,OAAO,QAAQ,SAC1B,QAAO;CAER,MAAM,SAAS;AACf,KAAI,CAAC,OAAO,YAAY,OAAO,OAAO,aAAa,SAClD,QAAO;CAER,MAAM,WAAW,OAAO;CACxB,MAAM,aAAoD,EAAE;AAC5D,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS,EAAE;AACpD,MAAI,CAAC,SAAS,OAAO,UAAU,SAC9B;EAED,MAAM,QAAQ;AACd,MAAI,MAAM,SAAS,aAAa,MAAM,SAAS,WAAW,MAAM,SAAS,QACxE;AAED,MAAI,CAAC,MAAM,SACV;AAED,aAAW,OAAO;;CAEnB,MAAM,QACL,OAAO,SAAS,OAAO,OAAO,UAAU,WACrC,OAAO,QAAQ,OAAO,MAAiC,CAAC,QACxD,KAAK,CAAC,UAAU,WAAW;AAC3B,MAAI,CAAC,MAAM,QAAQ,MAAM,CACxB,QAAO;EAER,MAAM,OAAO,MACX,KAAK,UAAW,OAAO,UAAU,WAAW,MAAM,MAAM,GAAG,GAAI,CAC/D,OAAO,QAAQ;AACjB,MAAI,KAAK,WAAW,EACnB,QAAO;AAER,MAAI,YAAY;AAChB,SAAO;IAER,EAAE,CACF,GACC,KAAA;AACJ,QAAO;EACN,SAAS,OAAO,OAAO,WAAW,mBAAmB;EACrD,UAAU;EACV;EACA,UACC,OAAO,YAAY,OAAO,OAAO,aAAa,WAC1C,OAAO,WACR,KAAA;EACJ,YACC,OAAO,cAAc,OAAO,OAAO,eAAe,WAC9C,OAAO,aACR,KAAA;EACJ;;;AAyCF,SAAgB,qBAAqB,SAAoC;CAGxE,MAAM,UAAU,gBADJ,aADK,qBAAqB,QACL,CACE,CAAC;AACpC,KAAI,QACH,QAAO;AAGR,QAAO;EAAE,SAAS;EAAoB,UAAU,EAAE;EAAE;;;AAIrD,SAAgB,uBAAuB,SAAoC;AAE1E,qBADiB,qBAAqB,QACV,CAAC;AAC7B,QAAO,qBAAqB,QAAQ;;;AAIrC,SAAgB,qBAAqB,OAAyB,SAAwB;AASrF,cARiB,qBAAqB,QAQjB,EAAE;EANtB,SAAS;EACT,UAAU,MAAM;EAChB,OAAO,MAAM,SAAS,KAAA;EACtB,UAAU,MAAM,YAAY,KAAA;EAC5B,YAAY,MAAM,cAAc,KAAA;EAEH,CAAC"}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
//#region src/channels/attachments/attachment-classifiers.ts
|
|
2
|
+
const VISION_IMAGE_MIME_TYPES = new Set([
|
|
3
|
+
"image/jpeg",
|
|
4
|
+
"image/png",
|
|
5
|
+
"image/gif",
|
|
6
|
+
"image/webp"
|
|
7
|
+
]);
|
|
8
|
+
function isSupportedVisionImageMimeType(mimeType) {
|
|
9
|
+
const normalized = mimeType?.split(";")[0]?.trim().toLowerCase();
|
|
10
|
+
return normalized ? VISION_IMAGE_MIME_TYPES.has(normalized) : false;
|
|
11
|
+
}
|
|
2
12
|
function isImageInboundAttachment(att) {
|
|
3
|
-
|
|
13
|
+
if (att.mimeType) return isSupportedVisionImageMimeType(att.mimeType);
|
|
14
|
+
return att.type === "image" || att.type === "photo";
|
|
4
15
|
}
|
|
5
16
|
function isVoiceInboundAttachment(att) {
|
|
6
17
|
return att.type === "voice" || att.type === "audio" || att.mimeType?.startsWith("audio/") === true;
|
|
7
18
|
}
|
|
8
19
|
//#endregion
|
|
9
|
-
export { isImageInboundAttachment, isVoiceInboundAttachment };
|
|
20
|
+
export { isImageInboundAttachment, isSupportedVisionImageMimeType, isVoiceInboundAttachment };
|
|
10
21
|
|
|
11
22
|
//# sourceMappingURL=attachment-classifiers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment-classifiers.js","names":[],"sources":["../../../../src/channels/attachments/attachment-classifiers.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"attachment-classifiers.js","names":[],"sources":["../../../../src/channels/attachments/attachment-classifiers.ts"],"sourcesContent":["const VISION_IMAGE_MIME_TYPES = new Set([\n 'image/jpeg',\n 'image/png',\n 'image/gif',\n 'image/webp',\n]);\n\nexport function isSupportedVisionImageMimeType(mimeType: string | undefined): boolean {\n const normalized = mimeType?.split(';')[0]?.trim().toLowerCase();\n return normalized ? VISION_IMAGE_MIME_TYPES.has(normalized) : false;\n}\n\nexport function isImageInboundAttachment(att: {\n type?: string;\n mimeType?: string;\n}): boolean {\n if (att.mimeType) {\n return isSupportedVisionImageMimeType(att.mimeType);\n }\n\n return (\n att.type === 'image' ||\n att.type === 'photo'\n );\n}\n\nexport function isVoiceInboundAttachment(att: { type?: string; mimeType?: string }): boolean {\n return (\n att.type === 'voice' ||\n att.type === 'audio' ||\n att.mimeType?.startsWith('audio/') === true\n );\n}\n"],"mappings":";AAAA,MAAM,0BAA0B,IAAI,IAAI;CACtC;CACA;CACA;CACA;CACD,CAAC;AAEF,SAAgB,+BAA+B,UAAuC;CACpF,MAAM,aAAa,UAAU,MAAM,IAAI,CAAC,IAAI,MAAM,CAAC,aAAa;AAChE,QAAO,aAAa,wBAAwB,IAAI,WAAW,GAAG;;AAGhE,SAAgB,yBAAyB,KAG7B;AACV,KAAI,IAAI,SACN,QAAO,+BAA+B,IAAI,SAAS;AAGrD,QACE,IAAI,SAAS,WACb,IAAI,SAAS;;AAIjB,SAAgB,yBAAyB,KAAoD;AAC3F,QACE,IAAI,SAAS,WACb,IAAI,SAAS,WACb,IAAI,UAAU,WAAW,SAAS,KAAK"}
|
|
@@ -66,10 +66,10 @@ const switchCommand = {
|
|
|
66
66
|
"group"
|
|
67
67
|
],
|
|
68
68
|
acceptsArgs: true,
|
|
69
|
-
examples: ["/switch openai/gpt-
|
|
69
|
+
examples: ["/switch openai/gpt-5.5", "/switch minimax/minimax-m2.7"],
|
|
70
70
|
handler: async (ctx, args) => {
|
|
71
71
|
if (!args.trim()) return {
|
|
72
|
-
content: joinBlocks("❌ Missing model ref.", "**Usage:** `/switch provider/model-id`", "Run `/models` — each line shows a display name and a `provider/model` ref in backticks. Copy that ref.", "**Example:** `/switch openai/gpt-
|
|
72
|
+
content: joinBlocks("❌ Missing model ref.", "**Usage:** `/switch provider/model-id`", "Run `/models` — each line shows a display name and a `provider/model` ref in backticks. Copy that ref.", "**Example:** `/switch openai/gpt-5.5`"),
|
|
73
73
|
success: false
|
|
74
74
|
};
|
|
75
75
|
await ctx.setTyping(true);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.js","names":[],"sources":["../../../../src/chat-commands/builtins/model.ts"],"sourcesContent":["/**\n * Model Commands\n *\n * Built-in commands for model management:\n * - /models - List models with display names and `provider/model` refs for /switch\n * - /switch - Switch model using the ref from /models\n * - /usage - Show token usage statistics\n */\n\nimport type { CommandDefinition, CommandContext, UIComponent } from '../types.js';\nimport { commandRegistry } from '../registry.js';\nimport { bulletList, code, hint, joinBlocks, kvList, section } from '../format-output.js';\n\nconst modelsCommand: CommandDefinition = {\n id: 'model.list',\n name: 'models',\n aliases: ['model'],\n description: 'List models with display names and `provider/model` refs for /switch',\n category: 'model',\n scope: ['global', 'private', 'group'],\n handler: async (ctx: CommandContext) => {\n await ctx.setTyping(true);\n\n const models = await ctx.listModels();\n const currentModel = ctx.getCurrentModel();\n\n if (models.length === 0) {\n return {\n content: '🤖 No models available. Please check your configuration.',\n success: true,\n };\n }\n\n // Group by provider\n const byProvider = new Map<string, typeof models>();\n for (const m of models) {\n if (!byProvider.has(m.provider)) {\n byProvider.set(m.provider, []);\n }\n byProvider.get(m.provider)!.push(m);\n }\n\n /** `m.id` from listModels is always `serviceId/modelId` (canonical `/switch` ref). */\n const providerBlocks = Array.from(byProvider.entries()).map(([provider, providerModels]) => {\n const items = providerModels.slice(0, 5).map((m) => {\n const marker = m.id === currentModel ? '▶️ ' : '';\n return `${marker}**${m.name}** — ${code(m.id)}`;\n });\n if (providerModels.length > 5) {\n items.push(hint(`… and ${providerModels.length - 5} more in this provider`));\n }\n return joinBlocks(section(provider), bulletList(items));\n });\n\n const content = joinBlocks(\n '🤖 Available models (use the `provider/model` ref with `/switch`):',\n ...providerBlocks,\n hint('Copy the ref in backticks after each name, then: `/switch provider/model-id`.'),\n );\n\n // Create UI component if supported\n if (ctx.supports('buttons')) {\n const component: UIComponent = {\n type: 'model-picker',\n providers: Array.from(byProvider.entries()).map(([id, models]) => ({\n id,\n name: id,\n models: models.map((m) => ({\n id: m.id,\n name: m.name,\n provider: m.provider,\n })),\n })),\n currentModel,\n };\n\n return {\n content,\n success: true,\n components: [component],\n };\n }\n\n return {\n content,\n success: true,\n };\n },\n};\n\nconst switchCommand: CommandDefinition = {\n id: 'model.switch',\n name: 'switch',\n description: 'Switch model — pass the `provider/model` ref shown by /models',\n category: 'model',\n scope: ['global', 'private', 'group'],\n acceptsArgs: true,\n examples: ['/switch openai/gpt-
|
|
1
|
+
{"version":3,"file":"model.js","names":[],"sources":["../../../../src/chat-commands/builtins/model.ts"],"sourcesContent":["/**\n * Model Commands\n *\n * Built-in commands for model management:\n * - /models - List models with display names and `provider/model` refs for /switch\n * - /switch - Switch model using the ref from /models\n * - /usage - Show token usage statistics\n */\n\nimport type { CommandDefinition, CommandContext, UIComponent } from '../types.js';\nimport { commandRegistry } from '../registry.js';\nimport { bulletList, code, hint, joinBlocks, kvList, section } from '../format-output.js';\n\nconst modelsCommand: CommandDefinition = {\n id: 'model.list',\n name: 'models',\n aliases: ['model'],\n description: 'List models with display names and `provider/model` refs for /switch',\n category: 'model',\n scope: ['global', 'private', 'group'],\n handler: async (ctx: CommandContext) => {\n await ctx.setTyping(true);\n\n const models = await ctx.listModels();\n const currentModel = ctx.getCurrentModel();\n\n if (models.length === 0) {\n return {\n content: '🤖 No models available. Please check your configuration.',\n success: true,\n };\n }\n\n // Group by provider\n const byProvider = new Map<string, typeof models>();\n for (const m of models) {\n if (!byProvider.has(m.provider)) {\n byProvider.set(m.provider, []);\n }\n byProvider.get(m.provider)!.push(m);\n }\n\n /** `m.id` from listModels is always `serviceId/modelId` (canonical `/switch` ref). */\n const providerBlocks = Array.from(byProvider.entries()).map(([provider, providerModels]) => {\n const items = providerModels.slice(0, 5).map((m) => {\n const marker = m.id === currentModel ? '▶️ ' : '';\n return `${marker}**${m.name}** — ${code(m.id)}`;\n });\n if (providerModels.length > 5) {\n items.push(hint(`… and ${providerModels.length - 5} more in this provider`));\n }\n return joinBlocks(section(provider), bulletList(items));\n });\n\n const content = joinBlocks(\n '🤖 Available models (use the `provider/model` ref with `/switch`):',\n ...providerBlocks,\n hint('Copy the ref in backticks after each name, then: `/switch provider/model-id`.'),\n );\n\n // Create UI component if supported\n if (ctx.supports('buttons')) {\n const component: UIComponent = {\n type: 'model-picker',\n providers: Array.from(byProvider.entries()).map(([id, models]) => ({\n id,\n name: id,\n models: models.map((m) => ({\n id: m.id,\n name: m.name,\n provider: m.provider,\n })),\n })),\n currentModel,\n };\n\n return {\n content,\n success: true,\n components: [component],\n };\n }\n\n return {\n content,\n success: true,\n };\n },\n};\n\nconst switchCommand: CommandDefinition = {\n id: 'model.switch',\n name: 'switch',\n description: 'Switch model — pass the `provider/model` ref shown by /models',\n category: 'model',\n scope: ['global', 'private', 'group'],\n acceptsArgs: true,\n examples: ['/switch openai/gpt-5.5', '/switch minimax/minimax-m2.7'],\n handler: async (ctx: CommandContext, args: string) => {\n if (!args.trim()) {\n return {\n content: joinBlocks(\n '❌ Missing model ref.',\n '**Usage:** `/switch provider/model-id`',\n 'Run `/models` — each line shows a display name and a `provider/model` ref in backticks. Copy that ref.',\n '**Example:** `/switch openai/gpt-5.5`',\n ),\n success: false,\n };\n }\n\n await ctx.setTyping(true);\n\n const modelId = args.trim();\n const success = await ctx.switchModel(modelId);\n\n if (success) {\n const modelName = modelId.split('/').pop() || modelId;\n return {\n content: joinBlocks(\n `✅ Switched to ${code(modelId)} (${modelName}).`,\n 'This model will be used for your next message.',\n ),\n success: true,\n };\n }\n\n return {\n content: joinBlocks(\n `❌ Could not switch to ${code(modelId)}.`,\n 'Use the exact `provider/model` ref from `/models` (not only the display name).',\n '**Example:** `/switch anthropic/claude-sonnet-4-20250514`',\n ),\n success: false,\n };\n },\n};\n\nconst usageCommand: CommandDefinition = {\n id: 'model.usage',\n name: 'usage',\n description: 'Show token usage statistics for current session',\n category: 'model',\n scope: ['global', 'private', 'group'],\n handler: async (ctx: CommandContext) => {\n await ctx.setTyping(true);\n\n const stats = await ctx.getUsage();\n const modelName = ctx.getCurrentModel().split('/').pop() || 'Unknown';\n\n const content = joinBlocks(\n section('📊 Session Token Usage'),\n kvList([\n { key: 'Model', value: modelName },\n { key: 'Prompt', value: `${stats.promptTokens.toLocaleString()} tokens` },\n { key: 'Completion', value: `${stats.completionTokens.toLocaleString()} tokens` },\n { key: 'Total', value: `${stats.totalTokens.toLocaleString()} tokens` },\n { key: 'Messages', value: String(stats.messageCount) },\n ]),\n );\n\n // Create UI component if supported\n if (ctx.supports('buttons')) {\n const component: UIComponent = {\n type: 'usage-display',\n stats,\n modelName,\n };\n\n return {\n content,\n success: true,\n components: [component],\n };\n }\n\n return {\n content,\n success: true,\n };\n },\n};\n\n// Register all model commands\nexport function registerModelCommands(): void {\n commandRegistry.register(modelsCommand);\n commandRegistry.register(switchCommand);\n commandRegistry.register(usageCommand);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command-manifest.js","names":[],"sources":["../../../src/cli/command-manifest.ts"],"sourcesContent":["export type RootHelpOption = {\n flags: string;\n description: string;\n};\n\nexport type RootHelpCommand = {\n name: string;\n description: string;\n};\n\nexport const ROOT_COMMAND_DESCRIPTION = 'Ultra-Lightweight Personal AI Assistant';\n\nexport const ROOT_HELP_OPTIONS: RootHelpOption[] = [\n { flags: '-V, --version', description: 'output the version number' },\n { flags: '--verbose', description: 'Enable verbose logging (default: false)' },\n { flags: '--config <path>', description: 'Config file path' },\n { flags: '--workspace <path>', description: 'Workspace directory' },\n { flags: '-h, --help', description: 'display help for command' },\n];\n\nexport const ROOT_HELP_COMMANDS: RootHelpCommand[] = [\n { name: 'init [options]', description: 'Initialize xopc state directories, config, and agent workspace' },\n { name: 'setup [options]', description: 'Initialize config file and workspace directory' },\n { name: 'profile', description: 'Manage xopc state profiles (~/.xopc vs ~/.xopc-<name>)' },\n { name: 'onboard [options]', description: 'Interactive setup wizard for xopc (gateway uses schema defaults)' },\n { name: 'channels', description: 'Messaging channel login and credentials' },\n { name: 'auth', description: 'Manage authentication credentials' },\n { name: 'agent [options]', description: 'Chat with the AI agent' },\n { name: 'tui [options]', description: 'Interactive terminal UI (pi-tui)' },\n { name: 'tunnel', description: 'Manage FRP remote access tunnel' },\n { name: 'gateway [options]', description: 'Start the xopc gateway server' },\n { name: 'session', description: 'Session management commands' },\n { name: 'doctor [options]', description: 'Check xopc installation health and diagnose common issues' },\n { name: 'update [options]', description: 'Check for and install xopc updates' },\n { name: 'logs', description: 'Manage and query logs' },\n { name: 'cron', description: 'Manage scheduled tasks' },\n { name: 'config [options]', description: 'View and edit configuration' },\n { name: 'image', description: 'Configure image generation and understanding models' },\n { name: 'models [options]', description: 'List and manage
|
|
1
|
+
{"version":3,"file":"command-manifest.js","names":[],"sources":["../../../src/cli/command-manifest.ts"],"sourcesContent":["export type RootHelpOption = {\n flags: string;\n description: string;\n};\n\nexport type RootHelpCommand = {\n name: string;\n description: string;\n};\n\nexport const ROOT_COMMAND_DESCRIPTION = 'Ultra-Lightweight Personal AI Assistant';\n\nexport const ROOT_HELP_OPTIONS: RootHelpOption[] = [\n { flags: '-V, --version', description: 'output the version number' },\n { flags: '--verbose', description: 'Enable verbose logging (default: false)' },\n { flags: '--config <path>', description: 'Config file path' },\n { flags: '--workspace <path>', description: 'Workspace directory' },\n { flags: '-h, --help', description: 'display help for command' },\n];\n\nexport const ROOT_HELP_COMMANDS: RootHelpCommand[] = [\n { name: 'init [options]', description: 'Initialize xopc state directories, config, and agent workspace' },\n { name: 'setup [options]', description: 'Initialize config file and workspace directory' },\n { name: 'profile', description: 'Manage xopc state profiles (~/.xopc vs ~/.xopc-<name>)' },\n { name: 'onboard [options]', description: 'Interactive setup wizard for xopc (gateway uses schema defaults)' },\n { name: 'channels', description: 'Messaging channel login and credentials' },\n { name: 'auth', description: 'Manage authentication credentials' },\n { name: 'agent [options]', description: 'Chat with the AI agent' },\n { name: 'tui [options]', description: 'Interactive terminal UI (pi-tui)' },\n { name: 'tunnel', description: 'Manage FRP remote access tunnel' },\n { name: 'gateway [options]', description: 'Start the xopc gateway server' },\n { name: 'session', description: 'Session management commands' },\n { name: 'doctor [options]', description: 'Check xopc installation health and diagnose common issues' },\n { name: 'update [options]', description: 'Check for and install xopc updates' },\n { name: 'logs', description: 'Manage and query logs' },\n { name: 'cron', description: 'Manage scheduled tasks' },\n { name: 'config [options]', description: 'View and edit configuration' },\n { name: 'image', description: 'Configure image generation and understanding models' },\n { name: 'models [options]', description: 'List and manage models and model auth' },\n { name: 'providers', description: 'Manage LLM provider credentials (user-friendly hub over `xopc auth`)' },\n { name: 'voice', description: 'Configure text-to-speech (TTS) output' },\n { name: 'search', description: 'Manage web-search providers (brave / tavily / bing / searxng)' },\n { name: 'skills', description: 'Manage skills' },\n { name: 'tailscale', description: 'Tailscale status for gateway remote access' },\n { name: 'browser', description: 'Browser automation commands (uses Playwright)' },\n { name: 'agents', description: 'Manage agents (config + workspace)' },\n { name: 'extensions', description: 'Manage extensions' },\n { name: 'help [command]', description: 'display help for command' },\n];\n\nfunction formatRows(rows: Array<{ label: string; description: string }>): string {\n const labelWidth = Math.max(...rows.map((row) => row.label.length)) + 2;\n return rows.map((row) => ` ${row.label.padEnd(labelWidth)}${row.description}`).join('\\n');\n}\n\nexport function formatRootHelp(): string {\n const options = formatRows(ROOT_HELP_OPTIONS.map((option) => ({ label: option.flags, description: option.description })));\n const commands = formatRows(\n ROOT_HELP_COMMANDS.map((command) => ({ label: command.name, description: command.description })),\n );\n return `Usage: xopc [options] [command]\n\n${ROOT_COMMAND_DESCRIPTION}\n\nOptions:\n${options}\n\nCommands:\n${commands}`;\n}\n"],"mappings":";AAUA,MAAa,2BAA2B;AAExC,MAAa,oBAAsC;CACjD;EAAE,OAAO;EAAiB,aAAa;EAA6B;CACpE;EAAE,OAAO;EAAa,aAAa;EAA2C;CAC9E;EAAE,OAAO;EAAmB,aAAa;EAAoB;CAC7D;EAAE,OAAO;EAAsB,aAAa;EAAuB;CACnE;EAAE,OAAO;EAAc,aAAa;EAA4B;CACjE;AAED,MAAa,qBAAwC;CACnD;EAAE,MAAM;EAAkB,aAAa;EAAkE;CACzG;EAAE,MAAM;EAAmB,aAAa;EAAkD;CAC1F;EAAE,MAAM;EAAW,aAAa;EAA0D;CAC1F;EAAE,MAAM;EAAqB,aAAa;EAAoE;CAC9G;EAAE,MAAM;EAAY,aAAa;EAA2C;CAC5E;EAAE,MAAM;EAAQ,aAAa;EAAqC;CAClE;EAAE,MAAM;EAAmB,aAAa;EAA0B;CAClE;EAAE,MAAM;EAAiB,aAAa;EAAoC;CAC1E;EAAE,MAAM;EAAU,aAAa;EAAmC;CAClE;EAAE,MAAM;EAAqB,aAAa;EAAiC;CAC3E;EAAE,MAAM;EAAW,aAAa;EAA+B;CAC/D;EAAE,MAAM;EAAoB,aAAa;EAA6D;CACtG;EAAE,MAAM;EAAoB,aAAa;EAAsC;CAC/E;EAAE,MAAM;EAAQ,aAAa;EAAyB;CACtD;EAAE,MAAM;EAAQ,aAAa;EAA0B;CACvD;EAAE,MAAM;EAAoB,aAAa;EAA+B;CACxE;EAAE,MAAM;EAAS,aAAa;EAAuD;CACrF;EAAE,MAAM;EAAoB,aAAa;EAAyC;CAClF;EAAE,MAAM;EAAa,aAAa;EAAwE;CAC1G;EAAE,MAAM;EAAS,aAAa;EAAyC;CACvE;EAAE,MAAM;EAAU,aAAa;EAAiE;CAChG;EAAE,MAAM;EAAU,aAAa;EAAiB;CAChD;EAAE,MAAM;EAAa,aAAa;EAA8C;CAChF;EAAE,MAAM;EAAW,aAAa;EAAiD;CACjF;EAAE,MAAM;EAAU,aAAa;EAAsC;CACrE;EAAE,MAAM;EAAc,aAAa;EAAqB;CACxD;EAAE,MAAM;EAAkB,aAAa;EAA4B;CACpE;AAED,SAAS,WAAW,MAA6D;CAC/E,MAAM,aAAa,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,IAAI,MAAM,OAAO,CAAC,GAAG;AACtE,QAAO,KAAK,KAAK,QAAQ,KAAK,IAAI,MAAM,OAAO,WAAW,GAAG,IAAI,cAAc,CAAC,KAAK,KAAK;;AAG5F,SAAgB,iBAAyB;AAKvC,QAAO;;EAEP,yBAAyB;;;EANT,WAAW,kBAAkB,KAAK,YAAY;EAAE,OAAO,OAAO;EAAO,aAAa,OAAO;EAAa,EAAE,CASjH,CAAC;;;EARS,WACf,mBAAmB,KAAK,aAAa;EAAE,OAAO,QAAQ;EAAM,aAAa,QAAQ;EAAa,EAAE,CAU1F"}
|