codemaxxing 1.1.2 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agent.d.ts +15 -0
- package/dist/agent.js +73 -1
- package/dist/index.js +3 -1
- package/dist/ui/connection.js +11 -0
- package/dist/ui/input-router.d.ts +2 -0
- package/dist/ui/input-router.js +7 -0
- package/dist/ui/paste-interceptor.js +9 -0
- package/dist/utils/auth.js +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
Open-source terminal coding agent. Connect **any** LLM — local or remote — and start building. Like Claude Code, but you bring your own model.
|
|
12
12
|
|
|
13
|
-
**🆕 v1.1.
|
|
13
|
+
**🆕 v1.1.3:** GPT-5.4 via ChatGPT Plus OAuth, Anthropic OAuth auto-refresh, better Windows terminal behavior, Escape-to-cancel, model picker fixes, and smoother first-run auth/model selection.
|
|
14
14
|
|
|
15
15
|
## Why?
|
|
16
16
|
|
package/dist/agent.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ChatCompletionTool } from "openai/resources/chat/completions";
|
|
1
2
|
import { type ConnectedServer } from "./utils/mcp.js";
|
|
2
3
|
import type { ProviderConfig } from "./config.js";
|
|
3
4
|
export declare function getModelCost(model: string): {
|
|
@@ -28,6 +29,7 @@ export declare class CodingAgent {
|
|
|
28
29
|
private providerType;
|
|
29
30
|
private currentApiKey;
|
|
30
31
|
private currentBaseUrl;
|
|
32
|
+
private aborted;
|
|
31
33
|
private messages;
|
|
32
34
|
private tools;
|
|
33
35
|
private cwd;
|
|
@@ -97,6 +99,19 @@ export declare class CodingAgent {
|
|
|
97
99
|
/**
|
|
98
100
|
* Switch to a different model mid-session
|
|
99
101
|
*/
|
|
102
|
+
/**
|
|
103
|
+
* Get available tools (for UI hints, capabilities display, etc.)
|
|
104
|
+
*/
|
|
105
|
+
getTools(): ChatCompletionTool[];
|
|
106
|
+
/**
|
|
107
|
+
* Abort the current generation. Safe to call from any thread.
|
|
108
|
+
*/
|
|
109
|
+
abort(): void;
|
|
110
|
+
/**
|
|
111
|
+
* Check if generation was aborted, and reset the flag.
|
|
112
|
+
*/
|
|
113
|
+
isAborted(): boolean;
|
|
114
|
+
private resetAbort;
|
|
100
115
|
switchModel(model: string, baseUrl?: string, apiKey?: string, providerType?: "openai" | "anthropic"): void;
|
|
101
116
|
/**
|
|
102
117
|
* Attempt to refresh an expired Anthropic OAuth token.
|
package/dist/agent.js
CHANGED
|
@@ -153,6 +153,7 @@ export class CodingAgent {
|
|
|
153
153
|
providerType;
|
|
154
154
|
currentApiKey = null;
|
|
155
155
|
currentBaseUrl = "";
|
|
156
|
+
aborted = false;
|
|
156
157
|
messages = [];
|
|
157
158
|
tools = FILE_TOOLS;
|
|
158
159
|
cwd;
|
|
@@ -268,6 +269,7 @@ export class CodingAgent {
|
|
|
268
269
|
* and loops until the model responds with text (no more tool calls).
|
|
269
270
|
*/
|
|
270
271
|
async chat(userMessage) {
|
|
272
|
+
this.resetAbort();
|
|
271
273
|
const userMsg = { role: "user", content: userMessage };
|
|
272
274
|
this.messages.push(userMsg);
|
|
273
275
|
saveMessage(this.sessionId, userMsg);
|
|
@@ -312,6 +314,14 @@ export class CodingAgent {
|
|
|
312
314
|
let chunkPromptTokens = 0;
|
|
313
315
|
let chunkCompletionTokens = 0;
|
|
314
316
|
for await (const chunk of stream) {
|
|
317
|
+
// Check for abort
|
|
318
|
+
if (this.aborted) {
|
|
319
|
+
try {
|
|
320
|
+
stream.controller?.abort();
|
|
321
|
+
}
|
|
322
|
+
catch { }
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
315
325
|
// Capture usage from the final chunk
|
|
316
326
|
if (chunk.usage) {
|
|
317
327
|
chunkPromptTokens = chunk.usage.prompt_tokens ?? 0;
|
|
@@ -382,6 +392,11 @@ export class CodingAgent {
|
|
|
382
392
|
(this.totalCompletionTokens / 1_000_000) * costs.output;
|
|
383
393
|
updateSessionCost(this.sessionId, this.totalPromptTokens, this.totalCompletionTokens, this.totalCost);
|
|
384
394
|
}
|
|
395
|
+
// If aborted, return what we have so far
|
|
396
|
+
if (this.aborted) {
|
|
397
|
+
updateTokenEstimate(this.sessionId, this.estimateTokens());
|
|
398
|
+
return contentText ? contentText + "\n\n_(cancelled)_" : "_(cancelled)_";
|
|
399
|
+
}
|
|
385
400
|
// If no tool calls, we're done — return the text
|
|
386
401
|
if (toolCalls.size === 0) {
|
|
387
402
|
updateTokenEstimate(this.sessionId, this.estimateTokens());
|
|
@@ -542,12 +557,33 @@ export class CodingAgent {
|
|
|
542
557
|
});
|
|
543
558
|
}
|
|
544
559
|
}
|
|
545
|
-
|
|
560
|
+
// Sanitize: remove tool_result messages that don't have a matching tool_use
|
|
561
|
+
const validToolUseIds = new Set();
|
|
562
|
+
for (const m of msgs) {
|
|
563
|
+
if (m.role === "assistant" && Array.isArray(m.content)) {
|
|
564
|
+
for (const block of m.content) {
|
|
565
|
+
if (block.type === "tool_use") {
|
|
566
|
+
validToolUseIds.add(block.id);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return msgs.filter((m) => {
|
|
572
|
+
if (m.role === "user" && Array.isArray(m.content)) {
|
|
573
|
+
const toolResults = m.content.filter((b) => b.type === "tool_result");
|
|
574
|
+
if (toolResults.length > 0) {
|
|
575
|
+
// Only keep if ALL tool_results have matching tool_use
|
|
576
|
+
return toolResults.every((tr) => validToolUseIds.has(tr.tool_use_id));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return true;
|
|
580
|
+
});
|
|
546
581
|
}
|
|
547
582
|
/**
|
|
548
583
|
* Anthropic-native streaming chat
|
|
549
584
|
*/
|
|
550
585
|
async chatAnthropic(_userMessage) {
|
|
586
|
+
this.resetAbort();
|
|
551
587
|
let iterations = 0;
|
|
552
588
|
const MAX_ITERATIONS = 20;
|
|
553
589
|
while (iterations < MAX_ITERATIONS) {
|
|
@@ -587,6 +623,10 @@ export class CodingAgent {
|
|
|
587
623
|
tools: anthropicTools,
|
|
588
624
|
});
|
|
589
625
|
stream.on("text", (text) => {
|
|
626
|
+
if (this.aborted) {
|
|
627
|
+
stream.abort();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
590
630
|
contentText += text;
|
|
591
631
|
this.options.onToken?.(text);
|
|
592
632
|
});
|
|
@@ -607,6 +647,11 @@ export class CodingAgent {
|
|
|
607
647
|
// Re-throw if we can't handle it
|
|
608
648
|
throw err;
|
|
609
649
|
}
|
|
650
|
+
// If aborted, return what we have
|
|
651
|
+
if (this.aborted) {
|
|
652
|
+
updateTokenEstimate(this.sessionId, this.estimateTokens());
|
|
653
|
+
return contentText ? contentText + "\n\n_(cancelled)_" : "_(cancelled)_";
|
|
654
|
+
}
|
|
610
655
|
// Track usage
|
|
611
656
|
if (finalMessage.usage) {
|
|
612
657
|
const promptTokens = finalMessage.usage.input_tokens;
|
|
@@ -735,6 +780,7 @@ export class CodingAgent {
|
|
|
735
780
|
* OpenAI Responses API chat (for Codex OAuth tokens + GPT-5.4)
|
|
736
781
|
*/
|
|
737
782
|
async chatOpenAIResponses(userMessage) {
|
|
783
|
+
this.resetAbort();
|
|
738
784
|
let iterations = 0;
|
|
739
785
|
const MAX_ITERATIONS = 20;
|
|
740
786
|
while (iterations < MAX_ITERATIONS) {
|
|
@@ -771,6 +817,11 @@ export class CodingAgent {
|
|
|
771
817
|
}
|
|
772
818
|
this.messages.push(assistantMessage);
|
|
773
819
|
saveMessage(this.sessionId, assistantMessage);
|
|
820
|
+
// If aborted, return what we have
|
|
821
|
+
if (this.aborted) {
|
|
822
|
+
updateTokenEstimate(this.sessionId, this.estimateTokens());
|
|
823
|
+
return contentText ? contentText + "\n\n_(cancelled)_" : "_(cancelled)_";
|
|
824
|
+
}
|
|
774
825
|
// If no tool calls, we're done
|
|
775
826
|
if (toolCalls.length === 0) {
|
|
776
827
|
updateTokenEstimate(this.sessionId, this.estimateTokens());
|
|
@@ -868,6 +919,27 @@ export class CodingAgent {
|
|
|
868
919
|
/**
|
|
869
920
|
* Switch to a different model mid-session
|
|
870
921
|
*/
|
|
922
|
+
/**
|
|
923
|
+
* Get available tools (for UI hints, capabilities display, etc.)
|
|
924
|
+
*/
|
|
925
|
+
getTools() {
|
|
926
|
+
return this.tools;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Abort the current generation. Safe to call from any thread.
|
|
930
|
+
*/
|
|
931
|
+
abort() {
|
|
932
|
+
this.aborted = true;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Check if generation was aborted, and reset the flag.
|
|
936
|
+
*/
|
|
937
|
+
isAborted() {
|
|
938
|
+
return this.aborted;
|
|
939
|
+
}
|
|
940
|
+
resetAbort() {
|
|
941
|
+
this.aborted = false;
|
|
942
|
+
}
|
|
871
943
|
switchModel(model, baseUrl, apiKey, providerType) {
|
|
872
944
|
this.model = model;
|
|
873
945
|
if (apiKey)
|
package/dist/index.js
CHANGED
|
@@ -825,6 +825,8 @@ function App() {
|
|
|
825
825
|
setLoading,
|
|
826
826
|
setSpinnerMsg,
|
|
827
827
|
agent,
|
|
828
|
+
streaming,
|
|
829
|
+
loading,
|
|
828
830
|
setModelName,
|
|
829
831
|
addMsg,
|
|
830
832
|
exit,
|
|
@@ -854,7 +856,7 @@ function App() {
|
|
|
854
856
|
default:
|
|
855
857
|
return _jsx(Text, { children: msg.text }, msg.id);
|
|
856
858
|
}
|
|
857
|
-
}), loading && !approval && !streaming && _jsx(NeonSpinner, { message: spinnerMsg, colors: theme.colors }), streaming && !loading && _jsx(StreamingIndicator, { colors: theme.colors }), approval && (_jsx(ApprovalPrompt, { approval: approval, colors: theme.colors })), loginPicker && (_jsx(LoginPicker, { loginPickerIndex: loginPickerIndex, colors: theme.colors })), loginMethodPicker && (_jsx(LoginMethodPickerUI, { loginMethodPicker: loginMethodPicker, loginMethodIndex: loginMethodIndex, colors: theme.colors })), skillsPicker === "menu" && (_jsx(SkillsMenu, { skillsPickerIndex: skillsPickerIndex, colors: theme.colors })), skillsPicker === "browse" && (_jsx(SkillsBrowse, { skillsPickerIndex: skillsPickerIndex, colors: theme.colors })), skillsPicker === "installed" && (_jsx(SkillsInstalled, { skillsPickerIndex: skillsPickerIndex, sessionDisabledSkills: sessionDisabledSkills, colors: theme.colors })), skillsPicker === "remove" && (_jsx(SkillsRemove, { skillsPickerIndex: skillsPickerIndex, colors: theme.colors })), themePicker && (_jsx(ThemePickerUI, { themePickerIndex: themePickerIndex, theme: theme })), sessionPicker && (_jsx(SessionPicker, { sessions: sessionPicker, selectedIndex: sessionPickerIndex, colors: theme.colors })), deleteSessionPicker && (_jsx(DeleteSessionPicker, { sessions: deleteSessionPicker, selectedIndex: deleteSessionPickerIndex, colors: theme.colors })), deleteSessionConfirm && (_jsx(DeleteSessionConfirm, { session: deleteSessionConfirm, colors: theme.colors })), providerPicker && !selectedProvider && (_jsx(ProviderPicker, { providers: providerPicker, selectedIndex: providerPickerIndex, colors: theme.colors })), selectedProvider && modelPickerGroups && modelPickerGroups[selectedProvider] && (_jsx(ModelPicker, { providerName: selectedProvider, models: modelPickerGroups[selectedProvider], selectedIndex: modelPickerIndex, activeModel: modelName, colors: theme.colors })), ollamaDeletePicker && (_jsx(OllamaDeletePicker, { models: ollamaDeletePicker.models, selectedIndex: ollamaDeletePickerIndex, colors: theme.colors })), ollamaPullPicker && (_jsx(OllamaPullPicker, { selectedIndex: ollamaPullPickerIndex, colors: theme.colors })), ollamaDeleteConfirm && (_jsx(OllamaDeleteConfirm, { model: ollamaDeleteConfirm.model, size: ollamaDeleteConfirm.size, colors: theme.colors })), ollamaPulling && (_jsx(OllamaPullProgress, { model: ollamaPulling.model, progress: ollamaPulling.progress, colors: theme.colors })), ollamaExitPrompt && (_jsx(OllamaExitPrompt, { colors: theme.colors })), wizardScreen === "connection" && (_jsx(WizardConnection, { wizardIndex: wizardIndex, colors: theme.colors })), wizardScreen === "models" && wizardHardware && (_jsx(WizardModels, { wizardIndex: wizardIndex, wizardHardware: wizardHardware, wizardModels: wizardModels, colors: theme.colors })), wizardScreen === "install-ollama" && (_jsx(WizardInstallOllama, { wizardHardware: wizardHardware, colors: theme.colors })), wizardScreen === "pulling" && (wizardSelectedModel || wizardPullProgress) && (_jsx(WizardPulling, { wizardSelectedModel: wizardSelectedModel, wizardPullProgress: wizardPullProgress, wizardPullError: wizardPullError, colors: theme.colors })), showSuggestions && (_jsx(CommandSuggestions, { cmdMatches: cmdMatches, cmdIndex: cmdIndex, colors: theme.colors })), _jsxs(Box, { borderStyle: "single", borderColor: approval ? theme.colors.warning : theme.colors.border, paddingX: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "> " }), approval ? (_jsx(Text, { color: theme.colors.warning, children: "waiting for approval..." })) : ready && !loading && !wizardScreen ? (_jsxs(Box, { children: [pastedChunks.map((p) => (_jsxs(Text, { color: theme.colors.muted, children: ["[Pasted text #", p.id, " +", p.lines, " lines]"] }, p.id))), _jsx(TextInput, { value: input, onChange: (v) => { setInput(sanitizeInputArtifacts(v)); setCmdIndex(0); }, onSubmit: handleSubmit }, inputKey)] })) : (_jsx(Text, { dimColor: true, children: loading ? "waiting for response..." : "initializing..." }))] }), agent && (_jsx(StatusBar, { agent: agent, modelName: modelName, sessionDisabledSkills: sessionDisabledSkills }))] }));
|
|
859
|
+
}), loading && !approval && !streaming && _jsx(NeonSpinner, { message: spinnerMsg, colors: theme.colors }), streaming && !loading && _jsx(StreamingIndicator, { colors: theme.colors }), approval && (_jsx(ApprovalPrompt, { approval: approval, colors: theme.colors })), loginPicker && (_jsx(LoginPicker, { loginPickerIndex: loginPickerIndex, colors: theme.colors })), loginMethodPicker && (_jsx(LoginMethodPickerUI, { loginMethodPicker: loginMethodPicker, loginMethodIndex: loginMethodIndex, colors: theme.colors })), skillsPicker === "menu" && (_jsx(SkillsMenu, { skillsPickerIndex: skillsPickerIndex, colors: theme.colors })), skillsPicker === "browse" && (_jsx(SkillsBrowse, { skillsPickerIndex: skillsPickerIndex, colors: theme.colors })), skillsPicker === "installed" && (_jsx(SkillsInstalled, { skillsPickerIndex: skillsPickerIndex, sessionDisabledSkills: sessionDisabledSkills, colors: theme.colors })), skillsPicker === "remove" && (_jsx(SkillsRemove, { skillsPickerIndex: skillsPickerIndex, colors: theme.colors })), themePicker && (_jsx(ThemePickerUI, { themePickerIndex: themePickerIndex, theme: theme })), sessionPicker && (_jsx(SessionPicker, { sessions: sessionPicker, selectedIndex: sessionPickerIndex, colors: theme.colors })), deleteSessionPicker && (_jsx(DeleteSessionPicker, { sessions: deleteSessionPicker, selectedIndex: deleteSessionPickerIndex, colors: theme.colors })), deleteSessionConfirm && (_jsx(DeleteSessionConfirm, { session: deleteSessionConfirm, colors: theme.colors })), providerPicker && !selectedProvider && (_jsx(ProviderPicker, { providers: providerPicker, selectedIndex: providerPickerIndex, colors: theme.colors })), selectedProvider && modelPickerGroups && modelPickerGroups[selectedProvider] && (_jsx(ModelPicker, { providerName: selectedProvider, models: modelPickerGroups[selectedProvider], selectedIndex: modelPickerIndex, activeModel: modelName, colors: theme.colors })), ollamaDeletePicker && (_jsx(OllamaDeletePicker, { models: ollamaDeletePicker.models, selectedIndex: ollamaDeletePickerIndex, colors: theme.colors })), ollamaPullPicker && (_jsx(OllamaPullPicker, { selectedIndex: ollamaPullPickerIndex, colors: theme.colors })), ollamaDeleteConfirm && (_jsx(OllamaDeleteConfirm, { model: ollamaDeleteConfirm.model, size: ollamaDeleteConfirm.size, colors: theme.colors })), ollamaPulling && (_jsx(OllamaPullProgress, { model: ollamaPulling.model, progress: ollamaPulling.progress, colors: theme.colors })), ollamaExitPrompt && (_jsx(OllamaExitPrompt, { colors: theme.colors })), wizardScreen === "connection" && (_jsx(WizardConnection, { wizardIndex: wizardIndex, colors: theme.colors })), wizardScreen === "models" && wizardHardware && (_jsx(WizardModels, { wizardIndex: wizardIndex, wizardHardware: wizardHardware, wizardModels: wizardModels, colors: theme.colors })), wizardScreen === "install-ollama" && (_jsx(WizardInstallOllama, { wizardHardware: wizardHardware, colors: theme.colors })), wizardScreen === "pulling" && (wizardSelectedModel || wizardPullProgress) && (_jsx(WizardPulling, { wizardSelectedModel: wizardSelectedModel, wizardPullProgress: wizardPullProgress, wizardPullError: wizardPullError, colors: theme.colors })), showSuggestions && (_jsx(CommandSuggestions, { cmdMatches: cmdMatches, cmdIndex: cmdIndex, colors: theme.colors })), _jsxs(Box, { borderStyle: process.platform === "win32" && !process.env.WT_SESSION ? "classic" : "single", borderColor: approval ? theme.colors.warning : theme.colors.border, paddingX: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "> " }), approval ? (_jsx(Text, { color: theme.colors.warning, children: "waiting for approval..." })) : ready && !loading && !wizardScreen ? (_jsxs(Box, { children: [pastedChunks.map((p) => (_jsxs(Text, { color: theme.colors.muted, children: ["[Pasted text #", p.id, " +", p.lines, " lines]"] }, p.id))), _jsx(TextInput, { value: input, onChange: (v) => { setInput(sanitizeInputArtifacts(v)); setCmdIndex(0); }, onSubmit: handleSubmit }, inputKey)] })) : (_jsx(Text, { dimColor: true, children: loading ? "waiting for response..." : "initializing..." }))] }), agent && (_jsx(StatusBar, { agent: agent, modelName: modelName, sessionDisabledSkills: sessionDisabledSkills }))] }));
|
|
858
860
|
}
|
|
859
861
|
// Clear screen before render
|
|
860
862
|
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
package/dist/ui/connection.js
CHANGED
|
@@ -192,4 +192,15 @@ export async function connectToProvider(isRetry, ctx) {
|
|
|
192
192
|
if (isRetry) {
|
|
193
193
|
ctx.addMsg("info", `✅ Connected to ${provider.model}`);
|
|
194
194
|
}
|
|
195
|
+
else {
|
|
196
|
+
// First-time connection — show capabilities hint
|
|
197
|
+
const tools = a.getTools();
|
|
198
|
+
const toolCount = tools.length;
|
|
199
|
+
const toolNames = tools
|
|
200
|
+
.map((t) => t.function.name.replace(/_/g, " "))
|
|
201
|
+
.slice(0, 3)
|
|
202
|
+
.join(", ");
|
|
203
|
+
ctx.addMsg("info", `💡 You can: ${toolNames}${toolCount > 3 ? `, +${toolCount - 3} more` : ""}\n` +
|
|
204
|
+
` Try: "list files in src/" or "read main.ts"`);
|
|
205
|
+
}
|
|
195
206
|
}
|
|
@@ -169,6 +169,8 @@ export interface InputRouterContext extends WizardContext {
|
|
|
169
169
|
setCtrlCPressed: (val: boolean) => void;
|
|
170
170
|
agent: CodingAgent | null;
|
|
171
171
|
setModelName: (val: string) => void;
|
|
172
|
+
streaming: boolean;
|
|
173
|
+
loading: boolean;
|
|
172
174
|
exit: () => void;
|
|
173
175
|
refreshConnectionBanner: () => Promise<void>;
|
|
174
176
|
handleSubmit: (value: string) => void;
|
package/dist/ui/input-router.js
CHANGED
|
@@ -43,6 +43,13 @@ export function routeKeyPress(inputChar, key, ctx) {
|
|
|
43
43
|
return true;
|
|
44
44
|
if (handleApprovalPrompts(inputChar, key, ctx))
|
|
45
45
|
return true;
|
|
46
|
+
// Escape to abort generation (when loading or streaming)
|
|
47
|
+
if (key.escape && (ctx.streaming || ctx.loading) && ctx.agent) {
|
|
48
|
+
ctx.agent.abort();
|
|
49
|
+
ctx.setLoading(false);
|
|
50
|
+
ctx.addMsg("info", "⏹ Generation cancelled.");
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
46
53
|
if (handleCtrlCExit(inputChar, key, ctx))
|
|
47
54
|
return true;
|
|
48
55
|
return false;
|
|
@@ -17,6 +17,15 @@ import { consumePendingPasteEndMarkerChunk, shouldSwallowPostPasteDebris } from
|
|
|
17
17
|
*/
|
|
18
18
|
export function setupPasteInterceptor() {
|
|
19
19
|
const pasteEvents = new EventEmitter();
|
|
20
|
+
// Detect Windows CMD/conhost — these don't support bracketed paste or ANSI sequences well.
|
|
21
|
+
// On these terminals, skip paste interception entirely to avoid eating keystrokes.
|
|
22
|
+
const isWindowsLegacyTerminal = process.platform === "win32" && (!process.env.WT_SESSION && // Not Windows Terminal
|
|
23
|
+
!process.env.TERM_PROGRAM // Not a modern terminal emulator
|
|
24
|
+
);
|
|
25
|
+
if (isWindowsLegacyTerminal) {
|
|
26
|
+
// Just return a dummy event bus — no interception, no burst buffering
|
|
27
|
+
return pasteEvents;
|
|
28
|
+
}
|
|
20
29
|
// Enable bracketed paste mode — terminal wraps pastes in escape sequences
|
|
21
30
|
process.stdout.write("\x1b[?2004h");
|
|
22
31
|
// ── Internal state ──
|
package/dist/utils/auth.js
CHANGED
|
@@ -149,10 +149,12 @@ function generatePKCE() {
|
|
|
149
149
|
export async function openRouterOAuth(onStatus) {
|
|
150
150
|
const { verifier, challenge } = generatePKCE();
|
|
151
151
|
return new Promise((resolve, reject) => {
|
|
152
|
+
let handled = false; // Guard against duplicate callbacks
|
|
152
153
|
// Start local callback server
|
|
153
154
|
const server = createServer(async (req, res) => {
|
|
154
155
|
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
155
|
-
if (url.pathname === "/callback") {
|
|
156
|
+
if (url.pathname === "/callback" && !handled) {
|
|
157
|
+
handled = true;
|
|
156
158
|
const code = url.searchParams.get("code");
|
|
157
159
|
if (!code) {
|
|
158
160
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
@@ -175,6 +177,9 @@ export async function openRouterOAuth(onStatus) {
|
|
|
175
177
|
});
|
|
176
178
|
if (!exchangeRes.ok) {
|
|
177
179
|
const errText = await exchangeRes.text();
|
|
180
|
+
if (exchangeRes.status === 409) {
|
|
181
|
+
throw new Error(`OpenRouter returned 409 (Conflict) — this usually means the auth code was already used. Please try /login again.`);
|
|
182
|
+
}
|
|
178
183
|
throw new Error(`Exchange failed (${exchangeRes.status}): ${errText}`);
|
|
179
184
|
}
|
|
180
185
|
const data = (await exchangeRes.json());
|