Skip to content

Commit bdc6df7

Browse files
Updated README.md
2 parents ffad325 + 5bf9875 commit bdc6df7

File tree

8 files changed

+510
-445
lines changed

8 files changed

+510
-445
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# Summarize
22

3-
The **Summarize** package was designed for developers who wish to leverage the use of Artificial Intelligence while
43
working on a project. The `summarize` command give you a powerful interface that is managed by arguments and environment
54
variables that define include/exclude extensions, and avoid substrings list while parsing paths. The binary has
65
concurrency built into it and has limits for the output file. It ignores its default output directory so it won't
76
recursively build summaries upon itself. It defaults to writing to a new directory that it'll try to create in the
87
current working directory called `summaries`, that I recommend that you add to your `.gitignore` and `.dockerignore`.
98

9+
![Diagram](/assets/diagram.png)
10+
11+
The **Summarize** package was designed for developers who wish to leverage the use of Artificial Intelligence while
1012
I've found it useful to leverage the `make summary` command in all of my projects. This way, if I need to ask an AI a
1113
question about a piece of code, I can capture the source code of the entire directory quickly and then just `cat` the
1214
output file path provided and _voila_! The `-print` argument allows you to display the summary contents in the STDOUT

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v1.1.0
1+
v1.1.1

configure.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"github.com/andreimerlescu/goenv/env"
99
)
1010

