respawn-session 0.0.2 → 0.0.4

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
@@ -17,56 +17,116 @@ bun --version
17
17
  gh auth status
18
18
  ```
19
19
 
20
- ## Usage
20
+ ## Quick Start
21
21
 
22
- Initialize local storage and install autosave Stop hooks for Claude Code and Codex:
22
+ Initialize once on each machine. This creates `~/.respawn/index.json` and installs autosave Stop hooks for Claude Code and Codex:
23
23
 
24
24
  ```sh
25
25
  respawn init
26
26
  ```
27
27
 
28
- Save the current active agent session from inside a git worktree:
28
+ Work normally in Claude Code or Codex. `respawn init` makes sessions autosave when the agent stops. To save immediately from inside an active agent session, run:
29
29
 
30
30
  ```sh
31
31
  respawn save
32
32
  ```
33
33
 
34
- Autosave is what the installed hooks run. It skips unchanged transcripts so repeated Stop events do not create duplicate gists:
34
+ Resume the latest saved session for a branch:
35
35
 
36
36
  ```sh
37
- respawn autosave
37
+ respawn angela/fix-bugs
38
38
  ```
39
39
 
40
- Resume the latest saved session for a branch:
40
+ Resume from a PR that was tagged with `respawn tag`:
41
+
42
+ ```sh
43
+ respawn 123
44
+ respawn https://github.com/org/repo/pull/123
45
+ ```
46
+
47
+ ## Common Workflows
48
+
49
+ ### Manual Branch Save
50
+
51
+ Inside an active Claude Code or Codex session:
52
+
53
+ ```sh
54
+ respawn save
55
+ ```
56
+
57
+ Later, from a clone or worktree for the same repo:
58
+
59
+ ```sh
60
+ respawn <branch>
61
+ ```
62
+
63
+ Example:
41
64
 
42
65
  ```sh
43
66
  respawn angela/fix-bugs
44
67
  ```
45
68
 
46
- Attach the latest saved session to the current GitHub PR:
69
+ ### Autosave
70
+
71
+ Run this once per machine:
72
+
73
+ ```sh
74
+ respawn init
75
+ ```
76
+
77
+ After that, Claude Code and Codex Stop hooks run:
78
+
79
+ ```sh
80
+ respawn autosave
81
+ ```
82
+
83
+ Autosave hashes the transcript and skips unchanged sessions, so repeated Stop events do not create duplicate gists.
84
+
85
+ ### PR Tagging
86
+
87
+ Use this when you want a session to survive branch deletion after merge:
47
88
 
48
89
  ```sh
49
90
  respawn tag
50
91
  ```
51
92
 
52
- Resume from a tagged PR, even if the branch was deleted:
93
+ That writes or updates a hidden metadata comment on the current GitHub PR. The comment stores session pointers, not the transcript body. Transcripts still live in your private gists.
94
+
95
+ Later, resume from the PR:
53
96
 
54
97
  ```sh
55
98
  respawn 123
56
99
  respawn https://github.com/org/repo/pull/123
57
100
  ```
58
101
 
59
- List saved sessions:
102
+ ### List Saved Sessions
103
+
104
+ Show every saved session in your local index:
60
105
 
61
106
  ```sh
62
107
  respawn list
