claude-code-modes 0.2.8 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,6 +27,20 @@ Requires [Bun](https://bun.sh/) and [Claude Code](https://docs.anthropic.com/en/
27
27
 
28
28
  > **Migrating from an older git clone?** If you previously used `./claude-mode` directly from the repo, that bash wrapper has been removed. Run `bun link` in the repo to put `claude-mode` on your PATH via the package.json bin entry, or switch to the binary install above.
29
29
 
30
+ ## Updating
31
+
32
+ If you installed the binary via `install.sh`:
33
+
34
+ ```bash
35
+ claude-mode update # update to the latest release
36
+ claude-mode update --check # check without installing
37
+ claude-mode update 0.2.5 # pin a specific version (downgrade or reinstall)
38
+ claude-mode update --force # reinstall same version (repair a corrupt binary)
39
+ claude-mode update --dry-run # show what would happen
40
+ ```
41
+
42
+ `update` only works on binaries built from the upstream repo. If you're running from a `git clone` checkout (`bun link`), use `git pull && bun install` instead. If you're running a fork build, update via your fork's release process.
43
+
30
44
  ## Usage
31
45
 
32
46
  Pick a preset that matches your task:
@@ -40,6 +54,7 @@ claude-mode explore # Read-only — understand code without changing it
40
54
  claude-mode debug # Investigation-first debugging (chill base)
41
55
  claude-mode methodical # Step-by-step precision (chill base)
42
56
  claude-mode director # Delegate to sub-agents, orchestrate and verify (chill base)
57
+ claude-mode partner # Pair-of-equals: terse, test-first, decisive on craft (chill base)
43
58
  claude-mode none # Strip all behavioral opinions, use your own CLAUDE.md
44
59
  ```
45
60
 
@@ -53,6 +68,7 @@ claude-mode none # Strip all behavioral opinions, use your own CLAUDE.md
53
68
  | `debug` | collaborative | pragmatic | narrow | Find root causes — evidence-first, ask for guidance when stuck |
54
69
  | `methodical` | surgical | architect | narrow | Step-by-step craftsmanship — follow instructions, stop when done |
55
70
  | `director` | collaborative | architect | unrestricted | Orchestrate sub-agents — delegate implementation, verify results |
71
+ | `partner` | partner | pragmatic | adjacent | Pair-of-equals collaboration — decisive on craft, test-first, terse by default |
56
72
  | `none` | — | — | — | Strip all behavioral instructions, use your own |
57
73
 
58
74
  ### Alternative base: chill
@@ -93,7 +109,7 @@ prompts/
93
109
  base/ Standard base (derived from upstream Claude Code)
94
110
  chill/ Alternative base (emotion-research-informed, leaner)
95
111
  axis/ Behavioral prompts organized by three axes
96
- modifiers/ Behavioral layers (bold, debug, methodical, director, readonly, context-pacing)
112
+ modifiers/ Behavioral layers (bold, debug, methodical, director, readonly, context-pacing, speak-plain, tdd)
97
113
  ```
98
114
 
99
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.121**.
@@ -159,6 +175,14 @@ Check the installed version:
159
175
  claude-mode --version
160
176
  ```
161
177
 
178
+ Release builds print a single line (`claude-mode 0.2.8`). Builds compiled from a git checkout — forks, dev builds — also print build provenance (`repo`, `branch`, `commit`, plus `(dirty)` if the worktree had uncommitted changes), so a binary built from a fork is unmistakably identifiable.
179
+
180
+ `--version` is standalone-only — combining it with any other flag exits with a non-zero status. To pass `--version` through to Claude Code itself, use the `--` escape hatch:
181
+
182
+ ```bash
183
+ claude-mode -- --version # Forwards --version to claude
184
+ ```
185
+
162
186
  ## Config file
163
187
 
164
188
  Create a `.claude-mode.json` in your project root to define reusable custom modifiers, axis values, and presets. Manage it with the CLI or edit directly.
@@ -236,6 +260,7 @@ claude-mode config remove-preset <name> # Remove custom preset
236
260
  - **autonomous** — Makes decisions, creates files, restructures without asking
237
261
  - **collaborative** — Explains reasoning, checks in at decision points
238
262
  - **surgical** — Executes exactly what was asked, nothing more
263
+ - **partner** — Pair of equals: commits decisively on execution choices (naming, structure, idiom), defers to the user on direction (priorities, scope, what counts as done), keeps mental models in sync
239
264
 
240
265
  **Quality** — What code standard should it target?
241
266
  - **architect** — Proper abstractions, error handling, forward-thinking structure
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-modes",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Behaviorally-tuned system prompts for Claude Code",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/build-info.ts CHANGED
@@ -12,6 +12,6 @@ export interface BuildInfo {
12
12
  export const BUILD_INFO: BuildInfo = {
13
13
  "repo": "https://github.com/nklisch/claude-code-modes",
14
14
  "branch": null,
15
- "commit": "ed8dffb",
15
+ "commit": "981e82f",
16
16
  "dirty": false
17
17
  };
package/src/cli.ts CHANGED
@@ -8,6 +8,7 @@ import { assemblePrompt, writeTempPrompt } from "./assemble.js";
8
8
  import { detectEnv, buildTemplateVars } from "./env.js";
9
9
  import { runConfigCommand } from "./config-cli.js";
10
10
  import { runInspectCommand } from "./inspect.js";
11
+ import { runUpdateCommand } from "./update.js";
11
12
  import { printUsage } from "./usage.js";
12
13
  import { formatVersion } from "./version.js";
13
14
 
@@ -63,6 +64,17 @@ async function main(): Promise<void> {
63
64
  process.exit(0);
64
65
  }
65
66
 
67
+ // Update subcommand routing
68
+ if (argv[0] === "update") {
69
+ try {
70
+ await runUpdateCommand(argv.slice(1));
71
+ } catch (err) {
72
+ process.stderr.write(`Error: ${(err as Error).message}\n`);
73
+ process.exit(1);
74
+ }
75
+ process.exit(0);
76
+ }
77
+
66
78
  let parsed;
67
79
  try {
68
80
  parsed = parseCliArgs(argv);
package/src/update.ts ADDED
@@ -0,0 +1,467 @@
1
+ import { writeFileSync, chmodSync, renameSync } from "node:fs";
2
+ import { execSync } from "node:child_process";
3
+ import { createHash } from "node:crypto";
4
+ import { basename } from "node:path";
5
+ import { VERSION } from "./version.js";
6
+ import { BUILD_INFO, type BuildInfo } from "./build-info.js";
7
+
8
+ // ----------------------------------------------------------------------
9
+ // Constants
10
+ // ----------------------------------------------------------------------
11
+
12
+ /** Upstream repo slug — also referenced by install.sh and the release workflow. */
13
+ const UPSTREAM_REPO = "nklisch/claude-code-modes";
14
+
15
+ /** Normalized form of the upstream remote URL captured by build-info. */
16
+ const UPSTREAM_REPO_URL = "https://github.com/nklisch/claude-code-modes.git";
17
+
18
+ const USER_AGENT = `claude-mode/${VERSION}`;
19
+
20
+ const GITHUB_API_BASE = "https://api.github.com";
21
+
22
+ /** Basenames of interpreter runtimes — signals we're running from source. */
23
+ const SOURCE_EXEC_NAMES = new Set(["bun", "bun-debug", "node"]);
24
+
25
+ // ----------------------------------------------------------------------
26
+ // Public types
27
+ // ----------------------------------------------------------------------
28
+
29
+ export interface UpdateOptions {
30
+ check: boolean;
31
+ force: boolean;
32
+ dryRun: boolean;
33
+ /** Positional version: "0.2.5" or "v0.2.5"; null for "latest". */
34
+ targetTag: string | null;
35
+ }
36
+
37
+ export type InstallClassification =
38
+ | { kind: "source"; reason: string }
39
+ | { kind: "dirty"; reason: string }
40
+ | { kind: "fork"; reason: string }
41
+ | { kind: "release"; binaryPath: string };
42
+
43
+ export interface ReleaseAsset {
44
+ name: string;
45
+ url: string;
46
+ }
47
+
48
+ export interface ReleaseInfo {
49
+ /** "v0.2.8" — exact tag from GitHub. */
50
+ tag: string;
51
+ /** "0.2.8" — tag with leading "v" stripped, comparable to package.json. */
52
+ version: string;
53
+ assets: ReleaseAsset[];
54
+ }
55
+
56
+ export interface PlatformArtifact {
57
+ binary: ReleaseAsset;
58
+ checksums: ReleaseAsset;
59
+ /** "claude-mode-linux-x64" etc. */
60
+ artifactName: string;
61
+ }
62
+
63
+ export type UpdateAction = "install" | "no-op" | "downgrade" | "reinstall";
64
+
65
+ export interface UpdatePlan {
66
+ current: string;
67
+ target: string;
68
+ tag: string;
69
+ artifact: PlatformArtifact;
70
+ binaryPath: string;
71
+ action: UpdateAction;
72
+ }
73
+
74
+ /** Injectable transport — default uses global fetch; tests inject a fake. */
75
+ export interface UpdateTransport {
76
+ fetchJson(url: string): Promise<unknown>;
77
+ fetchBytes(url: string): Promise<Uint8Array>;
78
+ fetchText(url: string): Promise<string>;
79
+ }
80
+
81
+ // ----------------------------------------------------------------------
82
+ // Default transport
83
+ // ----------------------------------------------------------------------
84
+
85
+ async function apiFetch(url: string): Promise<Response> {
86
+ const res = await fetch(url, {
87
+ redirect: "follow",
88
+ headers: { "User-Agent": USER_AGENT },
89
+ });
90
+ if (!res.ok) {
91
+ throw new Error(`HTTP ${res.status} fetching ${url}`);
92
+ }
93
+ return res;
94
+ }
95
+
96
+ export const defaultTransport: UpdateTransport = {
97
+ async fetchJson(url: string): Promise<unknown> {
98
+ const res = await apiFetch(url);
99
+ return res.json();
100
+ },
101
+ async fetchBytes(url: string): Promise<Uint8Array> {
102
+ const res = await apiFetch(url);
103
+ const buf = await res.arrayBuffer();
104
+ return new Uint8Array(buf);
105
+ },
106
+ async fetchText(url: string): Promise<string> {
107
+ const res = await apiFetch(url);
108
+ return res.text();
109
+ },
110
+ };
111
+
112
+ // ----------------------------------------------------------------------
113
+ // Public API — pure helpers
114
+ // ----------------------------------------------------------------------
115
+
116
+ /** Parses argv (after "update" has been stripped). Throws on unknown flags. */
117
+ export function parseUpdateArgs(argv: string[]): UpdateOptions {
118
+ const opts: UpdateOptions = { check: false, force: false, dryRun: false, targetTag: null };
119
+
120
+ for (const arg of argv) {
121
+ if (arg === "--check") {
122
+ opts.check = true;
123
+ } else if (arg === "--force") {
124
+ opts.force = true;
125
+ } else if (arg === "--dry-run") {
126
+ opts.dryRun = true;
127
+ } else if (arg.startsWith("--")) {
128
+ throw new Error(`Unknown flag: ${arg}`);
129
+ } else {
130
+ // Positional argument (version tag)
131
+ if (opts.targetTag !== null) {
132
+ throw new Error(`Unexpected argument: ${arg}`);
133
+ }
134
+ opts.targetTag = arg;
135
+ }
136
+ }
137
+
138
+ return opts;
139
+ }
140
+
141
+ /** Determines whether the running binary is safe to self-update. Pure. */
142
+ export function classifyInstall(
143
+ execPath: string = process.execPath,
144
+ buildInfo: BuildInfo = BUILD_INFO,
145
+ ): InstallClassification {
146
+ // 1. Runtime interpreter → source mode
147
+ if (SOURCE_EXEC_NAMES.has(basename(execPath))) {
148
+ return {
149
+ kind: "source",
150
+ reason: `Running as ${basename(execPath)} runtime (source mode)`,
151
+ };
152
+ }
153
+
154
+ // 2. Dirty working tree → dev build
155
+ if (buildInfo.dirty === true) {
156
+ return {
157
+ kind: "dirty",
158
+ reason: "Binary was built from a dirty working tree",
159
+ };
160
+ }
161
+
162
+ // 3. Non-upstream repo → fork build
163
+ if (buildInfo.repo && buildInfo.repo !== UPSTREAM_REPO_URL) {
164
+ return {
165
+ kind: "fork",
166
+ reason: `Binary was built from a fork: ${buildInfo.repo}`,
167
+ };
168
+ }
169
+
170
+ // 4. Otherwise → safe to update
171
+ return { kind: "release", binaryPath: execPath };
172
+ }
173
+
174
+ /** Maps process.platform/process.arch → release artifact name. Throws on unsupported. */
175
+ export function detectArtifactName(
176
+ platformVal: string = process.platform,
177
+ archVal: string = process.arch,
178
+ ): string {
179
+ const SUPPORTED_PLATFORMS = ["linux", "darwin"] as const;
180
+ const SUPPORTED_ARCHES = ["x64", "arm64"] as const;
181
+
182
+ type SupportedPlatform = (typeof SUPPORTED_PLATFORMS)[number];
183
+ type SupportedArch = (typeof SUPPORTED_ARCHES)[number];
184
+
185
+ if (!(SUPPORTED_PLATFORMS as readonly string[]).includes(platformVal)) {
186
+ throw new Error(
187
+ `Unsupported platform: "${platformVal}". ` +
188
+ `claude-mode update only supports: ${SUPPORTED_PLATFORMS.join(", ")}.`,
189
+ );
190
+ }
191
+ if (!(SUPPORTED_ARCHES as readonly string[]).includes(archVal)) {
192
+ throw new Error(
193
+ `Unsupported architecture: "${archVal}". ` +
194
+ `claude-mode update only supports: ${SUPPORTED_ARCHES.join(", ")}.`,
195
+ );
196
+ }
197
+
198
+ const platform = platformVal as SupportedPlatform;
199
+ const arch = archVal as SupportedArch;
200
+ return `claude-mode-${platform}-${arch}`;
201
+ }
202
+
203
+ /** Hits GET /repos/{repo}/releases/latest. */
204
+ export async function fetchLatestRelease(transport: UpdateTransport): Promise<ReleaseInfo> {
205
+ const url = `${GITHUB_API_BASE}/repos/${UPSTREAM_REPO}/releases/latest`;
206
+ const data = await transport.fetchJson(url);
207
+ return parseReleaseJson(data);
208
+ }
209
+
210
+ /** Hits GET /repos/{repo}/releases/tags/v{tag}. Accepts "0.2.5" or "v0.2.5". */
211
+ export async function fetchReleaseByTag(
212
+ tag: string,
213
+ transport: UpdateTransport,
214
+ ): Promise<ReleaseInfo> {
215
+ const normalizedTag = tag.startsWith("v") ? tag : `v${tag}`;
216
+ const url = `${GITHUB_API_BASE}/repos/${UPSTREAM_REPO}/releases/tags/${normalizedTag}`;
217
+ try {
218
+ const data = await transport.fetchJson(url);
219
+ return parseReleaseJson(data);
220
+ } catch (err) {
221
+ throw new Error(`Could not find release ${normalizedTag}: ${(err as Error).message}`);
222
+ }
223
+ }
224
+
225
+ /** Picks the binary asset and checksums.txt from a release. Throws if either is missing. */
226
+ export function selectArtifact(release: ReleaseInfo, artifactName: string): PlatformArtifact {
227
+ const binary = release.assets.find((a) => a.name === artifactName);
228
+ const checksums = release.assets.find((a) => a.name === "checksums.txt");
229
+
230
+ if (!binary) {
231
+ const available = release.assets.map((a) => a.name).join(", ");
232
+ throw new Error(
233
+ `Could not find binary "${artifactName}" in release ${release.tag}. ` +
234
+ `Available assets: ${available}`,
235
+ );
236
+ }
237
+ if (!checksums) {
238
+ throw new Error(
239
+ `Could not find "checksums.txt" in release ${release.tag}. ` +
240
+ `Available assets: ${release.assets.map((a) => a.name).join(", ")}`,
241
+ );
242
+ }
243
+
244
+ return { binary, checksums, artifactName };
245
+ }
246
+
247
+ /** Reads "<sha256> <filename>" lines and returns the hash for artifactName. */
248
+ export function parseChecksum(text: string, artifactName: string): string {
249
+ const lines = text.split("\n");
250
+ for (const line of lines) {
251
+ const match = line.match(/^([0-9a-f]{64})\s+\*?(.+)$/i);
252
+ if (match && match[2].trim() === artifactName) {
253
+ return match[1].toLowerCase();
254
+ }
255
+ }
256
+ throw new Error(`Could not find checksum for "${artifactName}" in checksums.txt`);
257
+ }
258
+
259
+ /** Returns lowercase hex SHA-256 of data. */
260
+ export function computeSha256(data: Uint8Array): string {
261
+ return createHash("sha256").update(data).digest("hex").toLowerCase();
262
+ }
263
+
264
+ /** Throws Error with expected/actual hashes if they don't match. */
265
+ export function verifyChecksum(data: Uint8Array, expected: string): void {
266
+ const actual = computeSha256(data);
267
+ if (actual !== expected.toLowerCase()) {
268
+ throw new Error(
269
+ `Checksum mismatch!\n` +
270
+ ` Expected: ${expected.toLowerCase()}\n` +
271
+ ` Actual: ${actual}`,
272
+ );
273
+ }
274
+ }
275
+
276
+ /** Numeric compare of "X.Y.Z" strings. -1 / 0 / 1. */
277
+ export function compareSemver(a: string, b: string): number {
278
+ const parseVer = (v: string): number[] =>
279
+ v.replace(/^v/, "").split(".").map((n) => parseInt(n, 10));
280
+
281
+ const aParts = parseVer(a);
282
+ const bParts = parseVer(b);
283
+
284
+ for (let i = 0; i < 3; i++) {
285
+ const diff = (aParts[i] ?? 0) - (bParts[i] ?? 0);
286
+ if (diff < 0) return -1;
287
+ if (diff > 0) return 1;
288
+ }
289
+ return 0;
290
+ }
291
+
292
+ /** Picks the action given current, target, and --force. */
293
+ export function computeAction(
294
+ current: string,
295
+ target: string,
296
+ force: boolean,
297
+ ): UpdateAction {
298
+ if (force) return "reinstall";
299
+ const cmp = compareSemver(current, target);
300
+ if (cmp === 0) return "no-op";
301
+ if (cmp < 0) return "install";
302
+ return "downgrade";
303
+ }
304
+
305
+ /**
306
+ * Atomic install: write to "<binaryPath>.new", chmod 0755, drop macOS
307
+ * quarantine xattr (best-effort), rename over binaryPath.
308
+ */
309
+ export function installBinary(
310
+ data: Uint8Array,
311
+ target: { binaryPath: string; isDarwin: boolean },
312
+ ): void {
313
+ const stagingPath = `${target.binaryPath}.new`;
314
+ writeFileSync(stagingPath, data);
315
+ chmodSync(stagingPath, 0o755);
316
+
317
+ if (target.isDarwin) {
318
+ try {
319
+ execSync(`xattr -d com.apple.quarantine "${stagingPath}" 2>/dev/null`, { stdio: "ignore" });
320
+ } catch {
321
+ // Best-effort — missing xattr attribute is normal
322
+ }
323
+ }
324
+
325
+ renameSync(stagingPath, target.binaryPath);
326
+ }
327
+
328
+ // ----------------------------------------------------------------------
329
+ // Orchestrator internals (unexported)
330
+ // ----------------------------------------------------------------------
331
+
332
+ function guidanceFor(kind: "source" | "dirty" | "fork"): string {
333
+ if (kind === "source") {
334
+ return "To update, use: git pull && bun install";
335
+ }
336
+ if (kind === "dirty") {
337
+ return "Commit or stash your changes, build a clean binary, then run update.";
338
+ }
339
+ // fork
340
+ return "Update via your fork's release process.";
341
+ }
342
+
343
+ function printDryRunPlan(plan: UpdatePlan): void {
344
+ process.stdout.write(`Dry-run plan:\n`);
345
+ process.stdout.write(` Action: ${plan.action}\n`);
346
+ process.stdout.write(` Current: ${plan.current}\n`);
347
+ process.stdout.write(` Target: ${plan.target} (${plan.tag})\n`);
348
+ process.stdout.write(` Binary URL: ${plan.artifact.binary.url}\n`);
349
+ process.stdout.write(` Checksums URL: ${plan.artifact.checksums.url}\n`);
350
+ process.stdout.write(` Artifact: ${plan.artifact.artifactName}\n`);
351
+ process.stdout.write(` Install path: ${plan.binaryPath}\n`);
352
+ process.stdout.write(` (No changes written)\n`);
353
+ }
354
+
355
+ async function executePlan(plan: UpdatePlan, transport: UpdateTransport): Promise<void> {
356
+ // Fetch binary and checksums in parallel
357
+ const [binaryData, checksumsText] = await Promise.all([
358
+ transport.fetchBytes(plan.artifact.binary.url),
359
+ transport.fetchText(plan.artifact.checksums.url),
360
+ ]);
361
+
362
+ const expectedHash = parseChecksum(checksumsText, plan.artifact.artifactName);
363
+ verifyChecksum(binaryData, expectedHash);
364
+
365
+ installBinary(binaryData, {
366
+ binaryPath: plan.binaryPath,
367
+ isDarwin: process.platform === "darwin",
368
+ });
369
+ }
370
+
371
+ /** Parses a raw GitHub release API response into a ReleaseInfo. */
372
+ function parseReleaseJson(data: unknown): ReleaseInfo {
373
+ const raw = data as Record<string, unknown>;
374
+ const tag = raw["tag_name"] as string;
375
+ const version = tag.startsWith("v") ? tag.slice(1) : tag;
376
+ const rawAssets = (raw["assets"] as Record<string, unknown>[]) ?? [];
377
+ const assets: ReleaseAsset[] = rawAssets.map((a) => ({
378
+ name: a["name"] as string,
379
+ url: a["browser_download_url"] as string,
380
+ }));
381
+ return { tag, version, assets };
382
+ }
383
+
384
+ // ----------------------------------------------------------------------
385
+ // Orchestrator (public entry point)
386
+ // ----------------------------------------------------------------------
387
+
388
+ /**
389
+ * Orchestrates the whole update operation. argv excludes the leading
390
+ * "update" subcommand. Throws on any failure; cli.ts converts to stderr+exit.
391
+ */
392
+ export async function runUpdateCommand(
393
+ argv: string[],
394
+ transport: UpdateTransport = defaultTransport,
395
+ ): Promise<void> {
396
+ // 1. Parse args
397
+ const opts = parseUpdateArgs(argv);
398
+
399
+ // 2. Classify install — refuse if not a release build
400
+ const classification = classifyInstall();
401
+ if (classification.kind !== "release") {
402
+ const guidance = guidanceFor(classification.kind);
403
+ throw new Error(
404
+ `Cannot self-update: ${classification.reason}.\n${guidance}`,
405
+ );
406
+ }
407
+ const binaryPath = classification.binaryPath;
408
+
409
+ // 3. Detect artifact name for this platform
410
+ const artifactName = detectArtifactName();
411
+
412
+ // 4. Fetch the target release
413
+ process.stdout.write("Checking for updates...\n");
414
+ const release = opts.targetTag
415
+ ? await fetchReleaseByTag(opts.targetTag, transport)
416
+ : await fetchLatestRelease(transport);
417
+
418
+ // 5. Compute action
419
+ const action = computeAction(VERSION, release.version, opts.force);
420
+
421
+ // 6. Print status
422
+ process.stdout.write(`Current: ${VERSION}\n`);
423
+ process.stdout.write(`Latest: ${release.version}\n`);
424
+
425
+ // 7. --check: print human-readable status and return
426
+ if (opts.check) {
427
+ if (action === "no-op") {
428
+ process.stdout.write("Up to date.\n");
429
+ } else if (action === "install") {
430
+ process.stdout.write(`Update available: ${VERSION} -> ${release.version}\n`);
431
+ } else if (action === "downgrade") {
432
+ process.stdout.write(`Older version available: ${release.version} (current: ${VERSION})\n`);
433
+ } else {
434
+ // reinstall (--force + --check)
435
+ process.stdout.write(`Will reinstall: ${release.version}\n`);
436
+ }
437
+ return;
438
+ }
439
+
440
+ // 8. no-op without --force
441
+ if (action === "no-op") {
442
+ process.stdout.write("Already up to date.\n");
443
+ return;
444
+ }
445
+
446
+ // 9. Select the platform artifact
447
+ const artifact = selectArtifact(release, artifactName);
448
+
449
+ const plan: UpdatePlan = {
450
+ current: VERSION,
451
+ target: release.version,
452
+ tag: release.tag,
453
+ artifact,
454
+ binaryPath,
455
+ action,
456
+ };
457
+
458
+ // 10. --dry-run: print the plan and return
459
+ if (opts.dryRun) {
460
+ printDryRunPlan(plan);
461
+ return;
462
+ }
463
+
464
+ // 11. Execute
465
+ await executePlan(plan, transport);
466
+ process.stdout.write(`Installed claude-mode ${plan.target} at ${binaryPath}\n`);
467
+ }
package/src/usage.ts CHANGED
@@ -4,6 +4,13 @@ export function printUsage(): void {
4
4
  Subcommands:
5
5
  config Manage configuration
6
6
  inspect [--print] Show prompt assembly plan with provenance and warnings
7
+ update [version] Update the installed binary from GitHub Releases
8
+
9
+ Update flags (passed after the "update" subcommand):
10
+ --check Check for updates without installing
11
+ --force Reinstall the same version (repair a corrupted install)
12
+ --dry-run Show what would happen without writing anything
13
+ <version> Install a specific release tag (e.g. "0.2.5"); default is latest
7
14
 
8
15
  Info:
9
16
  --version Print claude-mode version and exit
@@ -58,7 +65,11 @@ Examples:
58
65
  claude-mode director # delegate to sub-agents
59
66
  claude-mode partner # equal-pair: speak plainly, TDD by default
60
67
  claude-mode create --modifier bold # confident, idiomatic code
61
- claude-mode create -- --verbose --model sonnet`;
68
+ claude-mode create -- --verbose --model sonnet
69
+ claude-mode update # update to the latest release
70
+ claude-mode update --check # check for updates without installing
71
+ claude-mode update 0.2.5 # pin to a specific version (downgrade or reinstall)
72
+ claude-mode update --force # reinstall the same version (repair)`;
62
73
 
63
74
  process.stdout.write(usage + "\n");
64
75
  }