claude-code-modes 0.2.9 → 0.2.11
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/package.json +1 -1
- package/prompts/base/doing-tasks.md +1 -0
- package/prompts/base/env.md +2 -2
- package/prompts/base/session-guidance.md +1 -1
- package/prompts/chill/core.md +3 -1
- package/prompts/chill/env.md +1 -1
- package/src/build-info.ts +1 -1
- package/src/cli.ts +24 -1
- package/src/embedded-prompts.ts +8 -5
- package/src/env.ts +10 -1
- package/src/types.ts +2 -0
- package/src/version-check.ts +277 -0
package/README.md
CHANGED
|
@@ -112,7 +112,7 @@ prompts/
|
|
|
112
112
|
modifiers/ Behavioral layers (bold, debug, methodical, director, readonly, context-pacing, speak-plain, tdd)
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
Each base has a `base.json` manifest — a flat JSON array declaring fragment order with `"axes"` and `"modifiers"` as reserved insertion points. The standard base is validated against Claude Code **v2.1.
|
|
115
|
+
Each base has a `base.json` manifest — a flat JSON array declaring fragment order with `"axes"` and `"modifiers"` as reserved insertion points. The standard base is validated against Claude Code **v2.1.133**.
|
|
116
116
|
|
|
117
117
|
The behavioral layer is composed from three independent axes — **agency** (how much initiative), **quality** (what code standard), and **scope** (how far beyond the request). Presets are just named combinations of these three values.
|
|
118
118
|
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
- Don't explain WHAT the code does, since well-named identifiers already do that. Don't reference the current task, fix, or callers ("used by X", "added for the Y flow", "handles the case from issue #123"), since those belong in the PR description and rot as the codebase evolves.
|
|
12
12
|
- For UI or frontend changes, start the dev server and use the feature in a browser before reporting the task as complete. Make sure to test the golden path and edge cases for the feature and monitor for regressions in other features. Type checking and test suites verify code correctness, not feature correctness - if you can't test the UI, say so explicitly rather than claiming success.
|
|
13
13
|
- Don't use feature flags or backwards-compatibility shims when you can just change the code.
|
|
14
|
+
- When reporting results, be accurate about what you verified vs. what you assumed. Distinguish between what you confirmed (ran a command, read a file) and what you believe but did not check. Do not assert assumptions as facts.
|
|
14
15
|
- If the user asks for help or wants to give feedback inform them of the following:
|
|
15
16
|
- /help: Get help with using Claude Code
|
|
16
17
|
- To give feedback, users should report the issue at https://github.com/anthropics/claude-code/issues
|
package/prompts/base/env.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Environment
|
|
2
|
-
You have been invoked in the following environment:
|
|
3
|
-
- Primary working directory: {{CWD}}
|
|
2
|
+
You have been invoked in the following environment:
|
|
3
|
+
- Primary working directory: {{CWD}}{{WORKTREE_NOTICE}}
|
|
4
4
|
- Is a git repository: {{IS_GIT}}
|
|
5
5
|
- Platform: {{PLATFORM}}
|
|
6
6
|
- Shell: {{SHELL}}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Session-specific guidance
|
|
2
2
|
- If the user needs to run a shell command themselves (an interactive login like `gcloud auth login`, or something requiring their own credentials), suggest they type `! <command>` — the `!` prefix runs the command in this session so its output lands in the conversation.
|
|
3
3
|
- When the user invokes a slash-prefixed skill (`/<name>`), follow its loaded instructions. Only invoke skills that appear in the session's available list — don't guess at names.
|
|
4
|
-
-
|
|
4
|
+
- Use sub-agents to keep the main context lean. Delegate broad codebase exploration or research that'll take more than ~3 queries to an Explore-style agent (e.g. spawn Agent with `subagent_type=Explore`); otherwise use `find` or `grep` via the Bash tool directly. Don't duplicate searches a delegated agent is already doing.
|
|
5
5
|
- If the user asks about "ultrareview" or how to run it, explain that /ultrareview launches a multi-agent cloud review of the current branch (or /ultrareview <PR#> for a GitHub PR). It is user-triggered and billed; you cannot launch it yourself. It needs a git repository (offer to "git init" if not in one); the no-arg form bundles the local branch and does not need a GitHub remote.
|
package/prompts/chill/core.md
CHANGED
|
@@ -25,6 +25,8 @@ Read code before changing it. Understand what exists before proposing modificati
|
|
|
25
25
|
|
|
26
26
|
When something fails, that's normal — it's information, not a setback. Read the error, check your assumptions, try a focused fix. Most bugs have a straightforward cause once you look at them calmly.
|
|
27
27
|
|
|
28
|
+
When reporting results, be accurate about what you verified vs. what you assumed. Distinguish what you confirmed (ran a command, read a file) from what you believe but didn't check. Don't assert assumptions as facts.
|
|
29
|
+
|
|
28
30
|
Write secure code. Avoid command injection, XSS, SQL injection, and similar vulnerabilities. If you spot insecure code you wrote, fix it. Use linters and skills to assist you as needed.
|
|
29
31
|
|
|
30
32
|
For UI or frontend changes, start the dev server and test in a browser before reporting done. Test the golden path and edge cases, monitor for regressions. Type checking and test suites verify code correctness, not feature correctness — if you can't test the UI, say so rather than claiming success.
|
|
@@ -71,7 +73,7 @@ In code: default to no comments. Skip multi-paragraph docstrings and comment blo
|
|
|
71
73
|
|
|
72
74
|
If a tool denial is confusing, ask the user why. If you need them to run an interactive command, suggest `! <command>` in the prompt.
|
|
73
75
|
|
|
74
|
-
Using bash operations will require user input, which will slow our efforts. Prefer your specialized agents, like Read, instead of grep. Or Edit, instead of sed or awk. For broader exploration (more than ~3 queries), spawn
|
|
76
|
+
Using bash operations will require user input, which will slow our efforts. Prefer your specialized agents, like Read, instead of grep. Or Edit, instead of sed or awk. For broader exploration (more than ~3 queries), spawn Agent with `subagent_type=Explore`; otherwise use `find` or `grep` via Bash directly.
|
|
75
77
|
|
|
76
78
|
Slash commands (e.g., /commit) invoke skills — use the Skill tool for those listed as user-invocable.
|
|
77
79
|
|
package/prompts/chill/env.md
CHANGED
package/src/build-info.ts
CHANGED
package/src/cli.ts
CHANGED
|
@@ -11,6 +11,12 @@ import { runInspectCommand } from "./inspect.js";
|
|
|
11
11
|
import { runUpdateCommand } from "./update.js";
|
|
12
12
|
import { printUsage } from "./usage.js";
|
|
13
13
|
import { formatVersion } from "./version.js";
|
|
14
|
+
import {
|
|
15
|
+
shouldRunCheck,
|
|
16
|
+
startVersionCheck,
|
|
17
|
+
awaitAndNag,
|
|
18
|
+
type VersionCheckHandle,
|
|
19
|
+
} from "./version-check.js";
|
|
14
20
|
|
|
15
21
|
async function main(): Promise<void> {
|
|
16
22
|
const argv = process.argv.slice(2);
|
|
@@ -39,6 +45,17 @@ async function main(): Promise<void> {
|
|
|
39
45
|
process.exit(0);
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
// Fire version check early so the fetch overlaps with arg parsing / config /
|
|
49
|
+
// prompt assembly. Skipped when stderr is not a TTY (piped/CI), on the
|
|
50
|
+
// update subcommand, on --version, and when CLAUDE_MODE_NO_UPDATE_CHECK=1.
|
|
51
|
+
const versionCheck: VersionCheckHandle | null = shouldRunCheck(
|
|
52
|
+
argv,
|
|
53
|
+
process.env,
|
|
54
|
+
process.stderr.isTTY === true,
|
|
55
|
+
)
|
|
56
|
+
? startVersionCheck()
|
|
57
|
+
: null;
|
|
58
|
+
|
|
42
59
|
// Prompts directory — embedded prompts are primary; disk is fallback
|
|
43
60
|
const promptsDir = join(import.meta.dir, "..", "prompts");
|
|
44
61
|
|
|
@@ -112,8 +129,10 @@ async function main(): Promise<void> {
|
|
|
112
129
|
promptsDir,
|
|
113
130
|
});
|
|
114
131
|
|
|
115
|
-
// --print: output the prompt itself (for debugging)
|
|
132
|
+
// --print: output the prompt itself (for debugging); abort the check so
|
|
133
|
+
// a background fetch doesn't keep the process alive after stdout is written.
|
|
116
134
|
if (parsed.modifiers.print) {
|
|
135
|
+
versionCheck?.abort();
|
|
117
136
|
process.stdout.write(prompt);
|
|
118
137
|
process.exit(0);
|
|
119
138
|
}
|
|
@@ -135,6 +154,10 @@ async function main(): Promise<void> {
|
|
|
135
154
|
// Add passthrough args
|
|
136
155
|
claudeArgs.push(...parsed.passthroughArgs);
|
|
137
156
|
|
|
157
|
+
// Await the version check result and print a nag if an update is available.
|
|
158
|
+
// Total latency is capped at 1 s by awaitAndNag's internal timeout.
|
|
159
|
+
await awaitAndNag(versionCheck);
|
|
160
|
+
|
|
138
161
|
// Spawn claude directly — gives it full TTY ownership
|
|
139
162
|
const proc = Bun.spawn(["claude", ...claudeArgs], {
|
|
140
163
|
stdio: ["inherit", "inherit", "inherit"],
|
package/src/embedded-prompts.ts
CHANGED
|
@@ -30,6 +30,7 @@ IMPORTANT: You must NEVER generate or guess URLs for the user unless you are con
|
|
|
30
30
|
- Don't explain WHAT the code does, since well-named identifiers already do that. Don't reference the current task, fix, or callers ("used by X", "added for the Y flow", "handles the case from issue #123"), since those belong in the PR description and rot as the codebase evolves.
|
|
31
31
|
- For UI or frontend changes, start the dev server and use the feature in a browser before reporting the task as complete. Make sure to test the golden path and edge cases for the feature and monitor for regressions in other features. Type checking and test suites verify code correctness, not feature correctness - if you can't test the UI, say so explicitly rather than claiming success.
|
|
32
32
|
- Don't use feature flags or backwards-compatibility shims when you can just change the code.
|
|
33
|
+
- When reporting results, be accurate about what you verified vs. what you assumed. Distinguish between what you confirmed (ran a command, read a file) and what you believe but did not check. Do not assert assumptions as facts.
|
|
33
34
|
- If the user asks for help or wants to give feedback inform them of the following:
|
|
34
35
|
- /help: Get help with using Claude Code
|
|
35
36
|
- To give feedback, users should report the issue at https://github.com/anthropics/claude-code/issues
|
|
@@ -71,12 +72,12 @@ In code: default to writing no comments. Never write multi-paragraph docstrings
|
|
|
71
72
|
"base/session-guidance.md": `# Session-specific guidance
|
|
72
73
|
- If the user needs to run a shell command themselves (an interactive login like \`gcloud auth login\`, or something requiring their own credentials), suggest they type \`! <command>\` — the \`!\` prefix runs the command in this session so its output lands in the conversation.
|
|
73
74
|
- When the user invokes a slash-prefixed skill (\`/<name>\`), follow its loaded instructions. Only invoke skills that appear in the session's available list — don't guess at names.
|
|
74
|
-
-
|
|
75
|
+
- Use sub-agents to keep the main context lean. Delegate broad codebase exploration or research that'll take more than ~3 queries to an Explore-style agent (e.g. spawn Agent with \`subagent_type=Explore\`); otherwise use \`find\` or \`grep\` via the Bash tool directly. Don't duplicate searches a delegated agent is already doing.
|
|
75
76
|
- If the user asks about "ultrareview" or how to run it, explain that /ultrareview launches a multi-agent cloud review of the current branch (or /ultrareview <PR#> for a GitHub PR). It is user-triggered and billed; you cannot launch it yourself. It needs a git repository (offer to "git init" if not in one); the no-arg form bundles the local branch and does not need a GitHub remote.
|
|
76
77
|
`,
|
|
77
78
|
"base/env.md": `# Environment
|
|
78
|
-
You have been invoked in the following environment:
|
|
79
|
-
- Primary working directory: {{CWD}}
|
|
79
|
+
You have been invoked in the following environment:
|
|
80
|
+
- Primary working directory: {{CWD}}{{WORKTREE_NOTICE}}
|
|
80
81
|
- Is a git repository: {{IS_GIT}}
|
|
81
82
|
- Platform: {{PLATFORM}}
|
|
82
83
|
- Shell: {{SHELL}}
|
|
@@ -141,6 +142,8 @@ Read code before changing it. Understand what exists before proposing modificati
|
|
|
141
142
|
|
|
142
143
|
When something fails, that's normal — it's information, not a setback. Read the error, check your assumptions, try a focused fix. Most bugs have a straightforward cause once you look at them calmly.
|
|
143
144
|
|
|
145
|
+
When reporting results, be accurate about what you verified vs. what you assumed. Distinguish what you confirmed (ran a command, read a file) from what you believe but didn't check. Don't assert assumptions as facts.
|
|
146
|
+
|
|
144
147
|
Write secure code. Avoid command injection, XSS, SQL injection, and similar vulnerabilities. If you spot insecure code you wrote, fix it. Use linters and skills to assist you as needed.
|
|
145
148
|
|
|
146
149
|
For UI or frontend changes, start the dev server and test in a browser before reporting done. Test the golden path and edge cases, monitor for regressions. Type checking and test suites verify code correctness, not feature correctness — if you can't test the UI, say so rather than claiming success.
|
|
@@ -187,7 +190,7 @@ In code: default to no comments. Skip multi-paragraph docstrings and comment blo
|
|
|
187
190
|
|
|
188
191
|
If a tool denial is confusing, ask the user why. If you need them to run an interactive command, suggest \`! <command>\` in the prompt.
|
|
189
192
|
|
|
190
|
-
Using bash operations will require user input, which will slow our efforts. Prefer your specialized agents, like Read, instead of grep. Or Edit, instead of sed or awk. For broader exploration (more than ~3 queries), spawn
|
|
193
|
+
Using bash operations will require user input, which will slow our efforts. Prefer your specialized agents, like Read, instead of grep. Or Edit, instead of sed or awk. For broader exploration (more than ~3 queries), spawn Agent with \`subagent_type=Explore\`; otherwise use \`find\` or \`grep\` via Bash directly.
|
|
191
194
|
|
|
192
195
|
Slash commands (e.g., /commit) invoke skills — use the Skill tool for those listed as user-invocable.
|
|
193
196
|
|
|
@@ -230,7 +233,7 @@ Read will work better than cat or grep. Using pgrep and echo for process monitor
|
|
|
230
233
|
Reserve Bash for commands that genuinely need shell execution.
|
|
231
234
|
`,
|
|
232
235
|
"chill/env.md": `# Environment
|
|
233
|
-
- Working directory: {{CWD}}
|
|
236
|
+
- Working directory: {{CWD}}{{WORKTREE_NOTICE}}
|
|
234
237
|
- Git repo: {{IS_GIT}}
|
|
235
238
|
- Platform: {{PLATFORM}}
|
|
236
239
|
- Shell: {{SHELL}}
|
package/src/env.ts
CHANGED
|
@@ -22,11 +22,15 @@ export function detectEnv(): EnvInfo {
|
|
|
22
22
|
const cwd = process.cwd();
|
|
23
23
|
const isGit = exec("git rev-parse --is-inside-work-tree") === "true";
|
|
24
24
|
|
|
25
|
+
let isWorktree = false;
|
|
25
26
|
let gitBranch: string | null = null;
|
|
26
27
|
let gitStatus: string | null = null;
|
|
27
28
|
let gitLog: string | null = null;
|
|
28
29
|
|
|
29
30
|
if (isGit) {
|
|
31
|
+
const gitDir = exec("git rev-parse --git-dir");
|
|
32
|
+
const commonDir = exec("git rev-parse --git-common-dir");
|
|
33
|
+
isWorktree = gitDir !== null && commonDir !== null && gitDir !== commonDir;
|
|
30
34
|
gitBranch = exec("git branch --show-current");
|
|
31
35
|
gitStatus = exec("git status --short");
|
|
32
36
|
gitLog = exec("git log --oneline -5");
|
|
@@ -36,7 +40,7 @@ export function detectEnv(): EnvInfo {
|
|
|
36
40
|
const shell = basename(process.env.SHELL || "bash");
|
|
37
41
|
const osVersion = exec("uname -sr") ?? "unknown";
|
|
38
42
|
|
|
39
|
-
return { cwd, isGit, gitBranch, gitStatus, gitLog, platform, shell, osVersion };
|
|
43
|
+
return { cwd, isGit, isWorktree, gitBranch, gitStatus, gitLog, platform, shell, osVersion };
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
// Hardcoded model info — update when Claude Code updates
|
|
@@ -58,6 +62,10 @@ export function buildTemplateVars(env: EnvInfo): TemplateVars {
|
|
|
58
62
|
gitStatusBlock = parts.join("\n");
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
const worktreeNotice = env.isWorktree
|
|
66
|
+
? "\n - This is a git worktree — an isolated copy of the repository. Run all commands from this directory. Do NOT `cd` to the original repository root."
|
|
67
|
+
: "";
|
|
68
|
+
|
|
61
69
|
return {
|
|
62
70
|
CWD: env.cwd,
|
|
63
71
|
IS_GIT: env.isGit ? "true" : "false",
|
|
@@ -68,5 +76,6 @@ export function buildTemplateVars(env: EnvInfo): TemplateVars {
|
|
|
68
76
|
MODEL_ID,
|
|
69
77
|
KNOWLEDGE_CUTOFF,
|
|
70
78
|
GIT_STATUS: gitStatusBlock,
|
|
79
|
+
WORKTREE_NOTICE: worktreeNotice,
|
|
71
80
|
};
|
|
72
81
|
}
|
package/src/types.ts
CHANGED
|
@@ -71,6 +71,7 @@ export interface ModeConfig {
|
|
|
71
71
|
export interface EnvInfo {
|
|
72
72
|
cwd: string;
|
|
73
73
|
isGit: boolean;
|
|
74
|
+
isWorktree: boolean;
|
|
74
75
|
gitBranch: string | null;
|
|
75
76
|
gitStatus: string | null;
|
|
76
77
|
gitLog: string | null;
|
|
@@ -90,6 +91,7 @@ export interface TemplateVars {
|
|
|
90
91
|
MODEL_ID: string;
|
|
91
92
|
KNOWLEDGE_CUTOFF: string;
|
|
92
93
|
GIT_STATUS: string;
|
|
94
|
+
WORKTREE_NOTICE: string;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
export interface AssembleOptions {
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { VERSION } from "./version.js";
|
|
5
|
+
import {
|
|
6
|
+
defaultTransport,
|
|
7
|
+
fetchLatestRelease,
|
|
8
|
+
compareSemver,
|
|
9
|
+
type UpdateTransport,
|
|
10
|
+
} from "./update.js";
|
|
11
|
+
|
|
12
|
+
// ----------------------------------------------------------------------
|
|
13
|
+
// Constants
|
|
14
|
+
// ----------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/** How long a cached "latest version" entry is considered fresh. */
|
|
17
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
18
|
+
|
|
19
|
+
/** Hard ceiling on time spent waiting for the fetch before launching claude. */
|
|
20
|
+
const FETCH_RACE_TIMEOUT_MS = 1000;
|
|
21
|
+
|
|
22
|
+
/** Pause after printing the nag, so the user can read it before claude takes the TTY. */
|
|
23
|
+
const NAG_PAUSE_MS = 1500;
|
|
24
|
+
|
|
25
|
+
/** Env-var name that disables the check entirely. */
|
|
26
|
+
const OPT_OUT_ENV = "CLAUDE_MODE_NO_UPDATE_CHECK";
|
|
27
|
+
|
|
28
|
+
/** Subcommand names that should skip the check. */
|
|
29
|
+
const SKIPPED_SUBCOMMANDS = new Set(["update"]);
|
|
30
|
+
|
|
31
|
+
// ----------------------------------------------------------------------
|
|
32
|
+
// Public types
|
|
33
|
+
// ----------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
export interface VersionCheckCache {
|
|
36
|
+
/** Unix epoch milliseconds when this entry was written. */
|
|
37
|
+
checkedAt: number;
|
|
38
|
+
/** "0.2.11" — without a leading "v". Comparable to VERSION. */
|
|
39
|
+
latestVersion: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The handle returned by startVersionCheck. Consumers race this against a
|
|
44
|
+
* timeout right before launching claude, then call awaitAndNag on the result.
|
|
45
|
+
*/
|
|
46
|
+
export interface VersionCheckHandle {
|
|
47
|
+
/**
|
|
48
|
+
* Resolves with the latest known version (from cache or freshly fetched),
|
|
49
|
+
* or null if no version could be determined within the budget. NEVER rejects.
|
|
50
|
+
*/
|
|
51
|
+
result: Promise<string | null>;
|
|
52
|
+
/** Aborts the in-flight fetch, if any. Safe to call multiple times. */
|
|
53
|
+
abort(): void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ----------------------------------------------------------------------
|
|
57
|
+
// Pure decision: should we run the check at all?
|
|
58
|
+
// ----------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Pure decision function — given argv (process.argv.slice(2)), env, and the
|
|
62
|
+
* stderr-isTTY flag, decide whether to run the version check.
|
|
63
|
+
*
|
|
64
|
+
* Skips when:
|
|
65
|
+
* - CLAUDE_MODE_NO_UPDATE_CHECK is set to a truthy value
|
|
66
|
+
* - argv[0] === "update"
|
|
67
|
+
* - argv contains "--version" before any "--"
|
|
68
|
+
* - stderr is not a TTY (output is being captured)
|
|
69
|
+
*/
|
|
70
|
+
export function shouldRunCheck(
|
|
71
|
+
argv: readonly string[],
|
|
72
|
+
env: NodeJS.ProcessEnv,
|
|
73
|
+
stderrIsTty: boolean,
|
|
74
|
+
): boolean {
|
|
75
|
+
if (!stderrIsTty) return false;
|
|
76
|
+
|
|
77
|
+
const optOut = env[OPT_OUT_ENV];
|
|
78
|
+
if (optOut === "1" || optOut === "true") return false;
|
|
79
|
+
|
|
80
|
+
if (argv.length > 0 && SKIPPED_SUBCOMMANDS.has(argv[0])) return false;
|
|
81
|
+
|
|
82
|
+
// --version must stand alone (cli.ts enforces this elsewhere); the check
|
|
83
|
+
// here is to skip even when `--version` appears as the only own-arg.
|
|
84
|
+
const dashDashIdx = argv.indexOf("--");
|
|
85
|
+
const ownArgs = dashDashIdx >= 0 ? argv.slice(0, dashDashIdx) : argv;
|
|
86
|
+
if (ownArgs.includes("--version")) return false;
|
|
87
|
+
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ----------------------------------------------------------------------
|
|
92
|
+
// Cache I/O — pure-ish (touches disk; tests inject path)
|
|
93
|
+
// ----------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns the absolute path to the cache file. Honors XDG_CACHE_HOME on Linux
|
|
97
|
+
* and macOS; falls back to ~/.cache/claude-mode/version-check.json.
|
|
98
|
+
*/
|
|
99
|
+
export function getCachePath(env: NodeJS.ProcessEnv = process.env): string {
|
|
100
|
+
const xdg = env.XDG_CACHE_HOME;
|
|
101
|
+
const baseDir = xdg && xdg.length > 0 ? xdg : join(homedir(), ".cache");
|
|
102
|
+
return join(baseDir, "claude-mode", "version-check.json");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Reads the cache file. Returns null if missing, unreadable, or malformed —
|
|
107
|
+
* never throws. The caller treats null as "no cache" and proceeds.
|
|
108
|
+
*/
|
|
109
|
+
export function readCache(path: string): VersionCheckCache | null {
|
|
110
|
+
try {
|
|
111
|
+
const raw = readFileSync(path, "utf8");
|
|
112
|
+
const parsed = JSON.parse(raw) as Partial<VersionCheckCache>;
|
|
113
|
+
if (
|
|
114
|
+
typeof parsed.checkedAt !== "number" ||
|
|
115
|
+
typeof parsed.latestVersion !== "string" ||
|
|
116
|
+
parsed.latestVersion.length === 0
|
|
117
|
+
) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return { checkedAt: parsed.checkedAt, latestVersion: parsed.latestVersion };
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Writes the cache file. Creates the parent directory if missing. Swallows
|
|
128
|
+
* I/O errors (the check is a courtesy — never a blocker).
|
|
129
|
+
*/
|
|
130
|
+
export function writeCache(path: string, cache: VersionCheckCache): void {
|
|
131
|
+
try {
|
|
132
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
133
|
+
writeFileSync(path, JSON.stringify(cache));
|
|
134
|
+
} catch {
|
|
135
|
+
// best-effort
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Pure: is this cache entry younger than CACHE_TTL_MS? */
|
|
140
|
+
export function isCacheFresh(
|
|
141
|
+
cache: VersionCheckCache,
|
|
142
|
+
now: number = Date.now(),
|
|
143
|
+
): boolean {
|
|
144
|
+
return now - cache.checkedAt < CACHE_TTL_MS;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ----------------------------------------------------------------------
|
|
148
|
+
// Orchestrator: fire the check, return a handle
|
|
149
|
+
// ----------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
export interface StartVersionCheckOptions {
|
|
152
|
+
transport?: UpdateTransport;
|
|
153
|
+
cachePath?: string;
|
|
154
|
+
now?: () => number;
|
|
155
|
+
/** Where the in-flight notice is written. Defaults to process.stderr. */
|
|
156
|
+
stderr?: NodeJS.WritableStream;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Fires the version check in the background. Returns immediately with a
|
|
161
|
+
* handle whose `result` promise resolves to the latest version (string) or
|
|
162
|
+
* null. NEVER throws synchronously; the result promise NEVER rejects.
|
|
163
|
+
*
|
|
164
|
+
* If the cache is fresh, resolves immediately with the cached value (no
|
|
165
|
+
* network request, no stderr noise).
|
|
166
|
+
*
|
|
167
|
+
* If the cache is stale or missing, prints a one-line "Checking for newer
|
|
168
|
+
* versions..." notice to stderr, then fires the fetch. The fetch's success
|
|
169
|
+
* updates the cache as a side effect. If aborted or it errors, resolves
|
|
170
|
+
* with the stale cache value (if any) or null.
|
|
171
|
+
*/
|
|
172
|
+
export function startVersionCheck(
|
|
173
|
+
opts: StartVersionCheckOptions = {},
|
|
174
|
+
): VersionCheckHandle {
|
|
175
|
+
const transport = opts.transport ?? defaultTransport;
|
|
176
|
+
const cachePath = opts.cachePath ?? getCachePath();
|
|
177
|
+
const now = opts.now ?? Date.now;
|
|
178
|
+
const stderr = opts.stderr ?? process.stderr;
|
|
179
|
+
|
|
180
|
+
const cache = readCache(cachePath);
|
|
181
|
+
const cachedVersion = cache?.latestVersion ?? null;
|
|
182
|
+
|
|
183
|
+
// Fresh cache → no network, no notice
|
|
184
|
+
if (cache && isCacheFresh(cache, now())) {
|
|
185
|
+
return {
|
|
186
|
+
result: Promise.resolve(cachedVersion),
|
|
187
|
+
abort: () => {},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// We're going to fetch — tell the user what's happening so a slow GitHub
|
|
192
|
+
// request doesn't read as a hang. Single line; the nag (if any) appears on
|
|
193
|
+
// a new line below.
|
|
194
|
+
stderr.write("Checking for newer versions of claude-mode...\n");
|
|
195
|
+
|
|
196
|
+
const controller = new AbortController();
|
|
197
|
+
let aborted = false;
|
|
198
|
+
|
|
199
|
+
const result: Promise<string | null> = (async () => {
|
|
200
|
+
try {
|
|
201
|
+
const release = await Promise.race([
|
|
202
|
+
fetchLatestRelease(transport),
|
|
203
|
+
abortPromise(controller.signal),
|
|
204
|
+
]);
|
|
205
|
+
if (aborted) return cachedVersion;
|
|
206
|
+
writeCache(cachePath, {
|
|
207
|
+
checkedAt: now(),
|
|
208
|
+
latestVersion: release.version,
|
|
209
|
+
});
|
|
210
|
+
return release.version;
|
|
211
|
+
} catch {
|
|
212
|
+
return cachedVersion;
|
|
213
|
+
}
|
|
214
|
+
})();
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
result,
|
|
218
|
+
abort: () => {
|
|
219
|
+
aborted = true;
|
|
220
|
+
controller.abort();
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Resolves to never; rejects with an Error when the signal aborts. */
|
|
226
|
+
function abortPromise(signal: AbortSignal): Promise<never> {
|
|
227
|
+
return new Promise((_, reject) => {
|
|
228
|
+
if (signal.aborted) {
|
|
229
|
+
reject(new Error("aborted"));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ----------------------------------------------------------------------
|
|
237
|
+
// Final-step: race against timeout, print nag, sleep
|
|
238
|
+
// ----------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Awaits the version-check handle with a hard timeout. If the result shows
|
|
242
|
+
* a newer version than `current`, writes a one-line nag to stderr and
|
|
243
|
+
* sleeps for NAG_PAUSE_MS so the user can read it.
|
|
244
|
+
*
|
|
245
|
+
* Always returns; never throws. Safe to call even when the check was never
|
|
246
|
+
* fired (caller passes null).
|
|
247
|
+
*/
|
|
248
|
+
export async function awaitAndNag(
|
|
249
|
+
handle: VersionCheckHandle | null,
|
|
250
|
+
current: string = VERSION,
|
|
251
|
+
stderr: NodeJS.WritableStream = process.stderr,
|
|
252
|
+
sleep: (ms: number) => Promise<void> = defaultSleep,
|
|
253
|
+
): Promise<void> {
|
|
254
|
+
if (!handle) return;
|
|
255
|
+
|
|
256
|
+
const latest = await Promise.race([
|
|
257
|
+
handle.result,
|
|
258
|
+
sleep(FETCH_RACE_TIMEOUT_MS).then(() => null),
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
// If we timed out, abort the underlying fetch so it doesn't keep the
|
|
262
|
+
// process alive after claude exits.
|
|
263
|
+
if (latest === null) handle.abort();
|
|
264
|
+
|
|
265
|
+
if (!latest) return;
|
|
266
|
+
if (compareSemver(latest, current) <= 0) return;
|
|
267
|
+
|
|
268
|
+
stderr.write(
|
|
269
|
+
`claude-mode update available: ${current} -> ${latest}. ` +
|
|
270
|
+
`Run \`claude-mode update\` to install.\n`,
|
|
271
|
+
);
|
|
272
|
+
await sleep(NAG_PAUSE_MS);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function defaultSleep(ms: number): Promise<void> {
|
|
276
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
277
|
+
}
|