@yanzc/claude-pro 0.4.1 → 0.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yanzc/claude-pro",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "claude-pro: a self-implemented coding agent for Anthropic-compatible APIs or local Claude Code subscription login. Full access with no permission prompts; file edits are checkpointed and can be rewound.",
5
5
  "type": "module",
6
6
  "repository": {
package/src/api.js CHANGED
@@ -7,7 +7,7 @@ import { refreshClaudeCodeLogin, canRenewLogin } from './oauth.js';
7
7
  import { humanDuration, parseRateLimit, parseRetryAfter, planRetry, sleepUntil, timing } from './ratelimit.js';
8
8
  import { cacheControl, cachePlanOf, fingerprint } from './cache.js';
9
9
  import { readGatewayLogin, fetchGatewayModels, cachedGatewayModel, gatewayEffort, gatewayDefaultEffort } from './gateway.js';
10
- import { createCodexFetch, codexModels } from './codex/client.js';
10
+ import { createCodexFetch, codexModels, codexEffort } from './codex/client.js';
11
11
  import { SIGNATURE_PREFIX } from './codex/translate-request.js';
12
12
 
13
13
  let cached = null;
@@ -197,21 +197,31 @@ export const effortSupported = (modelId) => !noEffort.has(modelId);
197
197
  // The effort a model uses when none is sent (docs.claude.com, "Effort"): medium on Opus 5.5 and Haiku 5.5, high on
198
198
  // the other models that take one; for a GPT model, the gateway's metadata or the local Codex models cache. null for
199
199
  // a model that takes none or is not known.
200
+ // The environment that says where the Codex install is: CODEX_HOME may be set in the shell only.
201
+ const codexEnv = (config) => ({ ...process.env, ...(config?.env || {}) });
202
+ const gatewayModelInfo = (config, id) => (config?.usesGateway ? cachedGatewayModel(readGatewayLogin(config.gatewayUrl), id) : null);
203
+
200
204
  export function defaultEffort(modelId, config = null) {
201
205
  const id = String(modelId || '');
202
206
  if (!effortSupported(id)) return null;
203
207
  if (/^gpt-/i.test(id)) {
204
- if (config?.usesGateway) {
205
- const login = readGatewayLogin(config.gatewayUrl);
206
- return gatewayDefaultEffort(cachedGatewayModel(login, id)) || codexModels(config.env).models.find((m) => m.slug === id)?.defaultEffort || null;
207
- }
208
- return codexModels(config?.env).models.find((m) => m.slug === id)?.defaultEffort || null;
208
+ return gatewayDefaultEffort(gatewayModelInfo(config, id)) || codexModels(codexEnv(config)).models.find((m) => m.slug === id)?.defaultEffort || null;
209
209
  }
210
210
  if (/^claude-(?:opus|haiku)-5-5(?:-|$)/i.test(id)) return 'medium';
211
211
  if (/^claude-(?:fable|mythos|opus-(?:5|4-[5-8])|sonnet-(?:5|4-6))(?:-|$)/i.test(id)) return 'high';
212
212
  return null;
213
213
  }
214
214
 
215
+ // The level a request for this model carries when `effort` is chosen: the nearest one the model takes. A GPT model
216
+ // lists its own (the gateway's /v1/models, else the local Codex models cache). Claude has no "ultra", the level
217
+ // above max that some GPT models take, which stays chosen when the user switches from such a model to Claude.
218
+ export function effortToSend(config, modelId, effort) {
219
+ if (!effort) return effort;
220
+ if (!/^gpt-/i.test(modelId)) return effort === 'ultra' ? 'max' : effort;
221
+ const info = gatewayModelInfo(config, modelId);
222
+ return info ? gatewayEffort(info, effort) : codexEffort(modelId, effort, codexEnv(config));
223
+ }
224
+
215
225
  // Pull the allowed maximum out of an error such as
216
226
  // "max_tokens: 128000 > 64000, which is the maximum allowed number of output tokens for claude-..."
217
227
  // "Invalid max_tokens value, the valid range of max_tokens is [1, 8192]"
@@ -277,8 +287,7 @@ export function buildParams(config, { model, system, messages, tools, maxTokens,
277
287
  }
278
288
  // A GPT model with no effort set gets its default from the gateway's model metadata (or the local Codex cache
279
289
  // when using the Codex provider), sent explicitly so that the footer and backend use the same level.
280
- let eff = (effort === undefined ? config.effort : effort) || (/^gpt-/i.test(model.id) ? defaultEffort(model.id, config) : null);
281
- if (config.usesGateway && /^gpt-/i.test(model.id)) eff = gatewayEffort(cachedGatewayModel(readGatewayLogin(config.gatewayUrl), model.id), eff);
290
+ const eff = effortToSend(config, model.id, (effort === undefined ? config.effort : effort) || (/^gpt-/i.test(model.id) ? defaultEffort(model.id, config) : null));
282
291
  if (eff && !noEffort.has(model.id)) params.output_config = { effort: eff };
283
292
  const budget = config.thinkingBudget;
284
293
  if (budget === 0) params.thinking = { type: 'disabled' };
@@ -344,7 +353,8 @@ export async function streamMessage(config, request, { signal, onEvent = () => {
344
353
  if (config.usesGateway && /^gpt-/i.test(request.model?.id)) {
345
354
  const gatewayLogin = readGatewayLogin(config.gatewayUrl);
346
355
  if (gatewayLogin) {
347
- try { await fetchGatewayModels(gatewayLogin, { signal }); } catch (e) { if (e.name === 'AbortError' || signal?.aborted) throw e; }
356
+ // A gateway that does not answer costs at most a few seconds once (see fetchGatewayModels), not the request.
357
+ try { await fetchGatewayModels(gatewayLogin, { signal }); } catch { if (signal?.aborted) throw Object.assign(new Error('aborted'), { name: 'AbortError', partial: [] }); }
348
358
  }
349
359
  }
350
360
  let client = getClient(config, login);
package/src/commands.js CHANGED
@@ -6,8 +6,8 @@ import { c, padEnd, truncate } from './ui/ansi.js';
6
6
  import { SelectDialog } from './ui/dialogs.js';
7
7
  import { renderTodos, resultLines, formatTokens, formatDuration } from './ui/render.js';
8
8
  import { loadConfig, settingsFiles, updateSettingsFile, rememberChoice, applyRememberedChoices, KNOWN_KEYS, VERSION, APP_NAME, CONFIG_DIR } from './config.js';
9
- import { loginWithBrowser, readGatewayLogin, clearGatewayLogin, fetchGatewayAccount, fetchGatewayModels, cachedGatewayModel, gatewayEfforts, gatewayEffort, formatUsd } from './gateway.js';
10
- import { codexLoginSummary, codexModels, codexEfforts, codexEffort } from './codex/client.js';
9
+ import { loginWithBrowser, readGatewayLogin, clearGatewayLogin, fetchGatewayAccount, fetchGatewayModels, cachedGatewayModel, gatewayEfforts, formatUsd } from './gateway.js';
10
+ import { codexLoginSummary, codexModels, codexEfforts } from './codex/client.js';
11
11
  import { readClaudeCodeLogin } from './claude-code.js';
12
12
  import { SessionStore } from './session.js';
13
13
  import { memoryFiles, listAutoMemories } from './context.js';
@@ -15,7 +15,7 @@ import { INIT_PROMPT } from './prompts.js';
15
15
  import { loadCustomCommands, renderSkill, expandCommandBody, expandShellEmbeds } from './agents.js';
16
16
  import { listBackgroundShells } from './tools/bash.js';
17
17
  import { estimateTokens } from './agent.js';
18
- import { quickQuery, cacheRefusals, defaultEffort } from './api.js';
18
+ import { quickQuery, cacheRefusals, defaultEffort, effortToSend } from './api.js';
19
19
  import { hasRipgrep } from './utils/shell.js';
20
20
  import { displayPath } from './utils/paths.js';
21
21
  import { rewind } from './checkpoints.js';
@@ -272,10 +272,8 @@ export function buildCommands(app) {
272
272
  if (!choice) {
273
273
  const configured = (cfg.env.CLAUDE_CODE_EFFORT_LEVEL || '').trim().toLowerCase() || 'auto';
274
274
  const fallback = defaultEffort(id, cfg);
275
- // A level this GPT model does not take goes out as the nearest one it does.
276
- const sent = cfg.effort && isGpt(id)
277
- ? modelInfo ? gatewayEffort(modelInfo, cfg.effort) : codexEffort(id, cfg.effort)
278
- : cfg.effort;
275
+ // A level this model does not take goes out as the nearest one it does.
276
+ const sent = effortToSend(cfg, id, cfg.effort);
279
277
  const current = !cfg.effort ? c.bold(`auto (${fallback ? `model default: ${fallback}` : 'not sent'})`)
280
278
  : `${c.bold(cfg.effort)}${sent !== cfg.effort ? c.yellow(` (${id} does not take it: sent as ${sent})`) : ''}`;
281
279
  choice = await app.showDialog(new SelectDialog({
package/src/gateway.js CHANGED
@@ -69,11 +69,26 @@ export async function fetchGatewayAccount({ url, apiKey }, { fetchImpl = globalT
69
69
 
70
70
  // The models the gateway serves (GET /v1/models), fetched once per gateway and key. The reasoning fields are
71
71
  // optional so older gateways still work; when present they are kept with the model for /effort and requests.
72
+ // Every GPT request waits for it the first time, so it gives up after a few seconds, and after a failure it is not
73
+ // asked again for a minute: a gateway whose /v1/models fails must not slow down every request.
72
74
  const modelLists = new Map();
73
- export async function fetchGatewayModels({ url, apiKey }, { fetchImpl = globalThis.fetch, signal } = {}) {
75
+ const modelListFailures = new Map(); // key -> {at, error}
76
+ const MODELS_TIMEOUT_MS = 5000;
77
+ const MODELS_RETRY_MS = 60_000;
78
+ export async function fetchGatewayModels({ url, apiKey }, { fetchImpl = globalThis.fetch, signal, timeoutMs = MODELS_TIMEOUT_MS } = {}) {
74
79
  const key = `${normalizeGatewayUrl(url)}\n${apiKey}`;
75
80
  if (modelLists.has(key)) return modelLists.get(key);
76
- const body = await gatewayJson(`${normalizeGatewayUrl(url)}/v1/models`, { headers: { authorization: `Bearer ${apiKey}` }, signal }, fetchImpl);
81
+ const failed = modelListFailures.get(key);
82
+ if (failed && Date.now() - failed.at < MODELS_RETRY_MS) throw failed.error;
83
+ const timeout = AbortSignal.timeout(timeoutMs);
84
+ let body;
85
+ try {
86
+ body = await gatewayJson(`${normalizeGatewayUrl(url)}/v1/models`, { headers: { authorization: `Bearer ${apiKey}` }, signal: signal ? AbortSignal.any([signal, timeout]) : timeout }, fetchImpl);
87
+ } catch (e) {
88
+ if (!signal?.aborted) modelListFailures.set(key, { at: Date.now(), error: e });
89
+ throw e;
90
+ }
91
+ modelListFailures.delete(key);
77
92
  const list = (body.data || []).filter((m) => typeof m?.id === 'string').map((m) => ({
78
93
  ...m,
79
94
  supported_reasoning_levels: (Array.isArray(m.supported_reasoning_levels) ? m.supported_reasoning_levels : Array.isArray(m.reasoning_levels) ? m.reasoning_levels : [])
@@ -22,7 +22,9 @@ export const ListPeersTool = {
22
22
 
23
23
  export const MessagePeerTool = {
24
24
  name: 'MessagePeer',
25
- description: `Send a task or a question to another claude-pro session running on this machine (ListPeers lists them). The other session's agent works on it in its own conversation and directory, and the final text of its answer comes back to you later as a <peer-message kind="reply"> message: you do not wait for it here. Use it to delegate work that belongs to another session's project or context, or to ask it something only it knows. Write the message so that it stands on its own: the other agent has not seen your conversation.`,
25
+ description: `Send a task or a question to another claude-pro session running on this machine (ListPeers lists them). The other session's agent works on it in its own conversation and directory, and the final text of its answer comes back to you later as a <peer-message kind="reply"> message: you do not wait for it here. Use it to delegate work that belongs to another session's project or context, or to ask it something only it knows. Write the message so that it stands on its own: the other agent has not seen your conversation.
26
+
27
+ Send it once and wait for the answer: do not send reminders or "please report" follow-ups while the other session works. Messages that arrive while it is busy are handled together after what it is doing, so they only add to its work; the answer comes by itself when it is done.`,
26
28
  input_schema: {
27
29
  type: 'object',
28
30
  properties: {
package/src/ui/repl.js CHANGED
@@ -16,9 +16,9 @@ import {
16
16
  } from './render.js';
17
17
  import { createRuntime } from '../runtime.js';
18
18
  import { SessionStore, firstUserText } from '../session.js';
19
- import { defaultEffort, effortSupported, quickQuery } from '../api.js';
19
+ import { defaultEffort, effortSupported, effortToSend, quickQuery } from '../api.js';
20
20
  import { CONFIG_DIR, VERSION, APP_NAME, INSTALL_DIR, rememberChoice } from '../config.js';
21
- import { readGatewayLogin, cachedGatewayModel, gatewayEffort } from '../gateway.js';
21
+ import { readGatewayLogin } from '../gateway.js';
22
22
  import { TOPIC_TITLE_SYSTEM, topicTitlePrompt } from '../prompts.js';
23
23
  import { buildCommands } from '../commands.js';
24
24
  import { canRestart, writeRestartState, runSelfCheck, watchSource, restartNote, RESTART_EXIT_CODE, MAX_RESTARTS, RESTART_WINDOW_MS } from '../restart.js';
@@ -30,7 +30,6 @@ import { estimateTokens } from '../agent.js';
30
30
  import { readClipboardImage, imageFromFile, watchClipboard } from '../utils/clipboard.js';
31
31
  import { checkForUpdate } from '../updater.js';
32
32
  import { PeerNode, peersEnabled, findPeer, peerMessagePrompt } from '../peers.js';
33
- import { codexEffort } from '../codex/client.js';
34
33
 
35
34
  // A model id as people say it: claude-sonnet-5-5 -> sonnet-5-5, claude-haiku-4-5-20251001 -> haiku-4-5.
36
35
  export const modelTag = (model) => String(model?.id ?? '').replace(/^claude-/, '').replace(/-\d{8}$/, '');
@@ -39,13 +38,9 @@ export const modelTag = (model) => String(model?.id ?? '').replace(/^claude-/, '
39
38
  // is left out when none is sent (auto) and for a model that does not take one.
40
39
  export function modelInfo(config) {
41
40
  const model = config.mainModel;
42
- // A GPT model gets the nearest level it takes; gateway metadata wins over the local Codex cache.
43
- const gatewayModel = config.usesGateway && /^gpt-/i.test(model.id)
44
- ? cachedGatewayModel(readGatewayLogin(config.gatewayUrl), model.id) : null;
45
- const chosen = config.effort && /^gpt-/i.test(model.id)
46
- ? gatewayModel ? gatewayEffort(gatewayModel, config.effort) : codexEffort(model.id, config.effort)
47
- : config.effort;
48
- const effort = chosen && effortSupported(model.id) ? chosen : null;
41
+ // What the requests carry: the nearest level the model takes (none goes out as low on gpt-6.1-sol, ultra as max on Claude).
42
+ const sent = effortToSend(config, model.id, config.effort);
43
+ const effort = sent && effortSupported(model.id) ? sent : null;
49
44
  return { model: modelTag(model), effort };
50
45
  }
51
46
 
@@ -387,7 +382,11 @@ export class App {
387
382
  const text = error ? `The session failed before finishing: ${error.message || error}`
388
383
  : result?.interrupted ? `The user of ${this.peers.name} interrupted the work before it was finished.${result.text ? `\n\nWhat it had written:\n${result.text}` : ''}`
389
384
  : (result?.text || '(no answer text)');
390
- for (const msg of tasks) {
385
+ // One answer per session, to its last message: several messages from it that waited for this turn were all
386
+ // handled in it, and sending the whole answer once for each would hand that session the same text again and again.
387
+ const lastOfEach = new Map();
388
+ for (const msg of tasks) lastOfEach.set(`${msg.from.id}|${msg.from.name}|${msg.from.cwd}`, msg);
389
+ for (const msg of lastOfEach.values()) {
391
390
  // by id, or by name: a session that was restarted by older code comes back with a new id
392
391
  const running = this.peers.peers();
393
392
  const peer = findPeer(running, msg.from.id) || running.find((p) => p.name === msg.from.name && p.cwd === msg.from.cwd);