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 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.0:** Use GPT-5.4 with your ChatGPT Plus subscription no API key needed. Just `/login` OpenAI OAuth. Same access as Codex CLI.
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
- return msgs;
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");
@@ -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;
@@ -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 ──
@@ -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());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemaxxing",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "Open-source terminal coding agent. Connect any LLM. Max your code.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {