Full Code of initialcommit-com/git-story for AI

main 977198cf0963 cached
9 files
22.8 KB
5.4k tokens
9 symbols
1 requests
Download .txt
Repository: initialcommit-com/git-story
Branch: main
Commit: 977198cf0963
Files: 9
Total size: 22.8 KB

Directory structure:
gitextract_jc_yf9cl/

├── .gitignore
├── MANIFEST.in
├── README.md
├── git_story/
│   ├── __init__.py
│   ├── __main__.py
│   └── git_story.py
├── license
├── setup.py
└── test.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
.DS_Store
__pycache__
*.pyc
media/
git-story_media/
build/
dist/
git_story.egg-info/


================================================
FILE: MANIFEST.in
================================================
include git_story/logo.png


================================================
FILE: README.md
================================================
# git-story
Tell the story of your Git project by creating video animations (.mp4)
of your commit history directly from your Git repo.

## Use cases
- Visualizing Git projects
- Sharing desired parts of your workflow with your team
- Creating animated Git videos for blog posts or YouTube
- Helping newer developers learn Git

## Features
- Run a single command in the terminal to generate a custom Git animation (.mp4) from your repo
- Specify any commit id or ref to start animating from (default: `HEAD`)
- Specify the number of commits to include (default: `8`)
- Ref labels are drawn by default for `HEAD`, branch names, and tags
- Reverse commit ordering and reorient branch layout
- Works best with simpler branching structures, but should work with more complex ones as well
- Add custom branded intro/outro sequences if desired
- Dark mode and light mode

## Video animation example
https://user-images.githubusercontent.com/49353917/179362209-48748966-6d6c-46ff-9424-b1a7266fc83f.mp4

## Requirements
* Python 3.9 or greater
* Pip (Package manager for Python)
* [Manim (Community version)](https://www.manim.community/)
* GitPython

## Quickstart
1) Install [manim and manim dependencies for your OS](https://www.manim.community/)

2) Install GitPython

```console
$ pip3 install gitpython
```

3) Install `git-story`:

```console
$ pip3 install git-story
```

3) Browse to the Git repository you want create an animation from:

```console
$ cd path/to/project/root
```

4) Run the program:

```console
$ git-story
```

5) A default animation `.mp4` file will be created using the most recent 8 commits on your checked-out Git branch. By default, video output file is created in the current directory, within a subdirectory called `git-story_media`. The location this subdirectory is customizeable using the command line flag `--media-dir=path/to/output`.

6) Use command-line options for customization, see usage:

```console
$ git-story -h

usage: git-story [-h] [--commits COMMITS] [--commit-id COMMIT_ID] [--hide-merged-chains] [--reverse] [--title TITLE] [--logo LOGO] [--outro-top-text OUTRO_TOP_TEXT]
                 [--outro-bottom-text OUTRO_BOTTOM_TEXT] [--show-intro] [--show-outro] [--max-branches-per-commit MAX_BRANCHES_PER_COMMIT] [--max-tags-per-commit MAX_TAGS_PER_COMMIT]
                 [--media-dir MEDIA_DIR] [--low-quality] [--light-mode] [--invert-branches]

optional arguments:
  -h, --help            show this help message and exit
  --commits COMMITS     The number of commits to display in the Git animation (default: 8)
  --commit-id COMMIT_ID
                        The ref (branch/tag), or first 6 characters of the commit to animate backwards from (default: HEAD)
  --hide-merged-chains  Hide commits from merged branches, i.e. only display mainline commits (default: False)
  --reverse             Display commits in reverse order in the Git animation (default: False)
  --title TITLE         Custom title to display at the beginning of the animation (default: Git Story, by initialcommit.com)
  --logo LOGO           The path to a custom logo to use in the animation intro/outro (default: /usr/local/lib/python3.9/site-packages/git_story/logo.png)
  --outro-top-text OUTRO_TOP_TEXT
                        Custom text to display above the logo during the outro (default: Thanks for using Initial Commit!)
  --outro-bottom-text OUTRO_BOTTOM_TEXT
                        Custom text to display below the logo during the outro (default: Learn more at initialcommit.com)
  --show-intro          Add an intro sequence with custom logo and title (default: False)
  --show-outro          Add an outro sequence with custom logo and text (default: False)
  --max-branches-per-commit MAX_BRANCHES_PER_COMMIT
                        Maximum number of branch labels to display for each commit (default: 2)
  --max-tags-per-commit MAX_TAGS_PER_COMMIT
                        Maximum number of tags to display for each commit (default: 1)
  --media-dir MEDIA_DIR
                        The path to output the animation data and video file (default: .)
  --low-quality         Render output video in low quality, useful for faster testing (default: False)
  --light-mode          Enable light-mode with white background (default: False)
  --invert-branches     Invert positioning of branches where applicable (default: False)
  --speed SPEED         A multiple of the standard 1x animation speed (ex: 2 = twice as fast, 0.5 = half as fast) (default: 1)
```

