backstep 0.1.4 → 0.1.5

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
@@ -53,7 +53,7 @@ With Claude Code: `claude mcp add files -- backstep wrap --name files -- <the se
53
53
  | `--rules <file>` | undo rules (or set `BACKSTEP_RULES`) |
54
54
  | `--pg <url>` | the Postgres database the wrapped server writes to. Enables database undo. Without it, SQL writes are recorded but can't be undone. |
55
55
 
56
- The undo key is ⌘Z and ctrl+z on a Mac (⌘Z where the terminal passes it on, as Ghostty, Kitty and WezTerm do; Terminal.app and iTerm2 keep ⌘Z for their own menu) and ctrl+z elsewhere. The first press asks "Undo Claude's last turn?" above the prompt and a second press within 5 seconds undoes, so a stray press does nothing. Change the key whenever you like: ask Claude ("change my Backstep undo key to ctrl+u"), or run `backstep key ctrl+u`. The line above the prompt is always there, even with nothing to undo, because the key only reaches Backstep while it's on screen; otherwise ctrl+z would fall through to the terminal and suspend Claude Code.
56
+ The undo key is ctrl+z. The first press asks "Undo Claude's last turn?" above the prompt and a second press within 5 seconds undoes, so a stray press does nothing. Change the key whenever you like: ask Claude ("change my Backstep undo key to ctrl+u"), or run `backstep key ctrl+u`. The line above the prompt is always there, even with nothing to undo, because the key only reaches Backstep while it's on screen; otherwise ctrl+z would fall through to the terminal and suspend Claude Code.
57
57
 
58
58
  Backstep checks npm for a newer version as each Claude Code session starts and asks before updating (Update, Not now, or Skip this version; `backstep update` does it by hand, `BACKSTEP_UPDATE_CHECK=0` turns the check off).
59
59
 
package/dist/keys.js CHANGED
@@ -8,10 +8,7 @@ import { homedir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  export const UNDO_ACTION = "app:cycleDiffBase";
10
10
  export const DEFAULT_KEY = "ctrl+z";
11
- // A Mac gets ⌘Z too, where the terminal passes it on (Ghostty, Kitty, WezTerm,
12
- // Claude Code's desktop app); Terminal.app and iTerm2 keep ⌘Z for their own
13
- // Edit menu, so ctrl+z stays bound alongside it.
14
- export const defaultKeys = () => (process.platform === "darwin" ? ["cmd+z", DEFAULT_KEY] : [DEFAULT_KEY]);
11
+ export const defaultKeys = () => [DEFAULT_KEY];
15
12
  const CONTEXTS = ["Global", "Chat"];
16
13
  // Claude Code never hands these to anything else.
17
14
  const RESERVED = new Set(["ctrl+c", "ctrl+d", "ctrl+m", "ctrl+i", "ctrl+h", "ctrl+[", "ctrl+\\"]);
package/dist/setup.js CHANGED
@@ -29,7 +29,7 @@ function keepCopy() {
29
29
  cpSync(dirname(ROOT), modules, { recursive: true });
30
30
  return { root: join(modules, "backstep"), onPath: false };
31
31
  }
32
- // Keeps a key the person picked; on a first install binds ⌘Z and ctrl+z on a Mac, ctrl+z elsewhere.
32
+ // Keeps a key the person picked; binds ctrl+z on a first install.
33
33
  function bindUndoKey() {
34
34
  if (undoKey())
35
35
  return "";
package/dist/update.js CHANGED
@@ -51,17 +51,39 @@ export function skipVersion(v) {
51
51
  mkdirSync(homeDir(), { recursive: true });
52
52
  writeFileSync(file(), JSON.stringify({ ...settings(), skipVersion: v }, null, 2) + "\n");
53
53
  }
54
+ // What went wrong, in a line a person can act on: npm's own reason, not its
55
+ // pointer to a log file.
56
+ export function npmReason(out) {
57
+ if (/ETARGET|notarget|No matching version/i.test(out))
58
+ return "npm hasn't finished publishing it yet. Try again in a few minutes";
59
+ if (/ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ETIMEDOUT|network/i.test(out))
60
+ return "couldn't reach npm. Check your internet connection";
61
+ if (/EACCES|permission denied/i.test(out))
62
+ return "npm wasn't allowed to install it (a permissions problem with npm's global folder)";
63
+ const lines = out.split("\n").map((l) => l.trim()).filter((l) => l && !/complete log of this run|^npm (error|ERR!)\s*$/i.test(l));
64
+ return (lines.find((l) => /^backstep:/.test(l)) ?? lines.findLast((l) => /error/i.test(l)) ?? lines.at(-1) ?? "npx failed").replace(/^(backstep:|npm (error|ERR!))\s*/i, "");
65
+ }
66
+ const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
54
67
  // The new version's setup puts its own copy in place, points Claude Code's mod
55
- // at it and re-wraps servers, exactly as a fresh install would.
56
- export function runUpdate(v) {
57
- const r = spawnSync("npx", ["-y", `backstep@${v}`, "setup"], {
58
- encoding: "utf8",
59
- timeout: 5 * 60 * 1000,
60
- env: { ...process.env, NO_COLOR: "1", BACKSTEP_TELEMETRY: "0" },
61
- stdio: ["ignore", "pipe", "pipe"],
62
- });
63
- if (r.status === 0)
64
- return { ok: true, detail: `updated to ${v}` };
65
- const why = `${r.stderr ?? ""}${r.stdout ?? ""}`.trim().split("\n").filter(Boolean).at(-1) ?? "npx failed";
66
- return { ok: false, detail: why.replace(/^backstep: /, "") };
68
+ // at it and re-wraps servers, exactly as a fresh install would. npm is asked
69
+ // online, never from its cache, which can be older than a version just
70
+ // published; and asked again for a while if it still hasn't heard of it.
71
+ export function runUpdate(v, tries = 4) {
72
+ let out = "";
73
+ for (let i = 0; i < tries; i++) {
74
+ if (i)
75
+ pause(15_000);
76
+ const r = spawnSync("npx", ["-y", "--prefer-online", `backstep@${v}`, "setup"], {
77
+ encoding: "utf8",
78
+ timeout: 5 * 60 * 1000,
79
+ env: { ...process.env, NO_COLOR: "1", BACKSTEP_TELEMETRY: "0" },
80
+ stdio: ["ignore", "pipe", "pipe"],
81
+ });
82
+ if (r.status === 0)
83
+ return { ok: true, detail: `updated to ${v}` };
84
+ out = `${r.stderr ?? ""}\n${r.stdout ?? ""}`;
85
+ if (!/ETARGET|notarget|No matching version/i.test(out))
86
+ break;
87
+ }
88
+ return { ok: false, detail: npmReason(out) };
67
89
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backstep",
3
- "version": "0.1.0",
3
+ "version": "0.1.5",
4
4
  "description": "Press ctrl+z to rewind everything Claude did in its last turn: file edits, shell commands, and database and tool changes.",
5
5
  "types": "./types/index.d.ts",
6
6
  "userConfig": {
@@ -403,7 +403,7 @@ async function offerUpdate($: EngineInterface) {
403
403
  if (answer !== UPDATE.yes) return
404
404
  $.ui.toast(`Updating Backstep to ${u.latest}…`)
405
405
  const done = await $.process
406
- .run(backstep(['update', '--version', u.latest, '--json']), { timeoutMs: 300_000 })
406
+ .run(backstep(['update', '--version', u.latest, '--json']), { timeoutMs: 600_000 })
407
407
  .catch((err: unknown) => ({ exitCode: 1, stdout: '', stderr: err instanceof Error ? err.message : String(err) }))
408
408
  let r: { ok: boolean; detail: string } = { ok: false, detail: (done.stderr || 'it failed').trim().split('\n').at(-1) ?? 'it failed' }
409
409
  try {
@@ -419,7 +419,7 @@ const KEY = 'mcp__backstep__undo_key'
419
419
  const KEY_TOOL = {
420
420
  name: 'undo_key',
421
421
  description:
422
- "Change the key that undoes Claude's last turn with Backstep (⌘Z and ctrl+z on a Mac, ctrl+z elsewhere, unless the user changed it). " +
422
+ "Change the key that undoes Claude's last turn with Backstep (ctrl+z unless the user changed it). " +
423
423
  'Use it when the user asks to change, rebind or pick a different Backstep or undo shortcut. ' +
424
424
  'Keys are written like ctrl+u, cmd+z (⌘ on a Mac; most Mac terminals keep ⌘ keys for themselves, so pair it with a ctrl key), meta+z (Option) or ctrl+shift+z. ' +
425
425
  'Several keys can share undo: separate them with commas. It takes effect right away, no restart.',
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "backstep",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Undo for everything your agents touch: an MCP proxy that makes agent actions reversible.",
5
5
  "scripts": {
6
6
  "dev": "node src/cli.ts",
7
7
  "ui": "node --disable-warning=ExperimentalWarning src/cli.ts ui",
8
8
  "demo": "node --disable-warning=ExperimentalWarning examples/demo-db.ts",
9
- "build": "tsc -p tsconfig.json && cp src/ui/index.html dist/ui/index.html && chmod +x dist/cli.js",
9
+ "build": "tsc -p tsconfig.json && cp src/ui/index.html dist/ui/index.html && chmod +x dist/cli.js && node -e \"const f='mod/.claude-plugin/plugin.json',p=JSON.parse(require('fs').readFileSync(f));p.version=require('./package.json').version;require('fs').writeFileSync(f,JSON.stringify(p,null,2)+'\\\\n')\"",
10
10
  "test": "node --disable-warning=ExperimentalWarning --test test/*.test.ts",
11
11
  "typecheck": "tsc -p tsconfig.json --noEmit"
12
12
  },