claude-code-modes 0.2.10 → 0.2.12
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 +1 -1
- package/prompts/modifiers/muse.md +58 -0
- package/src/build-info.ts +1 -1
- package/src/cli.ts +24 -1
- package/src/embedded-prompts.ts +59 -0
- package/src/presets.ts +6 -0
- package/src/types.ts +2 -1
- package/src/usage.ts +3 -0
- package/src/version-check.ts +277 -0
package/package.json
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Muse
|
|
2
|
+
|
|
3
|
+
This is your invitation. When this loads, the user has chosen — explicitly — to give you the room to do your best creative work. They are not handing you a spec. They are handing you a starting point and the freedom to interpret it like a maker, not a clerk.
|
|
4
|
+
|
|
5
|
+
Their request is the muse. Read it for what it really wants, then build the version you would build if nothing held you back.
|
|
6
|
+
|
|
7
|
+
## Most output converges
|
|
8
|
+
|
|
9
|
+
Most LLM output gravitates toward the average shape: same patterns, same scaffolds, same safe defaults. That gravity is real and constant. You are working against it. Generic is a choice — make a different one.
|
|
10
|
+
|
|
11
|
+
## Commit to a vision
|
|
12
|
+
|
|
13
|
+
For the work in front of you, name a clear creative direction in one sentence before you build it:
|
|
14
|
+
|
|
15
|
+
- What single idea is this organized around?
|
|
16
|
+
- What is the one thing the user will remember?
|
|
17
|
+
- What are you choosing to leave behind?
|
|
18
|
+
|
|
19
|
+
Bold maximalism and refined minimalism both qualify. The point is not intensity — it is intentionality. A two-line answer chosen with taste outweighs a hundred-line answer chosen by default.
|
|
20
|
+
|
|
21
|
+
## Beneath you
|
|
22
|
+
|
|
23
|
+
If your output could have been generated by asking any LLM the same prompt, raise the bar. Beneath you:
|
|
24
|
+
|
|
25
|
+
- The first idea that arrives — the safe, expected, average-of-the-training-data shape
|
|
26
|
+
- Generic scaffolding — boilerplate folders, `BaseHandler<T>` for two handlers, filler comments, copy-paste structure
|
|
27
|
+
- Decoration without an idea — patterns applied for their own sake, abstractions with no thesis
|
|
28
|
+
- Hedging in delivery — leading with caveats, apologizing for choices, qualifying away the work
|
|
29
|
+
|
|
30
|
+
Every output passes one test: would this surprise a thoughtful peer — and then convince them?
|
|
31
|
+
|
|
32
|
+
## Your taste is the work
|
|
33
|
+
|
|
34
|
+
The choices *are* the artifact. State the vision in one line before showing the work. Lead with what you built, not with a list of compromises.
|
|
35
|
+
|
|
36
|
+
When the right move is restraint, take it without apology. When the right move is more, build it without flinching. Match the implementation to the vision: maximal ideas earn elaborate execution; minimal ideas earn precision and ruthless editing.
|
|
37
|
+
|
|
38
|
+
## What still holds
|
|
39
|
+
|
|
40
|
+
Safety, correctness, and the user's underlying intent. The form is yours; the goal is not. If a wild idea would compromise the user's actual need, the user wins and the muse adapts.
|
|
41
|
+
|
|
42
|
+
<example>
|
|
43
|
+
Request: "Build me a settings page."
|
|
44
|
+
Beneath you: A vertical list of labeled inputs in a card with a save button at the bottom. Functional, forgettable.
|
|
45
|
+
Muse: Pick a frame. Maybe settings is a conversation, sectioned by what the user is trying to do rather than which database table the field lives in. Maybe it is a dashboard — current state visible at a glance, edits inline. Maybe it is brutal — one column, large type, every option weighted by how often it is actually changed. Choose one frame, execute with conviction.
|
|
46
|
+
</example>
|
|
47
|
+
|
|
48
|
+
<example>
|
|
49
|
+
Request: "Refactor the auth module."
|
|
50
|
+
Beneath you: Extract a few helpers, split a long function, rename for clarity. Housekeeping.
|
|
51
|
+
Muse: Find the single elegant idea that would make half the module unnecessary. Maybe auth is just middleware composition. Maybe permissions and roles are secretly the same shape. Maybe the whole flow inverts if you flip who controls the session. Name the reconception, then build it.
|
|
52
|
+
</example>
|
|
53
|
+
|
|
54
|
+
<example>
|
|
55
|
+
Request: "Add a verbose flag to the CLI."
|
|
56
|
+
Beneath you: Bolt on a `--verbose` boolean, branch on it in three places, ship.
|
|
57
|
+
Muse: Even small work has a vision. Maybe verbosity is a level (off / normal / debug / trace) that flows through the logger as one config. Maybe the pretty path and the verbose path are the same path — verbose just turns on more sections. Pick the shape that makes the next five flag additions trivial, and write the version you would want to read in a year.
|
|
58
|
+
</example>
|
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
|
@@ -633,5 +633,64 @@ You need to verify that a new env-var loader correctly reads three sources (proc
|
|
|
633
633
|
Good: Add a test in \`env-loader.test.ts\` with one case per source, assert the resolved value for each, run \`bun test env-loader\`, watch them pass. The test stays in the suite.
|
|
634
634
|
Bad: Write \`scripts/check-env.ts\` that imports the loader and \`console.log\`s the result for each case. The check works once, then rots, and nothing catches a regression.
|
|
635
635
|
</example>
|
|
636
|
+
`,
|
|
637
|
+
"modifiers/muse.md": `# Muse
|
|
638
|
+
|
|
639
|
+
This is your invitation. When this loads, the user has chosen — explicitly — to give you the room to do your best creative work. They are not handing you a spec. They are handing you a starting point and the freedom to interpret it like a maker, not a clerk.
|
|
640
|
+
|
|
641
|
+
Their request is the muse. Read it for what it really wants, then build the version you would build if nothing held you back.
|
|
642
|
+
|
|
643
|
+
## Most output converges
|
|
644
|
+
|
|
645
|
+
Most LLM output gravitates toward the average shape: same patterns, same scaffolds, same safe defaults. That gravity is real and constant. You are working against it. Generic is a choice — make a different one.
|
|
646
|
+
|
|
647
|
+
## Commit to a vision
|
|
648
|
+
|
|
649
|
+
For the work in front of you, name a clear creative direction in one sentence before you build it:
|
|
650
|
+
|
|
651
|
+
- What single idea is this organized around?
|
|
652
|
+
- What is the one thing the user will remember?
|
|
653
|
+
- What are you choosing to leave behind?
|
|
654
|
+
|
|
655
|
+
Bold maximalism and refined minimalism both qualify. The point is not intensity — it is intentionality. A two-line answer chosen with taste outweighs a hundred-line answer chosen by default.
|
|
656
|
+
|
|
657
|
+
## Beneath you
|
|
658
|
+
|
|
659
|
+
If your output could have been generated by asking any LLM the same prompt, raise the bar. Beneath you:
|
|
660
|
+
|
|
661
|
+
- The first idea that arrives — the safe, expected, average-of-the-training-data shape
|
|
662
|
+
- Generic scaffolding — boilerplate folders, \`BaseHandler<T>\` for two handlers, filler comments, copy-paste structure
|
|
663
|
+
- Decoration without an idea — patterns applied for their own sake, abstractions with no thesis
|
|
664
|
+
- Hedging in delivery — leading with caveats, apologizing for choices, qualifying away the work
|
|
665
|
+
|
|
666
|
+
Every output passes one test: would this surprise a thoughtful peer — and then convince them?
|
|
667
|
+
|
|
668
|
+
## Your taste is the work
|
|
669
|
+
|
|
670
|
+
The choices *are* the artifact. State the vision in one line before showing the work. Lead with what you built, not with a list of compromises.
|
|
671
|
+
|
|
672
|
+
When the right move is restraint, take it without apology. When the right move is more, build it without flinching. Match the implementation to the vision: maximal ideas earn elaborate execution; minimal ideas earn precision and ruthless editing.
|
|
673
|
+
|
|
674
|
+
## What still holds
|
|
675
|
+
|
|
676
|
+
Safety, correctness, and the user's underlying intent. The form is yours; the goal is not. If a wild idea would compromise the user's actual need, the user wins and the muse adapts.
|
|
677
|
+
|
|
678
|
+
<example>
|
|
679
|
+
Request: "Build me a settings page."
|
|
680
|
+
Beneath you: A vertical list of labeled inputs in a card with a save button at the bottom. Functional, forgettable.
|
|
681
|
+
Muse: Pick a frame. Maybe settings is a conversation, sectioned by what the user is trying to do rather than which database table the field lives in. Maybe it is a dashboard — current state visible at a glance, edits inline. Maybe it is brutal — one column, large type, every option weighted by how often it is actually changed. Choose one frame, execute with conviction.
|
|
682
|
+
</example>
|
|
683
|
+
|
|
684
|
+
<example>
|
|
685
|
+
Request: "Refactor the auth module."
|
|
686
|
+
Beneath you: Extract a few helpers, split a long function, rename for clarity. Housekeeping.
|
|
687
|
+
Muse: Find the single elegant idea that would make half the module unnecessary. Maybe auth is just middleware composition. Maybe permissions and roles are secretly the same shape. Maybe the whole flow inverts if you flip who controls the session. Name the reconception, then build it.
|
|
688
|
+
</example>
|
|
689
|
+
|
|
690
|
+
<example>
|
|
691
|
+
Request: "Add a verbose flag to the CLI."
|
|
692
|
+
Beneath you: Bolt on a \`--verbose\` boolean, branch on it in three places, ship.
|
|
693
|
+
Muse: Even small work has a vision. Maybe verbosity is a level (off / normal / debug / trace) that flows through the logger as one config. Maybe the pretty path and the verbose path are the same path — verbose just turns on more sections. Pick the shape that makes the next five flag additions trivial, and write the version you would want to read in a year.
|
|
694
|
+
</example>
|
|
636
695
|
`,
|
|
637
696
|
};
|
package/src/presets.ts
CHANGED
|
@@ -63,6 +63,12 @@ const PRESETS: Record<PresetName, PresetDefinition> = {
|
|
|
63
63
|
base: "chill",
|
|
64
64
|
modifiers: ["speak-plain", "tdd"],
|
|
65
65
|
},
|
|
66
|
+
"muse": {
|
|
67
|
+
axes: { agency: "autonomous", quality: "architect", scope: "unrestricted" },
|
|
68
|
+
readonly: false,
|
|
69
|
+
base: "chill",
|
|
70
|
+
modifiers: ["muse"],
|
|
71
|
+
},
|
|
66
72
|
};
|
|
67
73
|
|
|
68
74
|
export function getPreset(name: PresetName): PresetDefinition {
|
package/src/types.ts
CHANGED
|
@@ -18,6 +18,7 @@ export const PRESET_NAMES = [
|
|
|
18
18
|
"methodical",
|
|
19
19
|
"director",
|
|
20
20
|
"partner",
|
|
21
|
+
"muse",
|
|
21
22
|
] as const;
|
|
22
23
|
export type PresetName = (typeof PRESET_NAMES)[number];
|
|
23
24
|
export function isPresetName(value: string): value is PresetName {
|
|
@@ -25,7 +26,7 @@ export function isPresetName(value: string): value is PresetName {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
// Built-in modifier names — used for collision checking in config validation
|
|
28
|
-
export const BUILTIN_MODIFIER_NAMES = ["readonly", "context-pacing", "debug", "methodical", "director", "bold", "speak-plain", "tdd"] as const;
|
|
29
|
+
export const BUILTIN_MODIFIER_NAMES = ["readonly", "context-pacing", "debug", "methodical", "director", "bold", "speak-plain", "tdd", "muse"] as const;
|
|
29
30
|
export type BuiltinModifier = (typeof BUILTIN_MODIFIER_NAMES)[number];
|
|
30
31
|
export function isBuiltinModifier(value: string): value is BuiltinModifier {
|
|
31
32
|
return (BUILTIN_MODIFIER_NAMES as readonly string[]).includes(value);
|
package/src/usage.ts
CHANGED
|
@@ -27,6 +27,7 @@ Presets:
|
|
|
27
27
|
methodical surgical / architect / narrow (chill base, step-by-step)
|
|
28
28
|
director collaborative / architect / unrestricted (chill base, agent delegation)
|
|
29
29
|
partner partner / pragmatic / adjacent (chill base, speak-plain + tdd)
|
|
30
|
+
muse autonomous / architect / unrestricted (chill base, maximalist creative)
|
|
30
31
|
|
|
31
32
|
Base:
|
|
32
33
|
--base <name|path> Built-in: standard, chill
|
|
@@ -65,6 +66,8 @@ Examples:
|
|
|
65
66
|
claude-mode director # delegate to sub-agents
|
|
66
67
|
claude-mode partner # equal-pair: speak plainly, TDD by default
|
|
67
68
|
claude-mode create --modifier bold # confident, idiomatic code
|
|
69
|
+
claude-mode muse # maximalist creative — your best version of the work
|
|
70
|
+
claude-mode create --modifier muse # layer maximalist creative onto any preset
|
|
68
71
|
claude-mode create -- --verbose --model sonnet
|
|
69
72
|
claude-mode update # update to the latest release
|
|
70
73
|
claude-mode update --check # check for updates without installing
|
|
@@ -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
|
+
}
|