11-
// init creates a new figtree with options to use CONFIG_FILE as a way of reading a YAML file while ignoring the env
11+
// configure creates a new figtree with options to use CONFIG_FILE as a way of reading a YAML file while ignoring the env
1212
func configure() {
1313
// figs is a tree of figs that ignore the ENV
1414
figs = figtree.With(figtree.Options{

internal.go

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io/fs"
8+
"os"
9+
"path/filepath"
10+
"slices"
11+
"strings"
12+
"sync"
13+
"sync/atomic"
14+
)
15+
16+
// analyze is called from investigate where the path is inspected and the resultsChan is written to
17+
func analyze(ext, filePath string) {
18+
defer maxFileSemaphore.Release() // maxFileSemaphore prevents excessive files from being opened
19+
defer wg.Done() // keep the main thread running while this file is being processed
20+
if strings.HasSuffix(filePath, ".DS_Store") ||
21+
strings.HasSuffix(filePath, ".exe") ||
22+
strings.HasSuffix(filePath, "-amd64") ||
23+
strings.HasSuffix(filePath, "-arm64") ||
24+
strings.HasSuffix(filePath, "aarch64") {
25+
return
26+
}
27+
type tFileInfo struct {
28+
Name string `json:"name"`
29+
Size int64 `json:"size"`
30+
Mode os.FileMode `json:"mode"`
31+
}
32+
info, err := os.Stat(filePath)
33+
if err != nil {
34+
errs = append(errs, err)
35+
return
36+
}
37+
fileInfo := &tFileInfo{
38+
Name: filepath.Base(filePath),
39+
Size: info.Size(),
40+
Mode: info.Mode(),
41+
}
42+
infoJson, err := json.MarshalIndent(fileInfo, "", " ")
43+
if err != nil {
44+
errs = append(errs, err)
45+
return
46+
}
47+
var sb bytes.Buffer // capture what we write to file in a bytes buffer
48+
sb.WriteString("## " + filepath.Base(filePath) + "\n\n")
49+
sb.WriteString("The `os.Stat` for the " + filePath + " is: \n\n")
50+
sb.WriteString("```json\n")
51+
sb.WriteString(string(infoJson) + "\n")
52+
sb.WriteString("```\n\n")
53+
sb.WriteString("Source Code:\n\n")
54+
sb.WriteString("```" + ext + "\n")
55+
content, err := os.ReadFile(filePath) // open the file and get its contents
56+
if err != nil {
57+
errs = append(errs, fmt.Errorf("Error reading file %s: %v\n", filePath, err))
58+
return
59+
}
60+
if _, writeErr := sb.Write(content); writeErr != nil {
61+
errs = append(errs, fmt.Errorf("Error writing file %s: %v\n", filePath, err))
62+
return
63+
}
64+
content = []byte{} // clear memory after its written
65+
sb.WriteString("\n```\n\n") // close out the file footer
66+
seen.Add(filePath)
67+
resultsChan <- Result{
68+
Path: filePath,
69+
Contents: sb.Bytes(),
70+
Size: int64(sb.Len()),
71+
}
72+
}
73+
74+
// done is responsible for printing the results to STDOUT when the summarize program is finished
75+
func done() {
76+
// Print completion message
77+
if *figs.Bool(kJson) {
78+
r := M{
79+
Message: fmt.Sprintf("Summary generated: %s\n",
80+
filepath.Join(*figs.String(kOutputDir), *figs.String(kFilename)),
81+
),
82+
}
83+
jb, err := json.MarshalIndent(r, "", " ")
84+
if err != nil {
85+
terminate(os.Stderr, "Error marshalling results: %v\n", err)
86+
} else {
87+
fmt.Println(string(jb))
88+
}
89+
} else {
90+
fmt.Printf("Summary generated: %s\n",
91+
filepath.Join(*figs.String(kOutputDir), *figs.String(kFilename)),
92+
)
93+
}
94+
}
95+
96+
// iterate is a func that gets passed directly into the data.Range(iterate) that runs investigate concurrently with the
97+
// wg and throttler enabled
98+
func iterate(e, p any) bool {
99+
ext, ok := e.(string)
100+
if !ok {
101+
return true // continue
102+
}
103+
thisData, ok := p.(mapData)
104+
if !ok {
105+
return true // continue
106+
}
107+
paths := slices.Clone(thisData.Paths)
108+
109+
throttler.Acquire() // throttler is used to protect the runtime from excessive use
110+
wg.Add(1) // wg is used to prevent the runtime from exiting early
111+
go investigate(&thisData, &toUpdate, ext, paths)
112+
return true
113+
}
114+
115+
// investigate is called from iterate where it takes an extension and a slice of paths to analyze each path
116+
func investigate(innerData *mapData, toUpdate *[]mapData, ext string, paths []string) {
117+
defer throttler.Release() // when we're done, release the throttler
118+
defer wg.Done() // then tell the sync.WaitGroup that we are done
119+
120+
paths = simplify(paths)
121+
122+
innerData.Paths = paths
123+
*toUpdate = append(*toUpdate, *innerData)
124+
125+
// process each file in the ext list (one ext per throttle slot in the semaphore)
126+
for _, filePath := range paths {
127+
if seen.Exists(filePath) {
128+
continue
129+
}
130+
maxFileSemaphore.Acquire()
131+
wg.Add(1)
132+
go analyze(ext, filePath)
133+
}
134+
}
135+
136+
// populate is responsible for loading new paths into the *sync.Map called data
137+
func populate(ext, path string) {
138+
todo := make([]mapData, 0)
139+
if data == nil {
140+
panic("data is nil")
141+
}
142+
// populate the -inc list in data
143+
data.Range(func(e any, p any) bool {
144+
key, ok := e.(string)
145+
if !ok {
146+
return true // continue
147+
}
148+
value, ok := p.(mapData)
149+
if !ok {
150+
return true
151+
}
152+
if strings.EqualFold(key, ext) {
153+
value.Ext = key
154+
}
155+
value.Paths = append(value.Paths, path)
156+
todo = append(todo, value)
157+
return true
158+
})
159+
for _, value := range todo {
160+
data.Store(value.Ext, value)
161+
}
162+
}
163+
164+
// receive will accept Result from resultsChan and write to the summary file. If `-chat` is enabled, the StartChat will
165+
// get called. Once the chat session is completed, the contents of the chat log is injected into the summary file.
166+
func receive() {
167+
if writerWG == nil {
168+
panic("writer wg is nil")
169+
}
170+
defer writerWG.Done()
171+
172+
// Create output file
173+
srcDir := *figs.String(kSourceDir)
174+
outputFileName := filepath.Join(*figs.String(kOutputDir), *figs.String(kFilename))
175+
var buf bytes.Buffer
176+
buf.WriteString("# Project Summary - " + filepath.Base(*figs.String(kFilename)) + "\n")
177+
buf.WriteString("Generated by " + projectName + " " + Version() + "\n\n")
178+
buf.WriteString("AI Instructions are the user requests that you analyze their project workspace ")
179+
buf.WriteString("as provided here by filename followed by the contents. You are to answer their ")
180+
buf.WriteString("question using the source code provided as the basis of your responses. You are to ")
181+
buf.WriteString("completely modify each individual file as per-the request and provide the completely ")
182+
buf.WriteString("updated form of the file. Do not abbreviate the file, and if the file is excessive in ")
183+
buf.WriteString("length, then print the entire contents in your response with your updates to the ")
184+
buf.WriteString("specific components while retaining all existing functionality and maintaining comments ")
185+
buf.WriteString("within the code. \n\n")
186+
buf.WriteString("### Workspace\n\n")
187+
abs, err := filepath.Abs(srcDir)
188+
if err == nil {
189+
buf.WriteString("`" + abs + "`\n\n")
190+
} else {
191+
buf.WriteString("`" + srcDir + "`\n\n")
192+
}
193+
194+
renderMu := &sync.Mutex{}
195+
renderedPaths := make(map[string]int64)
196+
totalSize := int64(buf.Len())
197+
for in := range resultsChan {
198+
if _, exists := renderedPaths[in.Path]; exists {
199+
continue
200+
}
201+
runningSize := atomic.AddInt64(&totalSize, in.Size)
202+
if runningSize >= *figs.Int64(kMaxOutputSize) {
203+
continue
204+
}
205+
renderMu.Lock()
206+
renderedPaths[in.Path] = in.Size
207+
buf.Write(in.Contents)
208+
renderMu.Unlock()
209+
}
210+
211+
if *figs.Bool(kChat) {
212+
StartChat(&buf)
213+
path := latestChatLog()
214+
contents, err := os.ReadFile(path)
215+
if err == nil {
216+
old := buf.String()
217+
buf.Reset()
218+
buf.WriteString("## Chat Log \n\n")
219+
body := string(contents)
220+
body = strings.ReplaceAll(body, "You: ", "\n### ")
221+
buf.WriteString(body)
222+
buf.WriteString("\n\n")
223+
buf.WriteString("## Summary \n\n")
224+
buf.WriteString(old)
225+
}
226+
}
227+
render(&buf, outputFileName)
228+
}
229+
230+
// render will take the summary and either write it to a file, STDOUT or present an error to STDERR
231+
func render(buf *bytes.Buffer, outputFileName string) {
232+
shouldPrint := *figs.Bool(kPrint)
233+
canWrite := *figs.Bool(kWrite)
234+
showJson := *figs.Bool(kJson)
235+
wrote := false
236+
237+
if *figs.Bool(kCompress) {
238+
compressed, err := compress(bytes.Clone(buf.Bytes()))
239+
capture("compressing bytes buffer", err)
240+
buf.Reset()
241+
buf.Write(compressed)
242+
outputFileName += ".gz"
243+
}
244+
245+
if !shouldPrint && !canWrite {
246+
capture("saving output file during write", os.WriteFile(outputFileName, buf.Bytes(), 0644))
247+
wrote = true
248+
}
249+
250+
if canWrite && !wrote {
251+
capture("saving output file during write", os.WriteFile(outputFileName, buf.Bytes(), 0644))
252+
wrote = true
253+
}
254+
255+
if shouldPrint {
256+
if showJson {
257+
r := Final{
258+
Path: outputFileName,
259+
Size: int64(buf.Len()),
260+
Contents: buf.String(),
261+
}
262+
jb, err := json.MarshalIndent(r, "", " ")
263+
if err != nil {
264+
_, _ = fmt.Fprintln(os.Stderr, err)
265+
}
266+
fmt.Println(string(jb))
267+
} else {
268+
fmt.Println(buf.String())
269+
}
270+
os.Exit(0)
271+
272+
}
273+
}
274+
275+
// summarize walks through a filepath recursively and matches paths that get stored inside the
276+
// data *sync.Map for the extension.
277+
func summarize(path string, info fs.FileInfo, err error) error {
278+
if err != nil {
279+
return err // return the error received
280+
}
281+
if !info.IsDir() {
282+
283+
// get the filename
284+
filename := filepath.Base(path)
285+
286+
if *figs.Bool(kDotFiles) {
287+
if strings.HasPrefix(filename, ".") {
288+
return nil // skip without error
289+
}
290+
}
291+
292+
// check the -avoid list
293+
for _, avoidThis := range lSkipContains {
294+
a := strings.Contains(filename, avoidThis) || strings.Contains(path, avoidThis)
295+
b := strings.HasPrefix(filename, avoidThis) || strings.HasPrefix(path, avoidThis)
296+
c := strings.HasSuffix(filename, avoidThis) || strings.HasSuffix(path, avoidThis)
297+
if a || b || c {
298+
if isDebug {
299+
fmt.Printf("ignoring %s in %s\n", filename, path)
300+
}
301+
return nil // skip without error
302+
}
303+
304+
parts, err := filepath.Glob(path)
305+
if err != nil {
306+
errs = append(errs, err)
307+
continue
308+
}
309+
for i := 0; i < len(parts); i++ {
310+
part := parts[i]
311+
if strings.EqualFold(part, string(os.PathSeparator)) {
312+
continue
313+
}
314+
if strings.Contains(part, avoidThis) || strings.HasPrefix(part, avoidThis) || strings.HasSuffix(part, avoidThis) {
315+
if isDebug {
316+
fmt.Printf("skipping file %q\n", part)
317+
}
318+
return nil
319+
}
320+
}
321+
322+
}
323+
324+
// get the extension
325+
ext := filepath.Ext(path)
326+
ext = strings.ToLower(ext)
327+
ext = strings.TrimPrefix(ext, ".")
328+
329+
if isDebug {
330+
fmt.Printf("ext: %s\n", ext)
331+
}
332+
333+
// check the -exc list
334+
for _, excludeThis := range lExcludeExt {
335+
if strings.EqualFold(excludeThis, ext) {
336+
if isDebug {
337+
fmt.Printf("ignoring %s\n", path)
338+
}
339+
return nil // skip without error
340+
}
341+
}
342+
populate(ext, path)
343+
}
344+
345+
// continue to the next file
346+
return nil
347+
}

0 commit comments

Comments
 (0)