Repository: farouqaldori/aiformat
Branch: main
Commit: a11bdb6f53c6
Files: 9
Total size: 18.3 KB
Directory structure:
gitextract_0fe2y8sq/
├── .editorconfig
├── .gitignore
├── LICENSE
├── package.json
├── readme.md
├── source/
│ ├── app.tsx
│ ├── cli.tsx
│ └── utils/
│ └── generateOutput.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.yml]
indent_style = space
indent_size = 2
================================================
FILE: .gitignore
================================================
.DS_Store
node_modules
dist
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 farouqaldori
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: package.json
================================================
{
"name": "aiformat",
"version": "0.0.5",
"license": "MIT",
"bin": "dist/cli.js",
"type": "module",
"engines": {
"node": ">=16"
},
"repository": {
"type": "git",
"url": "https://github.com/farouqaldori/aiformat.git"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "prettier --check . && xo && ava"
},
"files": [
"dist"
],
"dependencies": {
"clipboardy": "^4.0.0",
"figures": "^6.1.0",
"ink": "^4.1.0",
"meow": "^11.0.0",
"react": "^18.2.0"
},
"devDependencies": {
"@sindresorhus/tsconfig": "^3.0.1",
"@types/react": "^18.0.32",
"@vdemedes/prettier-config": "^2.0.1",
"ava": "^5.2.0",
"chalk": "^5.2.0",
"eslint-config-xo-react": "^0.27.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"ink-testing-library": "^3.0.0",
"prettier": "^2.8.7",
"ts-node": "^10.9.1",
"typescript": "^5.0.3",
"xo": "^0.53.1"
},
"ava": {
"extensions": {
"ts": "module",
"tsx": "module"
},
"nodeArguments": [
"--loader=ts-node/esm"
]
},
"xo": {
"extends": "xo-react",
"prettier": true,
"rules": {
"react/prop-types": "off"
}
},
"prettier": "@vdemedes/prettier-config"
}
================================================
FILE: readme.md
================================================
# aiformat
https://github.com/farouqaldori/aiformat/assets/16778033/2dd13fc7-5859-4169-893a-4bfe99bd8f0a
aiformat is a simple tool you can use from the command line. It helps you select files and folders and change them into a format that AI assistants like Claude can understand.
This way, you can share code snippets and project structures faster and easier directly from the console, without having to copy and paste them manually.
This cli tool is built using [Ink](https://github.com/vadimdemedes/ink).
## Updates
### **Mar 18 2024:** Folder navigation support
* Added searching inside deeply nested files.
* Added the ability to expand/collapse folders with the `Tab` key.
* Added emojis to differentiate between folders (🗂️) and files (📄).
* Full code re-write, including ID based navigation.
## Features
- Interactively select files and folders from the current directory
- Filter files and folders using a search query
- Navigate through the list using arrow keys
- Select/deselect items using left/right arrow keys
- Convert selected files and folders into a format compatible with Claude
- Automatically copy the formatted output to the clipboard
## Install
To install aiformat, make sure you have Node.js installed on your system. Then, run the following command:
```bash
$ npm install --global aiformat
```
## Usage
To use aiformat, navigate to the directory containing the files and folders you want to share with Claude. Then, run the following command:
```bash
aiformat
```
The CLI will display a list of files and folders in the current directory. You can navigate through the list using the up and down arrow keys. To select or deselect an item, use the left or right arrow keys.
You can also filter the list by typing a search query. The list will update in real-time as you type.
Once you have selected the desired files and folders, press Enter. The CLI will format the selected items into a structure that Claude can understand and automatically copy the output to your clipboard.
## Example
```bash
$ cd /path/to/your/project
$ aiformat
```

Navigate through the list, select the desired files and folders, and press Enter. The formatted output will be copied to your clipboard, ready to be pasted into your conversation with your AI assistant.
The output is optimized for usage with Claude, by wrapping files with XML tags.
Example prompt:
```
<file name="package.json">
{
"name": "aiformat",
"version": "0.0.1",
"license": "MIT",
...
}
</file>
<directory name="source">
<file name="source/app.tsx">
import React, { FC, useState, useEffect } from 'react';
const App: FC = () => {
return (
...
);
};
export default App;
</file>
<file name="source/cli.tsx">
#!/usr/bin/env node
import React from 'react';
import App from './app.js';
render(<App />);
</file>
</directory>
// Add this part manually
<task>
Modify the files above and update the version from 0.0.1 to 0.0.2
</task>
```
## Local Development
To start developing aiformat locally, follow these steps:
1. Clone the repository:
```bash
git clone https://github.com/farouqaldori/aiformat.git
```
2. Change to the project directory:
```bash
cd aiformat
```
3. Install the dependencies:
```bash
npm install
```
4. Build the project:
```bash
npm run build
```
5. Link the package globally:
```bash
npm link
```
6. Now you can use the `aiformat` command globally to test your local changes.
## Contributing
If you find any issues or have suggestions for improvements, please feel free to open an issue or submit a pull request on the [GitHub repository](https://github.com/farouqaldori/aiformat).
## License
This project is licensed under the [MIT License](LICENSE).
================================================
FILE: source/app.tsx
================================================
import React, { FC, ReactNode, useEffect, useState } from 'react';
import { Box, Text, useInput } from 'ink';
import fs from 'fs';
import path from 'path';
import { outputXml } from './utils/generateOutput.js';
import clipboard from 'clipboardy';
interface Item {
id: string;
name: string;
isDirectory: boolean;
children: Item[];
path: string;
isExpanded: boolean;
level: number;
}
const generateId = (itemPath: string): string => {
return itemPath;
};
// Clear console
process.stdout.write('\x1Bc');
const App: FC = () => {
const [excludedFolders] = useState<string[]>(['node_modules', '.git', 'dist', 'build', 'coverage', 'public']);
const [currentItemId, setCurrentItemId] = useState<string | null>(null);
const [items, setItems] = useState<Item[]>([]);
const [selectedItems, setSelectedItems] = useState<Item[]>([]);
const [searchQuery, setSearchQuery] = useState<string>('');
const [message, setMessage] = useState<ReactNode | null>(null)
useInput((input, key) => {
if (key.return) {
copyContentsOfFilesAndFolders();
return;
}
if (input) {
setSearchQuery((prev) => prev + input);
}
if (key.backspace || key.delete) {
setSearchQuery((prev) => prev.slice(0, -1));
}
if (key.downArrow) {
navigateToNextItem();
}
if (key.upArrow) {
navigateToPreviousItem();
}
if (key.tab) {
toggleFolderExpansion();
}
if (key.leftArrow || key.rightArrow) {
toggleSelection();
}
});
const copyContentsOfFilesAndFolders = () => {
const files = outputXml(selectedItems);
clipboard.writeSync(files.content);
setMessage(
<Text color="white">✨ Successfully copied <Text color="cyan">{files.fileCount}</Text> file{files.fileCount > 1 && "s"} to clipboard</Text>
);
setTimeout(() => {
process.exit(0);
}, 300);
};
const toggleSelection = () => {
if (!currentItemId) {
return;
}
const currentItem = findItemById(currentItemId, items);
if (!currentItem) {
return;
}
if (currentItem.isDirectory) {
const itemsInFolder = getItemsFromFolder(currentItem);
const allItemsInFolderAreSelected = itemsInFolder.every((item) => selectedItems.includes(item));
if (allItemsInFolderAreSelected) {
setSelectedItems(selectedItems.filter((item) => !itemsInFolder.find((i) => i.id === item.id)));
} else {
const newSelectedItems = selectedItems.filter((item) => !itemsInFolder.find((i) => i.id === item.id));
setSelectedItems([...newSelectedItems, ...itemsInFolder]);
}
} else {
if (selectedItems.find((item) => item.id === currentItem.id)) {
setSelectedItems(selectedItems.filter((item) => item.id !== currentItem.id));
} else {
setSelectedItems([...selectedItems, currentItem]);
}
}
}
const getItemsFromFolder = (folder: Item): Item[] => {
const items: Item[] = [];
const traverseItems = (item: Item) => {
items.push(item);
if (item.isDirectory) {
item.children.forEach(traverseItems);
}
};
traverseItems(folder);
return items;
}
const loadFilesAndFolders = (dirPath: string, level: number = 0): Item[] => {
const items: Item[] = [];
const dirItems = fs.readdirSync(dirPath);
const sortedItems = dirItems.sort((a, b) => {
const aIsDir = fs.statSync(path.join(dirPath, a)).isDirectory();
const bIsDir = fs.statSync(path.join(dirPath, b)).isDirectory();
if (aIsDir && !bIsDir) return -1;
if (!aIsDir && bIsDir) return 1;
return a.localeCompare(b);
});
for (const item of sortedItems) {
const itemPath = path.join(dirPath, item);
const isDirectory = fs.statSync(itemPath).isDirectory();
const id = generateId(itemPath);
if (!excludedFolders.includes(item)) {
const newItem: Item = {
id,
name: item,
isDirectory,
children: [],
path: itemPath,
isExpanded: false,
level,
};
if (isDirectory) {
newItem.children = loadFilesAndFolders(itemPath, level + 1);
}
items.push(newItem);
}
}
return items;
};
useEffect(() => {
const items = loadFilesAndFolders(process.cwd());
setItems(items);
setCurrentItemId(items[0]?.id || null);
}, []);
const findItemByIdInFilteredItems = (itemId: string, items: Item[]): Item | undefined => {
for (const item of items) {
if (item.id === itemId) {
return item;
}
if (item.isDirectory && item.isExpanded) {
const foundItem = findItemByIdInFilteredItems(itemId, item.children);
if (foundItem) {
return foundItem;
}
}
}
return undefined;
};
const findItemById = (itemId: string, items: Item[]): Item | undefined => {
for (const item of items) {
if (item.id === itemId) {
return item;
}
if (item.isDirectory) {
const foundItem = findItemById(itemId, item.children);
if (foundItem) {
return foundItem;
}
}
}
return undefined;
};
const navigateToNextItem = () => {
if (!currentItemId) {
setCurrentItemId(expandedItems[0]?.id || null);
return;
}
const currentItem = findItemByIdInFilteredItems(currentItemId, expandedItems);
if (!currentItem) {
return;
}
if (currentItem.isDirectory && currentItem.isExpanded && currentItem.children.length > 0) {
setCurrentItemId(currentItem.children[0]?.id || null);
} else {
const flattenedItems = flattenItems(expandedItems);
const currentIndex = flattenedItems.findIndex((item) => item.id === currentItemId);
const nextIndex = (currentIndex + 1) % flattenedItems.length;
setCurrentItemId(flattenedItems[nextIndex]?.id || null);
}
};
const navigateToPreviousItem = () => {
if (!currentItemId) {
return;
}
const flattenedItems = flattenItems(expandedItems);
const currentIndex = flattenedItems.findIndex((item) => item.id === currentItemId);
const previousIndex = (currentIndex - 1 + flattenedItems.length) % flattenedItems.length;
setCurrentItemId(flattenedItems[previousIndex]?.id || null);
};
const flattenItems = (items: Item[]): Item[] => {
const flattenedItems: Item[] = [];
const traverseItems = (items: Item[]) => {
for (const item of items) {
flattenedItems.push(item);
if (item.isDirectory && item.isExpanded) {
traverseItems(item.children);
}
}
};
traverseItems(items);
return flattenedItems;
};
const toggleFolderExpansion = () => {
if (!currentItemId) {
return;
}
const currentItem = findItemById(currentItemId, items);
if (currentItem && currentItem.isDirectory) {
currentItem.isExpanded = !currentItem.isExpanded;
}
setItems(items.map((item) => {
if (item.id === currentItem?.id) {
return currentItem;
}
return item;
}));
};
const expandParentFolders = (item: Item, items: Item[]): Item[] => {
return items.map((i) => {
if (i.id === item.id) {
return { ...i, isExpanded: true };
}
if (i.isDirectory && item.path.startsWith(i.path)) {
return { ...i, isExpanded: true, children: expandParentFolders(item, i.children) };
}
return i;
});
};
const searchItems = (items: Item[], query: string): Item[] => {
return items.reduce((result, item) => {
if (item.isDirectory) {
const matchingChildren = searchItems(item.children, query);
if (matchingChildren.length > 0) {
const expandedItem = { ...item, isExpanded: true, children: matchingChildren };
result.push(expandedItem);
}
} else if (item.name.toLowerCase().includes(query.toLowerCase())) {
result.push(item);
}
return result;
}, [] as Item[]);
};
const renderItems = (items: Item[], indentationLevel = 0): ReactNode[] => {
return items.map((item) => (
<Box key={item.path} flexDirection="column">
<Box marginLeft={indentationLevel} key={item.id}>
<Text color={item.id === currentItemId ? 'green' : selectedItems.find((selectedItem) => selectedItem.id === item.id) ? 'cyan' : 'white'}>
{selectedItems.find((selectedItem) => selectedItem.id === item.id) ? '[X]' : '[ ]'}{' '}
{item.isDirectory ? '🗂️ ' : '📄 '} {item.name}{item.isDirectory && "/"}
</Text>
</Box>
{item.isDirectory && item.isExpanded && item.children.length > 0 && renderItems(item.children, indentationLevel + 1)}
</Box>
));
};
const filteredItems = searchQuery ? searchItems(items, searchQuery) : items;
const expandedItems = filteredItems.reduce((result, item) => {
if (item.isDirectory && item.isExpanded) {
return expandParentFolders(item, result);
}
return result;
}, filteredItems);
useEffect(() => {
// Get the first file that is not a directory
const firstFile = expandedItems[0];
if (firstFile && firstFile.isDirectory) {
const itemsInFolder = getItemsFromFolder(firstFile);
const firstItem = itemsInFolder.find((item) => !item.isDirectory);
if (searchQuery === "") {
setCurrentItemId(firstFile.id);
} else {
setCurrentItemId(firstItem?.id || null);
}
} else {
if (firstFile) {
setCurrentItemId(firstFile.id);
}
}
}, [searchQuery]);
return (
<Box flexDirection="column" marginTop={2} marginBottom={2}>
<Box flexDirection="column" marginBottom={1}>
<Text>Select files and folders to include.</Text>
<Text>
Selected files: <Text color="cyan">{selectedItems.length}</Text>
</Text>
<Text>
Search query: {searchQuery ? searchQuery : <Text color="gray" italic>None</Text>}
</Text>
</Box>
<Box marginBottom={1} flexDirection="column">
{renderItems(expandedItems)}
{expandedItems.length === 0 && <Text color="gray" italic>No items found</Text>}
</Box>
<Box flexDirection="column">
<Text>
Use <Text color="green">Up</Text> / <Text color="green">Down</Text> to
navigate, and <Text color="green">Left</Text> /{' '}
<Text color="green">Right</Text> to select
</Text>
<Text>
Use <Text color="green">Tab</Text> to expand/collapse, and{' '}
<Text color="green">Enter</Text> to copy selected files.
</Text>
</Box>
<Box marginTop={1}>
{message && message}
</Box>
</Box>
);
};
export default App;
================================================
FILE: source/cli.tsx
================================================
#!/usr/bin/env node
import React from 'react';
import { render } from 'ink';
import App from './app.js';
// import meow from 'meow';
// This code is commented out in case we want to use it later to pass arguments to the CLI
// const cli = meow(
// `
// Usage
// $ aiformat
// Options
// --name Your name
// Examples
// $ aiformat --name=Jane
// Hello, Jane
// `,
// {
// importMeta: import.meta,
// flags: {
// name: {
// type: 'string',
// },
// },
// },
// );
render(<App />);
================================================
FILE: source/utils/generateOutput.ts
================================================
import fs from 'fs';
interface FileOrFolder {
id: string;
name: string;
isDirectory: boolean;
children: FileOrFolder[];
path: string;
isExpanded: boolean;
level: number;
}
const cleanupFileTree = (fileTree: FileOrFolder[]): FileOrFolder[] => {
const idSet = new Set<string>();
function traverse(node: FileOrFolder) {
if (idSet.has(node.id)) {
return null;
}
idSet.add(node.id);
if (node.isDirectory) {
node.children = node.children.map(traverse).filter(Boolean) as FileOrFolder[];
}
return node;
}
return fileTree.map(traverse).filter(Boolean) as FileOrFolder[];
};
export const outputXml = (fileTree: FileOrFolder[]): {
content: string;
fileCount: number;
} => {
const cleanedFileTree = cleanupFileTree(fileTree);
function generateXml(node: FileOrFolder, parentPath: string = ''): string {
const currentPath = parentPath ? `${parentPath}/${node.name}` : node.name;
if (node.isDirectory) {
const childXml = node.children.map(child => generateXml(child, currentPath)).join('\n');
return `<folder name="${currentPath}">\n${childXml}\n</folder>`;
} else {
const fileContent = fs.readFileSync(node.path, 'utf8');
return `<file name="${currentPath}">\n${fileContent}\n</file>`;
}
}
function countFiles(node: FileOrFolder): number {
if (node.isDirectory) {
return node.children.reduce((acc, child) => acc + countFiles(child), 0);
} else {
return 1;
}
}
return {
content: cleanedFileTree.map(node => generateXml(node)).join('\n\n'),
fileCount: cleanedFileTree.reduce((acc, node) => acc + countFiles(node), 0),
};
};
================================================
FILE: tsconfig.json
================================================
{
"extends": "@sindresorhus/tsconfig",
"compilerOptions": {
"outDir": "dist"
},
"include": ["source"]
}
gitextract_0fe2y8sq/ ├── .editorconfig ├── .gitignore ├── LICENSE ├── package.json ├── readme.md ├── source/ │ ├── app.tsx │ ├── cli.tsx │ └── utils/ │ └── generateOutput.ts └── tsconfig.json
SYMBOL INDEX (5 symbols across 2 files)
FILE: source/app.tsx
type Item (line 8) | interface Item {
FILE: source/utils/generateOutput.ts
type FileOrFolder (line 3) | interface FileOrFolder {
function traverse (line 16) | function traverse(node: FileOrFolder) {
function generateXml (line 38) | function generateXml(node: FileOrFolder, parentPath: string = ''): string {
function countFiles (line 50) | function countFiles(node: FileOrFolder): number {
Condensed preview — 9 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (21K chars).
[
{
"path": ".editorconfig",
"chars": 175,
"preview": "root = true\n\n[*]\nindent_style = tab\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newlin"
},
{
"path": ".gitignore",
"chars": 28,
"preview": ".DS_Store\nnode_modules\ndist\n"
},
{
"path": "LICENSE",
"chars": 1068,
"preview": "MIT License\n\nCopyright (c) 2024 farouqaldori\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
},
{
"path": "package.json",
"chars": 1187,
"preview": "{\n\t\"name\": \"aiformat\",\n\t\"version\": \"0.0.5\",\n\t\"license\": \"MIT\",\n\t\"bin\": \"dist/cli.js\",\n\t\"type\": \"module\",\n\t\"engines\": {\n\t"
},
{
"path": "readme.md",
"chars": 3796,
"preview": "# aiformat\n\nhttps://github.com/farouqaldori/aiformat/assets/16778033/2dd13fc7-5859-4169-893a-4bfe99bd8f0a\n\naiformat is a"
},
{
"path": "source/app.tsx",
"chars": 9957,
"preview": "import React, { FC, ReactNode, useEffect, useState } from 'react';\nimport { Box, Text, useInput } from 'ink';\nimport fs "
},
{
"path": "source/cli.tsx",
"chars": 574,
"preview": "#!/usr/bin/env node\nimport React from 'react';\nimport { render } from 'ink';\nimport App from './app.js';\n// import meow "
},
{
"path": "source/utils/generateOutput.ts",
"chars": 1815,
"preview": "import fs from 'fs';\n\ninterface FileOrFolder {\n id: string;\n name: string;\n isDirectory: boolean;\n children:"
},
{
"path": "tsconfig.json",
"chars": 110,
"preview": "{\n\t\"extends\": \"@sindresorhus/tsconfig\",\n\t\"compilerOptions\": {\n\t\t\"outDir\": \"dist\"\n\t},\n\t\"include\": [\"source\"]\n}\n"
}
]
About this extraction
This page contains the full source code of the farouqaldori/aiformat GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 9 files (18.3 KB), approximately 5.3k tokens, and a symbol index with 5 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.