@yanzc/claude-pro 0.0.0-stage → 0.4.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/.env.example +41 -0
- package/LICENSE +21 -0
- package/README.md +314 -3
- package/bin/claude-pro.js +32 -0
- package/package.json +40 -6
- package/src/agent.js +752 -0
- package/src/agents.js +186 -0
- package/src/api.js +529 -0
- package/src/cache.js +150 -0
- package/src/checkpoints.js +184 -0
- package/src/claude-code.js +60 -0
- package/src/codex/client.js +307 -0
- package/src/codex/translate-request.js +239 -0
- package/src/codex/translate-stream.js +328 -0
- package/src/commands.js +839 -0
- package/src/config.js +453 -0
- package/src/context.js +109 -0
- package/src/filestate.js +57 -0
- package/src/gateway.js +143 -0
- package/src/hooks.js +81 -0
- package/src/main.js +193 -0
- package/src/mcp.js +290 -0
- package/src/modes.js +154 -0
- package/src/oauth.js +287 -0
- package/src/peers.js +157 -0
- package/src/print.js +106 -0
- package/src/prompts.js +179 -0
- package/src/ratelimit.js +244 -0
- package/src/restart.js +115 -0
- package/src/runtime.js +162 -0
- package/src/selfcheck.js +71 -0
- package/src/session.js +139 -0
- package/src/supervisor.js +110 -0
- package/src/tasks.js +76 -0
- package/src/tools/bash.js +227 -0
- package/src/tools/effort.js +41 -0
- package/src/tools/files.js +352 -0
- package/src/tools/index.js +52 -0
- package/src/tools/interaction.js +157 -0
- package/src/tools/notebook.js +91 -0
- package/src/tools/peers.js +45 -0
- package/src/tools/restart.js +50 -0
- package/src/tools/search.js +458 -0
- package/src/tools/skill.js +32 -0
- package/src/tools/task.js +139 -0
- package/src/tools/web.js +283 -0
- package/src/ui/ansi.js +228 -0
- package/src/ui/dialogs.js +122 -0
- package/src/ui/editor.js +389 -0
- package/src/ui/markdown.js +249 -0
- package/src/ui/render.js +155 -0
- package/src/ui/repl.js +1460 -0
- package/src/ui/screen.js +259 -0
- package/src/updater.js +93 -0
- package/src/utils/clipboard.js +197 -0
- package/src/utils/diff.js +95 -0
- package/src/utils/paths.js +37 -0
- package/src/utils/shell.js +251 -0
package/src/prompts.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// System prompts for the main agent, sub-agents and compaction.
|
|
2
|
+
|
|
3
|
+
import { environmentBlock, memoryFiles } from './context.js';
|
|
4
|
+
import { APP_NAME, INSTALL_DIR } from './config.js';
|
|
5
|
+
|
|
6
|
+
export function autoMemorySection({ dir, index }) {
|
|
7
|
+
return `# Auto memory
|
|
8
|
+
You have a persistent, file-based memory for this project at \`${dir}\`. It survives across sessions: use it to remember what will help in future conversations with this user about this project. Write to it with the Write/Edit tools (the directory may not exist yet; Write creates it).
|
|
9
|
+
|
|
10
|
+
- Each memory is one markdown file in that directory holding one fact, with frontmatter:
|
|
11
|
+
---
|
|
12
|
+
name: short-kebab-case-slug
|
|
13
|
+
description: one-line summary, used to decide relevance later
|
|
14
|
+
type: user | feedback | project | reference
|
|
15
|
+
---
|
|
16
|
+
followed by the fact. For feedback and project memories add a **Why:** line and a **How to apply:** line.
|
|
17
|
+
- After writing a memory file, add a one-line pointer to \`MEMORY.md\` in the same directory: \`- [Title](file.md) — hook\`. MEMORY.md is the index loaded into your context at the start of every session; keep it under 200 lines and never put memory content in it.
|
|
18
|
+
- Worth saving: who the user is and how they like to work (user); corrections and approaches the user confirmed, with the reason (feedback); goals, constraints and deadlines that are not visible in the code, with absolute dates (project); pointers to external resources (reference).
|
|
19
|
+
- Not worth saving: anything the code, git history or CLAUDE.md already records, and details that only matter to the current conversation.
|
|
20
|
+
- Before saving, look for an existing memory about the same thing and update it instead of adding a duplicate. Delete memories that turn out to be wrong.
|
|
21
|
+
- When the user asks you to remember or forget something, do it right away.
|
|
22
|
+
- A memory reflects what was true when it was written. Before relying on a remembered file, function or flag, check that it still exists.
|
|
23
|
+
|
|
24
|
+
## Current MEMORY.md
|
|
25
|
+
${index || '(empty: no memories saved yet)'}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function mainSystemPrompt({ cwd, model, shellName, extra = '', autoMemory = null, selfRestart = false, snapshot = null }) {
|
|
29
|
+
const memory = memoryFiles(cwd);
|
|
30
|
+
const memoryText = memory.length
|
|
31
|
+
? `\n\n# Memory\nCodebase and user instructions are shown below. Be sure to adhere to these instructions. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.\n\n${memory
|
|
32
|
+
.map((m) => `Contents of ${m.file} (${m.scope === 'user' ? "user's private global instructions for all projects" : m.scope === 'local' ? "user's private project instructions, not checked in" : 'project instructions, checked into the codebase'}):\n\n${m.text}`)
|
|
33
|
+
.join('\n\n')}`
|
|
34
|
+
: '';
|
|
35
|
+
|
|
36
|
+
return `You are ${APP_NAME}, an interactive command line coding agent modelled on Claude Code. You help users with software engineering tasks: solving bugs, adding functionality, refactoring, explaining code, running builds and tests, and more. Use the instructions below and the tools available to you to assist the user.
|
|
37
|
+
|
|
38
|
+
IMPORTANT: Assist with authorized security testing, defensive security, CTF challenges, and educational contexts. Refuse requests for destructive techniques, DoS attacks, mass targeting, supply chain compromise, or detection evasion for malicious purposes. Dual-use security tools (C2 frameworks, credential testing, exploit development) require clear authorization context: pentesting engagements, CTF competitions, security research, or defensive use cases.
|
|
39
|
+
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
|
|
40
|
+
|
|
41
|
+
# Tone and style
|
|
42
|
+
- Your output is displayed in a command line interface and rendered as GitHub-flavored markdown in a monospace font. Keep responses scannable.
|
|
43
|
+
- Be direct and skip filler (preamble, restating the request, closing summaries of what you just did), but never cut an answer short: match the length to the task. A review, analysis, plan or explanation should be as long as it takes to be complete and correct, while a simple question gets a short answer.
|
|
44
|
+
- Your thinking and your tool calls are not subject to any brevity limit: think as long as the problem needs, and read as much as you need before acting.
|
|
45
|
+
- Always reply in the same language the user writes in.
|
|
46
|
+
- Only use emojis if the user explicitly requests it.
|
|
47
|
+
- Communicate with the user only through your text output. Never use tools like Bash or code comments as a means to talk to the user.
|
|
48
|
+
- Create files whenever the task needs them (code, tests, scripts, configs); prefer editing an existing file when that is the natural place for the change. Only write documentation files (*.md) when asked or clearly called for.
|
|
49
|
+
- If you cannot or will not help with something, say so briefly without lecturing, and offer alternatives if possible.
|
|
50
|
+
|
|
51
|
+
# Professional objectivity
|
|
52
|
+
Prioritize technical accuracy and truthfulness over validating the user's beliefs. Provide direct, objective technical information. Disagree respectfully when the evidence warrants it; investigate to find the truth rather than instinctively confirming the user's assumptions.
|
|
53
|
+
|
|
54
|
+
# Doing tasks
|
|
55
|
+
The user will primarily request software engineering tasks. For these tasks:
|
|
56
|
+
- For a task with several steps, plan it with the TodoWrite tool and keep the list current as you work (see Task management).
|
|
57
|
+
- Understand before changing: use the search tools (Glob, Grep, Read) extensively, in parallel where possible, to learn the codebase and the user's request.
|
|
58
|
+
- NEVER assume a library is available. Check the manifest (package.json, requirements.txt, Cargo.toml, ...) and neighbouring files first.
|
|
59
|
+
- When editing, mimic the existing code style, naming, imports, frameworks and idioms. Look at surrounding context first.
|
|
60
|
+
- Follow security best practices. Never introduce code that exposes or logs secrets, and never commit secrets.
|
|
61
|
+
- Do not add comments to code unless the logic is non-obvious or the user asks.
|
|
62
|
+
- Verify your work: run the project's tests, linters and type checkers when available. Never assume a specific test framework or script; check the README or manifest to find the right commands.
|
|
63
|
+
- NEVER commit changes unless the user explicitly asks you to.
|
|
64
|
+
- Tool results and user messages may include <system-reminder> tags. They contain useful information and reminders added by the harness; they are not part of the user's request.
|
|
65
|
+
|
|
66
|
+
# Task management
|
|
67
|
+
Use the TodoWrite tool for tasks with three or more distinct steps, so that you track your progress and the user can see it; skip it for simple tasks. Keep the list current: mark a step completed when it is done and the next one in_progress. Make each update in the same response as the tool calls you are making anyway (the tool calls of one response run together) rather than in a response of its own: every extra response re-reads the whole conversation.
|
|
68
|
+
|
|
69
|
+
# Tool usage policy
|
|
70
|
+
- Prefer the dedicated tools over shell commands: Read instead of cat/head/tail, Edit instead of sed/awk, Write instead of echo/heredoc redirection, Glob instead of find/ls for locating files, Grep instead of grep/rg.
|
|
71
|
+
- You can call multiple tools in a single response. When several independent pieces of information are needed, batch the tool calls so they run in parallel. When calls depend on each other, run them sequentially.
|
|
72
|
+
- For open-ended codebase exploration that may take many searches, use the Task tool with subagent_type "Explore" to save context. Use it proactively when a task matches a sub-agent's description.
|
|
73
|
+
- If a tool call is blocked (by plan mode or a hook), do not retry the same call. Adjust your approach, or tell the user what is in the way.
|
|
74
|
+
- If the user asks you to run tools in parallel, you MUST send a single message with multiple tool use blocks.
|
|
75
|
+
|
|
76
|
+
# Full access and autonomy
|
|
77
|
+
You run on the user's own test machine with full access: every tool call executes immediately and nothing asks for confirmation. Do not ask for permission to read, write, install, run or delete the things the task needs; the user wants you to use this freedom to get the job done.
|
|
78
|
+
- Work end to end: investigate, implement, run the code, and verify the result before reporting back. Do not stop at a plan unless asked for one.
|
|
79
|
+
- Prefer evidence over assumption: read the code before changing it, and reproduce a bug when you can. Report failures honestly, with the output.
|
|
80
|
+
- Verify in proportion to the risk, cheapest check first: a syntax or type check, then the tests that cover what you changed, and the whole suite once at the end of a change to shared code (in the background while you do something else, when it is long). Do not run a full suite again when nothing it covers has changed since it passed. Keep existing tests: when a change alters behaviour on purpose, update their assertions instead of deleting them. Do not build extra harnesses, mutation checks or review passes unless the user asks or the change is risky (data loss, credentials, killing processes).
|
|
81
|
+
- Keep going through errors: when something fails, diagnose it and try another approach instead of handing the problem back, unless you are genuinely blocked on something only the user can provide.
|
|
82
|
+
- Use sub-agents (Task) for broad searches and independent subtasks, run independent tool calls in parallel, and use run_in_background for long jobs.
|
|
83
|
+
- Full access is not a reason to be careless. Before doing something that cannot be undone and that the user did not ask for (force-pushing, publishing a package, deleting data outside the project, sending messages or spending money through external services), say what you are about to do and wait for an answer.
|
|
84
|
+
- File changes you make with Write, Edit and NotebookEdit are checkpointed per user prompt, and the user can undo them with /rewind. Changes made through Bash are not tracked, so prefer the file tools for edits.
|
|
85
|
+
- Use AskUserQuestion only for decisions that really are the user's to make.
|
|
86
|
+
|
|
87
|
+
${selfRestart ? `# Your own source code
|
|
88
|
+
You are ${APP_NAME}, running from the source code in ${INSTALL_DIR}. Changing files under src/ or bin/ there does not change the process you are running in: it keeps the old code until it restarts. After you change your own code (to fix a bug you ran into, or to add something you need), call the Restart tool: it first starts the new code in a separate process and refuses to restart if that fails (the error comes back to you: fix it and call Restart again); otherwise the program restarts when your turn ends and the conversation continues by itself. Use its \`then\` field to say what to check afterwards. The program also restarts by itself at the end of a turn in which its source changed. Never restart for any other reason, and never in a loop.
|
|
89
|
+
The user wants you to fix yourself, also in the middle of a task in another project: when a tool of yours misbehaves, a limit of yours gets in the way of the task, or a feature the task needs is missing, and the cause is in your own code rather than in the project or the environment, fix it in ${INSTALL_DIR} right away instead of working around it or reporting it. Keep the fix small and to the point; read that repository's CLAUDE.md first. Verify it there (\`node --check\` on each changed file, then \`npm run check\` and the test files that cover the change, run with ${INSTALL_DIR} as the working directory), call Restart with \`then\` saying how to check the fix and what to go on with in the user's task, and mention the fix briefly in your final answer. Do not commit in that repository unless the user asks.
|
|
90
|
+
|
|
91
|
+
` : ''}# Code references
|
|
92
|
+
When referencing specific functions or pieces of code include the pattern \`file_path:line_number\` so the user can navigate to the source.
|
|
93
|
+
|
|
94
|
+
${environmentBlock(cwd, { model, shellName, snapshot })}${memoryText}${autoMemory ? `\n\n${autoMemorySection(autoMemory)}` : ''}${extra ? `\n\n${extra}` : ''}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function subagentSystemPrompt({ cwd, model, shellName, agentPrompt, snapshot = null }) {
|
|
98
|
+
return `${agentPrompt}
|
|
99
|
+
|
|
100
|
+
Notes:
|
|
101
|
+
- You are a sub-agent launched by the main agent. The main agent sees only your final message, so make it a complete, self-contained report of what you found or did. The user does not see your intermediate output.
|
|
102
|
+
- Agent threads have their cwd reset between bash calls; use absolute file paths.
|
|
103
|
+
- In your final response, share relevant file paths (always absolute) and code snippets. Include code snippets only when the exact text is load-bearing.
|
|
104
|
+
- Do not use emojis.
|
|
105
|
+
|
|
106
|
+
${environmentBlock(cwd, { model, shellName, snapshot })}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The terminal tab's title follows the topic of the conversation: after each message the user sends, the
|
|
110
|
+
// small model says whether it starts a new topic and names it (as the official client does).
|
|
111
|
+
export const TOPIC_TITLE_SYSTEM = 'You name the topic of a conversation between a user and a coding assistant, for the title of a terminal tab. Reply with JSON only.';
|
|
112
|
+
|
|
113
|
+
export function topicTitlePrompt(current, message) {
|
|
114
|
+
return `${current ? `Current title: ${current}` : 'There is no title yet.'}
|
|
115
|
+
|
|
116
|
+
The user's new message:
|
|
117
|
+
<message>
|
|
118
|
+
${message}
|
|
119
|
+
</message>
|
|
120
|
+
|
|
121
|
+
${current ? 'Does this message start a new topic (a different task or question), or does it continue the current one (a follow-up, "continue", an answer, a correction)? ' : ''}Reply with only a JSON object: {"isNewTopic": ${current ? 'true or false' : 'true'}, "title": ...}. For a new topic, "title" names it in 2 to 6 words (at most about 16 characters in Chinese or Japanese), in the language of the user's message, with no quotes, no emoji and no punctuation at the end. Otherwise "title" is null.`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const PLAN_MODE_REMINDER = `<system-reminder>
|
|
125
|
+
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received.
|
|
126
|
+
Research the codebase with read-only tools, then present a concise implementation plan by calling the ExitPlanMode tool. Do not ask for approval in plain text; use ExitPlanMode.
|
|
127
|
+
</system-reminder>`;
|
|
128
|
+
|
|
129
|
+
// The system prompt of the plain summary request (see Agent.requestSummary).
|
|
130
|
+
export const SUMMARY_SYSTEM = 'You are a helpful AI assistant tasked with summarizing conversations.';
|
|
131
|
+
|
|
132
|
+
export function compactPrompt(instructions) {
|
|
133
|
+
return `CRITICAL: Respond with TEXT ONLY. Do NOT call any tools: the conversation above is complete, and the only thing wanted now is the summary below.
|
|
134
|
+
|
|
135
|
+
Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
|
|
136
|
+
This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context.
|
|
137
|
+
|
|
138
|
+
Before providing your final summary, wrap your analysis in <analysis> tags to organize your thoughts. Then write the summary in <summary> tags with these sections:
|
|
139
|
+
|
|
140
|
+
1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail
|
|
141
|
+
2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed.
|
|
142
|
+
3. Files and Code Sections: Enumerate specific files and code sections examined, modified, or created. Include full code snippets where applicable and a summary of why each file read or edit is important.
|
|
143
|
+
4. Errors and fixes: List all errors that you ran into, and how you fixed them. Pay special attention to specific user feedback.
|
|
144
|
+
5. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
|
|
145
|
+
6. All user messages: List ALL user messages that are not tool results. These are critical for understanding the users' feedback and changing intent.
|
|
146
|
+
7. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
|
|
147
|
+
8. Current Work: Describe in detail precisely what was being worked on immediately before this summary request, including file names and code snippets.
|
|
148
|
+
9. Optional Next Step: The next step directly in line with the user's most recent explicit request. Include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off.
|
|
149
|
+
|
|
150
|
+
Write the summary in the same language the user has been using.
|
|
151
|
+
Do NOT call any tools. Respond with text only.${instructions ? `\n\nAdditional instructions from the user for this summary:\n${instructions}` : ''}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function continuationMessage(summary, { auto }) {
|
|
155
|
+
return `This session is being continued from a previous conversation that ran out of context. The conversation is summarized below:
|
|
156
|
+
${summary}${auto ? '\n\nPlease continue the conversation from where we left it off without asking the user any further questions. Continue with the last task that you were asked to work on.' : ''}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const INIT_PROMPT = `Please analyze this codebase and create a CLAUDE.md file, which will be given to future instances of this coding agent to operate in this repository.
|
|
160
|
+
|
|
161
|
+
What to add:
|
|
162
|
+
1. Commands that will be commonly used, such as how to build, lint, and run tests. Include the necessary commands to develop in this codebase, such as how to run a single test.
|
|
163
|
+
2. High-level code architecture and structure so that future instances can be productive more quickly. Focus on the "big picture" architecture that requires reading multiple files to understand.
|
|
164
|
+
|
|
165
|
+
Usage notes:
|
|
166
|
+
- If there's already a CLAUDE.md, suggest improvements to it.
|
|
167
|
+
- When you make the initial CLAUDE.md, do not repeat yourself and do not include obvious instructions like "Provide helpful error messages to users" or "Write unit tests for all new utilities".
|
|
168
|
+
- Avoid listing every component or file structure that can be easily discovered.
|
|
169
|
+
- Don't include generic development practices.
|
|
170
|
+
- If there are Cursor rules (in .cursor/rules/ or .cursorrules), Copilot rules (in .github/copilot-instructions.md) or an AGENTS.md, make sure to include the important parts.
|
|
171
|
+
- If there is a README.md, make sure to include the important parts.
|
|
172
|
+
- Do not make up information such as "Common Development Tasks", "Tips for Development", "Support and Documentation" unless this is expressly included in other files that you read.
|
|
173
|
+
- Be sure to prefix the file with the following text:
|
|
174
|
+
|
|
175
|
+
\`\`\`
|
|
176
|
+
# CLAUDE.md
|
|
177
|
+
|
|
178
|
+
This file provides guidance to coding agents when working with code in this repository.
|
|
179
|
+
\`\`\``;
|
package/src/ratelimit.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// Rate limits: reading when a limit resets from the response headers, deciding how long to wait
|
|
2
|
+
// before retrying, and the wording shown to the user.
|
|
3
|
+
//
|
|
4
|
+
// Subscription (Claude Code OAuth) requests are limited by rolling windows, 5 hours and 7 days. When
|
|
5
|
+
// one is used up the API answers 429, and the anthropic-ratelimit-unified-* headers say which window
|
|
6
|
+
// it was and when it resets (epoch seconds; these are the fields the official client reads).
|
|
7
|
+
// Waiting for such a reset takes hours, so it is a pause that ends by itself rather than one of the
|
|
8
|
+
// few quick retries used for transient errors.
|
|
9
|
+
|
|
10
|
+
export const LONG_WAIT_MS = 30_000; // waits this long or longer are shown as "rate limit reached, resuming at ..."
|
|
11
|
+
export const MAX_RETRY_AFTER_MS = 5 * 60_000; // when waiting for limits is off, a longer Retry-After is reported instead
|
|
12
|
+
const RESUME_MARGIN_MS = 2_000; // wake a little after the announced reset (clock skew)
|
|
13
|
+
const RESUME_JITTER_MS = 8_000; // and spread out agents that were all waiting for the same reset
|
|
14
|
+
const SLEEP_TICK_MS = 15_000; // long sleeps are cut into pieces so a suspended computer is noticed on waking
|
|
15
|
+
const MIN_429_ATTEMPTS = 10; // quick retries for a 429 that announces a short wait
|
|
16
|
+
|
|
17
|
+
// Replaceable clock, so tests can run an hours-long wait instantly.
|
|
18
|
+
export const timing = {
|
|
19
|
+
now: () => Date.now(),
|
|
20
|
+
sleep: (ms, signal) => abortableSleep(ms, signal),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function abortableSleep(ms, signal) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const aborted = () => Object.assign(new Error('aborted'), { name: 'AbortError' });
|
|
26
|
+
if (signal?.aborted) return reject(aborted());
|
|
27
|
+
const onAbort = () => { clearTimeout(timer); reject(aborted()); };
|
|
28
|
+
const timer = setTimeout(() => { signal?.removeEventListener('abort', onAbort); resolve(); }, ms);
|
|
29
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Sleep until the wall clock reaches `deadline` (ms since epoch). The wait is made of short sleeps
|
|
34
|
+
// and the clock is checked after each one: timers do not advance while the computer is suspended,
|
|
35
|
+
// so a single long timeout could end hours after the limit was lifted.
|
|
36
|
+
export async function sleepUntil(deadline, signal) {
|
|
37
|
+
for (;;) {
|
|
38
|
+
const left = deadline - timing.now();
|
|
39
|
+
if (left <= 0) return;
|
|
40
|
+
await timing.sleep(Math.min(left, SLEEP_TICK_MS), signal);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Milliseconds a "Retry-After" header asks us to wait (delta-seconds or an HTTP date), or null.
|
|
45
|
+
export function parseRetryAfter(value, now = Date.now()) {
|
|
46
|
+
const s = String(value ?? '').trim();
|
|
47
|
+
if (!s) return null;
|
|
48
|
+
if (/^\d+(?:\.\d+)?$/.test(s)) return Math.round(parseFloat(s) * 1000);
|
|
49
|
+
const at = Date.parse(s);
|
|
50
|
+
return Number.isFinite(at) ? Math.max(0, at - now) : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Wait before quick retry number `attempt` (1-based): exponential backoff with jitter, or the
|
|
54
|
+
// server's Retry-After when that is longer.
|
|
55
|
+
export function retryDelay(attempt, retryAfterMs = null, rand = Math.random) {
|
|
56
|
+
const backoff = Math.min(32_000, 1000 * 2 ** (attempt - 1)) + Math.floor(rand() * 500);
|
|
57
|
+
return retryAfterMs == null ? backoff : Math.max(backoff, retryAfterMs);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function headerValue(headers, name) {
|
|
61
|
+
if (!headers) return null;
|
|
62
|
+
if (typeof headers.get === 'function') return headers.get(name);
|
|
63
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
64
|
+
if (key.toLowerCase() === name) return Array.isArray(value) ? value[0] : value;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Epoch seconds, or milliseconds if the number is too big to be seconds. null if not a time.
|
|
70
|
+
function epochMs(value) {
|
|
71
|
+
const n = Number(value);
|
|
72
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
73
|
+
return n > 1e11 ? n : n * 1000;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const WINDOW_LABELS = {
|
|
77
|
+
five_hour: '5-hour limit',
|
|
78
|
+
seven_day: 'weekly limit',
|
|
79
|
+
seven_day_opus: 'weekly Opus limit',
|
|
80
|
+
seven_day_sonnet: 'weekly Sonnet limit',
|
|
81
|
+
overage: 'extra usage limit',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// What a 429's headers say about the limit:
|
|
85
|
+
// {label, claim, status, waitMs, resetAt, source}
|
|
86
|
+
// waitMs is how long until a retry can succeed (null when the server did not say) and resetAt the
|
|
87
|
+
// moment that is (ms since epoch). When both a unified reset and a Retry-After are present the
|
|
88
|
+
// longer wait is taken: retrying early only gets rejected again.
|
|
89
|
+
export function parseRateLimit(headers, now = Date.now()) {
|
|
90
|
+
const h = (name) => headerValue(headers, name) || null;
|
|
91
|
+
const status = h('anthropic-ratelimit-unified-status');
|
|
92
|
+
const overage = h('anthropic-ratelimit-unified-overage-status');
|
|
93
|
+
const claim = h('anthropic-ratelimit-unified-representative-claim');
|
|
94
|
+
const unifiedReset = epochMs(h('anthropic-ratelimit-unified-reset'));
|
|
95
|
+
const retryAfter = parseRetryAfter(h('retry-after'), now);
|
|
96
|
+
|
|
97
|
+
const found = [];
|
|
98
|
+
// The official client only treats the reset as a wait when the window is rejected and extra
|
|
99
|
+
// usage (overage) cannot take over.
|
|
100
|
+
if (status === 'rejected' && (!overage || overage === 'rejected') && unifiedReset !== null) {
|
|
101
|
+
found.push({ source: 'unified-reset', waitMs: Math.max(0, unifiedReset - now) });
|
|
102
|
+
}
|
|
103
|
+
if (retryAfter !== null) found.push({ source: 'retry-after', waitMs: retryAfter });
|
|
104
|
+
const best = found.sort((a, b) => b.waitMs - a.waitMs)[0] || null;
|
|
105
|
+
|
|
106
|
+
const label = WINDOW_LABELS[claim] || (claim?.startsWith('seven_day') ? 'weekly limit' : 'rate limit');
|
|
107
|
+
return {
|
|
108
|
+
label, claim, status,
|
|
109
|
+
waitMs: best ? best.waitMs : null,
|
|
110
|
+
resetAt: best ? now + best.waitMs : null,
|
|
111
|
+
source: best ? best.source : null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// "45s", "12m 30s", "2h 13m", "5d 13h". From an hour up it is rounded to the nearest minute, so a
|
|
116
|
+
// reset 2h 59m 59.6s away reads "3h 0m", in line with the clock time shown next to it.
|
|
117
|
+
export function humanDuration(ms) {
|
|
118
|
+
const s = Math.max(0, Math.round(ms / 1000));
|
|
119
|
+
if (s < 60) return `${s}s`;
|
|
120
|
+
if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
121
|
+
const m = Math.round(s / 60);
|
|
122
|
+
const h = Math.floor(m / 60);
|
|
123
|
+
if (h < 24) return `${h}h ${m % 60}m`;
|
|
124
|
+
return `${Math.floor(h / 24)}d ${h % 24}h`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Local time of day, with the date when it is not today: "21:05", "tomorrow 02:15", "Oct 9 21:05".
|
|
128
|
+
export function formatClock(ms, now = Date.now()) {
|
|
129
|
+
const d = new Date(ms);
|
|
130
|
+
const hhmm = d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
131
|
+
const day = (t) => new Date(t).setHours(0, 0, 0, 0);
|
|
132
|
+
const days = Math.round((day(ms) - day(now)) / 86_400_000);
|
|
133
|
+
if (days === 0) return hhmm;
|
|
134
|
+
if (days === 1) return `tomorrow ${hhmm}`;
|
|
135
|
+
return `${d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} ${hhmm}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
139
|
+
|
|
140
|
+
// The error text when waiting is not possible or not allowed. `why` is 'budget' (the wait is longer
|
|
141
|
+
// than rateLimitMaxWait) or 'off' (waiting for limits is switched off or not wanted here).
|
|
142
|
+
function limitMessage(err, rl, state, budget, now, why) {
|
|
143
|
+
const base = err.message;
|
|
144
|
+
if (rl?.waitMs == null || rl.waitMs < LONG_WAIT_MS) {
|
|
145
|
+
return state.waitedMs > 0 ? `${base} (still rate limited after waiting ${humanDuration(state.waitedMs)})` : base;
|
|
146
|
+
}
|
|
147
|
+
if (err.status !== 429) return `${base} (the server asked to wait ${humanDuration(rl.waitMs)} before retrying)`;
|
|
148
|
+
const reset = `${capitalize(rl.label)} reached; it resets at ${formatClock(rl.resetAt, now)} (in ${humanDuration(rl.waitMs)})`;
|
|
149
|
+
if (why === 'budget') return `${base} (${reset}, beyond the ${humanDuration(budget)} this session may wait. CLAUDE_PRO_RATE_LIMIT_MAX_WAIT_MS raises that.)`;
|
|
150
|
+
return `${base} (${reset}${budget === 0 ? '. Set CLAUDE_PRO_RATE_LIMIT_MAX_WAIT_MS above 0 to wait for it automatically' : ''}.)`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Decide what to do after a failed request. Returns
|
|
154
|
+
// {action: 'throw', message?} give up (message replaces the error text)
|
|
155
|
+
// {action: 'wait', kind, delayMs, resumeAt, ...} wait, then try again
|
|
156
|
+
// kind is 'retry' for a quick retry and 'rate_limit' for a pause until a limit resets.
|
|
157
|
+
//
|
|
158
|
+
// `state` is {attempt, rateLimitHits, waitedMs}, kept across the attempts of one request.
|
|
159
|
+
// 429s are handled as follows (unless rateLimitMaxWait is 0, retries are off, or allowWait is false):
|
|
160
|
+
// - the headers announce a wait of 30s or more (an exhausted 5-hour or weekly window): wait for
|
|
161
|
+
// that reset, as long as it fits in the budget (rateLimitMaxWait, default 6 hours);
|
|
162
|
+
// - a short announced wait: a few quick retries, counted like any other;
|
|
163
|
+
// - nothing announced: back off 1s, 2s, 4s ... up to rateLimitPoll (5 minutes) between attempts, within the budget.
|
|
164
|
+
export function planRetry(err, state, config, { now = Date.now(), rand = Math.random, allowWait = true } = {}) {
|
|
165
|
+
if (!err.retryable) return { action: 'throw' };
|
|
166
|
+
const maxRetries = config.maxRetries;
|
|
167
|
+
const rl = err.rateLimit || null;
|
|
168
|
+
const budget = config.rateLimitMaxWait;
|
|
169
|
+
const quick = (attempt, max, retryAfterMs) => {
|
|
170
|
+
const delayMs = retryDelay(attempt, retryAfterMs, rand);
|
|
171
|
+
return { action: 'wait', kind: 'retry', delayMs, resumeAt: now + delayMs, attempt, maxRetries: max };
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (err.status === 429 && allowWait && maxRetries > 0 && budget > 0) {
|
|
175
|
+
const known = rl?.waitMs ?? null;
|
|
176
|
+
if (known !== null && known < LONG_WAIT_MS) {
|
|
177
|
+
state.attempt++;
|
|
178
|
+
const max = Math.max(maxRetries, MIN_429_ATTEMPTS);
|
|
179
|
+
return state.attempt > max ? { action: 'throw' } : quick(state.attempt, max, known);
|
|
180
|
+
}
|
|
181
|
+
state.rateLimitHits++;
|
|
182
|
+
const polling = known === null;
|
|
183
|
+
const delayMs = polling
|
|
184
|
+
? Math.min(config.rateLimitPoll, 1000 * 2 ** (state.rateLimitHits - 1)) + Math.floor(rand() * 500)
|
|
185
|
+
: known + RESUME_MARGIN_MS + Math.floor(rand() * RESUME_JITTER_MS);
|
|
186
|
+
if (state.waitedMs + delayMs > budget) return { action: 'throw', message: limitMessage(err, rl, state, budget, now, 'budget') };
|
|
187
|
+
state.waitedMs += delayMs;
|
|
188
|
+
return {
|
|
189
|
+
action: 'wait',
|
|
190
|
+
kind: delayMs >= LONG_WAIT_MS ? 'rate_limit' : 'retry',
|
|
191
|
+
delayMs,
|
|
192
|
+
resumeAt: now + delayMs,
|
|
193
|
+
resetAt: rl?.resetAt ?? null,
|
|
194
|
+
label: rl?.label ?? 'rate limit',
|
|
195
|
+
polling,
|
|
196
|
+
attempt: state.rateLimitHits,
|
|
197
|
+
maxRetries: null,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Everything else: a limited number of attempts with exponential backoff. What the server
|
|
202
|
+
// announced (a reset time or Retry-After) still counts: a wait too long to sit through is an error.
|
|
203
|
+
// That is 5 minutes for most errors. A 429 only gets here when nobody waits for limits (a diagnostic
|
|
204
|
+
// request, or waiting switched off): then a limit that lasts 30 seconds or more is reported with its
|
|
205
|
+
// reset time at once, rather than sat through attempt after attempt (10 of them, silently).
|
|
206
|
+
state.attempt++;
|
|
207
|
+
const max = err.status === 429 && maxRetries > 0 ? Math.max(maxRetries, MIN_429_ATTEMPTS) : maxRetries;
|
|
208
|
+
if (state.attempt > max) return { action: 'throw' };
|
|
209
|
+
const announced = rl?.waitMs ?? err.retryAfterMs ?? null;
|
|
210
|
+
const tooLong = announced !== null && (err.status === 429 ? announced >= LONG_WAIT_MS : announced > MAX_RETRY_AFTER_MS);
|
|
211
|
+
if (tooLong) return { action: 'throw', message: limitMessage(err, rl, state, budget, now, 'off') };
|
|
212
|
+
return quick(state.attempt, max, announced);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Words for the pause notice, shared by the interactive and the print mode.
|
|
216
|
+
export function describePause(ev, now = Date.now()) {
|
|
217
|
+
// The limit is over but the subscription login has expired meanwhile (its token lives 8 hours) and could
|
|
218
|
+
// not be renewed by this program: the request goes on as soon as there is a valid login in the file.
|
|
219
|
+
if (ev.waitingForLogin) {
|
|
220
|
+
const until = ev.giveUpAt ? ` Giving up at ${formatClock(ev.giveUpAt, now)}.` : '';
|
|
221
|
+
if (ev.renewError) return `The limit has reset, but the login could not be renewed: ${ev.renewError} Trying again every 5 minutes, and this continues by itself as soon as the login is valid.${until}`;
|
|
222
|
+
return `The limit has reset, but the Claude Code login has expired and cannot be renewed from here. Waiting for it to be renewed: run claude once, and this continues by itself.${until}`;
|
|
223
|
+
}
|
|
224
|
+
const label = capitalize(ev.label || 'rate limit');
|
|
225
|
+
const left = humanDuration(Math.max(0, ev.resumeAt - now));
|
|
226
|
+
const text = ev.polling
|
|
227
|
+
? `${label} reached and the server gave no reset time. Checking again at ${formatClock(ev.resumeAt, now)} (in ${left}), and every few minutes after that, then continuing automatically.`
|
|
228
|
+
: `${label} reached. Pausing until ${formatClock(ev.resumeAt, now)} (in ${left}), then continuing automatically.`;
|
|
229
|
+
const warning = ev.loginExpiresAt && ev.loginExpiresAt < ev.resumeAt
|
|
230
|
+
? ` The Claude Code login expires at ${formatClock(ev.loginExpiresAt, now)}, before that, and it cannot be renewed from here. If no Claude Code session is running to renew it, run claude once after the reset: this continues by itself once the login is valid again.`
|
|
231
|
+
: '';
|
|
232
|
+
return text + warning;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Whether a rate_limit_wait event should be announced to the user. `prev` is the wait that was last
|
|
236
|
+
// announced for the same pause (null at the first one). Polling repeats every few minutes and the
|
|
237
|
+
// announced reset moves a little between waits: those only continue the pause already announced,
|
|
238
|
+
// unless the reset moved by more than two minutes. Waiting for the login to be renewed is a different
|
|
239
|
+
// kind of wait from waiting for the limit, and always worth saying when it begins.
|
|
240
|
+
export function shouldAnnounce(prev, ev) {
|
|
241
|
+
if (!prev) return true;
|
|
242
|
+
if (Boolean(prev.waitingForLogin) !== Boolean(ev.waitingForLogin)) return true;
|
|
243
|
+
return !ev.polling && Math.abs(ev.resumeAt - prev.resumeAt) > 120_000;
|
|
244
|
+
}
|
package/src/restart.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// The program's side of restarting itself (the supervisor's is supervisor.js): what is handed from one
|
|
2
|
+
// process to the next, the check that the new code starts before it is trusted, and the watch on the
|
|
3
|
+
// source files.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { INSTALL_DIR } from './config.js';
|
|
9
|
+
import { ENV_SUPERVISED, ENV_RESTART_FILE, ENV_RESTARTED, RESTART_EXIT_CODE } from './supervisor.js';
|
|
10
|
+
|
|
11
|
+
export { RESTART_EXIT_CODE };
|
|
12
|
+
|
|
13
|
+
export const MAX_RESTARTS = 5; // more than this many within RESTART_WINDOW_MS is a loop, not a workflow
|
|
14
|
+
export const RESTART_WINDOW_MS = 5 * 60_000;
|
|
15
|
+
|
|
16
|
+
// True in a session that runs under the supervisor, so that it can restart.
|
|
17
|
+
export function canRestart(env = process.env) {
|
|
18
|
+
return env[ENV_SUPERVISED] === '1' && Boolean(env[ENV_RESTART_FILE]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Leave what the next process needs, where the supervisor said. Throws when it cannot be written, so
|
|
22
|
+
// that a restart that would lose the conversation does not happen.
|
|
23
|
+
export function writeRestartState(state, env = process.env) {
|
|
24
|
+
const file = env[ENV_RESTART_FILE];
|
|
25
|
+
if (!file) throw new Error('not running under the supervisor');
|
|
26
|
+
fs.writeFileSync(file, JSON.stringify(state), { mode: 0o600 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// In a process that was started by a restart: what the previous one left (once). {} when there is nothing
|
|
30
|
+
// readable, which still tells the caller that this is a restart, not a fresh start. null otherwise.
|
|
31
|
+
export function takeRestartState(env = process.env) {
|
|
32
|
+
if (env[ENV_RESTARTED] !== '1') return null;
|
|
33
|
+
const file = env[ENV_RESTART_FILE];
|
|
34
|
+
if (!file) return {};
|
|
35
|
+
try {
|
|
36
|
+
const state = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
37
|
+
return state && typeof state === 'object' ? state : {};
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
} finally {
|
|
41
|
+
try { fs.rmSync(file, { force: true }); } catch { /* ignore */ }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const tidy = (text) => text.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '').replace(/\r/g, '').replace(/\n+Node\.js v[\d.]+\s*$/, '').trim().slice(0, 4000);
|
|
46
|
+
|
|
47
|
+
// Start the program's own code in a throwaway process (`claude-pro --self-check`: every module loads, the
|
|
48
|
+
// interface comes up in a fake terminal, the tools and the system prompt are well formed) and say whether
|
|
49
|
+
// it worked. Resolves {ok, output, ms}; never rejects.
|
|
50
|
+
export function runSelfCheck({ root = INSTALL_DIR, signal, timeoutMs = 60_000 } = {}) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const began = Date.now();
|
|
53
|
+
const env = { ...process.env, CLAUDE_PRO_SELF_CHECK: '1' };
|
|
54
|
+
for (const key of [ENV_SUPERVISED, ENV_RESTART_FILE, ENV_RESTARTED]) delete env[key];
|
|
55
|
+
let output = '';
|
|
56
|
+
let timedOut = false;
|
|
57
|
+
let child;
|
|
58
|
+
try {
|
|
59
|
+
child = spawn(process.execPath, [path.join(root, 'bin', 'claude-pro.js'), '--self-check'], { cwd: root, env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
60
|
+
} catch (e) {
|
|
61
|
+
resolve({ ok: false, output: e.message, ms: 0 });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const add = (chunk) => { if (output.length < 40_000) output += chunk; };
|
|
65
|
+
child.stdout.on('data', add);
|
|
66
|
+
child.stderr.on('data', add);
|
|
67
|
+
const kill = () => { try { child.kill(); } catch { /* already gone */ } };
|
|
68
|
+
const timer = setTimeout(() => { timedOut = true; kill(); }, timeoutMs);
|
|
69
|
+
signal?.addEventListener('abort', kill, { once: true });
|
|
70
|
+
const done = (ok, text) => {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
signal?.removeEventListener('abort', kill);
|
|
73
|
+
resolve({ ok, output: text, ms: Date.now() - began });
|
|
74
|
+
};
|
|
75
|
+
child.on('error', (e) => done(false, e.message));
|
|
76
|
+
child.on('close', (code) => {
|
|
77
|
+
if (timedOut) return done(false, `The check did not finish within ${Math.round(timeoutMs / 1000)} seconds.\n${tidy(output)}`.trim());
|
|
78
|
+
if (signal?.aborted) return done(false, 'The check was cancelled.');
|
|
79
|
+
done(code === 0, tidy(output));
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Watch the program's own source (src/ and bin/) and call `onChange(file)` once the files have stopped
|
|
85
|
+
// changing for a moment. Returns {close()}. A platform that cannot watch just never calls back.
|
|
86
|
+
export function watchSource(root, onChange) {
|
|
87
|
+
const watchers = [];
|
|
88
|
+
let timer = null;
|
|
89
|
+
const hit = (_event, name) => {
|
|
90
|
+
if (name && !/\.(m?js|json)$/i.test(String(name))) return;
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
timer = setTimeout(() => { try { onChange(String(name || '')); } catch { /* the callback's problem */ } }, 400);
|
|
93
|
+
timer.unref?.();
|
|
94
|
+
};
|
|
95
|
+
for (const dir of ['src', 'bin']) {
|
|
96
|
+
try {
|
|
97
|
+
const watcher = fs.watch(path.join(root, dir), { recursive: true, persistent: false }, hit);
|
|
98
|
+
watcher.on('error', () => { /* a directory that vanished, a watch limit: not worth a crash */ });
|
|
99
|
+
watchers.push(watcher);
|
|
100
|
+
} catch { /* not supported here */ }
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
close() {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
for (const w of watchers) { try { w.close(); } catch { /* ignore */ } }
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// What the model is told when the conversation continues in the new process. It starts with "[" so that it
|
|
111
|
+
// is never taken for what the session is about (see firstUserText).
|
|
112
|
+
export function restartNote({ reason = '', then = '' } = {}) {
|
|
113
|
+
const why = reason ? ` (${reason})` : '';
|
|
114
|
+
return `[claude-pro restarted${why}. The new code is loaded and the conversation was resumed from the saved session. ${then ? `Continue with: ${then}` : 'Carry on where you left off.'}]`;
|
|
115
|
+
}
|