@inetafrica/open-claudia 2.6.30 → 2.6.32
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/core/actions.js +17 -0
- package/core/auth-flow.js +11 -0
- package/core/config.js +1 -1
- package/core/handlers.js +61 -47
- package/core/router.js +9 -2
- package/core/web-auth.js +160 -0
- package/package.json +1 -1
- package/web.js +189 -14
package/core/actions.js
CHANGED
|
@@ -19,6 +19,9 @@ const { finishOnboarding } = require("./onboarding");
|
|
|
19
19
|
const { startSession } = require("./handlers");
|
|
20
20
|
const jobs = require("./jobs");
|
|
21
21
|
const scheduler = require("./scheduler");
|
|
22
|
+
const {
|
|
23
|
+
getClaudeOAuthToken, runClaudeAuthCommand, runCodexLoginStatus, runCodexDeviceLogin,
|
|
24
|
+
} = require("./auth-flow");
|
|
22
25
|
|
|
23
26
|
async function handleAction(envelope) {
|
|
24
27
|
const adapter = envelope.adapter;
|
|
@@ -251,6 +254,20 @@ async function handleAction(envelope) {
|
|
|
251
254
|
state.settings.model = model;
|
|
252
255
|
saveState();
|
|
253
256
|
const beLabel = be === "cursor" ? "Cursor Agent" : be === "codex" ? "Codex" : "Claude Code";
|
|
257
|
+
// Auth-aware: if the chosen provider isn't connected, start its login flow
|
|
258
|
+
// now. The model is already set, so once auth completes the user is on it.
|
|
259
|
+
if (be === "claude" && !getClaudeOAuthToken().value) {
|
|
260
|
+
if (!isChatOwner(envelope.channelId)) { await send("Claude isn't connected and only the owner can sign it in."); return; }
|
|
261
|
+
await send(`Set to ${model}, but Claude isn't connected yet. Starting login — open the URL I send, then paste the code here.`);
|
|
262
|
+
await runClaudeAuthCommand(["auth", "login", "--claudeai"], "Claude login");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (be === "codex" && !runCodexLoginStatus().ok) {
|
|
266
|
+
if (!isChatOwner(envelope.channelId)) { await send("Codex isn't connected and only the owner can sign it in."); return; }
|
|
267
|
+
await send(`Set to ${model}, but Codex isn't connected yet. Starting device login — open the URL and enter the code I send.`);
|
|
268
|
+
await runCodexDeviceLogin();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
254
271
|
await send(switched ? `Switched to ${beLabel}.\nModel: ${model}` : `Model: ${model}`);
|
|
255
272
|
return;
|
|
256
273
|
}
|
package/core/auth-flow.js
CHANGED
|
@@ -317,6 +317,16 @@ function runCodexLoginStatus() {
|
|
|
317
317
|
return { ...result, ok: result.ok && !isCodexAuthErrorText(result.output) };
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
// Quick "is this provider usable non-interactively" check for the unified
|
|
321
|
+
// /login model picker. Claude is cheap (token presence is what the bot relies
|
|
322
|
+
// on). Codex spawns `codex login status` once — fine for a setup screen.
|
|
323
|
+
function providerAuthStatus() {
|
|
324
|
+
return {
|
|
325
|
+
claude: !!getClaudeOAuthToken().value,
|
|
326
|
+
codex: resolvedCodexPath ? runCodexLoginStatus().ok : false,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
320
330
|
async function sendCodexAuthStatusSummary(prefix = "Codex auth status") {
|
|
321
331
|
const status = runCodexLoginStatus();
|
|
322
332
|
const version = resolvedCodexPath ? runCommand(resolvedCodexPath, ["--version"]) : { ok: false, output: "not found" };
|
|
@@ -427,6 +437,7 @@ module.exports = {
|
|
|
427
437
|
clearPendingCodexAuth,
|
|
428
438
|
runCommand,
|
|
429
439
|
runCodexLoginStatus,
|
|
440
|
+
providerAuthStatus,
|
|
430
441
|
sendCodexAuthStatusSummary,
|
|
431
442
|
saveCodexApiKeyWithCli,
|
|
432
443
|
runCodexDeviceLogin,
|
package/core/config.js
CHANGED
|
@@ -90,7 +90,7 @@ const WORKSPACE = config.WORKSPACE;
|
|
|
90
90
|
const CLAUDE_PATH = resolveExecutablePath(config.CLAUDE_PATH, null, "Claude CLI", { required: true });
|
|
91
91
|
const CURSOR_PATH = config.CURSOR_PATH || null;
|
|
92
92
|
const CODEX_PATH = config.CODEX_PATH || null;
|
|
93
|
-
const DEFAULT_CLAUDE_MODEL = config.CLAUDE_MODEL || process.env.CLAUDE_MODEL || "claude-
|
|
93
|
+
const DEFAULT_CLAUDE_MODEL = config.CLAUDE_MODEL || process.env.CLAUDE_MODEL || "claude-opus-4-8";
|
|
94
94
|
const AUTO_COMPACT_TOKENS = parseInt(config.AUTO_COMPACT_TOKENS || process.env.AUTO_COMPACT_TOKENS || "380000", 10);
|
|
95
95
|
const MIN_COMPACT_INTERVAL_MS = parseInt(config.MIN_COMPACT_INTERVAL_MS || process.env.MIN_COMPACT_INTERVAL_MS || "1800000", 10);
|
|
96
96
|
const PROJECT_TRANSCRIPTS = configTruthy(config.PROJECT_TRANSCRIPTS || process.env.PROJECT_TRANSCRIPTS, true);
|
package/core/handlers.js
CHANGED
|
@@ -38,7 +38,7 @@ const {
|
|
|
38
38
|
sendClaudeAuthStatusSummary,
|
|
39
39
|
runClaudeAuthCommand, clearPendingClaudeAuth,
|
|
40
40
|
sendCodexAuthStatusSummary, runCodexDeviceLogin, clearPendingCodexAuth,
|
|
41
|
-
saveCodexApiKeyWithCli,
|
|
41
|
+
saveCodexApiKeyWithCli, providerAuthStatus,
|
|
42
42
|
} = require("./auth-flow");
|
|
43
43
|
|
|
44
44
|
const CURRENT_VERSION = require(path.join(__dirname, "..", "package.json")).version;
|
|
@@ -126,7 +126,8 @@ register({
|
|
|
126
126
|
"Identity: /whoami /link",
|
|
127
127
|
"Team: /people /intros /auth (owner)",
|
|
128
128
|
"Automation: /cron /vault /soul /dreamsummary",
|
|
129
|
-
"
|
|
129
|
+
"Connect: /login (pick model + sign in)",
|
|
130
|
+
"Claude auth: /auth_status /setup_token /use_oauth_token /clear_oauth_token",
|
|
130
131
|
"Codex auth: /codex_auth_status /codex_login /codex_setup_token",
|
|
131
132
|
"System: /doctor /requirements /restart /upgrade",
|
|
132
133
|
"",
|
|
@@ -605,6 +606,57 @@ register({
|
|
|
605
606
|
},
|
|
606
607
|
});
|
|
607
608
|
|
|
609
|
+
// Shared model picker used by /model and the unified /login. When authAware is
|
|
610
|
+
// set, the provider section headers show connection status so the user knows a
|
|
611
|
+
// tap on an unconnected provider will start its auth flow (handled in the mb:
|
|
612
|
+
// callback in actions.js).
|
|
613
|
+
function buildModelPicker({ authAware = false } = {}) {
|
|
614
|
+
const { settings } = currentState();
|
|
615
|
+
const auth = authAware ? providerAuthStatus() : null;
|
|
616
|
+
const mark = (ok) => authAware ? (ok ? " · connected" : " · tap to connect") : "";
|
|
617
|
+
const rows = [];
|
|
618
|
+
rows.push([{ text: `── Claude${mark(auth && auth.claude)} ──`, callback_data: "noop" }]);
|
|
619
|
+
rows.push([
|
|
620
|
+
{ text: "Fable 5", callback_data: "mb:claude:claude-fable-5" },
|
|
621
|
+
{ text: "Opus 4.8", callback_data: "mb:claude:claude-opus-4-8" },
|
|
622
|
+
]);
|
|
623
|
+
rows.push([
|
|
624
|
+
{ text: "Opus 4.7", callback_data: "mb:claude:claude-opus-4-7" },
|
|
625
|
+
{ text: "Opus 4.6", callback_data: "mb:claude:claude-opus-4-6" },
|
|
626
|
+
]);
|
|
627
|
+
rows.push([
|
|
628
|
+
{ text: "Sonnet 4.6", callback_data: "mb:claude:claude-sonnet-4-6" },
|
|
629
|
+
{ text: "Haiku", callback_data: "mb:claude:claude-haiku-4-5-20251001" },
|
|
630
|
+
]);
|
|
631
|
+
if (resolvedCursorPath) {
|
|
632
|
+
rows.push([{ text: "── Cursor ──", callback_data: "noop" }]);
|
|
633
|
+
rows.push([
|
|
634
|
+
{ text: "Composer 2", callback_data: "mb:cursor:composer-2" },
|
|
635
|
+
{ text: "Composer 2 Fast", callback_data: "mb:cursor:composer-2-fast" },
|
|
636
|
+
{ text: "Auto", callback_data: "mb:cursor:auto" },
|
|
637
|
+
]);
|
|
638
|
+
rows.push([
|
|
639
|
+
{ text: "Opus 4.6 Thinking", callback_data: "mb:cursor:claude-4.6-opus-high-thinking" },
|
|
640
|
+
{ text: "GPT-5.4", callback_data: "mb:cursor:gpt-5.4-medium" },
|
|
641
|
+
]);
|
|
642
|
+
}
|
|
643
|
+
if (resolvedCodexPath) {
|
|
644
|
+
rows.push([{ text: `── Codex${mark(auth && auth.codex)} ──`, callback_data: "noop" }]);
|
|
645
|
+
rows.push([
|
|
646
|
+
{ text: "GPT-5.5", callback_data: "mb:codex:gpt-5.5" },
|
|
647
|
+
{ text: "gpt-5", callback_data: "mb:codex:gpt-5" },
|
|
648
|
+
{ text: "gpt-5-codex", callback_data: "mb:codex:gpt-5-codex" },
|
|
649
|
+
]);
|
|
650
|
+
rows.push([
|
|
651
|
+
{ text: "o3", callback_data: "mb:codex:o3" },
|
|
652
|
+
{ text: "o4-mini", callback_data: "mb:codex:o4-mini" },
|
|
653
|
+
]);
|
|
654
|
+
}
|
|
655
|
+
rows.push([{ text: `Default (Claude: ${DEFAULT_CLAUDE_MODEL})`, callback_data: "m:default" }]);
|
|
656
|
+
const beLabel = settings.backend === "cursor" ? "Cursor" : settings.backend === "codex" ? "Codex" : "Claude";
|
|
657
|
+
return { rows, beLabel, model: settings.model };
|
|
658
|
+
}
|
|
659
|
+
|
|
608
660
|
register({
|
|
609
661
|
name: "model", description: "Switch model (opus/sonnet/haiku)", args: "[<model>]",
|
|
610
662
|
handler: async (env, { tail }) => {
|
|
@@ -615,48 +667,8 @@ register({
|
|
|
615
667
|
if (settings.model === "default") settings.model = null;
|
|
616
668
|
return send(`Model: ${settings.model || "default"}`);
|
|
617
669
|
}
|
|
618
|
-
const {
|
|
619
|
-
|
|
620
|
-
rows.push([{ text: "── Claude ──", callback_data: "noop" }]);
|
|
621
|
-
rows.push([
|
|
622
|
-
{ text: "Fable 5", callback_data: "mb:claude:claude-fable-5" },
|
|
623
|
-
{ text: "Opus 4.8", callback_data: "mb:claude:claude-opus-4-8" },
|
|
624
|
-
]);
|
|
625
|
-
rows.push([
|
|
626
|
-
{ text: "Opus 4.7", callback_data: "mb:claude:claude-opus-4-7" },
|
|
627
|
-
{ text: "Opus 4.6", callback_data: "mb:claude:claude-opus-4-6" },
|
|
628
|
-
]);
|
|
629
|
-
rows.push([
|
|
630
|
-
{ text: "Sonnet 4.6", callback_data: "mb:claude:claude-sonnet-4-6" },
|
|
631
|
-
{ text: "Haiku", callback_data: "mb:claude:claude-haiku-4-5-20251001" },
|
|
632
|
-
]);
|
|
633
|
-
if (resolvedCursorPath) {
|
|
634
|
-
rows.push([{ text: "── Cursor ──", callback_data: "noop" }]);
|
|
635
|
-
rows.push([
|
|
636
|
-
{ text: "Composer 2", callback_data: "mb:cursor:composer-2" },
|
|
637
|
-
{ text: "Composer 2 Fast", callback_data: "mb:cursor:composer-2-fast" },
|
|
638
|
-
{ text: "Auto", callback_data: "mb:cursor:auto" },
|
|
639
|
-
]);
|
|
640
|
-
rows.push([
|
|
641
|
-
{ text: "Opus 4.6 Thinking", callback_data: "mb:cursor:claude-4.6-opus-high-thinking" },
|
|
642
|
-
{ text: "GPT-5.4", callback_data: "mb:cursor:gpt-5.4-medium" },
|
|
643
|
-
]);
|
|
644
|
-
}
|
|
645
|
-
if (resolvedCodexPath) {
|
|
646
|
-
rows.push([{ text: "── Codex ──", callback_data: "noop" }]);
|
|
647
|
-
rows.push([
|
|
648
|
-
{ text: "GPT-5.5", callback_data: "mb:codex:gpt-5.5" },
|
|
649
|
-
{ text: "gpt-5", callback_data: "mb:codex:gpt-5" },
|
|
650
|
-
{ text: "gpt-5-codex", callback_data: "mb:codex:gpt-5-codex" },
|
|
651
|
-
]);
|
|
652
|
-
rows.push([
|
|
653
|
-
{ text: "o3", callback_data: "mb:codex:o3" },
|
|
654
|
-
{ text: "o4-mini", callback_data: "mb:codex:o4-mini" },
|
|
655
|
-
]);
|
|
656
|
-
}
|
|
657
|
-
rows.push([{ text: `Default (Claude: ${DEFAULT_CLAUDE_MODEL})`, callback_data: "m:default" }]);
|
|
658
|
-
const beLabel = settings.backend === "cursor" ? "Cursor" : settings.backend === "codex" ? "Codex" : "Claude";
|
|
659
|
-
send(`Current: ${beLabel} · ${settings.model || "default"}\n\nPick a model — backend switches automatically.\nOr type /model <name>.`, { keyboard: { inline_keyboard: rows } });
|
|
670
|
+
const { rows, beLabel, model } = buildModelPicker();
|
|
671
|
+
send(`Current: ${beLabel} · ${model || "default"}\n\nPick a model — backend switches automatically.\nOr type /model <name>.`, { keyboard: { inline_keyboard: rows } });
|
|
660
672
|
},
|
|
661
673
|
});
|
|
662
674
|
|
|
@@ -1148,10 +1160,12 @@ register({
|
|
|
1148
1160
|
});
|
|
1149
1161
|
|
|
1150
1162
|
register({
|
|
1151
|
-
name: "login", description: "
|
|
1163
|
+
name: "login", description: "Pick a model and connect its provider", ownerOnly: true,
|
|
1152
1164
|
handler: async (env) => {
|
|
1153
|
-
if (!ownerEnv(env)) return send("Owner only —
|
|
1154
|
-
await
|
|
1165
|
+
if (!ownerEnv(env)) return send("Owner only — provider auth is shared across users.");
|
|
1166
|
+
await send("Connecting… checking which providers are signed in.");
|
|
1167
|
+
const { rows, beLabel, model } = buildModelPicker({ authAware: true });
|
|
1168
|
+
send(`Current: ${beLabel} · ${model || "default"}\n\nPick a model. If its provider isn't connected yet, tapping it starts the login flow — then you're on that model.`, { keyboard: { inline_keyboard: rows } });
|
|
1155
1169
|
},
|
|
1156
1170
|
});
|
|
1157
1171
|
|
package/core/router.js
CHANGED
|
@@ -228,10 +228,17 @@ async function handleText(envelope) {
|
|
|
228
228
|
if (state.pendingClaudeAuthProcess && state.pendingClaudeAuthLabel !== "manual OAuth token save") {
|
|
229
229
|
const text = (envelope.text || "").trim();
|
|
230
230
|
if (looksLikeClaudeAuthReply(text)) {
|
|
231
|
-
await
|
|
231
|
+
await deleteMessage(envelope.messageId);
|
|
232
|
+
try {
|
|
233
|
+
state.pendingClaudeAuthProcess.stdin.write(text + "\n");
|
|
234
|
+
await send("Got it — sent the code to Claude. I'll confirm with an auth-status check when it finishes.");
|
|
235
|
+
} catch (e) {
|
|
236
|
+
clearPendingClaudeAuth(state);
|
|
237
|
+
await send(`Could not send the auth code to Claude: ${redactSensitive(e.message)}`);
|
|
238
|
+
}
|
|
232
239
|
return;
|
|
233
240
|
}
|
|
234
|
-
await send("Claude login is still waiting.
|
|
241
|
+
await send("Claude login is still waiting. Paste the code/callback Claude gave you and I'll submit it, or send /cancel_auth to stop.");
|
|
235
242
|
}
|
|
236
243
|
|
|
237
244
|
if (!isOnboarded() && state.onboardingStep) {
|
package/core/web-auth.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Web-driven Claude/Codex authentication. Runs the same interactive CLIs
|
|
2
|
+
// the Telegram /login flow uses, but captures the URL/output into in-memory
|
|
3
|
+
// state that the web dashboard polls, instead of pushing it to a chat.
|
|
4
|
+
// A single admin owns the dashboard, so one module-level slot per provider
|
|
5
|
+
// is enough. Lives in-process with the bot (web.js is started by bot.js).
|
|
6
|
+
|
|
7
|
+
const { spawn } = require("child_process");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const { CLAUDE_PATH, resolvedCodexPath, botSubprocessEnv } = require("./config");
|
|
10
|
+
const { redactSensitive, stripTerminalControls, extractUrls } = require("./redact");
|
|
11
|
+
const {
|
|
12
|
+
claudeSubprocessEnv, saveClaudeOAuthToken, getClaudeOAuthToken,
|
|
13
|
+
extractClaudeToken, isClaudeAuthUrl, looksLikeClaudeToken, looksLikeOpenAIKey,
|
|
14
|
+
runClaudeAuthStatusDiagnostic, isClaudeAuthErrorText,
|
|
15
|
+
runCodexLoginStatus, saveCodexApiKeyWithCli,
|
|
16
|
+
} = require("./auth-flow");
|
|
17
|
+
|
|
18
|
+
const HOME = process.env.HOME || os.homedir();
|
|
19
|
+
|
|
20
|
+
// ── Claude interactive browser login ────────────────────────────────
|
|
21
|
+
let claude = null;
|
|
22
|
+
|
|
23
|
+
function startClaudeLogin() {
|
|
24
|
+
if (claude && claude.proc && !claude.done) return { ok: true, already: true };
|
|
25
|
+
const proc = spawn(CLAUDE_PATH, ["auth", "login", "--claudeai"], {
|
|
26
|
+
cwd: HOME, env: claudeSubprocessEnv(), stdio: ["pipe", "pipe", "pipe"],
|
|
27
|
+
});
|
|
28
|
+
claude = { proc, url: null, log: "", awaitingCode: false, done: false, ok: false, error: null, tokenStored: false };
|
|
29
|
+
const onChunk = (buf) => {
|
|
30
|
+
const chunk = buf.toString();
|
|
31
|
+
claude.log = (claude.log + chunk).slice(-4000);
|
|
32
|
+
const token = extractClaudeToken(chunk) || extractClaudeToken(claude.log);
|
|
33
|
+
if (token && !claude.tokenStored) claude.tokenStored = saveClaudeOAuthToken(token);
|
|
34
|
+
for (const u of extractUrls(chunk)) {
|
|
35
|
+
if (isClaudeAuthUrl(u) && !claude.url) { claude.url = u; claude.awaitingCode = true; }
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
proc.stdout.on("data", onChunk);
|
|
39
|
+
proc.stderr.on("data", onChunk);
|
|
40
|
+
proc.on("close", (code) => {
|
|
41
|
+
claude.done = true;
|
|
42
|
+
claude.awaitingCode = false;
|
|
43
|
+
const tty = /raw mode is not supported|process\.stdin|not a tty|inappropriate ioctl/i.test(claude.log);
|
|
44
|
+
claude.ok = claude.tokenStored || code === 0;
|
|
45
|
+
if (!claude.ok) claude.error = tty ? "tty" : "failed";
|
|
46
|
+
});
|
|
47
|
+
proc.on("error", (err) => { claude.done = true; claude.ok = false; claude.error = redactSensitive(err.message); });
|
|
48
|
+
return { ok: true };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function submitClaudeCode(code) {
|
|
52
|
+
if (!claude || !claude.proc || claude.done) return { ok: false, error: "No Claude login is in progress." };
|
|
53
|
+
try {
|
|
54
|
+
claude.proc.stdin.write(String(code || "").trim() + "\n");
|
|
55
|
+
claude.awaitingCode = false;
|
|
56
|
+
return { ok: true };
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return { ok: false, error: redactSensitive(e.message) };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function cancelClaude() {
|
|
63
|
+
if (claude && claude.proc && claude.proc.kill) { try { claude.proc.kill("SIGTERM"); } catch (e) {} }
|
|
64
|
+
claude = null;
|
|
65
|
+
return { ok: true };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function claudeState() {
|
|
69
|
+
if (!claude) return { running: false };
|
|
70
|
+
return {
|
|
71
|
+
running: !claude.done,
|
|
72
|
+
url: claude.url,
|
|
73
|
+
awaitingCode: claude.awaitingCode,
|
|
74
|
+
done: claude.done,
|
|
75
|
+
ok: claude.ok,
|
|
76
|
+
error: claude.error,
|
|
77
|
+
log: redactSensitive(stripTerminalControls(claude.log)).slice(-600),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Reliable path: save a pasted Claude OAuth token directly.
|
|
82
|
+
function saveClaudeToken(token) {
|
|
83
|
+
const t = String(token || "").trim();
|
|
84
|
+
if (!looksLikeClaudeToken(t)) return { ok: false, error: "That does not look like a Claude OAuth token." };
|
|
85
|
+
return saveClaudeOAuthToken(t) ? { ok: true } : { ok: false, error: "Could not store the token." };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Codex device login ──────────────────────────────────────────────
|
|
89
|
+
let codex = null;
|
|
90
|
+
|
|
91
|
+
function startCodexLogin() {
|
|
92
|
+
if (!resolvedCodexPath) return { ok: false, error: "Codex CLI not found." };
|
|
93
|
+
if (codex && codex.proc && !codex.done) return { ok: true, already: true };
|
|
94
|
+
const proc = spawn(resolvedCodexPath, ["login", "--device-auth"], {
|
|
95
|
+
cwd: HOME, env: botSubprocessEnv(), stdio: ["pipe", "pipe", "pipe"],
|
|
96
|
+
});
|
|
97
|
+
codex = { proc, url: null, code: null, log: "", done: false, ok: false, error: null };
|
|
98
|
+
const onChunk = (buf) => {
|
|
99
|
+
const clean = redactSensitive(stripTerminalControls(buf.toString()));
|
|
100
|
+
codex.log = (codex.log + clean).slice(-4000);
|
|
101
|
+
for (const u of extractUrls(clean)) { if (!codex.url) codex.url = u; }
|
|
102
|
+
const m = clean.match(/(?:code|device code)[:\s]+([A-Z0-9-]{6,})/i);
|
|
103
|
+
if (m && !codex.code) codex.code = m[1];
|
|
104
|
+
};
|
|
105
|
+
proc.stdout.on("data", onChunk);
|
|
106
|
+
proc.stderr.on("data", onChunk);
|
|
107
|
+
proc.on("close", (code) => {
|
|
108
|
+
codex.done = true;
|
|
109
|
+
const tty = /raw mode is not supported|not a tty|inappropriate ioctl/i.test(codex.log);
|
|
110
|
+
codex.ok = code === 0;
|
|
111
|
+
if (!codex.ok) codex.error = tty ? "tty" : "failed";
|
|
112
|
+
});
|
|
113
|
+
proc.on("error", (err) => { codex.done = true; codex.ok = false; codex.error = redactSensitive(err.message); });
|
|
114
|
+
return { ok: true };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function cancelCodex() {
|
|
118
|
+
if (codex && codex.proc && codex.proc.kill) { try { codex.proc.kill("SIGTERM"); } catch (e) {} }
|
|
119
|
+
codex = null;
|
|
120
|
+
return { ok: true };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function codexState() {
|
|
124
|
+
if (!codex) return { running: false };
|
|
125
|
+
return {
|
|
126
|
+
running: !codex.done,
|
|
127
|
+
url: codex.url,
|
|
128
|
+
code: codex.code,
|
|
129
|
+
done: codex.done,
|
|
130
|
+
ok: codex.ok,
|
|
131
|
+
error: codex.error,
|
|
132
|
+
log: codex.log.slice(-600),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Reliable path: save a pasted OpenAI API key via the Codex CLI.
|
|
137
|
+
async function saveCodexApiKey(key) {
|
|
138
|
+
if (!resolvedCodexPath) return { ok: false, error: "Codex CLI not found." };
|
|
139
|
+
const k = String(key || "").trim();
|
|
140
|
+
if (!looksLikeOpenAIKey(k)) return { ok: false, error: "That does not look like an OpenAI API key." };
|
|
141
|
+
const result = await saveCodexApiKeyWithCli(k);
|
|
142
|
+
return result.ok ? { ok: true } : { ok: false, error: redactSensitive(result.output).slice(-300) };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Combined status ─────────────────────────────────────────────────
|
|
146
|
+
function authStatus() {
|
|
147
|
+
const tokenInfo = getClaudeOAuthToken();
|
|
148
|
+
const diag = runClaudeAuthStatusDiagnostic();
|
|
149
|
+
const codexStatus = resolvedCodexPath ? runCodexLoginStatus() : { ok: false };
|
|
150
|
+
return {
|
|
151
|
+
claude: { configured: !!tokenInfo.value, source: tokenInfo.source, loggedIn: !isClaudeAuthErrorText(diag) },
|
|
152
|
+
codex: { cliFound: !!resolvedCodexPath, loggedIn: !!codexStatus.ok },
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
startClaudeLogin, submitClaudeCode, cancelClaude, claudeState, saveClaudeToken,
|
|
158
|
+
startCodexLogin, cancelCodex, codexState, saveCodexApiKey,
|
|
159
|
+
authStatus,
|
|
160
|
+
};
|
package/package.json
CHANGED
package/web.js
CHANGED
|
@@ -15,6 +15,7 @@ const ENV_FILE = path.join(CONFIG_DIR, ".env");
|
|
|
15
15
|
const AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
|
|
16
16
|
const SOUL_FILE = path.join(CONFIG_DIR, "soul.md");
|
|
17
17
|
const CRONS_FILE = path.join(CONFIG_DIR, "crons.json");
|
|
18
|
+
const JOBS_FILE = path.join(CONFIG_DIR, "jobs.json");
|
|
18
19
|
const SESSIONS_FILE = path.join(CONFIG_DIR, "sessions.json");
|
|
19
20
|
const WEB_PASSWORD_FILE = path.join(CONFIG_DIR, ".web-password");
|
|
20
21
|
const PORT = parseInt(process.env.WEB_PORT || "8080", 10);
|
|
@@ -173,6 +174,13 @@ function loadSoul() {
|
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
function loadCrons() {
|
|
177
|
+
// Crons now live in the unified jobs store (jobs.json), tagged kind:"cron".
|
|
178
|
+
// The legacy crons.json was migrated and renamed, so read jobs first and
|
|
179
|
+
// only fall back to the old file for un-migrated installs.
|
|
180
|
+
try {
|
|
181
|
+
const jobs = JSON.parse(fs.readFileSync(JOBS_FILE, "utf-8"));
|
|
182
|
+
if (Array.isArray(jobs)) return jobs.filter((j) => j && j.kind === "cron");
|
|
183
|
+
} catch (e) { /* fall through to legacy */ }
|
|
176
184
|
try { return JSON.parse(fs.readFileSync(CRONS_FILE, "utf-8")); } catch (e) { return []; }
|
|
177
185
|
}
|
|
178
186
|
|
|
@@ -261,6 +269,24 @@ async function handleAPI(req, res, body) {
|
|
|
261
269
|
return res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
262
270
|
}
|
|
263
271
|
|
|
272
|
+
// ── Authentication (Claude / Codex) ──
|
|
273
|
+
if (url.startsWith("/api/auth/")) {
|
|
274
|
+
const webAuth = require("./core/web-auth");
|
|
275
|
+
const json = (obj, c = 200) => { res.writeHead(c, { "Content-Type": "application/json" }); return res.end(JSON.stringify(obj)); };
|
|
276
|
+
const post = req.method === "POST";
|
|
277
|
+
if (url === "/api/auth/status") return json(webAuth.authStatus());
|
|
278
|
+
if (url === "/api/auth/claude/start" && post) return json(webAuth.startClaudeLogin());
|
|
279
|
+
if (url === "/api/auth/claude/state") return json(webAuth.claudeState());
|
|
280
|
+
if (url === "/api/auth/claude/code" && post) return json(webAuth.submitClaudeCode(JSON.parse(body || "{}").code));
|
|
281
|
+
if (url === "/api/auth/claude/token" && post) return json(webAuth.saveClaudeToken(JSON.parse(body || "{}").token));
|
|
282
|
+
if (url === "/api/auth/claude/cancel" && post) return json(webAuth.cancelClaude());
|
|
283
|
+
if (url === "/api/auth/codex/start" && post) return json(webAuth.startCodexLogin());
|
|
284
|
+
if (url === "/api/auth/codex/state") return json(webAuth.codexState());
|
|
285
|
+
if (url === "/api/auth/codex/key" && post) return json(await webAuth.saveCodexApiKey(JSON.parse(body || "{}").key));
|
|
286
|
+
if (url === "/api/auth/codex/cancel" && post) return json(webAuth.cancelCodex());
|
|
287
|
+
return json({ error: "Unknown auth route" }, 404);
|
|
288
|
+
}
|
|
289
|
+
|
|
264
290
|
// Status
|
|
265
291
|
if (url === "/api/status") {
|
|
266
292
|
const env = loadEnv();
|
|
@@ -489,13 +515,30 @@ async function handleAPI(req, res, body) {
|
|
|
489
515
|
|
|
490
516
|
// ── HTML UI ────────────────────────────────────────────────────────
|
|
491
517
|
|
|
518
|
+
// The dashboard is generic "Open Claudia" by default, but each pod is its
|
|
519
|
+
// own named bot (e.g. euvy). Prefer an explicit BOT_NAME, else derive the
|
|
520
|
+
// pod name from its public hostname, else fall back to the generic brand.
|
|
521
|
+
function botDisplayName() {
|
|
522
|
+
if (process.env.BOT_NAME && process.env.BOT_NAME.trim()) return process.env.BOT_NAME.trim();
|
|
523
|
+
try {
|
|
524
|
+
const u = process.env.WEB_PUBLIC_URL;
|
|
525
|
+
if (u) {
|
|
526
|
+
const host = new URL(u).hostname;
|
|
527
|
+
const label = host.split(".")[0];
|
|
528
|
+
if (label) return label;
|
|
529
|
+
}
|
|
530
|
+
} catch (e) { /* ignore */ }
|
|
531
|
+
return "Open Claudia";
|
|
532
|
+
}
|
|
533
|
+
|
|
492
534
|
function getHTML() {
|
|
535
|
+
const botName = botDisplayName();
|
|
493
536
|
return `<!DOCTYPE html>
|
|
494
537
|
<html lang="en">
|
|
495
538
|
<head>
|
|
496
539
|
<meta charset="UTF-8">
|
|
497
540
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
498
|
-
<title
|
|
541
|
+
<title>${botName}</title>
|
|
499
542
|
<style>
|
|
500
543
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
501
544
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f0f0f; color: #e0e0e0; min-height: 100vh; }
|
|
@@ -570,7 +613,7 @@ async function init() {
|
|
|
570
613
|
function showLogin() {
|
|
571
614
|
$("#app").innerHTML = \`
|
|
572
615
|
<div class="login-page"><div class="login-box">
|
|
573
|
-
<h1
|
|
616
|
+
<h1>${botName}</h1>
|
|
574
617
|
<p class="subtitle">Enter your admin password</p>
|
|
575
618
|
<div id="login-msg"></div>
|
|
576
619
|
<div class="card">
|
|
@@ -625,6 +668,7 @@ function render() {
|
|
|
625
668
|
{ id: "dashboard", label: "Dashboard" },
|
|
626
669
|
{ id: "usage", label: "Usage" },
|
|
627
670
|
{ id: "auth", label: "Users" },
|
|
671
|
+
{ id: "credentials", label: "Credentials" },
|
|
628
672
|
{ id: "soul", label: "Soul" },
|
|
629
673
|
{ id: "crons", label: "Crons" },
|
|
630
674
|
{ id: "sessions", label: "Sessions" },
|
|
@@ -637,7 +681,7 @@ function render() {
|
|
|
637
681
|
}
|
|
638
682
|
$("#app").innerHTML = \`
|
|
639
683
|
<div class="container">
|
|
640
|
-
<h1
|
|
684
|
+
<h1>${botName}</h1>
|
|
641
685
|
<p class="subtitle">v\${status.version}</p>
|
|
642
686
|
<div class="tabs">
|
|
643
687
|
\${tabs.map(t => \`<div class="tab \${currentTab===t.id?'active':''}" onclick="switchTab('\${t.id}')">\${t.label}</div>\`).join("")}
|
|
@@ -724,6 +768,34 @@ async function loadTab() {
|
|
|
724
768
|
</div>\`).join("")}
|
|
725
769
|
</div>\` : ""}\`;
|
|
726
770
|
}
|
|
771
|
+
else if (currentTab === "credentials") {
|
|
772
|
+
el.innerHTML = \`
|
|
773
|
+
<div class="card" id="cred-status"><h2>Credentials</h2><p class="subtitle">Loading status…</p></div>
|
|
774
|
+
<div class="card">
|
|
775
|
+
<h2>Claude</h2>
|
|
776
|
+
<button onclick="startClaudeAuth()">Authenticate via browser</button>
|
|
777
|
+
<div id="claude-flow" style="margin-top:12px;"></div>
|
|
778
|
+
<div style="margin-top:16px;border-top:1px solid #2a2a2a;padding-top:16px;">
|
|
779
|
+
<label>Or paste a Claude OAuth token</label>
|
|
780
|
+
<div style="color:#777;font-size:11px;margin:2px 0 6px;">Generate with <code>claude setup-token</code> in a terminal if the browser flow can't finish here.</div>
|
|
781
|
+
<input id="claude-token" type="password" placeholder="sk-ant-...">
|
|
782
|
+
<button onclick="saveClaudeTokenWeb()" style="margin-top:8px;">Save token</button>
|
|
783
|
+
<span id="claude-token-msg" style="margin-left:10px;font-size:12px;color:#888;"></span>
|
|
784
|
+
</div>
|
|
785
|
+
</div>
|
|
786
|
+
<div class="card">
|
|
787
|
+
<h2>Codex</h2>
|
|
788
|
+
<button onclick="startCodexAuth()">Authenticate via device</button>
|
|
789
|
+
<div id="codex-flow" style="margin-top:12px;"></div>
|
|
790
|
+
<div style="margin-top:16px;border-top:1px solid #2a2a2a;padding-top:16px;">
|
|
791
|
+
<label>Or paste an OpenAI API key</label>
|
|
792
|
+
<input id="codex-key" type="password" placeholder="sk-...">
|
|
793
|
+
<button onclick="saveCodexKeyWeb()" style="margin-top:8px;">Save key</button>
|
|
794
|
+
<span id="codex-key-msg" style="margin-left:10px;font-size:12px;color:#888;"></span>
|
|
795
|
+
</div>
|
|
796
|
+
</div>\`;
|
|
797
|
+
loadCredStatus();
|
|
798
|
+
}
|
|
727
799
|
else if (currentTab === "soul") {
|
|
728
800
|
const soul = await api("/api/soul");
|
|
729
801
|
if (!soul) return;
|
|
@@ -789,23 +861,44 @@ async function loadTab() {
|
|
|
789
861
|
</div>
|
|
790
862
|
<div class="card">
|
|
791
863
|
<h2>Configuration</h2>
|
|
792
|
-
<p style="color:#888;font-size:13px;margin-bottom:
|
|
864
|
+
<p style="color:#888;font-size:13px;margin-bottom:16px;">Changes are saved to the bot's environment and take effect after the next restart.</p>
|
|
793
865
|
<div id="config-fields"></div>
|
|
794
866
|
<button onclick="saveConfig()">Save</button>
|
|
795
867
|
<span id="config-msg" style="margin-left:12px;font-size:13px;color:#888;"></span>
|
|
796
868
|
</div>\`;
|
|
797
869
|
const config = await api("/api/config");
|
|
798
870
|
if (!config) return;
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
"
|
|
871
|
+
const groups = [
|
|
872
|
+
{ title: "General", fields: [
|
|
873
|
+
{ k: "WORKSPACE", label: "Workspace folder", desc: "Root directory where the bot reads and writes project files.", ph: "/data/Workspace" },
|
|
874
|
+
{ k: "CLAUDE_PATH", label: "Claude CLI path", desc: "Path or command name for the Claude Code CLI.", ph: "claude" },
|
|
875
|
+
]},
|
|
876
|
+
{ title: "Voice transcription", fields: [
|
|
877
|
+
{ k: "WHISPER_CLI", label: "Whisper CLI", desc: "Path to the whisper binary for transcribing voice notes. Leave blank to disable voice.", ph: "whisper" },
|
|
878
|
+
{ k: "FFMPEG", label: "ffmpeg path", desc: "Path to ffmpeg, used to convert incoming voice audio.", ph: "ffmpeg" },
|
|
879
|
+
{ k: "WHISPER_MODEL", label: "Whisper model", desc: "Transcription model size, e.g. base / small / medium.", ph: "base" },
|
|
880
|
+
]},
|
|
881
|
+
{ title: "Memory", fields: [
|
|
882
|
+
{ k: "MEMORY_RECALL_MAX_CHARS", label: "Memory recall limit", desc: "Max characters of memory injected into context per turn.", ph: "e.g. 6000" },
|
|
883
|
+
]},
|
|
884
|
+
{ title: "Usage alerts (advanced)", fields: [
|
|
885
|
+
{ k: "USAGE_ALERT_CONTEXT_TOKENS", label: "Context-token threshold", desc: "Warn when a turn's context exceeds this many tokens.", ph: "e.g. 150000" },
|
|
886
|
+
{ k: "USAGE_ALERT_RATE_MULTIPLIER", label: "Rate multiplier", desc: "Warn when usage exceeds the recent baseline by this factor.", ph: "e.g. 2" },
|
|
887
|
+
{ k: "USAGE_ALERT_BASELINE_TURNS", label: "Baseline window (turns)", desc: "How many recent turns form the usage baseline.", ph: "e.g. 20" },
|
|
888
|
+
{ k: "USAGE_ALERT_MIN_BASELINE_TURNS", label: "Min baseline turns", desc: "Minimum turns required before alerts activate.", ph: "e.g. 5" },
|
|
889
|
+
{ k: "USAGE_ALERT_COOLDOWN_MS", label: "Alert cooldown (ms)", desc: "Minimum gap between repeated usage alerts.", ph: "e.g. 300000" },
|
|
890
|
+
]},
|
|
805
891
|
];
|
|
806
|
-
$("#config-fields").innerHTML =
|
|
807
|
-
<div
|
|
808
|
-
|
|
892
|
+
$("#config-fields").innerHTML = groups.map(g => \`
|
|
893
|
+
<div style="margin-bottom:20px;">
|
|
894
|
+
<div style="font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:#9a9aff;border-bottom:1px solid #2a2a2a;padding-bottom:6px;margin-bottom:12px;">\${g.title}</div>
|
|
895
|
+
\${g.fields.map(f => \`
|
|
896
|
+
<div class="form-group">
|
|
897
|
+
<label>\${f.label}</label>
|
|
898
|
+
<div style="color:#777;font-size:11px;margin:2px 0 6px;">\${f.desc}</div>
|
|
899
|
+
<input id="cfg-\${f.k}" value="\${config[f.k] || ""}" placeholder="\${f.ph || ""}">
|
|
900
|
+
</div>\`).join("")}
|
|
901
|
+
</div>\`).join("");
|
|
809
902
|
}
|
|
810
903
|
}
|
|
811
904
|
|
|
@@ -878,6 +971,88 @@ async function removeAuth(chatId) { await api("/api/auth/remove", { method: "POS
|
|
|
878
971
|
async function approveAuth(chatId) { await api("/api/auth/approve", { method: "POST", body: { chatId } }); loadTab(); }
|
|
879
972
|
async function denyAuth(chatId) { await api("/api/auth/deny", { method: "POST", body: { chatId } }); loadTab(); }
|
|
880
973
|
|
|
974
|
+
async function loadCredStatus() {
|
|
975
|
+
const s = await api("/api/auth/status");
|
|
976
|
+
if (!s) return;
|
|
977
|
+
const yn = (b) => b ? "<span style='color:#22c55e'>yes</span>" : "<span style='color:#dc2626'>no</span>";
|
|
978
|
+
$("#cred-status").innerHTML = \`<h2>Credentials</h2>
|
|
979
|
+
<div class="list-item"><span>Claude OAuth token saved</span><span>\${yn(s.claude.configured)}</span></div>
|
|
980
|
+
<div class="list-item"><span>Claude logged in</span><span>\${yn(s.claude.loggedIn)}</span></div>
|
|
981
|
+
<div class="list-item"><span>Codex CLI present</span><span>\${yn(s.codex.cliFound)}</span></div>
|
|
982
|
+
<div class="list-item"><span>Codex logged in</span><span>\${yn(s.codex.loggedIn)}</span></div>\`;
|
|
983
|
+
}
|
|
984
|
+
async function startClaudeAuth() {
|
|
985
|
+
$("#claude-flow").innerHTML = "<p class='subtitle'>Starting…</p>";
|
|
986
|
+
await api("/api/auth/claude/start", { method: "POST" });
|
|
987
|
+
pollClaudeAuth();
|
|
988
|
+
}
|
|
989
|
+
async function pollClaudeAuth() {
|
|
990
|
+
const st = await api("/api/auth/claude/state");
|
|
991
|
+
if (!st) return;
|
|
992
|
+
const box = $("#claude-flow");
|
|
993
|
+
if (st.done) {
|
|
994
|
+
box.innerHTML = st.ok ? "<div class='msg ok'>Claude authenticated.</div>"
|
|
995
|
+
: (st.error === "tty" ? "<div class='msg err'>The Claude CLI needs a real terminal here. Paste an OAuth token below instead.</div>"
|
|
996
|
+
: "<div class='msg err'>Login didn't complete. Paste a token below instead.</div>");
|
|
997
|
+
loadCredStatus();
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (st.url) {
|
|
1001
|
+
box.innerHTML = \`<p style="font-size:13px;">Open this URL, sign in, then paste the code Claude gives you:</p>
|
|
1002
|
+
<p style="word-break:break-all;"><a href="\${st.url}" target="_blank">\${st.url}</a></p>
|
|
1003
|
+
<input id="claude-code" placeholder="Paste code here">
|
|
1004
|
+
<button onclick="submitClaudeCodeWeb()" style="margin-top:8px;">Submit code</button>\`;
|
|
1005
|
+
} else {
|
|
1006
|
+
box.innerHTML = "<p class='subtitle'>Starting…</p>";
|
|
1007
|
+
}
|
|
1008
|
+
setTimeout(pollClaudeAuth, 2000);
|
|
1009
|
+
}
|
|
1010
|
+
async function submitClaudeCodeWeb() {
|
|
1011
|
+
const code = $("#claude-code").value;
|
|
1012
|
+
await api("/api/auth/claude/code", { method: "POST", body: { code } });
|
|
1013
|
+
$("#claude-flow").innerHTML = "<p class='subtitle'>Submitting…</p>";
|
|
1014
|
+
setTimeout(pollClaudeAuth, 1500);
|
|
1015
|
+
}
|
|
1016
|
+
async function saveClaudeTokenWeb() {
|
|
1017
|
+
const token = $("#claude-token").value;
|
|
1018
|
+
const r = await api("/api/auth/claude/token", { method: "POST", body: { token } });
|
|
1019
|
+
$("#claude-token-msg").textContent = r && r.ok ? "Saved — restart to apply." : ((r && r.error) || "Failed");
|
|
1020
|
+
if (r && r.ok) { $("#claude-token").value = ""; loadCredStatus(); }
|
|
1021
|
+
}
|
|
1022
|
+
async function startCodexAuth() {
|
|
1023
|
+
$("#codex-flow").innerHTML = "<p class='subtitle'>Starting…</p>";
|
|
1024
|
+
const r = await api("/api/auth/codex/start", { method: "POST" });
|
|
1025
|
+
if (r && r.error) { $("#codex-flow").innerHTML = "<div class='msg err'>" + r.error + "</div>"; return; }
|
|
1026
|
+
pollCodexAuth();
|
|
1027
|
+
}
|
|
1028
|
+
async function pollCodexAuth() {
|
|
1029
|
+
const st = await api("/api/auth/codex/state");
|
|
1030
|
+
if (!st) return;
|
|
1031
|
+
const box = $("#codex-flow");
|
|
1032
|
+
if (st.done) {
|
|
1033
|
+
box.innerHTML = st.ok ? "<div class='msg ok'>Codex authenticated.</div>"
|
|
1034
|
+
: (st.error === "tty" ? "<div class='msg err'>The Codex CLI needs a real terminal here. Paste an API key below instead.</div>"
|
|
1035
|
+
: "<div class='msg err'>Login didn't complete. Paste an API key below instead.</div>");
|
|
1036
|
+
loadCredStatus();
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (st.url) {
|
|
1040
|
+
box.innerHTML = \`<p style="font-size:13px;">Open this URL and enter the code:</p>
|
|
1041
|
+
<p style="word-break:break-all;"><a href="\${st.url}" target="_blank">\${st.url}</a></p>
|
|
1042
|
+
\${st.code ? "<p>Device code: <b>" + st.code + "</b></p>" : ""}
|
|
1043
|
+
<p class='subtitle'>Waiting for you to finish in the browser…</p>\`;
|
|
1044
|
+
} else {
|
|
1045
|
+
box.innerHTML = "<p class='subtitle'>Starting…</p>";
|
|
1046
|
+
}
|
|
1047
|
+
setTimeout(pollCodexAuth, 2000);
|
|
1048
|
+
}
|
|
1049
|
+
async function saveCodexKeyWeb() {
|
|
1050
|
+
const key = $("#codex-key").value;
|
|
1051
|
+
const r = await api("/api/auth/codex/key", { method: "POST", body: { key } });
|
|
1052
|
+
$("#codex-key-msg").textContent = r && r.ok ? "Saved." : ((r && r.error) || "Failed");
|
|
1053
|
+
if (r && r.ok) { $("#codex-key").value = ""; loadCredStatus(); }
|
|
1054
|
+
}
|
|
1055
|
+
|
|
881
1056
|
async function saveSoul() {
|
|
882
1057
|
await api("/api/soul", { method: "POST", body: { content: $("#soul-content").value } });
|
|
883
1058
|
$("#soul-msg").textContent = "Saved";
|
|
@@ -903,7 +1078,7 @@ async function saveConfig() {
|
|
|
903
1078
|
function renderSetup() {
|
|
904
1079
|
$("#app").innerHTML = \`
|
|
905
1080
|
<div class="container">
|
|
906
|
-
<h1
|
|
1081
|
+
<h1>${botName} Setup</h1>
|
|
907
1082
|
<p class="subtitle">Configure your AI coding assistant</p>
|
|
908
1083
|
<div id="setup-msg"></div>
|
|
909
1084
|
<div class="card">
|