@vitorcen/context-resume 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 +44 -0
- package/dist/adapters/index.js +170 -0
- package/dist/index.js +18 -0
- package/dist/ui/app.js +74 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 vitorcen
|
|
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,44 @@
|
|
|
1
|
+
# Context Resume CLI
|
|
2
|
+
|
|
3
|
+
Context Resume lets you quickly resume work across Claude Code (`~/.claude`) and Codex CLI (`~/.codex`) by listing recent sessions for the current working directory, showing a detailed prompt history preview, and printing a ready-to-use bilingual (English/Chinese) resume prompt.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- **Dual-Panel View**: Split view for Claude and Codex sessions; use `TAB` to switch.
|
|
7
|
+
- **Detailed Preview**: Shows a list of user prompts from the selected session (truncated to 50 chars) at the top.
|
|
8
|
+
- **Configurable Limit**: Use `-n <count>` to control how many sessions to load per source.
|
|
9
|
+
- **Bilingual Output**: Prints both English and Chinese prompts pointing to the session file.
|
|
10
|
+
- **Privacy First**: Works entirely locally; no network calls.
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
- Node.js 18+ (tested with ESM build).
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
```bash
|
|
17
|
+
npm install
|
|
18
|
+
npm run build
|
|
19
|
+
npm link # optional, to install the global `context` command
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
From any project directory you want to resume:
|
|
24
|
+
```bash
|
|
25
|
+
context # Load default 10 sessions per source
|
|
26
|
+
context -n 20 # Load 20 sessions per source
|
|
27
|
+
```
|
|
28
|
+
- Use **TAB** to switch between Claude and Codex panels.
|
|
29
|
+
- Use **Arrow Keys** to select a session.
|
|
30
|
+
- Preview at the top shows the sequence of user prompts.
|
|
31
|
+
- Press **Enter** to print the resume prompts (with absolute paths) and exit.
|
|
32
|
+
|
|
33
|
+
## How it works
|
|
34
|
+
- **Claude**: Reads `~/.claude/projects/<encoded-path>/*.jsonl`.
|
|
35
|
+
- **Codex**: Scans `~/.codex/sessions/**/*.jsonl`, filters by `cwd` metadata.
|
|
36
|
+
- **Prompt Extraction**: Parses the session files to extract user inputs for the preview, giving you a quick summary of "what was I doing?".
|
|
37
|
+
|
|
38
|
+
## Project layout
|
|
39
|
+
- `src/index.tsx` – Entry point, handles CLI arguments.
|
|
40
|
+
- `src/ui/app.tsx` – Ink UI with split panels and preview.
|
|
41
|
+
- `src/adapters/index.ts` – File parsers and prompt extractors.
|
|
42
|
+
|
|
43
|
+
## Limitations
|
|
44
|
+
- Codex scanning involves globbing which might be slow on very large histories.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
// --- Claude Adapter ---
|
|
6
|
+
function getClaudeEncodedPath(projectPath) {
|
|
7
|
+
// Claude encodes paths by replacing / and . with -
|
|
8
|
+
// e.g. /home/user/project -> -home-user-project
|
|
9
|
+
// e.g. /path/v1.0 -> -path-v1-0
|
|
10
|
+
return projectPath.replace(/[\/\.]/g, '-');
|
|
11
|
+
}
|
|
12
|
+
export async function getClaudeSessions(cwd, limit = 10) {
|
|
13
|
+
const homeDir = os.homedir();
|
|
14
|
+
const encodedPath = getClaudeEncodedPath(cwd);
|
|
15
|
+
const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
|
|
16
|
+
if (!fs.existsSync(projectDir)) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
const files = glob.sync('*.jsonl', { cwd: projectDir });
|
|
20
|
+
// Sort by modification time (descending)
|
|
21
|
+
const sortedFiles = files.map((file) => {
|
|
22
|
+
const filePath = path.join(projectDir, file);
|
|
23
|
+
const stats = fs.statSync(filePath);
|
|
24
|
+
return { file, mtime: stats.mtimeMs, filePath };
|
|
25
|
+
})
|
|
26
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
27
|
+
.slice(0, limit);
|
|
28
|
+
const sessions = [];
|
|
29
|
+
for (const { filePath, file } of sortedFiles) {
|
|
30
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
31
|
+
const lines = content.split('\n').filter(line => line.trim() !== '');
|
|
32
|
+
if (lines.length === 0)
|
|
33
|
+
continue;
|
|
34
|
+
let firstUserMessage = 'New Session';
|
|
35
|
+
let fullContent = '';
|
|
36
|
+
const userPrompts = [];
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
try {
|
|
39
|
+
const data = JSON.parse(line);
|
|
40
|
+
// Try to find the first user message for the title
|
|
41
|
+
if (data.type === 'user' && data.message?.content) {
|
|
42
|
+
const text = typeof data.message.content === 'string'
|
|
43
|
+
? data.message.content
|
|
44
|
+
: (Array.isArray(data.message.content) ? data.message.content.map((c) => c.text || '').join(' ') : '');
|
|
45
|
+
userPrompts.push(text);
|
|
46
|
+
if (firstUserMessage === 'New Session') {
|
|
47
|
+
firstUserMessage = text;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Accumulate content for preview (simplified)
|
|
51
|
+
if (data.type === 'user' && data.message?.content) {
|
|
52
|
+
const text = typeof data.message.content === 'string' ? data.message.content : 'User Input...';
|
|
53
|
+
fullContent += `User: ${text}\n`;
|
|
54
|
+
}
|
|
55
|
+
else if (data.message?.content && data.type !== 'user') { // Assistant or other
|
|
56
|
+
const text = typeof data.message.content === 'string' ? data.message.content : 'Assistant Output...';
|
|
57
|
+
fullContent += `Assistant: ${text}\n`;
|
|
58
|
+
}
|
|
59
|
+
else if (data.display) {
|
|
60
|
+
fullContent += `System: ${data.display}\n`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
// ignore parse errors
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Create preview: Start ... End
|
|
68
|
+
const preview = createPreview(fullContent);
|
|
69
|
+
const stats = fs.statSync(filePath);
|
|
70
|
+
sessions.push({
|
|
71
|
+
id: path.basename(file, '.jsonl'),
|
|
72
|
+
title: firstUserMessage.slice(0, 50) + (firstUserMessage.length > 50 ? '...' : ''),
|
|
73
|
+
preview,
|
|
74
|
+
userPrompts,
|
|
75
|
+
timestamp: stats.mtimeMs,
|
|
76
|
+
source: 'claude',
|
|
77
|
+
path: filePath
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return sessions;
|
|
81
|
+
}
|
|
82
|
+
// --- Codex Adapter ---
|
|
83
|
+
export async function getCodexSessions(cwd, limit = 10) {
|
|
84
|
+
const homeDir = os.homedir();
|
|
85
|
+
const sessionsDir = path.join(homeDir, '.codex', 'sessions');
|
|
86
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
// We need to search deeply because of the date structure YYYY/MM/DD
|
|
90
|
+
// Optimization: Start searching from current year/month/day backwards could be better but glob is easier for MVP
|
|
91
|
+
// To avoid scanning ALL history, maybe we limit glob depth or just look at recent folders?
|
|
92
|
+
// For now, let's just glob all .jsonl and filter by CWD. In production, this should be optimized.
|
|
93
|
+
// Actually, reading all files to check CWD is slow.
|
|
94
|
+
// Strategy: Walk directories from today backwards?
|
|
95
|
+
// Let's use a simpler approach: glob all jsonl in the last 3 levels of directories
|
|
96
|
+
// Note: This might be slow if history is huge.
|
|
97
|
+
const files = glob.sync('**/*.jsonl', { cwd: sessionsDir, absolute: true });
|
|
98
|
+
// Filter by CWD and sort by time
|
|
99
|
+
const relevantFiles = [];
|
|
100
|
+
for (const filePath of files) {
|
|
101
|
+
// We need to peek at the first line to check CWD
|
|
102
|
+
try {
|
|
103
|
+
const fd = fs.openSync(filePath, 'r');
|
|
104
|
+
const buffer = Buffer.alloc(4096); // Read first 4KB
|
|
105
|
+
const bytesRead = fs.readSync(fd, buffer, 0, 4096, 0);
|
|
106
|
+
fs.closeSync(fd);
|
|
107
|
+
const chunk = buffer.toString('utf-8', 0, bytesRead);
|
|
108
|
+
const firstLine = chunk.split('\n')[0];
|
|
109
|
+
if (firstLine) {
|
|
110
|
+
const meta = JSON.parse(firstLine);
|
|
111
|
+
if (meta.type === 'session_meta' && meta.payload?.cwd === cwd) {
|
|
112
|
+
const stats = fs.statSync(filePath);
|
|
113
|
+
relevantFiles.push({ filePath, mtime: stats.mtimeMs });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
// ignore
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const sortedFiles = relevantFiles
|
|
122
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
123
|
+
.slice(0, limit);
|
|
124
|
+
const sessions = [];
|
|
125
|
+
for (const { filePath, mtime } of sortedFiles) {
|
|
126
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
127
|
+
const lines = content.split('\n').filter(line => line.trim() !== '');
|
|
128
|
+
let firstUserMessage = 'New Session';
|
|
129
|
+
let fullContent = '';
|
|
130
|
+
const userPrompts = [];
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
try {
|
|
133
|
+
const data = JSON.parse(line);
|
|
134
|
+
// Codex User Input
|
|
135
|
+
if (data.type === 'response_item' && data.payload?.type === 'message' && data.payload.role === 'user') {
|
|
136
|
+
const contentArr = data.payload.content || [];
|
|
137
|
+
const text = contentArr.find((c) => c.type === 'input_text')?.text || '';
|
|
138
|
+
userPrompts.push(text);
|
|
139
|
+
if (firstUserMessage === 'New Session' && text) {
|
|
140
|
+
firstUserMessage = text;
|
|
141
|
+
}
|
|
142
|
+
fullContent += `User: ${text}\n`;
|
|
143
|
+
}
|
|
144
|
+
// Codex Assistant Response (simplified logic)
|
|
145
|
+
// Codex format is a bit complex with chunks, we might just grab user inputs for now or simple responses if easily identifiable
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
// ignore
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const preview = createPreview(fullContent);
|
|
152
|
+
sessions.push({
|
|
153
|
+
id: path.basename(filePath, '.jsonl'),
|
|
154
|
+
title: firstUserMessage.slice(0, 50) + (firstUserMessage.length > 50 ? '...' : ''),
|
|
155
|
+
preview,
|
|
156
|
+
userPrompts,
|
|
157
|
+
timestamp: mtime,
|
|
158
|
+
source: 'codex',
|
|
159
|
+
path: filePath
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return sessions;
|
|
163
|
+
}
|
|
164
|
+
function createPreview(content, maxLength = 500) {
|
|
165
|
+
if (content.length <= maxLength)
|
|
166
|
+
return content;
|
|
167
|
+
const start = content.slice(0, maxLength / 2);
|
|
168
|
+
const end = content.slice(content.length - (maxLength / 2));
|
|
169
|
+
return `${start}\n\n... [${content.length - maxLength} characters omitted] ...\n\n${end}`;
|
|
170
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { render } from 'ink';
|
|
5
|
+
import App from './ui/app.js';
|
|
6
|
+
const program = new Command();
|
|
7
|
+
program
|
|
8
|
+
.name('context')
|
|
9
|
+
.description('Context Resume CLI')
|
|
10
|
+
.version('1.0.0', '-v, --version');
|
|
11
|
+
program
|
|
12
|
+
.option('-n, --number <count>', 'Number of sessions to show per source (claude/codex)', '10')
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const limit = parseInt(options.number, 10) || 10;
|
|
16
|
+
render(_jsx(App, { cwd: cwd, limit: limit }));
|
|
17
|
+
});
|
|
18
|
+
program.parse(process.argv);
|
package/dist/ui/app.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useApp } from 'ink';
|
|
4
|
+
import SelectInput from 'ink-select-input';
|
|
5
|
+
import { getClaudeSessions, getCodexSessions } from '../adapters/index.js';
|
|
6
|
+
const App = ({ cwd, limit = 10 }) => {
|
|
7
|
+
const [claudeItems, setClaudeItems] = useState([]);
|
|
8
|
+
const [codexItems, setCodexItems] = useState([]);
|
|
9
|
+
const [activePanel, setActivePanel] = useState('claude');
|
|
10
|
+
const [activeClaudeItem, setActiveClaudeItem] = useState(null);
|
|
11
|
+
const [activeCodexItem, setActiveCodexItem] = useState(null);
|
|
12
|
+
const { exit } = useApp();
|
|
13
|
+
// Tab switching and Arrow Navigation
|
|
14
|
+
useInput((input, key) => {
|
|
15
|
+
if (key.tab) {
|
|
16
|
+
setActivePanel(prev => prev === 'claude' ? 'codex' : 'claude');
|
|
17
|
+
}
|
|
18
|
+
if (key.leftArrow && activePanel === 'codex') {
|
|
19
|
+
setActivePanel('claude');
|
|
20
|
+
}
|
|
21
|
+
if (key.rightArrow && activePanel === 'claude') {
|
|
22
|
+
setActivePanel('codex');
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const loadSessions = async () => {
|
|
27
|
+
const [claudeSessions, codexSessions] = await Promise.all([
|
|
28
|
+
getClaudeSessions(cwd, limit),
|
|
29
|
+
getCodexSessions(cwd, limit)
|
|
30
|
+
]);
|
|
31
|
+
// Sort by timestamp desc
|
|
32
|
+
const sortFn = (a, b) => b.timestamp - a.timestamp;
|
|
33
|
+
const cItems = claudeSessions.sort(sortFn).map(s => ({
|
|
34
|
+
label: `${s.title} (${new Date(s.timestamp).toLocaleDateString()})`,
|
|
35
|
+
value: s.id,
|
|
36
|
+
session: s
|
|
37
|
+
}));
|
|
38
|
+
const cxItems = codexSessions.sort(sortFn).map(s => ({
|
|
39
|
+
label: `${s.title} (${new Date(s.timestamp).toLocaleDateString()})`,
|
|
40
|
+
value: s.id,
|
|
41
|
+
session: s
|
|
42
|
+
}));
|
|
43
|
+
setClaudeItems(cItems);
|
|
44
|
+
setCodexItems(cxItems);
|
|
45
|
+
if (cItems.length > 0)
|
|
46
|
+
setActiveClaudeItem(cItems[0]);
|
|
47
|
+
if (cxItems.length > 0)
|
|
48
|
+
setActiveCodexItem(cxItems[0]);
|
|
49
|
+
};
|
|
50
|
+
loadSessions();
|
|
51
|
+
}, [cwd, limit]);
|
|
52
|
+
const handleSelect = (item) => {
|
|
53
|
+
const selectedItem = item;
|
|
54
|
+
const englishPrompt = `Here's a context file ${selectedItem.session.path} from the user's previous operations. Analyze what the user was doing. Then use TodoWrite to list what might be incomplete, and what needs to be done next (if mentioned in the context), otherwise wait for user instructions.`;
|
|
55
|
+
const chinesePrompt = `这里有份上下文 ${selectedItem.session.path} ,是用户曾经的操作。你分析下用户在做什么。然后用TodoWrite列出可能没做完的事情,和接下来要的事情(如果上下文中有提到),如果没有就等待用户指令。`;
|
|
56
|
+
const output = `\n\n${englishPrompt}\n\n${chinesePrompt}\n\n`;
|
|
57
|
+
process.stdout.write(output);
|
|
58
|
+
exit();
|
|
59
|
+
};
|
|
60
|
+
const handleHighlightClaude = (item) => setActiveClaudeItem(item);
|
|
61
|
+
const handleHighlightCodex = (item) => setActiveCodexItem(item);
|
|
62
|
+
// Get current preview prompts
|
|
63
|
+
const currentItem = activePanel === 'claude' ? activeClaudeItem : activeCodexItem;
|
|
64
|
+
// Filter out empty prompts
|
|
65
|
+
const prompts = (currentItem?.session.userPrompts || []).filter(p => p && p.trim().length > 0);
|
|
66
|
+
// Truncate prompts logic
|
|
67
|
+
const previewText = prompts.map((p, i) => {
|
|
68
|
+
const clean = p.replace(/\n/g, ' ').trim();
|
|
69
|
+
const truncated = clean.length > 50 ? clean.slice(0, 50) + '...' : clean;
|
|
70
|
+
return `${i + 1}. ${truncated}`;
|
|
71
|
+
}).join('\n');
|
|
72
|
+
return (_jsxs(Box, { flexDirection: "column", height: 35, children: [_jsxs(Box, { height: 12, borderStyle: "single", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, underline: true, children: "Preview (User Prompts)" }), _jsx(Text, { children: previewText || 'Select a session to view prompts' })] }), _jsxs(Box, { flexDirection: "row", height: 20, children: [_jsxs(Box, { width: "50%", borderStyle: activePanel === 'claude' ? 'double' : 'single', flexDirection: "column", borderColor: activePanel === 'claude' ? 'green' : 'white', children: [_jsx(Text, { bold: true, underline: true, color: activePanel === 'claude' ? 'green' : 'white', children: "Claude Sessions" }), claudeItems.length === 0 ? (_jsx(Text, { children: "No sessions found." })) : (_jsx(SelectInput, { items: claudeItems, onSelect: handleSelect, onHighlight: handleHighlightClaude, isFocused: activePanel === 'claude' }))] }), _jsxs(Box, { width: "50%", borderStyle: activePanel === 'codex' ? 'double' : 'single', flexDirection: "column", borderColor: activePanel === 'codex' ? 'green' : 'white', children: [_jsx(Text, { bold: true, underline: true, color: activePanel === 'codex' ? 'green' : 'white', children: "Codex Sessions" }), codexItems.length === 0 ? (_jsx(Text, { children: "No sessions found." })) : (_jsx(SelectInput, { items: codexItems, onSelect: handleSelect, onHighlight: handleHighlightCodex, isFocused: activePanel === 'codex' }))] })] }), _jsx(Box, { marginTop: 0, children: _jsx(Text, { dimColor: true, children: "TAB/Arrows: Switch Panel | ENTER: Select | ESC: Exit" }) })] }));
|
|
73
|
+
};
|
|
74
|
+
export default App;
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vitorcen/context-resume",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Context Resume CLI - Browse and mutually restore the conversation history of Claude Code and Codex",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"context": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "tsc && node dist/index.js",
|
|
13
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"claude",
|
|
23
|
+
"codex",
|
|
24
|
+
"cli",
|
|
25
|
+
"context",
|
|
26
|
+
"history",
|
|
27
|
+
"resume",
|
|
28
|
+
"conversation",
|
|
29
|
+
"terminal"
|
|
30
|
+
],
|
|
31
|
+
"author": "vitorcen",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"commander": "^14.0.2",
|
|
38
|
+
"date-fns": "^4.1.0",
|
|
39
|
+
"glob": "^13.0.0",
|
|
40
|
+
"ink": "^6.5.1",
|
|
41
|
+
"ink-box": "^1.0.0",
|
|
42
|
+
"ink-select-input": "^6.2.0",
|
|
43
|
+
"react": "^19.2.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/glob": "^8.1.0",
|
|
47
|
+
"@types/node": "^24.10.1",
|
|
48
|
+
"@types/react": "^19.2.7",
|
|
49
|
+
"ts-node": "^10.9.2",
|
|
50
|
+
"typescript": "^5.9.3"
|
|
51
|
+
}
|
|
52
|
+
}
|