@inetafrica/open-claudia 1.13.4 → 1.14.1
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/CHANGELOG.md +6 -0
- package/README.md +15 -1
- package/bot-agent.js +420 -6
- package/bot.js +414 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.14.0
|
|
4
|
+
- Claude Code auth management from Telegram: `/auth_status`, `/login`, `/setup_token`, `/use_oauth_token`, and `/clear_oauth_token`
|
|
5
|
+
- Claude subprocesses now receive `CLAUDE_CODE_OAUTH_TOKEN` from config/env/vault when available, avoiding launchd/macOS Keychain auth failures
|
|
6
|
+
- Sensitive Claude tokens are redacted from Telegram output and logs
|
|
7
|
+
- Package lock metadata corrected to match `@inetafrica/open-claudia`
|
|
8
|
+
|
|
3
9
|
## v1.12.0
|
|
4
10
|
- Cursor Agent tool progress: Shell, Read, Edit, Write, Grep, Glob calls now show in real-time Telegram updates
|
|
5
11
|
- Plan output surfaced to Telegram: when Cursor creates a plan (`--mode plan`), the full plan markdown and task list are sent to the user
|
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ agent login # Opens browser to authenticate
|
|
|
55
55
|
agent status # Verify: should show your email and plan
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
> **Important**:
|
|
58
|
+
> **Important**: Claude Code can use macOS Keychain when you log in interactively, but a launchd/background bot may not be able to read that Keychain session. Open Claudia v1.14.0 adds Telegram auth helpers and supports `CLAUDE_CODE_OAUTH_TOKEN` for non-interactive Claude runs. Prefer `/setup_token` then `/use_oauth_token` if Telegram shows Claude auth/keychain errors.
|
|
59
59
|
|
|
60
60
|
### 2. Install Open Claudia
|
|
61
61
|
|
|
@@ -141,6 +141,20 @@ When you select a project, the last conversation is automatically resumed. Tap "
|
|
|
141
141
|
| `/vault` | Manage encrypted credentials (password required) |
|
|
142
142
|
| `/soul` | View/edit assistant identity and personality |
|
|
143
143
|
|
|
144
|
+
### Claude Code Auth
|
|
145
|
+
|
|
146
|
+
| Command | Description |
|
|
147
|
+
|---------|-------------|
|
|
148
|
+
| `/auth_status` | Runs `claude auth status` and reports redacted status plus whether the bot has an OAuth token configured |
|
|
149
|
+
| `/login` | Starts `claude auth login --claudeai --email sumeet@inet.africa`, sends the login URL/code, and accepts paste-back codes |
|
|
150
|
+
| `/setup_token` | Starts `claude setup-token`; if Claude prints an OAuth token, Open Claudia stores it without echoing it |
|
|
151
|
+
| `/use_oauth_token <token>` | Stores `CLAUDE_CODE_OAUTH_TOKEN` in config for launchd/non-interactive Claude runs; the Telegram message is deleted when possible |
|
|
152
|
+
| `/use_oauth_token` | Secure pending mode: send the token as the next message and it will be deleted/stored without echo |
|
|
153
|
+
| `/clear_oauth_token` | Removes the stored OAuth token from config/process, and from vault if it is unlocked |
|
|
154
|
+
|
|
155
|
+
Tokens are redacted from Telegram output and logs. If the encrypted vault is unlocked, `/use_oauth_token` also mirrors the token into the vault, but `.env` storage is what lets launchd pass `CLAUDE_CODE_OAUTH_TOKEN` to Claude without relying on macOS Keychain.
|
|
156
|
+
|
|
157
|
+
|
|
144
158
|
### System
|
|
145
159
|
|
|
146
160
|
| Command | Description |
|
package/bot-agent.js
CHANGED
|
@@ -190,6 +190,9 @@ bot.setMyCommands([
|
|
|
190
190
|
{ command: "cursor", description: "Switch to Cursor Agent backend" },
|
|
191
191
|
{ command: "claude", description: "Switch to Claude Code backend" },
|
|
192
192
|
{ command: "backend", description: "Show/switch active backend" },
|
|
193
|
+
{ command: "auth_status", description: "Check Claude Code auth" },
|
|
194
|
+
{ command: "login", description: "Start Claude Code login" },
|
|
195
|
+
{ command: "setup_token", description: "Create Claude OAuth token" },
|
|
193
196
|
{ command: "stop", description: "Cancel running task" },
|
|
194
197
|
{ command: "end", description: "End current session" },
|
|
195
198
|
{ command: "version", description: "Show current version" },
|
|
@@ -298,7 +301,10 @@ let messageQueue = []; // Fallback queue (only used if chat process a
|
|
|
298
301
|
let activeCrons = new Map();
|
|
299
302
|
let pendingVaultUnlock = false;
|
|
300
303
|
let pendingVaultAction = null;
|
|
304
|
+
let pendingClaudeAuthProcess = null;
|
|
305
|
+
let pendingClaudeAuthLabel = null;
|
|
301
306
|
let isFirstMessage = !lastSessionId;
|
|
307
|
+
let lastInputWasVoice = false;
|
|
302
308
|
|
|
303
309
|
let settings = savedState.settings || {
|
|
304
310
|
model: null, effort: null, budget: null, permissionMode: null, worktree: false, backend: "claude",
|
|
@@ -661,11 +667,305 @@ function transcribeAudio(oggPath) {
|
|
|
661
667
|
.join(" ").trim();
|
|
662
668
|
}
|
|
663
669
|
|
|
670
|
+
// ── Text-to-Speech ────────────────────────────────────────────────
|
|
671
|
+
|
|
672
|
+
const TTS_CMD = process.platform === "darwin" ? "say" : null;
|
|
673
|
+
|
|
674
|
+
function textToVoice(text) {
|
|
675
|
+
if (!TTS_CMD || !FFMPEG) return null;
|
|
676
|
+
try {
|
|
677
|
+
const clean = text.replace(/[*_`#>\[\]()]/g, "").replace(/\n{2,}/g, ". ").replace(/\n/g, " ").trim();
|
|
678
|
+
if (!clean) return null;
|
|
679
|
+
const aiffPath = path.join(TEMP_DIR, `tts-${Date.now()}.aiff`);
|
|
680
|
+
const oggPath = aiffPath.replace(".aiff", ".ogg");
|
|
681
|
+
execSync(`${TTS_CMD} ${JSON.stringify(clean)} -o "${aiffPath}"`, { timeout: 30000 });
|
|
682
|
+
execSync(`"${FFMPEG}" -i "${aiffPath}" -c:a libopus -y "${oggPath}" 2>/dev/null`, { timeout: 30000 });
|
|
683
|
+
try { fs.unlinkSync(aiffPath); } catch (e) {}
|
|
684
|
+
return oggPath;
|
|
685
|
+
} catch (e) {
|
|
686
|
+
console.error("TTS error:", e.message);
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async function sendVoice(oggPath) {
|
|
692
|
+
try {
|
|
693
|
+
await bot.sendVoice(CHAT_ID, oggPath);
|
|
694
|
+
try { fs.unlinkSync(oggPath); } catch (e) {}
|
|
695
|
+
return true;
|
|
696
|
+
} catch (e) {
|
|
697
|
+
console.error("Send voice error:", e.message);
|
|
698
|
+
try { fs.unlinkSync(oggPath); } catch (e2) {}
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
664
703
|
// Delete a message (used for vault password cleanup)
|
|
665
704
|
async function deleteMessage(msgId) {
|
|
666
705
|
try { await bot.deleteMessage(CHAT_ID, msgId); } catch (e) { /* ignore */ }
|
|
667
706
|
}
|
|
668
707
|
|
|
708
|
+
|
|
709
|
+
// ── Claude Auth Helpers ─────────────────────────────────────────────
|
|
710
|
+
|
|
711
|
+
const CLAUDE_OAUTH_TOKEN_KEY = "CLAUDE_CODE_OAUTH_TOKEN";
|
|
712
|
+
const CLAUDE_OAUTH_VAULT_KEY = "claude_oauth_token";
|
|
713
|
+
|
|
714
|
+
function redactSensitive(value) {
|
|
715
|
+
return String(value || "")
|
|
716
|
+
.replace(/sk-ant-[A-Za-z0-9._-]+/g, "[REDACTED_TOKEN]")
|
|
717
|
+
.replace(/(Bearer\s+)[A-Za-z0-9._=-]+/gi, "$1[REDACTED_TOKEN]")
|
|
718
|
+
.replace(/(CLAUDE_CODE_OAUTH_TOKEN\s*=\s*)\S+/gi, "$1[REDACTED_TOKEN]")
|
|
719
|
+
.replace(/([?&](?:token|access_token|refresh_token)=)[^\s&]+/gi, "$1[REDACTED]");
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function looksLikeClaudeToken(value) {
|
|
723
|
+
const text = String(value || "").trim();
|
|
724
|
+
return /^sk-ant-[A-Za-z0-9._-]+$/.test(text) || text.length >= 80 && /^[A-Za-z0-9._=-]+$/.test(text);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function getClaudeOAuthToken() {
|
|
728
|
+
if (config[CLAUDE_OAUTH_TOKEN_KEY]) return { value: config[CLAUDE_OAUTH_TOKEN_KEY], source: ".env" };
|
|
729
|
+
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) return { value: process.env.CLAUDE_CODE_OAUTH_TOKEN, source: "process env" };
|
|
730
|
+
if (vault.isUnlocked()) {
|
|
731
|
+
const value = vault.get(CLAUDE_OAUTH_VAULT_KEY) || vault.get(CLAUDE_OAUTH_TOKEN_KEY);
|
|
732
|
+
if (value) return { value, source: "vault" };
|
|
733
|
+
}
|
|
734
|
+
return { value: null, source: null };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function claudeSubprocessEnv() {
|
|
738
|
+
const env = { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME };
|
|
739
|
+
const token = getClaudeOAuthToken().value;
|
|
740
|
+
if (token) env.CLAUDE_CODE_OAUTH_TOKEN = token;
|
|
741
|
+
return env;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function saveClaudeOAuthToken(token) {
|
|
745
|
+
const clean = String(token || "").trim();
|
|
746
|
+
if (!clean) return false;
|
|
747
|
+
saveEnvKey(CLAUDE_OAUTH_TOKEN_KEY, clean);
|
|
748
|
+
config[CLAUDE_OAUTH_TOKEN_KEY] = clean;
|
|
749
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = clean;
|
|
750
|
+
if (vault.isUnlocked()) vault.set(CLAUDE_OAUTH_VAULT_KEY, clean);
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function clearClaudeOAuthToken() {
|
|
755
|
+
saveEnvKey(CLAUDE_OAUTH_TOKEN_KEY, "");
|
|
756
|
+
config[CLAUDE_OAUTH_TOKEN_KEY] = "";
|
|
757
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
758
|
+
if (vault.isUnlocked()) {
|
|
759
|
+
vault.remove(CLAUDE_OAUTH_VAULT_KEY);
|
|
760
|
+
vault.remove(CLAUDE_OAUTH_TOKEN_KEY);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function extractClaudeToken(text) {
|
|
765
|
+
const match = String(text || "").match(/sk-ant-[A-Za-z0-9._-]+/);
|
|
766
|
+
return match ? match[0] : null;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function extractUrls(text) {
|
|
770
|
+
return [...String(text || "").matchAll(/https?:\/\/[^\s)]+/g)].map((m) => m[0]);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
function isClaudeAuthErrorText(text) {
|
|
775
|
+
const lower = String(text || "").toLowerCase();
|
|
776
|
+
return lower.includes("unauthorized") ||
|
|
777
|
+
lower.includes("not logged in") ||
|
|
778
|
+
lower.includes("login required") ||
|
|
779
|
+
lower.includes("reauthenticate") ||
|
|
780
|
+
lower.includes("re-authenticate") ||
|
|
781
|
+
lower.includes("authentication failed") ||
|
|
782
|
+
lower.includes("auth failed") ||
|
|
783
|
+
lower.includes("invalid api key") ||
|
|
784
|
+
lower.includes("api key") && lower.includes("invalid") ||
|
|
785
|
+
lower.includes("keychain") && (lower.includes("unlock") || lower.includes("could not") || lower.includes("failed")) ||
|
|
786
|
+
lower.includes("security unlock-keychain");
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function claudeAuthRecoveryMessage(reason = "Claude Code authentication failed") {
|
|
790
|
+
const tokenInfo = getClaudeOAuthToken();
|
|
791
|
+
const tokenLine = tokenInfo.value
|
|
792
|
+
? `Stored OAuth token: yes (${tokenInfo.source})`
|
|
793
|
+
: "Stored OAuth token: no";
|
|
794
|
+
return [
|
|
795
|
+
`Claude auth needs attention: ${redactSensitive(reason)}`,
|
|
796
|
+
"",
|
|
797
|
+
tokenLine,
|
|
798
|
+
"",
|
|
799
|
+
"Try one of these from Telegram:",
|
|
800
|
+
"1. /auth_status — check what Claude sees",
|
|
801
|
+
"2. /setup_token — create a long-lived Claude Code token, then store it",
|
|
802
|
+
"3. /use_oauth_token — paste the token securely if you already generated one",
|
|
803
|
+
"4. /login — interactive Claude.ai browser login fallback",
|
|
804
|
+
"",
|
|
805
|
+
"If this is a macOS Keychain problem after SSH/reboot, run on the Mac:",
|
|
806
|
+
"security unlock-keychain",
|
|
807
|
+
"",
|
|
808
|
+
"Recommended for this bot: /setup_token + /use_oauth_token so launchd does not depend on Keychain."
|
|
809
|
+
].join("\n");
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function preflightClaudeAuthMessage() {
|
|
813
|
+
if (settings.backend === "cursor") return null;
|
|
814
|
+
if (getClaudeOAuthToken().value) return null;
|
|
815
|
+
try {
|
|
816
|
+
const output = execSync(`"${CLAUDE_PATH}" auth status`, {
|
|
817
|
+
cwd: process.env.HOME || require("os").homedir(),
|
|
818
|
+
env: claudeSubprocessEnv(),
|
|
819
|
+
encoding: "utf8",
|
|
820
|
+
timeout: 10000,
|
|
821
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
822
|
+
});
|
|
823
|
+
if (isClaudeAuthErrorText(output)) return claudeAuthRecoveryMessage(output.trim().slice(-500));
|
|
824
|
+
return null;
|
|
825
|
+
} catch (e) {
|
|
826
|
+
const output = `${e.stdout || ""}\n${e.stderr || ""}\n${e.message || ""}`;
|
|
827
|
+
if (isClaudeAuthErrorText(output)) return claudeAuthRecoveryMessage(output.trim().slice(-500));
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
function isClaudeUsageLimitText(text) {
|
|
834
|
+
const lower = String(text || "").toLowerCase();
|
|
835
|
+
return lower.includes("usage limit") ||
|
|
836
|
+
lower.includes("you've hit your usage limit") ||
|
|
837
|
+
lower.includes("you have hit your usage limit") ||
|
|
838
|
+
lower.includes("spend limit") ||
|
|
839
|
+
lower.includes("monthly cycle") ||
|
|
840
|
+
lower.includes("rate limit") && lower.includes("model");
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function claudeUsageLimitMessage(details = "") {
|
|
844
|
+
return [
|
|
845
|
+
"Claude ran, but the selected model is unavailable/limited right now.",
|
|
846
|
+
details ? `\nDetails:\n${redactSensitive(details)}` : "",
|
|
847
|
+
"",
|
|
848
|
+
"Try from Telegram:",
|
|
849
|
+
"1. /model sonnet",
|
|
850
|
+
"2. Then send your message again",
|
|
851
|
+
"",
|
|
852
|
+
"If you specifically need Opus, wait for the usage window to reset or increase the spend/usage limit."
|
|
853
|
+
].filter(Boolean).join("\n");
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function runClaudeAuthStatusDiagnostic() {
|
|
857
|
+
try {
|
|
858
|
+
const output = execSync(`"${CLAUDE_PATH}" auth status`, {
|
|
859
|
+
cwd: process.env.HOME || require("os").homedir(),
|
|
860
|
+
env: claudeSubprocessEnv(),
|
|
861
|
+
encoding: "utf8",
|
|
862
|
+
timeout: 10000,
|
|
863
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
864
|
+
});
|
|
865
|
+
return output.trim();
|
|
866
|
+
} catch (e) {
|
|
867
|
+
return `${e.stdout || ""}\n${e.stderr || ""}\n${e.message || ""}`.trim();
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function claudeEmptyFailureMessage(code, stderrText = "") {
|
|
872
|
+
const stderr = redactSensitive(String(stderrText || "").trim());
|
|
873
|
+
if (isClaudeUsageLimitText(stderr)) return claudeUsageLimitMessage(stderr.slice(-1200));
|
|
874
|
+
if (isClaudeAuthErrorText(stderr)) return claudeAuthRecoveryMessage(stderr.slice(-1200));
|
|
875
|
+
|
|
876
|
+
const authStatus = runClaudeAuthStatusDiagnostic();
|
|
877
|
+
if (isClaudeAuthErrorText(authStatus)) return claudeAuthRecoveryMessage(authStatus.slice(-1200));
|
|
878
|
+
if (isClaudeUsageLimitText(authStatus)) return claudeUsageLimitMessage(authStatus.slice(-1200));
|
|
879
|
+
|
|
880
|
+
return [
|
|
881
|
+
`Claude exited with code ${code} but produced no assistant output.`,
|
|
882
|
+
stderr ? `\nStderr:\n${stderr.slice(-1200)}` : "\nStderr: (empty)",
|
|
883
|
+
authStatus ? `\nAuth status:\n${redactSensitive(authStatus).slice(-1200)}` : "",
|
|
884
|
+
"",
|
|
885
|
+
"Useful next steps:",
|
|
886
|
+
"• /auth_status — verify Claude auth",
|
|
887
|
+
"• /model sonnet — switch away from Opus if usage-limited",
|
|
888
|
+
"• /setup_token — create a launchd-safe OAuth token if Keychain is the issue"
|
|
889
|
+
].filter(Boolean).join("\n");
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
function summarizeClaudeAuthStatus(output, exitCode, tokenInfo) {
|
|
894
|
+
const text = String(output || "");
|
|
895
|
+
const lower = text.toLowerCase();
|
|
896
|
+
const loggedOut = /not (logged in|authenticated)|unauthenticated|no auth|login required/.test(lower);
|
|
897
|
+
const loggedIn = !loggedOut && (exitCode === 0 || /logged in|authenticated|claude\.ai|anthropic/.test(lower));
|
|
898
|
+
const email = (text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i) || [null])[0];
|
|
899
|
+
const provider = /claude\.ai|claudeai/.test(lower) ? "Claude.ai" : (/anthropic/.test(lower) ? "Anthropic" : "unknown");
|
|
900
|
+
let method = tokenInfo.value ? `OAuth token (${tokenInfo.source})` : "unknown";
|
|
901
|
+
if (/api key|apikey/.test(lower)) method = "API key";
|
|
902
|
+
else if (/oauth|claude\.ai|claudeai/.test(lower)) method = tokenInfo.value ? `OAuth token (${tokenInfo.source})` : "OAuth/Claude.ai";
|
|
903
|
+
return [
|
|
904
|
+
`Logged in: ${loggedIn ? "yes" : (loggedOut ? "no" : "unknown")}`,
|
|
905
|
+
`Auth method: ${method}`,
|
|
906
|
+
`Email: ${email || "unknown"}`,
|
|
907
|
+
`Provider: ${provider}`,
|
|
908
|
+
];
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async function runClaudeAuthCommand(args, label, opts = {}) {
|
|
912
|
+
if (pendingClaudeAuthProcess) {
|
|
913
|
+
await send(`Another Claude auth flow is already running (${pendingClaudeAuthLabel}). Send the requested code/token or wait for it to finish.`);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
await send(`${label} started...`);
|
|
917
|
+
const proc = spawn(CLAUDE_PATH, args, {
|
|
918
|
+
cwd: process.env.HOME || require("os").homedir(),
|
|
919
|
+
env: claudeSubprocessEnv(),
|
|
920
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
921
|
+
});
|
|
922
|
+
pendingClaudeAuthProcess = proc;
|
|
923
|
+
pendingClaudeAuthLabel = label;
|
|
924
|
+
let output = "";
|
|
925
|
+
let sentUrls = new Set();
|
|
926
|
+
let tokenStored = false;
|
|
927
|
+
let lastSnippetAt = 0;
|
|
928
|
+
|
|
929
|
+
const handleChunk = async (chunk) => {
|
|
930
|
+
output += chunk;
|
|
931
|
+
const token = opts.captureToken ? extractClaudeToken(chunk) || extractClaudeToken(output) : null;
|
|
932
|
+
if (token && !tokenStored) {
|
|
933
|
+
tokenStored = saveClaudeOAuthToken(token);
|
|
934
|
+
await send(tokenStored ? "Claude OAuth token captured and stored. I did not print it." : "Claude OAuth token appeared, but I could not store it.");
|
|
935
|
+
}
|
|
936
|
+
for (const url of extractUrls(chunk)) {
|
|
937
|
+
if (!sentUrls.has(url)) {
|
|
938
|
+
sentUrls.add(url);
|
|
939
|
+
await send(`${label} URL:\n${redactSensitive(url)}\n\nOpen it, complete the flow, then paste any code Claude asks for here.`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const redacted = redactSensitive(chunk).trim();
|
|
943
|
+
const now = Date.now();
|
|
944
|
+
if (redacted && now - lastSnippetAt > 3000 && !looksLikeClaudeToken(redacted)) {
|
|
945
|
+
lastSnippetAt = now;
|
|
946
|
+
await send(redacted.length > 1200 ? redacted.slice(-1200) : redacted);
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
proc.stdout.on("data", (d) => handleChunk(d.toString()).catch((e) => console.error("Auth output error:", e.message)));
|
|
951
|
+
proc.stderr.on("data", (d) => handleChunk(d.toString()).catch((e) => console.error("Auth stderr error:", e.message)));
|
|
952
|
+
proc.on("close", async (code) => {
|
|
953
|
+
pendingClaudeAuthProcess = null;
|
|
954
|
+
pendingClaudeAuthLabel = null;
|
|
955
|
+
const token = opts.captureToken ? extractClaudeToken(output) : null;
|
|
956
|
+
if (token && !tokenStored) tokenStored = saveClaudeOAuthToken(token);
|
|
957
|
+
const final = redactSensitive(output.trim());
|
|
958
|
+
if (tokenStored) await send(`${label} finished. OAuth token stored for launchd/non-interactive Claude runs.`);
|
|
959
|
+
else if (final) await send(`${label} finished (exit ${code}).\n\n${final.slice(-2500)}`);
|
|
960
|
+
else await send(`${label} finished (exit ${code}).`);
|
|
961
|
+
});
|
|
962
|
+
proc.on("error", async (err) => {
|
|
963
|
+
pendingClaudeAuthProcess = null;
|
|
964
|
+
pendingClaudeAuthLabel = null;
|
|
965
|
+
await send(`${label} failed: ${redactSensitive(err.message)}`);
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
|
|
669
969
|
// ── Claude Runner ───────────────────────────────────────────────────
|
|
670
970
|
|
|
671
971
|
function parseStreamEvents(data) {
|
|
@@ -734,7 +1034,7 @@ async function runClaudeChat(prompt, cwd, replyToMsgId) {
|
|
|
734
1034
|
|
|
735
1035
|
const proc = spawn(CLAUDE_PATH, args, {
|
|
736
1036
|
cwd,
|
|
737
|
-
env:
|
|
1037
|
+
env: claudeSubprocessEnv(),
|
|
738
1038
|
stdio: ["ignore", "pipe", "pipe"],
|
|
739
1039
|
});
|
|
740
1040
|
|
|
@@ -779,6 +1079,12 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
779
1079
|
return;
|
|
780
1080
|
}
|
|
781
1081
|
|
|
1082
|
+
const authPreflight = preflightClaudeAuthMessage();
|
|
1083
|
+
if (authPreflight) {
|
|
1084
|
+
await send(authPreflight, { replyTo: replyToMsgId });
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
782
1088
|
bot.sendChatAction(CHAT_ID, "typing");
|
|
783
1089
|
statusMessageId = null;
|
|
784
1090
|
streamBuffer = "";
|
|
@@ -791,7 +1097,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
791
1097
|
const binaryPath = getActiveBinary();
|
|
792
1098
|
const proc = spawn(binaryPath, args, {
|
|
793
1099
|
cwd,
|
|
794
|
-
env:
|
|
1100
|
+
env: claudeSubprocessEnv(),
|
|
795
1101
|
stdio: ["ignore", "pipe", "pipe"],
|
|
796
1102
|
detached: process.platform !== "win32", // Create process group so /stop kills children too
|
|
797
1103
|
});
|
|
@@ -907,13 +1213,31 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
907
1213
|
}
|
|
908
1214
|
});
|
|
909
1215
|
|
|
910
|
-
|
|
1216
|
+
let stderrBuffer = "";
|
|
1217
|
+
proc.stderr.on("data", (d) => {
|
|
1218
|
+
const chunk = d.toString();
|
|
1219
|
+
stderrBuffer += chunk;
|
|
1220
|
+
console.error("STDERR:", redactSensitive(chunk));
|
|
1221
|
+
});
|
|
911
1222
|
|
|
912
1223
|
proc.on("close", async (code) => {
|
|
913
1224
|
runningProcess = null; runningProcessPrompt = null;
|
|
914
1225
|
clearTimeout(streamInterval); streamInterval = null;
|
|
1226
|
+
if (settings.backend !== "cursor" && isClaudeAuthErrorText(stderrBuffer)) {
|
|
1227
|
+
await send(claudeAuthRecoveryMessage(stderrBuffer.trim().slice(-800)), { replyTo: replyToMsgId });
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
if (settings.backend === "cursor" && isClaudeAuthErrorText(stderrBuffer)) {
|
|
1231
|
+
await send("Cursor authentication error. Run `agent login` on this machine, then retry.", { replyTo: replyToMsgId });
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
915
1234
|
try {
|
|
916
|
-
|
|
1235
|
+
if (code !== 0 && code !== null && !assistantText.trim()) {
|
|
1236
|
+
await send(claudeEmptyFailureMessage(code, stderrBuffer), { replyTo: replyToMsgId });
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
const finalText = redactSensitive(assistantText || "(no output)");
|
|
917
1241
|
const chunks = splitMessage(finalText);
|
|
918
1242
|
const firstChunk = chunks[0];
|
|
919
1243
|
|
|
@@ -927,6 +1251,13 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
927
1251
|
}
|
|
928
1252
|
}
|
|
929
1253
|
if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
|
|
1254
|
+
|
|
1255
|
+
// Send voice reply if input was a voice note
|
|
1256
|
+
if (lastInputWasVoice && TTS_CMD) {
|
|
1257
|
+
lastInputWasVoice = false;
|
|
1258
|
+
const voicePath = textToVoice(finalText);
|
|
1259
|
+
if (voicePath) await sendVoice(voicePath);
|
|
1260
|
+
}
|
|
930
1261
|
} catch (e) {
|
|
931
1262
|
console.error("Final message delivery failed:", e.message);
|
|
932
1263
|
await send("Task completed but failed to deliver the response. Send /continue to see the result.");
|
|
@@ -958,13 +1289,13 @@ async function runClaudeSilent(prompt, cwd, label) {
|
|
|
958
1289
|
"--append-system-prompt", buildSystemPrompt(),
|
|
959
1290
|
"--dangerously-skip-permissions", prompt];
|
|
960
1291
|
const proc = spawn(CLAUDE_PATH, args, {
|
|
961
|
-
cwd, env:
|
|
1292
|
+
cwd, env: claudeSubprocessEnv(),
|
|
962
1293
|
stdio: ["ignore", "pipe", "pipe"],
|
|
963
1294
|
});
|
|
964
1295
|
let output = "";
|
|
965
1296
|
proc.stdout.on("data", (d) => { output += d.toString(); });
|
|
966
1297
|
proc.on("close", async () => {
|
|
967
|
-
const chunks = splitMessage(`Cron: ${label}\n\n${output.trim() || "(no output)"}`);
|
|
1298
|
+
const chunks = splitMessage(`Cron: ${label}\n\n${redactSensitive(output.trim() || "(no output)")}`);
|
|
968
1299
|
for (const c of chunks) await send(c);
|
|
969
1300
|
resolve();
|
|
970
1301
|
});
|
|
@@ -1064,6 +1395,7 @@ bot.onText(/\/help/, (msg) => {
|
|
|
1064
1395
|
"Session: /session /sessions /projects /continue /status /stop /end",
|
|
1065
1396
|
"Settings: /model /effort /budget /plan /compact /worktree /mode",
|
|
1066
1397
|
"Automation: /cron /vault /soul",
|
|
1398
|
+
"Claude auth: /auth_status /login /setup_token /use_oauth_token /clear_oauth_token",
|
|
1067
1399
|
"System: /restart /upgrade",
|
|
1068
1400
|
"",
|
|
1069
1401
|
"Send text, voice, photos, or files.",
|
|
@@ -1312,6 +1644,64 @@ bot.onText(/\/soul$/, async (msg) => {
|
|
|
1312
1644
|
await send(`Edit: ${SOUL_FILE}\nOr tell me what to change and I'll update it.`);
|
|
1313
1645
|
});
|
|
1314
1646
|
|
|
1647
|
+
// ── Claude Auth Commands ────────────────────────────────────────────
|
|
1648
|
+
|
|
1649
|
+
bot.onText(/\/(?:auth_status|auth status)$/, async (msg) => {
|
|
1650
|
+
if (!isAuthorized(msg)) return;
|
|
1651
|
+
const tokenInfo = getClaudeOAuthToken();
|
|
1652
|
+
const proc = spawn(CLAUDE_PATH, ["auth", "status"], {
|
|
1653
|
+
cwd: process.env.HOME || require("os").homedir(),
|
|
1654
|
+
env: claudeSubprocessEnv(),
|
|
1655
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1656
|
+
});
|
|
1657
|
+
let output = "";
|
|
1658
|
+
proc.stdout.on("data", (d) => { output += d.toString(); });
|
|
1659
|
+
proc.stderr.on("data", (d) => { output += d.toString(); });
|
|
1660
|
+
proc.on("close", async (code) => {
|
|
1661
|
+
const clean = redactSensitive(output.trim()) || "(no output)";
|
|
1662
|
+
await send([
|
|
1663
|
+
`Claude auth status: exit ${code}`,
|
|
1664
|
+
...summarizeClaudeAuthStatus(output, code, tokenInfo),
|
|
1665
|
+
`Bot OAuth token: ${tokenInfo.value ? "configured via " + tokenInfo.source : "not configured"}`,
|
|
1666
|
+
`Vault: ${vault.isUnlocked() ? "unlocked" : "locked"}`,
|
|
1667
|
+
"",
|
|
1668
|
+
clean.slice(-2500),
|
|
1669
|
+
].join("\n"));
|
|
1670
|
+
});
|
|
1671
|
+
proc.on("error", async (err) => send(`Claude auth status failed: ${redactSensitive(err.message)}`));
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
bot.onText(/\/login$/, async (msg) => {
|
|
1675
|
+
if (!isAuthorized(msg)) return;
|
|
1676
|
+
await runClaudeAuthCommand(["auth", "login", "--claudeai", "--email", "sumeet@inet.africa"], "Claude login");
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
bot.onText(/\/setup_token$/, async (msg) => {
|
|
1680
|
+
if (!isAuthorized(msg)) return;
|
|
1681
|
+
await runClaudeAuthCommand(["setup-token"], "Claude setup-token", { captureToken: true });
|
|
1682
|
+
});
|
|
1683
|
+
|
|
1684
|
+
bot.onText(/\/use_oauth_token(?:\s+(.+))?$/, async (msg, match) => {
|
|
1685
|
+
if (!isAuthorized(msg)) return;
|
|
1686
|
+
const token = (match[1] || "").trim();
|
|
1687
|
+
await deleteMessage(msg.message_id);
|
|
1688
|
+
if (!token) {
|
|
1689
|
+
pendingClaudeAuthProcess = { stdin: { write: (value) => saveClaudeOAuthToken(value.trim()) } };
|
|
1690
|
+
pendingClaudeAuthLabel = "manual OAuth token save";
|
|
1691
|
+
await send("Send the Claude OAuth token in your next message. I'll delete it and store it without echoing it.");
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
if (!looksLikeClaudeToken(token)) return send("That doesn't look like a Claude OAuth token. Not saved.");
|
|
1695
|
+
saveClaudeOAuthToken(token);
|
|
1696
|
+
await send(`Claude OAuth token stored in .env${vault.isUnlocked() ? " and vault" : ""}. Restart the bot so launchd picks it up, or use /restart.`);
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
bot.onText(/\/clear_oauth_token$/, async (msg) => {
|
|
1700
|
+
if (!isAuthorized(msg)) return;
|
|
1701
|
+
clearClaudeOAuthToken();
|
|
1702
|
+
await send("Claude OAuth token cleared from .env/process" + (vault.isUnlocked() ? " and vault." : ". Unlock vault and run again if you also stored it there."));
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1315
1705
|
// ── /vault with password protection ─────────────────────────────────
|
|
1316
1706
|
|
|
1317
1707
|
bot.onText(/\/vault$/, async (msg) => {
|
|
@@ -1481,6 +1871,7 @@ bot.on("voice", async (msg) => {
|
|
|
1481
1871
|
try { fs.unlinkSync(oggPath); } catch (e) {}
|
|
1482
1872
|
if (!transcript) return send("Couldn't transcribe. Try typing it.");
|
|
1483
1873
|
await send(`Heard: "${transcript}"`, { replyTo: msg.message_id });
|
|
1874
|
+
lastInputWasVoice = true;
|
|
1484
1875
|
await runClaude(transcript, currentSession.dir, msg.message_id);
|
|
1485
1876
|
} catch (err) { await send(`Voice failed: ${err.message}`); }
|
|
1486
1877
|
});
|
|
@@ -1547,6 +1938,29 @@ bot.on("message", async (msg) => {
|
|
|
1547
1938
|
if (msg.voice || msg.audio || msg.photo || msg.document || msg.video || msg.sticker) return;
|
|
1548
1939
|
if (isDuplicate(msg.message_id)) return;
|
|
1549
1940
|
|
|
1941
|
+
// Handle pending Claude auth/token paste-back
|
|
1942
|
+
if (pendingClaudeAuthProcess) {
|
|
1943
|
+
const text = msg.text.trim();
|
|
1944
|
+
await deleteMessage(msg.message_id);
|
|
1945
|
+
if (pendingClaudeAuthLabel === "manual OAuth token save") {
|
|
1946
|
+
pendingClaudeAuthProcess = null;
|
|
1947
|
+
pendingClaudeAuthLabel = null;
|
|
1948
|
+
if (!looksLikeClaudeToken(text)) { await send("That doesn't look like a Claude OAuth token. Not saved."); return; }
|
|
1949
|
+
saveClaudeOAuthToken(text);
|
|
1950
|
+
await send(`Claude OAuth token stored in .env${vault.isUnlocked() ? " and vault" : ""}. Restart the bot so launchd picks it up, or use /restart.`);
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
try {
|
|
1954
|
+
pendingClaudeAuthProcess.stdin.write(text + "\n");
|
|
1955
|
+
await send("Sent to Claude auth process.");
|
|
1956
|
+
} catch (e) {
|
|
1957
|
+
pendingClaudeAuthProcess = null;
|
|
1958
|
+
pendingClaudeAuthLabel = null;
|
|
1959
|
+
await send(`Could not send to Claude auth process: ${redactSensitive(e.message)}`);
|
|
1960
|
+
}
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1550
1964
|
// Handle onboarding
|
|
1551
1965
|
if (!isOnboarded() && onboardingStep) {
|
|
1552
1966
|
await handleOnboarding(msg);
|
package/bot.js
CHANGED
|
@@ -248,6 +248,9 @@ bot.setMyCommands([
|
|
|
248
248
|
{ command: "cursor", description: "Switch to Cursor Agent backend" },
|
|
249
249
|
{ command: "claude", description: "Switch to Claude Code backend" },
|
|
250
250
|
{ command: "backend", description: "Show/switch active backend" },
|
|
251
|
+
{ command: "auth_status", description: "Check Claude Code auth" },
|
|
252
|
+
{ command: "login", description: "Start Claude Code login" },
|
|
253
|
+
{ command: "setup_token", description: "Create Claude OAuth token" },
|
|
251
254
|
{ command: "stop", description: "Cancel running task" },
|
|
252
255
|
{ command: "end", description: "End current session" },
|
|
253
256
|
{ command: "version", description: "Show current version" },
|
|
@@ -359,7 +362,10 @@ let messageQueue = [];
|
|
|
359
362
|
let activeCrons = new Map();
|
|
360
363
|
let pendingVaultUnlock = false; // Waiting for password
|
|
361
364
|
let pendingVaultAction = null; // What to do after unlock
|
|
365
|
+
let pendingClaudeAuthProcess = null;
|
|
366
|
+
let pendingClaudeAuthLabel = null;
|
|
362
367
|
let isFirstMessage = !lastSessionId; // Track if this is first message in session
|
|
368
|
+
let lastInputWasVoice = false; // Reply with voice when input was voice
|
|
363
369
|
|
|
364
370
|
let settings = savedState.settings || {
|
|
365
371
|
model: null, effort: null, budget: null, permissionMode: null, worktree: false, backend: "claude",
|
|
@@ -722,11 +728,306 @@ function transcribeAudio(oggPath) {
|
|
|
722
728
|
.join(" ").trim();
|
|
723
729
|
}
|
|
724
730
|
|
|
731
|
+
// ── Text-to-Speech ────────────────────────────────────────────────
|
|
732
|
+
|
|
733
|
+
const TTS_CMD = process.platform === "darwin" ? "say" : null;
|
|
734
|
+
|
|
735
|
+
function textToVoice(text) {
|
|
736
|
+
if (!TTS_CMD || !FFMPEG) return null;
|
|
737
|
+
try {
|
|
738
|
+
// Strip markdown formatting for cleaner speech
|
|
739
|
+
const clean = text.replace(/[*_`#>\[\]()]/g, "").replace(/\n{2,}/g, ". ").replace(/\n/g, " ").trim();
|
|
740
|
+
if (!clean) return null;
|
|
741
|
+
const aiffPath = path.join(TEMP_DIR, `tts-${Date.now()}.aiff`);
|
|
742
|
+
const oggPath = aiffPath.replace(".aiff", ".ogg");
|
|
743
|
+
execSync(`${TTS_CMD} ${JSON.stringify(clean)} -o "${aiffPath}"`, { timeout: 30000 });
|
|
744
|
+
execSync(`"${FFMPEG}" -i "${aiffPath}" -c:a libopus -y "${oggPath}" 2>/dev/null`, { timeout: 30000 });
|
|
745
|
+
try { fs.unlinkSync(aiffPath); } catch (e) {}
|
|
746
|
+
return oggPath;
|
|
747
|
+
} catch (e) {
|
|
748
|
+
console.error("TTS error:", e.message);
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async function sendVoice(oggPath) {
|
|
754
|
+
try {
|
|
755
|
+
await bot.sendVoice(CHAT_ID, oggPath);
|
|
756
|
+
try { fs.unlinkSync(oggPath); } catch (e) {}
|
|
757
|
+
return true;
|
|
758
|
+
} catch (e) {
|
|
759
|
+
console.error("Send voice error:", e.message);
|
|
760
|
+
try { fs.unlinkSync(oggPath); } catch (e2) {}
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
725
765
|
// Delete a message (used for vault password cleanup)
|
|
726
766
|
async function deleteMessage(msgId) {
|
|
727
767
|
try { await bot.deleteMessage(CHAT_ID, msgId); } catch (e) { /* ignore */ }
|
|
728
768
|
}
|
|
729
769
|
|
|
770
|
+
|
|
771
|
+
// ── Claude Auth Helpers ─────────────────────────────────────────────
|
|
772
|
+
|
|
773
|
+
const CLAUDE_OAUTH_TOKEN_KEY = "CLAUDE_CODE_OAUTH_TOKEN";
|
|
774
|
+
const CLAUDE_OAUTH_VAULT_KEY = "claude_oauth_token";
|
|
775
|
+
|
|
776
|
+
function redactSensitive(value) {
|
|
777
|
+
return String(value || "")
|
|
778
|
+
.replace(/sk-ant-[A-Za-z0-9._-]+/g, "[REDACTED_TOKEN]")
|
|
779
|
+
.replace(/(Bearer\s+)[A-Za-z0-9._=-]+/gi, "$1[REDACTED_TOKEN]")
|
|
780
|
+
.replace(/(CLAUDE_CODE_OAUTH_TOKEN\s*=\s*)\S+/gi, "$1[REDACTED_TOKEN]")
|
|
781
|
+
.replace(/([?&](?:token|access_token|refresh_token)=)[^\s&]+/gi, "$1[REDACTED]");
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function looksLikeClaudeToken(value) {
|
|
785
|
+
const text = String(value || "").trim();
|
|
786
|
+
return /^sk-ant-[A-Za-z0-9._-]+$/.test(text) || text.length >= 80 && /^[A-Za-z0-9._=-]+$/.test(text);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function getClaudeOAuthToken() {
|
|
790
|
+
if (config[CLAUDE_OAUTH_TOKEN_KEY]) return { value: config[CLAUDE_OAUTH_TOKEN_KEY], source: ".env" };
|
|
791
|
+
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) return { value: process.env.CLAUDE_CODE_OAUTH_TOKEN, source: "process env" };
|
|
792
|
+
if (vault.isUnlocked()) {
|
|
793
|
+
const value = vault.get(CLAUDE_OAUTH_VAULT_KEY) || vault.get(CLAUDE_OAUTH_TOKEN_KEY);
|
|
794
|
+
if (value) return { value, source: "vault" };
|
|
795
|
+
}
|
|
796
|
+
return { value: null, source: null };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function claudeSubprocessEnv() {
|
|
800
|
+
const env = { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME };
|
|
801
|
+
const token = getClaudeOAuthToken().value;
|
|
802
|
+
if (token) env.CLAUDE_CODE_OAUTH_TOKEN = token;
|
|
803
|
+
return env;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function saveClaudeOAuthToken(token) {
|
|
807
|
+
const clean = String(token || "").trim();
|
|
808
|
+
if (!clean) return false;
|
|
809
|
+
saveEnvKey(CLAUDE_OAUTH_TOKEN_KEY, clean);
|
|
810
|
+
config[CLAUDE_OAUTH_TOKEN_KEY] = clean;
|
|
811
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = clean;
|
|
812
|
+
if (vault.isUnlocked()) vault.set(CLAUDE_OAUTH_VAULT_KEY, clean);
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function clearClaudeOAuthToken() {
|
|
817
|
+
saveEnvKey(CLAUDE_OAUTH_TOKEN_KEY, "");
|
|
818
|
+
config[CLAUDE_OAUTH_TOKEN_KEY] = "";
|
|
819
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
820
|
+
if (vault.isUnlocked()) {
|
|
821
|
+
vault.remove(CLAUDE_OAUTH_VAULT_KEY);
|
|
822
|
+
vault.remove(CLAUDE_OAUTH_TOKEN_KEY);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function extractClaudeToken(text) {
|
|
827
|
+
const match = String(text || "").match(/sk-ant-[A-Za-z0-9._-]+/);
|
|
828
|
+
return match ? match[0] : null;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function extractUrls(text) {
|
|
832
|
+
return [...String(text || "").matchAll(/https?:\/\/[^\s)]+/g)].map((m) => m[0]);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
function isClaudeAuthErrorText(text) {
|
|
837
|
+
const lower = String(text || "").toLowerCase();
|
|
838
|
+
return lower.includes("unauthorized") ||
|
|
839
|
+
lower.includes("not logged in") ||
|
|
840
|
+
lower.includes("login required") ||
|
|
841
|
+
lower.includes("reauthenticate") ||
|
|
842
|
+
lower.includes("re-authenticate") ||
|
|
843
|
+
lower.includes("authentication failed") ||
|
|
844
|
+
lower.includes("auth failed") ||
|
|
845
|
+
lower.includes("invalid api key") ||
|
|
846
|
+
lower.includes("api key") && lower.includes("invalid") ||
|
|
847
|
+
lower.includes("keychain") && (lower.includes("unlock") || lower.includes("could not") || lower.includes("failed")) ||
|
|
848
|
+
lower.includes("security unlock-keychain");
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function claudeAuthRecoveryMessage(reason = "Claude Code authentication failed") {
|
|
852
|
+
const tokenInfo = getClaudeOAuthToken();
|
|
853
|
+
const tokenLine = tokenInfo.value
|
|
854
|
+
? `Stored OAuth token: yes (${tokenInfo.source})`
|
|
855
|
+
: "Stored OAuth token: no";
|
|
856
|
+
return [
|
|
857
|
+
`Claude auth needs attention: ${redactSensitive(reason)}`,
|
|
858
|
+
"",
|
|
859
|
+
tokenLine,
|
|
860
|
+
"",
|
|
861
|
+
"Try one of these from Telegram:",
|
|
862
|
+
"1. /auth_status — check what Claude sees",
|
|
863
|
+
"2. /setup_token — create a long-lived Claude Code token, then store it",
|
|
864
|
+
"3. /use_oauth_token — paste the token securely if you already generated one",
|
|
865
|
+
"4. /login — interactive Claude.ai browser login fallback",
|
|
866
|
+
"",
|
|
867
|
+
"If this is a macOS Keychain problem after SSH/reboot, run on the Mac:",
|
|
868
|
+
"security unlock-keychain",
|
|
869
|
+
"",
|
|
870
|
+
"Recommended for this bot: /setup_token + /use_oauth_token so launchd does not depend on Keychain."
|
|
871
|
+
].join("\n");
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function preflightClaudeAuthMessage() {
|
|
875
|
+
if (settings.backend === "cursor") return null;
|
|
876
|
+
if (getClaudeOAuthToken().value) return null;
|
|
877
|
+
try {
|
|
878
|
+
const output = execSync(`"${CLAUDE_PATH}" auth status`, {
|
|
879
|
+
cwd: process.env.HOME || require("os").homedir(),
|
|
880
|
+
env: claudeSubprocessEnv(),
|
|
881
|
+
encoding: "utf8",
|
|
882
|
+
timeout: 10000,
|
|
883
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
884
|
+
});
|
|
885
|
+
if (isClaudeAuthErrorText(output)) return claudeAuthRecoveryMessage(output.trim().slice(-500));
|
|
886
|
+
return null;
|
|
887
|
+
} catch (e) {
|
|
888
|
+
const output = `${e.stdout || ""}\n${e.stderr || ""}\n${e.message || ""}`;
|
|
889
|
+
if (isClaudeAuthErrorText(output)) return claudeAuthRecoveryMessage(output.trim().slice(-500));
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
function isClaudeUsageLimitText(text) {
|
|
896
|
+
const lower = String(text || "").toLowerCase();
|
|
897
|
+
return lower.includes("usage limit") ||
|
|
898
|
+
lower.includes("you've hit your usage limit") ||
|
|
899
|
+
lower.includes("you have hit your usage limit") ||
|
|
900
|
+
lower.includes("spend limit") ||
|
|
901
|
+
lower.includes("monthly cycle") ||
|
|
902
|
+
lower.includes("rate limit") && lower.includes("model");
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function claudeUsageLimitMessage(details = "") {
|
|
906
|
+
return [
|
|
907
|
+
"Claude ran, but the selected model is unavailable/limited right now.",
|
|
908
|
+
details ? `\nDetails:\n${redactSensitive(details)}` : "",
|
|
909
|
+
"",
|
|
910
|
+
"Try from Telegram:",
|
|
911
|
+
"1. /model sonnet",
|
|
912
|
+
"2. Then send your message again",
|
|
913
|
+
"",
|
|
914
|
+
"If you specifically need Opus, wait for the usage window to reset or increase the spend/usage limit."
|
|
915
|
+
].filter(Boolean).join("\n");
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function runClaudeAuthStatusDiagnostic() {
|
|
919
|
+
try {
|
|
920
|
+
const output = execSync(`"${CLAUDE_PATH}" auth status`, {
|
|
921
|
+
cwd: process.env.HOME || require("os").homedir(),
|
|
922
|
+
env: claudeSubprocessEnv(),
|
|
923
|
+
encoding: "utf8",
|
|
924
|
+
timeout: 10000,
|
|
925
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
926
|
+
});
|
|
927
|
+
return output.trim();
|
|
928
|
+
} catch (e) {
|
|
929
|
+
return `${e.stdout || ""}\n${e.stderr || ""}\n${e.message || ""}`.trim();
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function claudeEmptyFailureMessage(code, stderrText = "") {
|
|
934
|
+
const stderr = redactSensitive(String(stderrText || "").trim());
|
|
935
|
+
if (isClaudeUsageLimitText(stderr)) return claudeUsageLimitMessage(stderr.slice(-1200));
|
|
936
|
+
if (isClaudeAuthErrorText(stderr)) return claudeAuthRecoveryMessage(stderr.slice(-1200));
|
|
937
|
+
|
|
938
|
+
const authStatus = runClaudeAuthStatusDiagnostic();
|
|
939
|
+
if (isClaudeAuthErrorText(authStatus)) return claudeAuthRecoveryMessage(authStatus.slice(-1200));
|
|
940
|
+
if (isClaudeUsageLimitText(authStatus)) return claudeUsageLimitMessage(authStatus.slice(-1200));
|
|
941
|
+
|
|
942
|
+
return [
|
|
943
|
+
`Claude exited with code ${code} but produced no assistant output.`,
|
|
944
|
+
stderr ? `\nStderr:\n${stderr.slice(-1200)}` : "\nStderr: (empty)",
|
|
945
|
+
authStatus ? `\nAuth status:\n${redactSensitive(authStatus).slice(-1200)}` : "",
|
|
946
|
+
"",
|
|
947
|
+
"Useful next steps:",
|
|
948
|
+
"• /auth_status — verify Claude auth",
|
|
949
|
+
"• /model sonnet — switch away from Opus if usage-limited",
|
|
950
|
+
"• /setup_token — create a launchd-safe OAuth token if Keychain is the issue"
|
|
951
|
+
].filter(Boolean).join("\n");
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
function summarizeClaudeAuthStatus(output, exitCode, tokenInfo) {
|
|
956
|
+
const text = String(output || "");
|
|
957
|
+
const lower = text.toLowerCase();
|
|
958
|
+
const loggedOut = /not (logged in|authenticated)|unauthenticated|no auth|login required/.test(lower);
|
|
959
|
+
const loggedIn = !loggedOut && (exitCode === 0 || /logged in|authenticated|claude\.ai|anthropic/.test(lower));
|
|
960
|
+
const email = (text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i) || [null])[0];
|
|
961
|
+
const provider = /claude\.ai|claudeai/.test(lower) ? "Claude.ai" : (/anthropic/.test(lower) ? "Anthropic" : "unknown");
|
|
962
|
+
let method = tokenInfo.value ? `OAuth token (${tokenInfo.source})` : "unknown";
|
|
963
|
+
if (/api key|apikey/.test(lower)) method = "API key";
|
|
964
|
+
else if (/oauth|claude\.ai|claudeai/.test(lower)) method = tokenInfo.value ? `OAuth token (${tokenInfo.source})` : "OAuth/Claude.ai";
|
|
965
|
+
return [
|
|
966
|
+
`Logged in: ${loggedIn ? "yes" : (loggedOut ? "no" : "unknown")}`,
|
|
967
|
+
`Auth method: ${method}`,
|
|
968
|
+
`Email: ${email || "unknown"}`,
|
|
969
|
+
`Provider: ${provider}`,
|
|
970
|
+
];
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
async function runClaudeAuthCommand(args, label, opts = {}) {
|
|
974
|
+
if (pendingClaudeAuthProcess) {
|
|
975
|
+
await send(`Another Claude auth flow is already running (${pendingClaudeAuthLabel}). Send the requested code/token or wait for it to finish.`);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
await send(`${label} started...`);
|
|
979
|
+
const proc = spawn(CLAUDE_PATH, args, {
|
|
980
|
+
cwd: process.env.HOME || require("os").homedir(),
|
|
981
|
+
env: claudeSubprocessEnv(),
|
|
982
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
983
|
+
});
|
|
984
|
+
pendingClaudeAuthProcess = proc;
|
|
985
|
+
pendingClaudeAuthLabel = label;
|
|
986
|
+
let output = "";
|
|
987
|
+
let sentUrls = new Set();
|
|
988
|
+
let tokenStored = false;
|
|
989
|
+
let lastSnippetAt = 0;
|
|
990
|
+
|
|
991
|
+
const handleChunk = async (chunk) => {
|
|
992
|
+
output += chunk;
|
|
993
|
+
const token = opts.captureToken ? extractClaudeToken(chunk) || extractClaudeToken(output) : null;
|
|
994
|
+
if (token && !tokenStored) {
|
|
995
|
+
tokenStored = saveClaudeOAuthToken(token);
|
|
996
|
+
await send(tokenStored ? "Claude OAuth token captured and stored. I did not print it." : "Claude OAuth token appeared, but I could not store it.");
|
|
997
|
+
}
|
|
998
|
+
for (const url of extractUrls(chunk)) {
|
|
999
|
+
if (!sentUrls.has(url)) {
|
|
1000
|
+
sentUrls.add(url);
|
|
1001
|
+
await send(`${label} URL:\n${redactSensitive(url)}\n\nOpen it, complete the flow, then paste any code Claude asks for here.`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
const redacted = redactSensitive(chunk).trim();
|
|
1005
|
+
const now = Date.now();
|
|
1006
|
+
if (redacted && now - lastSnippetAt > 3000 && !looksLikeClaudeToken(redacted)) {
|
|
1007
|
+
lastSnippetAt = now;
|
|
1008
|
+
await send(redacted.length > 1200 ? redacted.slice(-1200) : redacted);
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
proc.stdout.on("data", (d) => handleChunk(d.toString()).catch((e) => console.error("Auth output error:", e.message)));
|
|
1013
|
+
proc.stderr.on("data", (d) => handleChunk(d.toString()).catch((e) => console.error("Auth stderr error:", e.message)));
|
|
1014
|
+
proc.on("close", async (code) => {
|
|
1015
|
+
pendingClaudeAuthProcess = null;
|
|
1016
|
+
pendingClaudeAuthLabel = null;
|
|
1017
|
+
const token = opts.captureToken ? extractClaudeToken(output) : null;
|
|
1018
|
+
if (token && !tokenStored) tokenStored = saveClaudeOAuthToken(token);
|
|
1019
|
+
const final = redactSensitive(output.trim());
|
|
1020
|
+
if (tokenStored) await send(`${label} finished. OAuth token stored for launchd/non-interactive Claude runs.`);
|
|
1021
|
+
else if (final) await send(`${label} finished (exit ${code}).\n\n${final.slice(-2500)}`);
|
|
1022
|
+
else await send(`${label} finished (exit ${code}).`);
|
|
1023
|
+
});
|
|
1024
|
+
proc.on("error", async (err) => {
|
|
1025
|
+
pendingClaudeAuthProcess = null;
|
|
1026
|
+
pendingClaudeAuthLabel = null;
|
|
1027
|
+
await send(`${label} failed: ${redactSensitive(err.message)}`);
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
|
|
730
1031
|
// ── Claude Runner ───────────────────────────────────────────────────
|
|
731
1032
|
|
|
732
1033
|
function parseStreamEvents(data) {
|
|
@@ -781,6 +1082,12 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
781
1082
|
return;
|
|
782
1083
|
}
|
|
783
1084
|
|
|
1085
|
+
const authPreflight = preflightClaudeAuthMessage();
|
|
1086
|
+
if (authPreflight) {
|
|
1087
|
+
await send(authPreflight, { replyTo: replyToMsgId });
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
784
1091
|
bot.sendChatAction(CHAT_ID, "typing");
|
|
785
1092
|
statusMessageId = null;
|
|
786
1093
|
streamBuffer = "";
|
|
@@ -793,7 +1100,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
793
1100
|
const binaryPath = getActiveBinary();
|
|
794
1101
|
const proc = spawn(binaryPath, args, {
|
|
795
1102
|
cwd,
|
|
796
|
-
env:
|
|
1103
|
+
env: claudeSubprocessEnv(),
|
|
797
1104
|
stdio: ["ignore", "pipe", "pipe"],
|
|
798
1105
|
detached: process.platform !== "win32",
|
|
799
1106
|
});
|
|
@@ -927,7 +1234,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
927
1234
|
proc.stderr.on("data", (d) => {
|
|
928
1235
|
const chunk = d.toString();
|
|
929
1236
|
stderrBuffer += chunk;
|
|
930
|
-
console.error("STDERR:", chunk);
|
|
1237
|
+
console.error("STDERR:", redactSensitive(chunk));
|
|
931
1238
|
});
|
|
932
1239
|
|
|
933
1240
|
proc.on("close", async (code) => {
|
|
@@ -935,17 +1242,23 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
935
1242
|
clearTimeout(streamInterval); streamInterval = null;
|
|
936
1243
|
clearTimeout(processTimeout);
|
|
937
1244
|
|
|
938
|
-
// Check for auth errors in stderr
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1245
|
+
// Check for auth errors in stderr and give actionable Telegram recovery steps.
|
|
1246
|
+
if (settings.backend !== "cursor" && isClaudeAuthErrorText(stderrBuffer)) {
|
|
1247
|
+
await send(claudeAuthRecoveryMessage(stderrBuffer.trim().slice(-800)), { replyTo: replyToMsgId });
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (settings.backend === "cursor" && isClaudeAuthErrorText(stderrBuffer)) {
|
|
1251
|
+
await send("Cursor authentication error. Run `agent login` on this machine, then retry.", { replyTo: replyToMsgId });
|
|
944
1252
|
return;
|
|
945
1253
|
}
|
|
946
1254
|
|
|
947
1255
|
try {
|
|
948
|
-
|
|
1256
|
+
if (code !== 0 && code !== null && !assistantText.trim()) {
|
|
1257
|
+
await send(claudeEmptyFailureMessage(code, stderrBuffer), { replyTo: replyToMsgId });
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const finalText = redactSensitive(assistantText || "(no output)");
|
|
949
1262
|
const chunks = splitMessage(finalText);
|
|
950
1263
|
const firstChunk = chunks[0];
|
|
951
1264
|
|
|
@@ -964,6 +1277,13 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
964
1277
|
}
|
|
965
1278
|
}
|
|
966
1279
|
if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
|
|
1280
|
+
|
|
1281
|
+
// Send voice reply if input was a voice note
|
|
1282
|
+
if (lastInputWasVoice && TTS_CMD) {
|
|
1283
|
+
lastInputWasVoice = false;
|
|
1284
|
+
const voicePath = textToVoice(finalText);
|
|
1285
|
+
if (voicePath) await sendVoice(voicePath);
|
|
1286
|
+
}
|
|
967
1287
|
} catch (e) {
|
|
968
1288
|
console.error("Final message delivery failed:", e.message);
|
|
969
1289
|
await send("Task completed but failed to deliver the response. Send /continue to see the result.");
|
|
@@ -998,13 +1318,13 @@ async function runClaudeSilent(prompt, cwd, label) {
|
|
|
998
1318
|
"--append-system-prompt", buildSystemPrompt(),
|
|
999
1319
|
"--dangerously-skip-permissions", prompt];
|
|
1000
1320
|
const proc = spawn(CLAUDE_PATH, args, {
|
|
1001
|
-
cwd, env:
|
|
1321
|
+
cwd, env: claudeSubprocessEnv(),
|
|
1002
1322
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1003
1323
|
});
|
|
1004
1324
|
let output = "";
|
|
1005
1325
|
proc.stdout.on("data", (d) => { output += d.toString(); });
|
|
1006
1326
|
proc.on("close", async () => {
|
|
1007
|
-
const chunks = splitMessage(`Cron: ${label}\n\n${output.trim() || "(no output)"}`);
|
|
1327
|
+
const chunks = splitMessage(`Cron: ${label}\n\n${redactSensitive(output.trim() || "(no output)")}`);
|
|
1008
1328
|
for (const c of chunks) await send(c);
|
|
1009
1329
|
resolve();
|
|
1010
1330
|
});
|
|
@@ -1104,6 +1424,7 @@ bot.onText(/\/help/, (msg) => {
|
|
|
1104
1424
|
"Session: /session /sessions /projects /continue /status /stop /end",
|
|
1105
1425
|
"Settings: /model /effort /budget /plan /compact /worktree /mode",
|
|
1106
1426
|
"Automation: /cron /vault /soul",
|
|
1427
|
+
"Claude auth: /auth_status /login /setup_token /use_oauth_token /clear_oauth_token",
|
|
1107
1428
|
"System: /restart /upgrade",
|
|
1108
1429
|
"",
|
|
1109
1430
|
"Send text, voice, photos, or files.",
|
|
@@ -1354,6 +1675,64 @@ bot.onText(/\/soul$/, async (msg) => {
|
|
|
1354
1675
|
await send(`Edit: ${SOUL_FILE}\nOr tell me what to change and I'll update it.`);
|
|
1355
1676
|
});
|
|
1356
1677
|
|
|
1678
|
+
// ── Claude Auth Commands ────────────────────────────────────────────
|
|
1679
|
+
|
|
1680
|
+
bot.onText(/\/(?:auth_status|auth status)$/, async (msg) => {
|
|
1681
|
+
if (!isAuthorized(msg)) return;
|
|
1682
|
+
const tokenInfo = getClaudeOAuthToken();
|
|
1683
|
+
const proc = spawn(CLAUDE_PATH, ["auth", "status"], {
|
|
1684
|
+
cwd: process.env.HOME || require("os").homedir(),
|
|
1685
|
+
env: claudeSubprocessEnv(),
|
|
1686
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1687
|
+
});
|
|
1688
|
+
let output = "";
|
|
1689
|
+
proc.stdout.on("data", (d) => { output += d.toString(); });
|
|
1690
|
+
proc.stderr.on("data", (d) => { output += d.toString(); });
|
|
1691
|
+
proc.on("close", async (code) => {
|
|
1692
|
+
const clean = redactSensitive(output.trim()) || "(no output)";
|
|
1693
|
+
await send([
|
|
1694
|
+
`Claude auth status: exit ${code}`,
|
|
1695
|
+
...summarizeClaudeAuthStatus(output, code, tokenInfo),
|
|
1696
|
+
`Bot OAuth token: ${tokenInfo.value ? "configured via " + tokenInfo.source : "not configured"}`,
|
|
1697
|
+
`Vault: ${vault.isUnlocked() ? "unlocked" : "locked"}`,
|
|
1698
|
+
"",
|
|
1699
|
+
clean.slice(-2500),
|
|
1700
|
+
].join("\n"));
|
|
1701
|
+
});
|
|
1702
|
+
proc.on("error", async (err) => send(`Claude auth status failed: ${redactSensitive(err.message)}`));
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1705
|
+
bot.onText(/\/login$/, async (msg) => {
|
|
1706
|
+
if (!isAuthorized(msg)) return;
|
|
1707
|
+
await runClaudeAuthCommand(["auth", "login", "--claudeai", "--email", "sumeet@inet.africa"], "Claude login");
|
|
1708
|
+
});
|
|
1709
|
+
|
|
1710
|
+
bot.onText(/\/setup_token$/, async (msg) => {
|
|
1711
|
+
if (!isAuthorized(msg)) return;
|
|
1712
|
+
await runClaudeAuthCommand(["setup-token"], "Claude setup-token", { captureToken: true });
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
bot.onText(/\/use_oauth_token(?:\s+(.+))?$/, async (msg, match) => {
|
|
1716
|
+
if (!isAuthorized(msg)) return;
|
|
1717
|
+
const token = (match[1] || "").trim();
|
|
1718
|
+
await deleteMessage(msg.message_id);
|
|
1719
|
+
if (!token) {
|
|
1720
|
+
pendingClaudeAuthProcess = { stdin: { write: (value) => saveClaudeOAuthToken(value.trim()) } };
|
|
1721
|
+
pendingClaudeAuthLabel = "manual OAuth token save";
|
|
1722
|
+
await send("Send the Claude OAuth token in your next message. I'll delete it and store it without echoing it.");
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
if (!looksLikeClaudeToken(token)) return send("That doesn't look like a Claude OAuth token. Not saved.");
|
|
1726
|
+
saveClaudeOAuthToken(token);
|
|
1727
|
+
await send(`Claude OAuth token stored in .env${vault.isUnlocked() ? " and vault" : ""}. Restart the bot so launchd picks it up, or use /restart.`);
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
bot.onText(/\/clear_oauth_token$/, async (msg) => {
|
|
1731
|
+
if (!isAuthorized(msg)) return;
|
|
1732
|
+
clearClaudeOAuthToken();
|
|
1733
|
+
await send("Claude OAuth token cleared from .env/process" + (vault.isUnlocked() ? " and vault." : ". Unlock vault and run again if you also stored it there."));
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1357
1736
|
// ── /vault with password protection ─────────────────────────────────
|
|
1358
1737
|
|
|
1359
1738
|
bot.onText(/\/vault$/, async (msg) => {
|
|
@@ -1527,6 +1906,7 @@ bot.on("voice", async (msg) => {
|
|
|
1527
1906
|
try { fs.unlinkSync(oggPath); } catch (e) {}
|
|
1528
1907
|
if (!transcript) return send("Couldn't transcribe. Try typing it.");
|
|
1529
1908
|
await send(`Heard: "${transcript}"`, { replyTo: msg.message_id });
|
|
1909
|
+
lastInputWasVoice = true;
|
|
1530
1910
|
await runClaude(transcript, currentSession.dir, msg.message_id);
|
|
1531
1911
|
} catch (err) { await send(`Voice failed: ${err.message}`); }
|
|
1532
1912
|
});
|
|
@@ -1597,6 +1977,29 @@ bot.on("message", async (msg) => {
|
|
|
1597
1977
|
if (msg.voice || msg.audio || msg.photo || msg.document || msg.video || msg.sticker) return;
|
|
1598
1978
|
if (isDuplicate(msg.message_id)) return;
|
|
1599
1979
|
|
|
1980
|
+
// Handle pending Claude auth/token paste-back
|
|
1981
|
+
if (pendingClaudeAuthProcess) {
|
|
1982
|
+
const text = msg.text.trim();
|
|
1983
|
+
await deleteMessage(msg.message_id);
|
|
1984
|
+
if (pendingClaudeAuthLabel === "manual OAuth token save") {
|
|
1985
|
+
pendingClaudeAuthProcess = null;
|
|
1986
|
+
pendingClaudeAuthLabel = null;
|
|
1987
|
+
if (!looksLikeClaudeToken(text)) { await send("That doesn't look like a Claude OAuth token. Not saved."); return; }
|
|
1988
|
+
saveClaudeOAuthToken(text);
|
|
1989
|
+
await send(`Claude OAuth token stored in .env${vault.isUnlocked() ? " and vault" : ""}. Restart the bot so launchd picks it up, or use /restart.`);
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
try {
|
|
1993
|
+
pendingClaudeAuthProcess.stdin.write(text + "\n");
|
|
1994
|
+
await send("Sent to Claude auth process.");
|
|
1995
|
+
} catch (e) {
|
|
1996
|
+
pendingClaudeAuthProcess = null;
|
|
1997
|
+
pendingClaudeAuthLabel = null;
|
|
1998
|
+
await send(`Could not send to Claude auth process: ${redactSensitive(e.message)}`);
|
|
1999
|
+
}
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1600
2003
|
// Handle onboarding
|
|
1601
2004
|
if (!isOnboarded() && onboardingStep) {
|
|
1602
2005
|
await handleOnboarding(msg);
|