## Command Examples
Default - draw 8 commits starting from `HEAD`, from oldest to newest:

```console
$ cd path/to/project/root
$ git-story
```

Customize the start commit and number of commits, and reverse their display order:

```console
$ git-story --commit-id a1b2c3 --commits=6 --reverse
```

Invert the branch orientation, if multiple branches exist in the commit range:

```console
$ git-story --invert-branches
```

Add an intro with custom title and logo:

```console
$ git-story --commit-id dev --commits=10 --show-intro --title "My Git Repo" --logo path/to/logo.png
```

Add an outro with custom text and logo:

```console
$ git-story --show-outro --outro-top-text "My Git Repo" --outro-bottom-text "Thanks for watching!" --logo path/to/logo.png
```

Customize the output video directory location:

```console
$ git-story --media-dir=path/to/output
```

Use light mode for white background and black text, instead of the default black background with white text:

```console
$ git-story --light-mode
```

Generate output video in low quality to speed up rendering time (useful for repeated testing):

```console
$ git-story --low-quality
```

## Installation
See **QuickStart** section for details on installing manim and GitPython dependencies. Then run:

```console
$ pip3 install git-story
```

## Learn More
Learn more about this tool on the [git-story project page](https://initialcommit.com/tools/git-story).

## Authors
**Jacob Stopak** - on behalf of [Initial Commit](https://initialcommit.com)


================================================
FILE: git_story/__init__.py
================================================


================================================
FILE: git_story/__main__.py
================================================
from git_story import git_story as gs
import os
import argparse
import pathlib
from manim import config, WHITE
from manim.utils.file_ops import open_file as open_media_file

def main():
    parser = argparse.ArgumentParser("git-story", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument("--commits", help="The number of commits to display in the Git animation", type=int, default=8)
    parser.add_argument("--commit-id", help="The ref (branch/tag), or first 6 characters of the commit to animate backwards from", type=str, default="HEAD")
    parser.add_argument("--hide-merged-chains", help="Hide commits from merged branches, i.e. only display mainline commits", action="store_true")
    parser.add_argument("--reverse", help="Display commits in reverse order in the Git animation", action="store_true")
    parser.add_argument("--title", help="Custom title to display at the beginning of the animation", type=str, default="Git Story, by initialcommit.com")
    parser.add_argument("--logo", help="The path to a custom logo to use in the animation intro/outro", type=str, default=os.path.join(str(pathlib.Path(__file__).parent.resolve()), "logo.png"))
    parser.add_argument("--outro-top-text", help="Custom text to display above the logo during the outro", type=str, default="Thanks for using Initial Commit!")
    parser.add_argument("--outro-bottom-text", help="Custom text to display below the logo during the outro", type=str, default="Learn more at initialcommit.com")
    parser.add_argument("--show-intro", help="Add an intro sequence with custom logo and title", action="store_true")
    parser.add_argument("--show-outro", help="Add an outro sequence with custom logo and text", action="store_true")
    parser.add_argument("--max-branches-per-commit", help="Maximum number of branch labels to display for each commit", type=int, default=2)
    parser.add_argument("--max-tags-per-commit", help="Maximum number of tags to display for each commit", type=int, default=1)
    parser.add_argument("--media-dir", help="The path to output the animation data and video file", type=str, default=".")
    parser.add_argument("--low-quality", help="Render output video in low quality, useful for faster testing", action="store_true")
    parser.add_argument("--light-mode", help="Enable light-mode with white background", action="store_true")
    parser.add_argument("--invert-branches", help="Invert positioning of branches where applicable", action="store_true")
    parser.add_argument("--speed", help="A multiple of the standard 1x animation speed (ex: 2 = twice as fast, 0.5 = half as fast)", type=float, default=1)

    args = parser.parse_args()

    config.media_dir = os.path.join(args.media_dir, "git-story_media")

    if ( args.low_quality ):
        config.quality = "low_quality"

    if ( args.light_mode ):
        config.background_color = WHITE

    scene = gs.GitStory(args)
    scene.render()

    try:
        open_media_file(scene.renderer.file_writer.movie_file_path)
    except FileNotFoundError:
        print("Error automatically opening video player, please manually open the video file to view animation.")

if __name__ == '__main__':
    main()


================================================
FILE: git_story/git_story.py
================================================
from manim import *
import git, sys, numpy

class GitStory(MovingCameraScene):
    def __init__(self, args):
        super().__init__()
        self.args = args
        self.drawnCommits = {}
        self.commits = []
        self.children = {}
        self.childChainLength = 0
        self.zoomOuts = 0

        if ( self.args.light_mode ):
            self.fontColor = BLACK
        else:
            self.fontColor = WHITE

    def measureChildChain(self, commit):
        try:
            if ( len(self.children[commit.hexsha]) > 0 ):
                for child in self.children[commit.hexsha]:
                    self.childChainLength += 1
                    return self.measureChildChain(child)
            else:
                return self.childChainLength
        except KeyError:
            return self.childChainLength

    def construct(self):
        try:
            self.repo = git.Repo(search_parent_directories=True)
        except git.exc.InvalidGitRepositoryError:
            print("git-story error: No Git repository found at current path.")
            sys.exit(1)
        
        try:
            self.commits = list(self.repo.iter_commits(self.args.commit_id))[:self.args.commits]
        except git.exc.GitCommandError:
            print("git-story error: No commits in current Git repository.")
            sys.exit(1)

        if ( len(self.commits) < self.args.commits ):
            self.args.commits = len(self.commits)

        if ( not self.args.reverse ):
            self.commits.reverse()
            for commit in self.commits:
                if ( len(commit.parents) > 0 ):
                    for parent in commit.parents:
                        self.children.setdefault(parent.hexsha, []).append(commit)
            z = 1
            while ( self.measureChildChain(self.commits[0]) < self.args.commits-1 ):
                self.commits = list(self.repo.iter_commits(self.args.commit_id))[:self.args.commits + z]
                self.commits.reverse()
                self.children = {}
                for commit in self.commits:
                    if ( len(commit.parents) > 0 ):
                        for parent in commit.parents:
                            self.children.setdefault(parent.hexsha, []).append(commit)
                z += 1

            if ( self.args.invert_branches ):
                for d in self.children:
                    if ( len(self.children[d]) > 1 ):
                        self.children[d].reverse()

        commit = self.commits[0]

        logo = ImageMobject(self.args.logo)
        logo.width = 3

        if ( self.args.show_intro ):
            self.add(logo)

            initialCommitText = Text(self.args.title, font="Monospace", font_size=36, color=self.fontColor).to_edge(UP, buff=1)
            self.add(initialCommitText)
            self.wait(2)
            self.play(FadeOut(initialCommitText))
            self.play(logo.animate.scale(0.25).to_edge(UP, buff=0).to_edge(RIGHT, buff=0))
    
            self.camera.frame.save_state()
            self.play(FadeOut(logo))

        else:
            logo.scale(0.25).to_edge(UP, buff=0).to_edge(RIGHT, buff=0)
            self.camera.frame.save_state()

        i = 0
        prevCircle = None
        toFadeOut = Group()
        self.parseCommits(commit, i, prevCircle, toFadeOut)

        self.play(self.camera.frame.animate.move_to(toFadeOut.get_center()), run_time=1/self.args.speed)
        self.play(self.camera.frame.animate.scale_to_fit_width(toFadeOut.get_width()*1.1), run_time=1/self.args.speed)

        if ( toFadeOut.get_height() >= self.camera.frame.get_height() ):
            self.play(self.camera.frame.animate.scale_to_fit_height(toFadeOut.get_height()*1.25), run_time=1/self.args.speed)

        self.wait(3)

        self.play(FadeOut(toFadeOut), run_time=1/self.args.speed)

        if ( self.args.show_outro ):

            self.play(Restore(self.camera.frame))

            self.play(logo.animate.scale(4).set_x(0).set_y(0))

            outroTopText = Text(self.args.outro_top_text, font="Monospace", font_size=36, color=self.fontColor).to_edge(UP, buff=1)
            self.play(AddTextLetterByLetter(outroTopText))

            outroBottomText = Text(self.args.outro_bottom_text, font="Monospace", font_size=36, color=self.fontColor).to_edge(DOWN, buff=1)
            self.play(AddTextLetterByLetter(outroBottomText))

            self.wait(3)

    def parseCommits(self, commit, i, prevCircle, toFadeOut):
        if ( i < self.args.commits and commit in self.commits ):

            if ( len(commit.parents) <= 1 ):
                commitFill = RED
            else:
                commitFill = GRAY

            circle = Circle(stroke_color=commitFill, fill_color=commitFill, fill_opacity=0.25)
            circle.height = 1

            if prevCircle:
                circle.next_to(prevCircle, RIGHT, buff=1.5)

            offset = 0
            while ( any((circle.get_center() == c).all() for c in self.getCenters()) ):
                circle.next_to(circle, DOWN, buff=3.5)
                offset += 1
                if ( self.zoomOuts == 0 ):
                    self.play(self.camera.frame.animate.scale(1.5), run_time=1/self.args.speed)
                self.zoomOuts += 1

            isNewCommit = commit.hexsha not in self.drawnCommits

            if ( not self.args.reverse ):
                if ( isNewCommit ):
                    start = circle.get_center()
                    end = prevCircle.get_center() if prevCircle else LEFT
                else:
                    start = self.drawnCommits[commit.hexsha].get_center()
                    end = prevCircle.get_center()

            else:
                if ( isNewCommit ):
                    start = prevCircle.get_center() if prevCircle else LEFT
                    end = circle.get_center()
                else:
                    start = prevCircle.get_center()
                    end = self.drawnCommits[commit.hexsha].get_center()

            arrow = Arrow(start, end, color=self.fontColor)
            length = numpy.linalg.norm(start-end) - ( 1.5 if start[1] == end[1] else 3  )
            arrow.set_length(length)

            angle = arrow.get_angle()
            lineRect = Rectangle(height=0.1, width=length, color="#123456").move_to(arrow.get_center()).rotate(angle)

            for commitCircle in self.drawnCommits.values():
                inter = Intersection(lineRect, commitCircle)
                if ( inter.has_points() ):
                    arrow = CurvedArrow(start, end)
                    if ( start[1] == end[1]  ):
                        arrow.shift(UP*1.25)
                    if ( start[0] < end[0] and start[1] == end[1] ):
                        arrow.flip(RIGHT).shift(UP)
                
            commitId = Text(commit.hexsha[0:6], font="Monospace", font_size=20, color=self.fontColor).next_to(circle, UP)

            commitMessage = commit.message[:40].replace("\n", " ")
            message = Text('\n'.join(commitMessage[j:j+20] for j in range(0, len(commitMessage), 20))[:100], font="Monospace", font_size=14, color=self.fontColor).next_to(circle, DOWN)

            if ( isNewCommit ):

                self.play(self.camera.frame.animate.move_to(circle.get_center()), Create(circle), AddTextLetterByLetter(commitId), AddTextLetterByLetter(message), run_time=1/self.args.speed)
                self.drawnCommits[commit.hexsha] = circle

                prevRef = commitId
                if ( commit.hexsha == self.repo.head.commit.hexsha ):
                    head = Rectangle(color=BLUE, fill_color=BLUE, fill_opacity=0.25)
                    head.width = 1
                    head.height = 0.4
                    head.next_to(commitId, UP)
                    headText = Text("HEAD", font="Monospace", font_size=20, color=self.fontColor).move_to(head.get_center())
                    self.play(Create(head), Create(headText), run_time=1/self.args.speed)
                    toFadeOut.add(head, headText)
                    prevRef = head

                x = 0
                for branch in self.repo.heads:
                    if ( commit.hexsha == branch.commit.hexsha ):
                        branchText = Text(branch.name, font="Monospace", font_size=20, color=self.fontColor)
                        branchRec = Rectangle(color=GREEN, fill_color=GREEN, fill_opacity=0.25, height=0.4, width=branchText.width+0.25)

                        branchRec.next_to(prevRef, UP)
                        branchText.move_to(branchRec.get_center())

                        prevRef = branchRec 

                        self.play(Create(branchRec), Create(branchText), run_time=1/self.args.speed)
                        toFadeOut.add(branchRec, branchText)

                        x += 1
                        if ( x >= self.args.max_branches_per_commit ):
                            break

                x = 0
                for tag in self.repo.tags:
                    if ( commit.hexsha == tag.commit.hexsha ):
                        tagText = Text(tag.name, font="Monospace", font_size=20, color=self.fontColor)
                        tagRec = Rectangle(color=YELLOW, fill_color=YELLOW, fill_opacity=0.25, height=0.4, width=tagText.width+0.25)

                        tagRec.next_to(prevRef, UP)
                        tagText.move_to(tagRec.get_center())

                        prevRef = tagRec

                        self.play(Create(tagRec), Create(tagText), run_time=1/self.args.speed)
                        toFadeOut.add(tagRec, tagText)

                        x += 1
                        if ( x >= self.args.max_tags_per_commit ):
                            break

            else:
                self.play(self.camera.frame.animate.move_to(self.drawnCommits[commit.hexsha].get_center()), run_time=1/self.args.speed)
                self.play(Create(arrow), run_time=1/self.args.speed)
                toFadeOut.add(arrow)
                return


            if ( prevCircle ):
                self.play(Create(arrow), run_time=1/self.args.speed)
                toFadeOut.add(arrow)

            prevCircle = circle

            toFadeOut.add(circle, commitId, message)

            if ( self.args.reverse ):
                commitParents = list(commit.parents)
                if ( len(commitParents) > 0 ):
                    if ( self.args.invert_branches ):
                        commitParents.reverse()

                    if ( self.args.hide_merged_chains ):
                        self.parseCommits(commitParents[0], i+1,  prevCircle, toFadeOut)
                    else:
                        for p in range(len(commitParents)):
                            self.parseCommits(commitParents[p], i+1, prevCircle, toFadeOut)

            else:
                try:
                    if ( len(self.children[commit.hexsha]) > 0 ):
                        if ( self.args.hide_merged_chains ):
                            self.parseCommits(self.children[commit.hexsha][0], i+1, prevCircle, toFadeOut)
                        else:
                            for p in range(len(self.children[commit.hexsha])):
                                self.parseCommits(self.children[commit.hexsha][p], i+1, prevCircle, toFadeOut)
                except KeyError:
                    pass

        else:
            return

    def getCenters(self):
        centers = []
        for commit in self.drawnCommits.values():
            centers.append(commit.get_center())
        return centers


================================================
FILE: license
================================================
Copyright 2022 Jacob Stopak, initialcommit.com

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: setup.py
================================================
import setuptools

with open("README.md", "r") as fh:
    long_description = fh.read()

setuptools.setup(
    name="git-story",
    version="0.1.4",
    author="Jacob Stopak",
    author_email="jacob@initialcommit.io",
    description="Tell the story of your Git project by creating video animations (.mp4) of your commit history directly from your Git repo.",
    long_description=long_description,
    long_description_content_type="text/markdown",
    url="https://initialcommit.com/tools/git-story",
    packages=setuptools.find_packages(),
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ],
    python_requires='>=3.7',
    install_requires=[
        'gitpython',
        'manim'
    ],
    keywords='git story git-story manim animation gitanimation',
    project_urls={
        'Homepage': 'https://initialcommit.com/tools/git-story',
    },
    entry_points={
        'console_scripts': [
            'git-story=git_story.__main__:main',
        ],
    },
    include_package_data=True
)


================================================
FILE: test.py
================================================
import unittest, git, argparse
from manim import *

from git_story.git_story import GitStory


class TestGitStory(unittest.TestCase):

    def test_git_story(self):
        """Test git story."""

        gs = GitStory(argparse.Namespace())

        self.assertEqual(1, 1)


if __name__ == '__main__':
    unittest.main()
Download .txt
gitextract_jc_yf9cl/

├── .gitignore
├── MANIFEST.in
├── README.md
├── git_story/
│   ├── __init__.py
│   ├── __main__.py
│   └── git_story.py
├── license
├── setup.py
└── test.py
Download .txt
SYMBOL INDEX (9 symbols across 3 files)

FILE: git_story/__main__.py
  function main (line 8) | def main():

FILE: git_story/git_story.py
  class GitStory (line 4) | class GitStory(MovingCameraScene):
    method __init__ (line 5) | def __init__(self, args):
    method measureChildChain (line 19) | def measureChildChain(self, commit):
    method construct (line 30) | def construct(self):
    method parseCommits (line 118) | def parseCommits(self, commit, i, prevCircle, toFadeOut):
    method getCenters (line 272) | def getCenters(self):

FILE: test.py
  class TestGitStory (line 7) | class TestGitStory(unittest.TestCase):
    method test_git_story (line 9) | def test_git_story(self):
Condensed preview — 9 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (24K chars).
[
  {
    "path": ".gitignore",
    "chars": 85,
    "preview": ".DS_Store\n__pycache__\n*.pyc\nmedia/\ngit-story_media/\nbuild/\ndist/\ngit_story.egg-info/\n"
  },
  {
    "path": "MANIFEST.in",
    "chars": 27,
    "preview": "include git_story/logo.png\n"
  },
  {
    "path": "README.md",
    "chars": 6005,
    "preview": "# git-story\nTell the story of your Git project by creating video animations (.mp4)\nof your commit history directly from "
  },
  {
    "path": "git_story/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "git_story/__main__.py",
    "chars": 3207,
    "preview": "from git_story import git_story as gs\nimport os\nimport argparse\nimport pathlib\nfrom manim import config, WHITE\nfrom mani"
  },
  {
    "path": "git_story/git_story.py",
    "chars": 11543,
    "preview": "from manim import *\nimport git, sys, numpy\n\nclass GitStory(MovingCameraScene):\n    def __init__(self, args):\n        sup"
  },
  {
    "path": "license",
    "chars": 1071,
    "preview": "Copyright 2022 Jacob Stopak, initialcommit.com\n\nPermission is hereby granted, free of charge, to any person obtaining a "
  },
  {
    "path": "setup.py",
    "chars": 1112,
    "preview": "import setuptools\n\nwith open(\"README.md\", \"r\") as fh:\n    long_description = fh.read()\n\nsetuptools.setup(\n    name=\"git-"
  },
  {
    "path": "test.py",
    "chars": 321,
    "preview": "import unittest, git, argparse\nfrom manim import *\n\nfrom git_story.git_story import GitStory\n\n\nclass TestGitStory(unitte"
  }
]

About this extraction

This page contains the full source code of the initialcommit-com/git-story GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 9 files (22.8 KB), approximately 5.4k tokens, and a symbol index with 9 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.

Copied to clipboard!