63
108
  ```
64
109
 
110
+ ## Commands
111
+
112
+ | Command | What it does |
113
+ | --- | --- |
114
+ | `respawn init` | Creates the local index and installs autosave hooks |
115
+ | `respawn save` | Saves the active Claude Code or Codex transcript |
116
+ | `respawn autosave` | Saves only if the transcript changed |
117
+ | `respawn tag` | Saves and attaches session metadata to the current PR |
118
+ | `respawn <branch>` | Restores the newest session for a branch |
119
+ | `respawn <pr-number>` | Restores the newest session from a tagged PR |
120
+ | `respawn <pr-url>` | Restores the newest session from a tagged PR URL |
121
+ | `respawn list` | Lists locally indexed sessions |
122
+ | `respawn version` | Prints the installed CLI version |
123
+ | `respawn update` | Updates the global npm install to the latest release |
124
+
65
125
  ## How It Works
66
126
 
67
127
  `respawn save` detects the active agent in this order:
68
128
 
69
- 1. Claude Code via `CLAUDE_SESSION_ID`
129
+ 1. Claude Code via `CLAUDE_SESSION_ID`, then `~/.claude/sessions/*.json`
70
130
  2. Codex via `CODEX_TUI_SESSION_LOG_PATH`, `CODEX_SESSION_ID`, or the newest `~/.codex/sessions/**.jsonl` transcript for the current cwd
71
131
 
72
132
  It then runs:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "respawn-session",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Save and resume Claude Code or Codex agent sessions by git branch.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,10 +1,10 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import type { LocatedTranscript, LocateOptions } from "./types";
5
5
 
6
6
  export function encodeClaudeProjectPath(cwd: string): string {
7
- return cwd.replaceAll("/", "-");
7
+ return cwd.replace(/[^A-Za-z0-9._-]/g, "-");
8
8
  }
9
9
 
10
10
  export function transcriptPath(
@@ -27,14 +27,55 @@ export function locateTranscript(
27
27
  ): LocatedTranscript | null {
28
28
  const env = options.env ?? process.env;
29
29
  const sessionId = env.CLAUDE_SESSION_ID;
30
- if (!sessionId) return null;
30
+ if (sessionId) {
31
+ const path = transcriptPath(sessionId, options);
32
+ if (existsSync(path)) return { agent: "claude", path, sessionId };
33
+ }
31
34
 
32
- const path = transcriptPath(sessionId, options);
33
- if (!existsSync(path)) return null;
34
-
35
- return { agent: "claude", path, sessionId };
35
+ return locateFromSessionRegistry(options);
36
36
  }
37
37
 
38
38
  export function resumeCmd(sessionId: string): string[] {
39
39
  return ["claude", "--resume", sessionId];
40
40
  }
41
+
42
+ type ClaudeSessionRecord = {
43
+ sessionId?: string;
44
+ cwd?: string;
45
+ updatedAt?: number;
46
+ };
47
+
48
+ function locateFromSessionRegistry(
49
+ options: LocateOptions = {},
50
+ ): LocatedTranscript | null {
51
+ const cwd = options.cwd ?? process.cwd();
52
+ const home = options.home ?? homedir();
53
+ const sessionsDir = join(home, ".claude", "sessions");
54
+ if (!existsSync(sessionsDir)) return null;
55
+
56
+ const records = readdirSync(sessionsDir, { withFileTypes: true })
57
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
58
+ .map((entry) => readSessionRecord(join(sessionsDir, entry.name)))
59
+ .filter(
60
+ (record): record is Required<Pick<ClaudeSessionRecord, "sessionId" | "cwd">> &
61
+ ClaudeSessionRecord => Boolean(record?.sessionId && record.cwd === cwd),
62
+ )
63
+ .sort((a, b) => (a.updatedAt ?? 0) - (b.updatedAt ?? 0));
64
+
65
+ for (const record of records.reverse()) {
66
+ const path = transcriptPath(record.sessionId, options);
67
+ if (existsSync(path)) {
68
+ return { agent: "claude", path, sessionId: record.sessionId };
69
+ }
70
+ }
71
+
72
+ return null;
73
+ }
74
+
75
+ function readSessionRecord(path: string): ClaudeSessionRecord | null {
76
+ try {
77
+ return JSON.parse(readFileSync(path, "utf8")) as ClaudeSessionRecord;
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
package/src/cli.ts CHANGED
@@ -4,6 +4,7 @@ import { listSessions } from "./commands/list";
4
4
  import { resumePrSession, resumeSession } from "./commands/resume";
5
5
  import { saveSession } from "./commands/save";
6
6
  import { tagCurrentPr } from "./commands/tag";
7
+ import { updateRespawn, versionText } from "./commands/update";
7
8
 
8
9
  export type Route =
9
10
  | { name: "help" }
@@ -12,6 +13,8 @@ export type Route =
12
13
  | { name: "list" }
13
14
  | { name: "init" }
14
15
  | { name: "tag" }
16
+ | { name: "version" }
17
+ | { name: "update" }
15
18
  | { name: "resume"; branch: string }
16
19
  | { name: "resume-pr"; prRef: string };
17
20
 
@@ -20,12 +23,16 @@ export function route(args: string[]): Route {
20
23
  if (!command || command === "help" || command === "--help" || command === "-h") {
21
24
  return { name: "help" };
22
25
  }
26
+ if (command === "version" || command === "--version" || command === "-v") {
27
+ return { name: "version" };
28
+ }
23
29
  if (
24
30
  command === "save" ||
25
31
  command === "autosave" ||
26
32
  command === "list" ||
27
33
  command === "init" ||
28
- command === "tag"
34
+ command === "tag" ||
35
+ command === "update"
29
36
  ) {
30
37
  return { name: command };
31
38
  }
@@ -61,6 +68,14 @@ export async function main(args = Bun.argv.slice(2)): Promise<void> {
61
68
  console.log((await tagCurrentPr()).message);
62
69
  return;
63
70
  }
71
+ if (selected.name === "version") {
72
+ console.log(versionText());
73
+ return;
74
+ }
75
+ if (selected.name === "update") {
76
+ console.log(await updateRespawn());
77
+ return;
78
+ }
64
79
 
65
80
  const result =
66
81
  selected.name === "resume-pr"
@@ -85,6 +100,8 @@ function helpText(): string {
85
100
  " respawn <pr-url|number>",
86
101
  " respawn list",
87
102
  " respawn init",
103
+ " respawn version",
104
+ " respawn update",
88
105
  ].join("\n");
89
106
  }
90
107
 
@@ -0,0 +1,26 @@
1
+ import packageJson from "../../package.json" with { type: "json" };
2
+ import { runCommand, type RunCommand } from "../shell";
3
+
4
+ export const currentVersion = packageJson.version;
5
+
6
+ export function versionText(version = currentVersion): string {
7
+ return `respawn-session ${version}`;
8
+ }
9
+
10
+ export type UpdateDeps = {
11
+ currentVersion?: string;
12
+ run?: RunCommand;
13
+ };
14
+
15
+ export async function updateRespawn(deps: UpdateDeps = {}): Promise<string> {
16
+ const version = deps.currentVersion ?? currentVersion;
17
+ const run = deps.run ?? runCommand;
18
+ const latest = (await run("npm", ["view", "respawn-session", "version"])).trim();
19
+
20
+ if (latest === version) {
21
+ return `respawn-session is already up to date at ${version}`;
22
+ }
23
+
24
+ await run("npm", ["install", "-g", "respawn-session@latest"]);
25
+ return `Updated respawn-session ${version} -> ${latest}`;
26
+ }