claude-session-fork 1.0.0
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/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +189 -0
- package/dist/components.d.ts +15 -0
- package/dist/components.js +160 -0
- package/dist/session.d.ts +19 -0
- package/dist/session.js +238 -0
- package/package.json +52 -0
- package/scripts/postinstall.js +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# claude-session-fork
|
|
2
|
+
|
|
3
|
+
[δΈζζζ‘£](./README_CN.md)
|
|
4
|
+
|
|
5
|
+
Fork Claude Code sessions at any conversation point and continue in a new terminal.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- π **Fork at any point** - Select any message to create a branch from
|
|
10
|
+
- π **Session browser** - Browse all sessions with preview
|
|
11
|
+
- π **Visual history** - Browse conversation with code change indicators (β)
|
|
12
|
+
- π₯οΈ **Multi-terminal** - Supports Terminal.app, iTerm2, VS Code, Cursor, Kiro
|
|
13
|
+
- β‘ **Auto-detect** - Automatically uses current session in Claude Code
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
### npm (Recommended)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g claude-session-fork
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Homebrew (macOS)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
brew tap duo121/claude-session-fork
|
|
27
|
+
brew install claude-session-fork
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### From source
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
git clone https://github.com/duo121/claude-session-fork.git
|
|
34
|
+
cd claude-session-fork
|
|
35
|
+
npm install && npm run build && npm link
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Uninstall
|
|
39
|
+
|
|
40
|
+
### npm
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm uninstall -g claude-session-fork
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Homebrew
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
brew uninstall claude-session-fork
|
|
50
|
+
brew untap duo121/claude-session-fork
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### From source
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm unlink -g claude-session-fork
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Fork current session (auto-detect in Claude Code)
|
|
63
|
+
sfork
|
|
64
|
+
|
|
65
|
+
# Show session list to choose from
|
|
66
|
+
sfork --list
|
|
67
|
+
sfork -l
|
|
68
|
+
|
|
69
|
+
# Fork specific session
|
|
70
|
+
sfork --session=<session-id>
|
|
71
|
+
|
|
72
|
+
# Specify terminal
|
|
73
|
+
sfork --terminal=iterm
|
|
74
|
+
sfork --terminal=vscode
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Commands:** `sfork`, `csfork`, `claude-session-fork`
|
|
78
|
+
|
|
79
|
+
## Workflow
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
|
|
83
|
+
β Session List β βββΊ β Message List β βββΊ β New Terminal β
|
|
84
|
+
β (--list mode) β β (select fork β β (forked β
|
|
85
|
+
β β β point) β β session) β
|
|
86
|
+
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
|
|
87
|
+
β β
|
|
88
|
+
β Esc β Esc
|
|
89
|
+
βΌ βΌ
|
|
90
|
+
Exit Back to Sessions
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Controls
|
|
94
|
+
|
|
95
|
+
### Session List (--list mode)
|
|
96
|
+
| Key | Action |
|
|
97
|
+
|-----|--------|
|
|
98
|
+
| `ββ` | Navigate sessions |
|
|
99
|
+
| `Enter` | Select session |
|
|
100
|
+
| `Esc` | Exit |
|
|
101
|
+
|
|
102
|
+
### Message List
|
|
103
|
+
| Key | Action |
|
|
104
|
+
|-----|--------|
|
|
105
|
+
| `ββ` | Navigate messages |
|
|
106
|
+
| `+/-` | Expand/collapse preview (1-10 lines) |
|
|
107
|
+
| `Space` | Toggle user-only filter |
|
|
108
|
+
| `Enter` | Fork at selected point |
|
|
109
|
+
| `Esc` | Back (list mode) / Exit |
|
|
110
|
+
|
|
111
|
+
## Command Line Options
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
sfork # Fork current session
|
|
115
|
+
sfork --list, -l # Show session list
|
|
116
|
+
sfork --session=<id> # Fork specific session
|
|
117
|
+
sfork --cwd=<path> # Specify working directory
|
|
118
|
+
sfork --terminal=<type> # Terminal: auto, iterm, terminal, vscode, cursor, kiro
|
|
119
|
+
sfork --help, -h # Show help
|
|
120
|
+
sfork --version, -v # Show version
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## How It Works
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
127
|
+
β Original Session β
|
|
128
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
|
|
129
|
+
β [0] You: "Help me build a REST API" β
|
|
130
|
+
β [1] Claude: "I'll help you create a REST API..." β
|
|
131
|
+
β [2] You: "Add authentication" βββ Fork Point β
|
|
132
|
+
β [3] Claude: "Let's add JWT authentication..." β
|
|
133
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
134
|
+
β
|
|
135
|
+
βΌ sfork (select message 2)
|
|
136
|
+
β
|
|
137
|
+
ββββββββββββββββββββ΄βββββββββββββββββββ
|
|
138
|
+
βΌ βΌ
|
|
139
|
+
βββββββββββββββββββββ βββββββββββββββββββββ
|
|
140
|
+
β Original Window β β New Terminal β
|
|
141
|
+
β (continues) β β (forked from β
|
|
142
|
+
β β β message 2) β
|
|
143
|
+
βββββββββββββββββββββ βββββββββββββββββββββ
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Requirements
|
|
147
|
+
|
|
148
|
+
- macOS (uses AppleScript for terminal control)
|
|
149
|
+
- Claude Code CLI
|
|
150
|
+
- Node.js 18+
|
|
151
|
+
|
|
152
|
+
## Documentation
|
|
153
|
+
|
|
154
|
+
https://claude-session-fork.vercel.app
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { render, Box, Text, useApp } from 'ink';
|
|
5
|
+
import { findAllSessions, parseSession, forkSession, launchClaudeSession, openTerminalWithUI, getProjectDir } from './session.js';
|
|
6
|
+
import { SessionList, MessageList } from './components.js';
|
|
7
|
+
// Parse CLI arguments
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const isInteractive = args.includes('--interactive');
|
|
10
|
+
const sessionArg = args.find(a => a.startsWith('--session='));
|
|
11
|
+
const cwdArg = args.find(a => a.startsWith('--cwd='));
|
|
12
|
+
const terminalArg = args.find(a => a.startsWith('--terminal='));
|
|
13
|
+
const showHelp = args.includes('--help') || args.includes('-h');
|
|
14
|
+
const showVersion = args.includes('--version') || args.includes('-v');
|
|
15
|
+
const showList = args.includes('--list') || args.includes('-l');
|
|
16
|
+
const cwd = cwdArg ? cwdArg.split('=')[1] : process.cwd();
|
|
17
|
+
const sessionId = sessionArg ? sessionArg.split('=')[1] : null;
|
|
18
|
+
const terminalType = terminalArg ? terminalArg.split('=')[1] : process.env.TERM_PROGRAM;
|
|
19
|
+
// Show help
|
|
20
|
+
if (showHelp) {
|
|
21
|
+
console.log(`
|
|
22
|
+
claude-session-fork (csfork/sfork) - Fork Claude Code sessions
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
csfork Fork current session (latest modified)
|
|
26
|
+
csfork --list Open session list to choose
|
|
27
|
+
csfork --session=<id> Fork specific session
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
--session=<id> Specify session ID
|
|
31
|
+
--list, -l Show session list to choose from
|
|
32
|
+
--cwd=<path> Working directory (default: current)
|
|
33
|
+
--terminal=<type> Terminal type: auto, iterm, terminal, vscode, cursor, kiro
|
|
34
|
+
-h, --help Show this help
|
|
35
|
+
-v, --version Show version
|
|
36
|
+
|
|
37
|
+
Controls:
|
|
38
|
+
ββ Navigate
|
|
39
|
+
Enter Select / Fork
|
|
40
|
+
+/- Expand/collapse message preview
|
|
41
|
+
Space Toggle user-only filter
|
|
42
|
+
Esc Back / Exit
|
|
43
|
+
|
|
44
|
+
Docs: https://claude-session-fork.vercel.app
|
|
45
|
+
`);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
// Show version
|
|
49
|
+
if (showVersion) {
|
|
50
|
+
console.log('1.0.0');
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
// Non-interactive mode: open UI in new terminal
|
|
54
|
+
if (!isInteractive) {
|
|
55
|
+
const sessions = findAllSessions(cwd);
|
|
56
|
+
if (sessions.length === 0) {
|
|
57
|
+
console.error('No sessions found for:', cwd);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
console.log(`Found ${sessions.length} session(s)`);
|
|
61
|
+
// --session=<id>: use specific session
|
|
62
|
+
// --list or -l: show session list
|
|
63
|
+
// default: use latest session (current session in Claude Code)
|
|
64
|
+
let targetSession;
|
|
65
|
+
if (sessionId) {
|
|
66
|
+
targetSession = sessionId;
|
|
67
|
+
console.log(`Using session: ${sessionId}`);
|
|
68
|
+
}
|
|
69
|
+
else if (showList) {
|
|
70
|
+
targetSession = '__list__';
|
|
71
|
+
console.log(`Opening session list...`);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Latest session = current session (most recently modified)
|
|
75
|
+
targetSession = sessions[0].id;
|
|
76
|
+
console.log(`Using current session: ${targetSession.slice(0, 8)}...`);
|
|
77
|
+
}
|
|
78
|
+
console.log(`Opening fork UI...`);
|
|
79
|
+
openTerminalWithUI(cwd, targetSession, terminalType);
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
function App() {
|
|
83
|
+
const { exit } = useApp();
|
|
84
|
+
const [step, setStep] = useState(sessionId && sessionId !== '__list__' ? 'messages' : 'sessions');
|
|
85
|
+
const [sessions, setSessions] = useState([]);
|
|
86
|
+
const [selectedSession, setSelectedSession] = useState(null);
|
|
87
|
+
const [messages, setMessages] = useState([]);
|
|
88
|
+
const [error, setError] = useState('');
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
// Clear screen
|
|
91
|
+
process.stdout.write('\x1Bc');
|
|
92
|
+
const allSessions = findAllSessions(cwd);
|
|
93
|
+
setSessions(allSessions);
|
|
94
|
+
// If session ID provided, go directly to messages
|
|
95
|
+
if (sessionId && sessionId !== '__list__') {
|
|
96
|
+
const projectDir = getProjectDir(cwd);
|
|
97
|
+
const file = `${projectDir}/${sessionId}.jsonl`;
|
|
98
|
+
try {
|
|
99
|
+
const msgs = parseSession(file);
|
|
100
|
+
if (msgs.length === 0) {
|
|
101
|
+
setError('No messages found in session');
|
|
102
|
+
setStep('error');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
setSelectedSession({ id: sessionId, file, mtime: new Date() });
|
|
106
|
+
setMessages(msgs);
|
|
107
|
+
setStep('messages');
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
setError(`Failed to parse session: ${e}`);
|
|
111
|
+
setStep('error');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (allSessions.length === 0) {
|
|
115
|
+
setError('No sessions found for this directory');
|
|
116
|
+
setStep('error');
|
|
117
|
+
}
|
|
118
|
+
}, []);
|
|
119
|
+
const handleSessionSelect = (session) => {
|
|
120
|
+
try {
|
|
121
|
+
const msgs = parseSession(session.file);
|
|
122
|
+
if (msgs.length === 0) {
|
|
123
|
+
setError('No messages found in session');
|
|
124
|
+
setStep('error');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
setSelectedSession(session);
|
|
128
|
+
setMessages(msgs);
|
|
129
|
+
setStep('messages');
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
setError(`Failed to parse session: ${e}`);
|
|
133
|
+
setStep('error');
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const handleMessageSelect = (uuid) => {
|
|
137
|
+
if (!selectedSession)
|
|
138
|
+
return;
|
|
139
|
+
setStep('forking');
|
|
140
|
+
setTimeout(() => {
|
|
141
|
+
try {
|
|
142
|
+
process.stdout.write('\x1Bc');
|
|
143
|
+
const newId = forkSession(selectedSession.file, uuid);
|
|
144
|
+
launchClaudeSession(newId, cwd, terminalType);
|
|
145
|
+
setStep('done');
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
exit();
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}, 100);
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
setError(`Failed to fork: ${e}`);
|
|
153
|
+
setStep('error');
|
|
154
|
+
}
|
|
155
|
+
}, 100);
|
|
156
|
+
};
|
|
157
|
+
const handleBack = () => {
|
|
158
|
+
// Only allow back if we started from session list
|
|
159
|
+
if (!sessionId || sessionId === '__list__') {
|
|
160
|
+
setStep('sessions');
|
|
161
|
+
setSelectedSession(null);
|
|
162
|
+
setMessages([]);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const handleExit = () => {
|
|
166
|
+
process.stdout.write('\x1Bc');
|
|
167
|
+
exit();
|
|
168
|
+
process.exit(0);
|
|
169
|
+
};
|
|
170
|
+
if (step === 'error') {
|
|
171
|
+
return (_jsxs(Box, { paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Text, { dimColor: true, children: "Press Esc to exit" })] }));
|
|
172
|
+
}
|
|
173
|
+
if (step === 'forking') {
|
|
174
|
+
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "yellow", children: "Creating fork..." }) }));
|
|
175
|
+
}
|
|
176
|
+
if (step === 'done') {
|
|
177
|
+
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "green", children: "\u2713 Fork created, launching Claude..." }) }));
|
|
178
|
+
}
|
|
179
|
+
if (step === 'sessions') {
|
|
180
|
+
return (_jsx(SessionList, { sessions: sessions, onSelect: handleSessionSelect, onExit: handleExit }));
|
|
181
|
+
}
|
|
182
|
+
if (step === 'messages') {
|
|
183
|
+
// Allow back only if we came from session list
|
|
184
|
+
const canGoBack = !sessionId || sessionId === '__list__';
|
|
185
|
+
return (_jsx(MessageList, { messages: messages, onSelect: handleMessageSelect, onBack: canGoBack ? handleBack : undefined, onExit: handleExit }));
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
render(_jsx(App, {}));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Message, Session } from './session.js';
|
|
2
|
+
interface SessionListProps {
|
|
3
|
+
sessions: Session[];
|
|
4
|
+
onSelect: (session: Session) => void;
|
|
5
|
+
onExit: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function SessionList({ sessions, onSelect, onExit }: SessionListProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
interface MessageListProps {
|
|
9
|
+
messages: Message[];
|
|
10
|
+
onSelect: (uuid: string) => void;
|
|
11
|
+
onBack?: () => void;
|
|
12
|
+
onExit: () => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function MessageList({ messages: allMessages, onSelect, onBack, onExit }: MessageListProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
|
+
export function SessionList({ sessions, onSelect, onExit }) {
|
|
5
|
+
const { stdout } = useStdout();
|
|
6
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
7
|
+
const terminalHeight = stdout?.rows || 24;
|
|
8
|
+
const maxVisible = Math.max(3, terminalHeight - 8);
|
|
9
|
+
useInput((input, key) => {
|
|
10
|
+
if (key.escape) {
|
|
11
|
+
onExit();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (key.upArrow) {
|
|
15
|
+
setSelectedIndex(i => Math.max(0, i - 1));
|
|
16
|
+
}
|
|
17
|
+
if (key.downArrow) {
|
|
18
|
+
setSelectedIndex(i => Math.min(sessions.length - 1, i + 1));
|
|
19
|
+
}
|
|
20
|
+
if (key.return && sessions.length > 0) {
|
|
21
|
+
onSelect(sessions[selectedIndex]);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
if (sessions.length === 0) {
|
|
25
|
+
return (_jsxs(Box, { paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "red", children: "No sessions found for this directory" }), _jsx(Text, { dimColor: true, children: "Press Esc to exit" })] }));
|
|
26
|
+
}
|
|
27
|
+
// Calculate visible window
|
|
28
|
+
let startIdx = 0;
|
|
29
|
+
let endIdx = sessions.length;
|
|
30
|
+
if (sessions.length > maxVisible) {
|
|
31
|
+
const halfWindow = Math.floor(maxVisible / 2);
|
|
32
|
+
startIdx = Math.max(0, selectedIndex - halfWindow);
|
|
33
|
+
endIdx = Math.min(sessions.length, startIdx + maxVisible);
|
|
34
|
+
if (endIdx === sessions.length) {
|
|
35
|
+
startIdx = Math.max(0, sessions.length - maxVisible);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const visibleSessions = sessions.slice(startIdx, endIdx);
|
|
39
|
+
const formatTime = (date) => {
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const diff = now.getTime() - date.getTime();
|
|
42
|
+
const minutes = Math.floor(diff / 60000);
|
|
43
|
+
const hours = Math.floor(diff / 3600000);
|
|
44
|
+
const days = Math.floor(diff / 86400000);
|
|
45
|
+
if (minutes < 1)
|
|
46
|
+
return 'just now';
|
|
47
|
+
if (minutes < 60)
|
|
48
|
+
return `${minutes}m ago`;
|
|
49
|
+
if (hours < 24)
|
|
50
|
+
return `${hours}h ago`;
|
|
51
|
+
if (days < 7)
|
|
52
|
+
return `${days}d ago`;
|
|
53
|
+
return date.toLocaleDateString();
|
|
54
|
+
};
|
|
55
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { marginBottom: 0, children: [_jsx(Text, { bold: true, color: "blue", children: "Sessions" }), _jsxs(Text, { dimColor: true, children: [" (", selectedIndex + 1, "/", sessions.length, ")"] })] }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Select a session to fork from" }) }), startIdx > 0 && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [" \u2191 ", startIdx, " more above"] }) })), visibleSessions.map((session, visibleIdx) => {
|
|
56
|
+
const actualIndex = startIdx + visibleIdx;
|
|
57
|
+
const isSelected = actualIndex === selectedIndex;
|
|
58
|
+
const isCurrent = actualIndex === 0;
|
|
59
|
+
const preview = session.firstMessage || `Session ${session.id.slice(0, 8)}...`;
|
|
60
|
+
const time = formatTime(session.mtime);
|
|
61
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, children: isSelected ? 'β― ' : ' ' }), _jsxs(Text, { color: isSelected ? 'white' : 'gray', bold: isSelected, children: [preview.slice(0, 60), preview.length > 60 ? '...' : ''] }), isCurrent && _jsx(Text, { color: "yellow", italic: true, children: " (latest)" })] }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: [" ", time, " \u00B7 ", session.id.slice(0, 8)] }) })] }, session.id));
|
|
62
|
+
}), endIdx < sessions.length && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [" \u2193 ", sessions.length - endIdx, " more below"] }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 Move \u00B7 Enter Select \u00B7 Esc Exit" }) })] }));
|
|
63
|
+
}
|
|
64
|
+
export function MessageList({ messages: allMessages, onSelect, onBack, onExit }) {
|
|
65
|
+
const { stdout } = useStdout();
|
|
66
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
67
|
+
const [expandLines, setExpandLines] = useState(1);
|
|
68
|
+
const [showOnlyUser, setShowOnlyUser] = useState(false);
|
|
69
|
+
// Filter messages based on mode
|
|
70
|
+
const messages = showOnlyUser
|
|
71
|
+
? allMessages.filter(m => m.type === 'user')
|
|
72
|
+
: allMessages;
|
|
73
|
+
// Reset selection when filter changes
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
setSelectedIndex(Math.min(selectedIndex, Math.max(0, messages.length - 1)));
|
|
76
|
+
}, [showOnlyUser, messages.length]);
|
|
77
|
+
// Calculate layout
|
|
78
|
+
const terminalHeight = stdout?.rows || 24;
|
|
79
|
+
const layoutLines = 5;
|
|
80
|
+
const reservedForExpand = 5;
|
|
81
|
+
const availableLines = terminalHeight - layoutLines - reservedForExpand;
|
|
82
|
+
const maxVisible = Math.max(3, availableLines);
|
|
83
|
+
useInput((input, key) => {
|
|
84
|
+
if (key.escape) {
|
|
85
|
+
if (onBack) {
|
|
86
|
+
onBack();
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
onExit();
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Space: toggle user-only filter
|
|
94
|
+
if (input === ' ') {
|
|
95
|
+
setShowOnlyUser(v => !v);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// +/= : increase expand lines
|
|
99
|
+
if (input === '=' || input === '+') {
|
|
100
|
+
setExpandLines(n => Math.min(n + 1, 10));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// - : decrease expand lines
|
|
104
|
+
if (input === '-') {
|
|
105
|
+
setExpandLines(n => Math.max(n - 1, 1));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (key.upArrow) {
|
|
109
|
+
setSelectedIndex(i => Math.max(0, i - 1));
|
|
110
|
+
}
|
|
111
|
+
if (key.downArrow) {
|
|
112
|
+
setSelectedIndex(i => Math.min(messages.length - 1, i + 1));
|
|
113
|
+
}
|
|
114
|
+
if (key.return && messages.length > 0) {
|
|
115
|
+
onSelect(messages[selectedIndex].uuid);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
if (messages.length === 0) {
|
|
119
|
+
return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "red", children: "No messages to display" }) }));
|
|
120
|
+
}
|
|
121
|
+
// Calculate visible window
|
|
122
|
+
const totalMessages = messages.length;
|
|
123
|
+
let startIdx = 0;
|
|
124
|
+
let endIdx = totalMessages;
|
|
125
|
+
if (totalMessages > maxVisible) {
|
|
126
|
+
const halfWindow = Math.floor(maxVisible / 2);
|
|
127
|
+
startIdx = Math.max(0, selectedIndex - halfWindow);
|
|
128
|
+
endIdx = Math.min(totalMessages, startIdx + maxVisible);
|
|
129
|
+
if (endIdx === totalMessages) {
|
|
130
|
+
startIdx = Math.max(0, totalMessages - maxVisible);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const visibleMessages = messages.slice(startIdx, endIdx);
|
|
134
|
+
// Helper to get content lines
|
|
135
|
+
const getContentLines = (content, maxLen, numLines) => {
|
|
136
|
+
const lines = [];
|
|
137
|
+
let remaining = content;
|
|
138
|
+
for (let i = 0; i < numLines && remaining.length > 0; i++) {
|
|
139
|
+
lines.push(remaining.slice(0, maxLen));
|
|
140
|
+
remaining = remaining.slice(maxLen);
|
|
141
|
+
}
|
|
142
|
+
if (remaining.length > 0 && lines.length > 0) {
|
|
143
|
+
lines[lines.length - 1] = lines[lines.length - 1].slice(0, -3) + '...';
|
|
144
|
+
}
|
|
145
|
+
return lines;
|
|
146
|
+
};
|
|
147
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { marginBottom: 0, children: [_jsx(Text, { bold: true, color: "blue", children: "Fork" }), _jsxs(Text, { dimColor: true, children: [" (", selectedIndex + 1, "/", totalMessages, ")"] }), _jsxs(Text, { color: "cyan", children: [" [", expandLines, "\u884C]"] }), showOnlyUser && _jsx(Text, { color: "yellow", children: " [User Only]" })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Select the point to fork from", onBack ? ' Β· Esc to go back' : ''] }) }), startIdx > 0 && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [" \u2191 ", startIdx, " more above"] }) })), visibleMessages.flatMap((msg, visibleIdx) => {
|
|
148
|
+
const actualIndex = startIdx + visibleIdx;
|
|
149
|
+
const isSelected = actualIndex === selectedIndex;
|
|
150
|
+
const isCurrent = actualIndex === messages.length - 1;
|
|
151
|
+
const isUser = msg.type === 'user';
|
|
152
|
+
const maxLen = 65;
|
|
153
|
+
const numLines = isSelected ? expandLines : 1;
|
|
154
|
+
const contentLines = getContentLines(msg.content, maxLen, numLines);
|
|
155
|
+
return contentLines.map((line, lineIdx) => {
|
|
156
|
+
const isFirstLine = lineIdx === 0;
|
|
157
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, children: isFirstLine ? (isSelected ? 'β― ' : ' ') : ' ' }), isFirstLine ? (_jsx(Text, { color: isUser ? 'green' : 'magenta', bold: true, inverse: isSelected, children: isUser ? ' You ' : ' AI ' })) : (_jsx(Text, { children: " " })), _jsx(Text, { children: " " }), isFirstLine ? (_jsx(Text, { color: msg.hasCodeChanges ? 'yellow' : 'gray', children: msg.hasCodeChanges ? 'β ' : ' ' })) : (_jsx(Text, { children: " " })), _jsx(Text, { color: isSelected ? 'white' : 'gray', bold: isSelected && isFirstLine, children: line }), isFirstLine && isCurrent && (_jsx(Text, { color: "yellow", italic: true, children: " (current)" }))] }, `${actualIndex}-${lineIdx}`));
|
|
158
|
+
});
|
|
159
|
+
}), endIdx < totalMessages && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [" \u2193 ", totalMessages - endIdx, " more below"] }) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["\u2191\u2193 Move \u00B7 +/- Lines \u00B7 Space Filter \u00B7 Enter Fork \u00B7 Esc ", onBack ? 'Back' : 'Exit'] }) })] }));
|
|
160
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface Message {
|
|
2
|
+
uuid: string;
|
|
3
|
+
type: 'user' | 'assistant';
|
|
4
|
+
content: string;
|
|
5
|
+
hasCodeChanges: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface Session {
|
|
8
|
+
id: string;
|
|
9
|
+
file: string;
|
|
10
|
+
mtime: Date;
|
|
11
|
+
firstMessage?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function getProjectDir(dir?: string): string;
|
|
14
|
+
export declare function findLatestSession(dir?: string): Session | null;
|
|
15
|
+
export declare function findAllSessions(dir?: string): Session[];
|
|
16
|
+
export declare function parseSession(sessionFile: string): Message[];
|
|
17
|
+
export declare function forkSession(sessionFile: string, forkUuid: string): string;
|
|
18
|
+
export declare function launchClaudeSession(sessionId: string, cwd: string, terminal?: string): void;
|
|
19
|
+
export declare function openTerminalWithUI(cwd: string, sessionId: string, terminal?: string): void;
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import { spawn, execSync } from 'child_process';
|
|
5
|
+
// Patterns to filter out system messages
|
|
6
|
+
const filterPatterns = [
|
|
7
|
+
/^Caveat:/,
|
|
8
|
+
/^<bash-input>/,
|
|
9
|
+
/^<bash-stdout>/,
|
|
10
|
+
/^<bash-stderr>/,
|
|
11
|
+
/^<system-reminder>/,
|
|
12
|
+
/^\[Request interrupted/,
|
|
13
|
+
/^ERROR\s/,
|
|
14
|
+
/^!\s*sfork/,
|
|
15
|
+
/^Last login:/,
|
|
16
|
+
];
|
|
17
|
+
function getProjectHash(dir) {
|
|
18
|
+
return dir.replace(/^\//g, '-').replace(/\//g, '-');
|
|
19
|
+
}
|
|
20
|
+
export function getProjectDir(dir = process.cwd()) {
|
|
21
|
+
const projectHash = getProjectHash(dir);
|
|
22
|
+
return path.join(os.homedir(), '.claude', 'projects', projectHash);
|
|
23
|
+
}
|
|
24
|
+
export function findLatestSession(dir = process.cwd()) {
|
|
25
|
+
const sessions = findAllSessions(dir);
|
|
26
|
+
return sessions[0] || null;
|
|
27
|
+
}
|
|
28
|
+
export function findAllSessions(dir = process.cwd()) {
|
|
29
|
+
const projectDir = getProjectDir(dir);
|
|
30
|
+
if (!fs.existsSync(projectDir)) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return fs.readdirSync(projectDir)
|
|
34
|
+
.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'))
|
|
35
|
+
.map(f => {
|
|
36
|
+
const filePath = path.join(projectDir, f);
|
|
37
|
+
const id = f.replace('.jsonl', '');
|
|
38
|
+
const stat = fs.statSync(filePath);
|
|
39
|
+
// Get first user message as preview
|
|
40
|
+
let firstMessage = '';
|
|
41
|
+
try {
|
|
42
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
43
|
+
for (const line of content.split('\n')) {
|
|
44
|
+
if (!line)
|
|
45
|
+
continue;
|
|
46
|
+
try {
|
|
47
|
+
const data = JSON.parse(line);
|
|
48
|
+
if (data.type === 'user' && data.message) {
|
|
49
|
+
const msg = typeof data.message.content === 'string'
|
|
50
|
+
? data.message.content
|
|
51
|
+
: data.message.content?.filter?.((c) => c.type === 'text')?.map?.((c) => c.text)?.join(' ') || '';
|
|
52
|
+
const cleaned = msg.replace(/^[\s\n\r]+/, '').replace(/\s+/g, ' ').trim();
|
|
53
|
+
if (cleaned && cleaned !== '[Request interrupted by user]') {
|
|
54
|
+
firstMessage = cleaned.slice(0, 60);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch { }
|
|
63
|
+
return {
|
|
64
|
+
id,
|
|
65
|
+
file: filePath,
|
|
66
|
+
mtime: stat.mtime,
|
|
67
|
+
firstMessage
|
|
68
|
+
};
|
|
69
|
+
})
|
|
70
|
+
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
71
|
+
}
|
|
72
|
+
export function parseSession(sessionFile) {
|
|
73
|
+
const messagesMap = new Map();
|
|
74
|
+
const codeChangesByUuid = new Set();
|
|
75
|
+
const content = fs.readFileSync(sessionFile, 'utf-8');
|
|
76
|
+
for (const line of content.split('\n')) {
|
|
77
|
+
if (!line)
|
|
78
|
+
continue;
|
|
79
|
+
try {
|
|
80
|
+
const data = JSON.parse(line);
|
|
81
|
+
// Track tool uses for code changes
|
|
82
|
+
if (data.type === 'assistant' && data.message?.content) {
|
|
83
|
+
const hasToolUse = Array.isArray(data.message.content) &&
|
|
84
|
+
data.message.content.some((c) => c.type === 'tool_use' &&
|
|
85
|
+
['Edit', 'Write', 'Bash', 'MultiEdit'].includes(c.name));
|
|
86
|
+
if (hasToolUse && data.uuid) {
|
|
87
|
+
codeChangesByUuid.add(data.uuid);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if ((data.type === 'user' || data.type === 'assistant') && data.uuid) {
|
|
91
|
+
let msg = '';
|
|
92
|
+
if (data.message) {
|
|
93
|
+
msg = typeof data.message.content === 'string'
|
|
94
|
+
? data.message.content
|
|
95
|
+
: data.message.content?.filter?.((c) => c.type === 'text')?.map?.((c) => c.text)?.join(' ') || '';
|
|
96
|
+
}
|
|
97
|
+
// Clean up message
|
|
98
|
+
msg = msg.replace(/^[\s\n\r]+/, '').replace(/\s+/g, ' ').trim();
|
|
99
|
+
// Filter out system messages
|
|
100
|
+
const shouldFilter = filterPatterns.some(pattern => pattern.test(msg));
|
|
101
|
+
if (msg && !shouldFilter && msg !== '[Request interrupted by user]' && msg !== 'No response requested.') {
|
|
102
|
+
if (!messagesMap.has(data.uuid)) {
|
|
103
|
+
messagesMap.set(data.uuid, {
|
|
104
|
+
uuid: data.uuid,
|
|
105
|
+
type: data.type,
|
|
106
|
+
content: msg,
|
|
107
|
+
hasCodeChanges: false
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch { }
|
|
114
|
+
}
|
|
115
|
+
// Mark code changes
|
|
116
|
+
for (const uuid of codeChangesByUuid) {
|
|
117
|
+
const msg = messagesMap.get(uuid);
|
|
118
|
+
if (msg) {
|
|
119
|
+
msg.hasCodeChanges = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return Array.from(messagesMap.values());
|
|
123
|
+
}
|
|
124
|
+
export function forkSession(sessionFile, forkUuid) {
|
|
125
|
+
const projectDir = path.dirname(sessionFile);
|
|
126
|
+
const newId = crypto.randomUUID();
|
|
127
|
+
const newFile = path.join(projectDir, `${newId}.jsonl`);
|
|
128
|
+
const content = fs.readFileSync(sessionFile, 'utf-8');
|
|
129
|
+
const lines = content.split('\n').filter(Boolean);
|
|
130
|
+
const newLines = [];
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
try {
|
|
133
|
+
const data = JSON.parse(line);
|
|
134
|
+
if (data.sessionId)
|
|
135
|
+
data.sessionId = newId;
|
|
136
|
+
newLines.push(JSON.stringify(data));
|
|
137
|
+
if (data.uuid === forkUuid)
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
newLines.push(line);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
fs.writeFileSync(newFile, newLines.join('\n') + '\n');
|
|
145
|
+
return newId;
|
|
146
|
+
}
|
|
147
|
+
export function launchClaudeSession(sessionId, cwd, terminal) {
|
|
148
|
+
const cmd = `cd '${cwd}' && claude --resume '${sessionId}'`;
|
|
149
|
+
const terminalLower = terminal?.toLowerCase() || '';
|
|
150
|
+
// VS Code / Cursor / Kiro - use code CLI to open terminal
|
|
151
|
+
if (terminalLower.includes('vscode') || terminalLower === 'code') {
|
|
152
|
+
execSync(`code --folder-uri "file://${cwd}" -r`);
|
|
153
|
+
// VS Code doesn't have a direct way to run command in terminal via CLI
|
|
154
|
+
// Copy command to clipboard and notify user
|
|
155
|
+
execSync(`echo "${cmd}" | pbcopy`);
|
|
156
|
+
console.log('Command copied to clipboard. Paste in VS Code terminal.');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (terminalLower.includes('cursor')) {
|
|
160
|
+
execSync(`cursor --folder-uri "file://${cwd}" -r`);
|
|
161
|
+
execSync(`echo "${cmd}" | pbcopy`);
|
|
162
|
+
console.log('Command copied to clipboard. Paste in Cursor terminal.');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (terminalLower.includes('kiro')) {
|
|
166
|
+
execSync(`kiro "${cwd}"`);
|
|
167
|
+
execSync(`echo "${cmd}" | pbcopy`);
|
|
168
|
+
console.log('Command copied to clipboard. Paste in Kiro terminal.');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// iTerm2
|
|
172
|
+
const isIterm = terminalLower === 'iterm' || terminalLower === 'iterm.app' ||
|
|
173
|
+
(terminalLower !== 'terminal' && terminalLower !== 'terminal.app' && fs.existsSync('/Applications/iTerm.app'));
|
|
174
|
+
if (isIterm) {
|
|
175
|
+
const script = `
|
|
176
|
+
tell application "iTerm"
|
|
177
|
+
activate
|
|
178
|
+
create window with default profile
|
|
179
|
+
tell current session of current window
|
|
180
|
+
write text "${cmd}"
|
|
181
|
+
end tell
|
|
182
|
+
end tell
|
|
183
|
+
`;
|
|
184
|
+
execSync(`osascript -e '${script}'`);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// Terminal.app
|
|
188
|
+
const script = `
|
|
189
|
+
tell application "Terminal"
|
|
190
|
+
activate
|
|
191
|
+
do script "${cmd}"
|
|
192
|
+
end tell
|
|
193
|
+
`;
|
|
194
|
+
execSync(`osascript -e '${script}'`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export function openTerminalWithUI(cwd, sessionId, terminal) {
|
|
198
|
+
const sforkPath = process.argv[1];
|
|
199
|
+
const cmd = `node '${sforkPath}' --interactive --session='${sessionId}' --cwd='${cwd}'`;
|
|
200
|
+
const fullCmd = `cd '${cwd}' && ${cmd}`;
|
|
201
|
+
const terminalLower = terminal?.toLowerCase() || '';
|
|
202
|
+
// VS Code / Cursor / Kiro - copy command to clipboard
|
|
203
|
+
if (terminalLower.includes('vscode') || terminalLower === 'code' ||
|
|
204
|
+
terminalLower.includes('cursor') || terminalLower.includes('kiro')) {
|
|
205
|
+
execSync(`echo "${fullCmd}" | pbcopy`);
|
|
206
|
+
console.log('Command copied to clipboard. Open a terminal and paste to run.');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// iTerm2
|
|
210
|
+
const isIterm = terminalLower === 'iterm' || terminalLower === 'iterm.app' ||
|
|
211
|
+
(terminalLower !== 'terminal' && terminalLower !== 'terminal.app' && fs.existsSync('/Applications/iTerm.app'));
|
|
212
|
+
if (isIterm) {
|
|
213
|
+
const script = `
|
|
214
|
+
tell application "iTerm"
|
|
215
|
+
activate
|
|
216
|
+
tell current window
|
|
217
|
+
create tab with default profile
|
|
218
|
+
tell current session
|
|
219
|
+
write text "${fullCmd}"
|
|
220
|
+
end tell
|
|
221
|
+
end tell
|
|
222
|
+
end tell
|
|
223
|
+
`;
|
|
224
|
+
spawn('osascript', ['-e', script], { detached: true, stdio: 'ignore' }).unref();
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
// Terminal.app
|
|
228
|
+
const script = `
|
|
229
|
+
tell application "Terminal"
|
|
230
|
+
activate
|
|
231
|
+
tell application "System Events" to keystroke "t" using command down
|
|
232
|
+
delay 0.3
|
|
233
|
+
do script "${fullCmd}" in front window
|
|
234
|
+
end tell
|
|
235
|
+
`;
|
|
236
|
+
spawn('osascript', ['-e', script], { detached: true, stdio: 'ignore' }).unref();
|
|
237
|
+
}
|
|
238
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-session-fork",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Fork Claude Code sessions at any conversation point",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-session-fork": "./dist/cli.js",
|
|
8
|
+
"csfork": "./dist/cli.js",
|
|
9
|
+
"sfork": "./dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "node dist/cli.js",
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc --watch",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"postinstall": "node scripts/postinstall.js || true"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"scripts/postinstall.js"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"ink": "^5.0.1",
|
|
24
|
+
"react": "^18.3.1"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.10.10",
|
|
28
|
+
"typescript": "^5.7.3"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"claude",
|
|
32
|
+
"claude-code",
|
|
33
|
+
"fork",
|
|
34
|
+
"session",
|
|
35
|
+
"cli",
|
|
36
|
+
"ai",
|
|
37
|
+
"conversation"
|
|
38
|
+
],
|
|
39
|
+
"author": "duo121",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/duo121/claude-session-fork.git"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://claude-session-fork.vercel.app",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/duo121/claude-session-fork/issues"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Post-install message
|
|
4
|
+
console.log(`
|
|
5
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
6
|
+
β β
|
|
7
|
+
β claude-session-fork installed! π β
|
|
8
|
+
β β
|
|
9
|
+
β Usage: β
|
|
10
|
+
β cd your-project β
|
|
11
|
+
β csfork β
|
|
12
|
+
β β
|
|
13
|
+
β Commands: csfork, sfork, claude-session-fork β
|
|
14
|
+
β Docs: https://claude-session-fork.vercel.app β
|
|
15
|
+
β β
|
|
16
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
17
|
+
`);
|