{"id":17437347,"url":"https://github.com/blixt/first-aid","last_synced_at":"2025-12-30T05:04:53.340Z","repository":{"id":241438118,"uuid":"805846793","full_name":"blixt/first-aid","owner":"blixt","description":"An annoying but helpful CLI called First Aid.","archived":false,"fork":false,"pushed_at":"2024-11-07T22:59:52.000Z","size":263,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-24T06:55:39.233Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/blixt.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-05-25T16:21:19.000Z","updated_at":"2024-11-07T22:59:56.000Z","dependencies_parsed_at":"2024-11-07T23:28:08.576Z","dependency_job_id":"183a2897-631d-4380-9f06-8f0281d837b2","html_url":"https://github.com/blixt/first-aid","commit_stats":null,"previous_names":["blixt/first-aid"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blixt%2Ffirst-aid","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blixt%2Ffirst-aid/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blixt%2Ffirst-aid/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blixt%2Ffirst-aid/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/blixt","download_url":"https://codeload.github.com/blixt/first-aid/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241360981,"owners_count":19950372,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-17T11:46:33.404Z","updated_at":"2025-12-30T05:04:53.335Z","avatar_url":"https://github.com/blixt.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# First Aid\n\nA little help from a reluctant AI on the command line.\n\n\u003e [!CAUTION]\n\u003e This tool gives an AI access to run commands and code on your computer.\n\u003e Furthermore, it’s sending everything it sees to OpenAI’s servers.\n\u003e\n\u003e If either of these things make you uncomfortable, don’t run this tool. I hope the code can be interesting nonetheless!\n\n## ToC\n\n- [First Aid](#first-aid)\n  - [ToC](#toc)\n  - [Usage](#usage)\n  - [Intended use cases for this tool](#intended-use-cases-for-this-tool)\n  - [Roadmap](#roadmap)\n  - [Tool ideas](#tool-ideas)\n  - [For developers](#for-developers)\n    - [The `tools` package](#the-tools-package)\n    - [Tools that return images](#tools-that-return-images)\n    - [The `writer`, `serif`, and `spinner` packages](#the-writer-serif-and-spinner-packages)\n    - [The `llms` package](#the-llms-package)\n  - [A quote from the tool itself](#a-quote-from-the-tool-itself)\n\n## Usage\n\n```sh\ngit clone https://github.com/blixt/first-aid.git\ncd first-aid\ngo mod download\nOPENAI_API_KEY=... go run main.go\n```\n\nYou can also `go install .` to add `first-aid` to your PATH if you’re so inclined.\n\n## Intended use cases for this tool\n\nThis tool is an exploration of how automation can be made more useful for anyone\nin day-to-day tasks. Example tasks:\n\n“Write a nice commit message for my changes in this repo”\n\n“Put a markdown table of a summary of files in this directory into my clipboard”\n\n“What does this error mean?” → Take screenshot and analyze the problem\n\n*(From phone)* “What’s the last page I looked at on my computer?”\n\n*(From phone)* “Did I leave my keys in the apartment?” → Remote control a camera\n\n## Roadmap\n\nThe development goals of this tool are roughly:\n\n- [ ] Have fun\n- [ ] Create a codebase that can be helpful to people building AI projects\n- [ ] Make the tool capable of helping with any computer related issue\n- [ ] Implement cross-device support (ask about your computer from your phone)\n- [ ] Add in multimodal flows (ability to see and hear)\n- [ ] Play with realtime, async, and parallel flows\n- [ ] Support local models and/or other LLM providers\n- [ ] Sandboxing (e.g. Docker) for security and privacy\n- [ ] Introduce ways to clear the context window (effective memory)\n- [ ] Add a server layer that can run / synchronize multiple instances of an agent\n- [ ] Solve for session based tools, such as long-running command line tools\n- [ ] Answer the question of asking the LLM to write a script vs. use tools\n  - Or both... maybe?\n\n## Tool ideas\n\n- [ ] Control Chrome via extension\n  - See list of open tabs\n  - Activate tab\n  - Screenshot tab\n  - Click/type in tab\n- [ ] Schedule a task for later\n  - Something like “check the weather tomorrow morning and speak it out loud”\n  - Also includes repeating tasks like “every day at 2pm”\n\n## For developers\n\nI’m aiming to make this codebase approachable and to contain little pieces of\ncode that can be helpful to other people building AI related tools in Go. So\nbelow I’ll point at a few parts of the codebase I think could be useful.\n\n### The `tools` package\n\n\u003e [!TIP]\n\u003e This code has been moved into a new repository: https://github.com/blixt/go-llms\n\nThe `tools` package makes it very easy to create tools for the LLM to use. The\nmain goal was the ergonomy of defining a tool. Here’s an example of a tool:\n\n```go\npackage mypkg\n\nimport (\n    \"fmt\"\n    \"os/exec\"\n\n    \"github.com/flitsinc/go-llms/tools\"\n)\n\ntype RunPowerShellCmdParams struct {\n    Command string `json:\"command\" description:\"The PowerShell command to run\"`\n}\n\nvar RunPowerShellCmd = tools.Func(\n    \"Run PowerShell command\",\n    \"Run a shell command on the user's computer (a Windows machine) and return the output\",\n    \"run_powershell_cmd\",\n    func(r tools.Runner, p RunShellCmdParams) tools.Result {\n        // Run the PowerShell command and capture the output or error.\n        cmd := exec.Command(\"powershell\", \"-Command\", p.Command)\n        output, err := cmd.CombinedOutput() // Combines both STDOUT and STDERR\n        if err != nil {\n            return tools.Error(p.Command, fmt.Errorf(\"%w: %s\", err, firstLineBytes(output)))\n        }\n        return tools.Success(p.Command, map[string]any{\"output\": string(output)})\n    })\n```\n\nThis can now be turned into a JSON schema (which is what most LLM APIs accept\nfor tool use) by calling `RunPowerShellCmd.Schema()`.\n\nTo run the tool with the data received from the LLM:\n\n```go\narguments := json.RawMessage(`{\"command\":\"Get-ComputerInfo\"}`)\nresult := RunPowerShellCmd.Run(tools.NopRunner, arguments)\n```\n\nThis will parse the JSON into the parameters type, validate it, and call the\nfunction defined above with the correct parameters.\n\nThe API has been optimized to be able to show human readable representations of\nthe tool before, during, and after running it, which explains the extra `label`\nvalue and the `tools.Runner` interface.\n\nObviously you usually have more than one tool, and for this we have toolboxes:\n\n```go\ntoolbox := tools.Box(\n    mypkg.ListFiles,\n    mypkg.RunPowerShellCmd,\n    mypkg.RunPython,\n)\n\nschema := openai.Tools(toolbox) // Can be used directly for \"tools\" in OpenAI's API\n\n// The function name and JSON arguments can be used directly from \"tool_calls\"\narguments := json.RawMessage(`{\"code\":\"print('hi')\"}`)\nresult := toolbox.Run(tools.NopRunner, \"run_python\", arguments)\n```\n\n### Tools that return images\n\nOne thing that OpenAI’s API strangely does not allow is a tool returning an\nimage. It makes a lot of sense that with a multimodal LLM you will want to\nprocess images not directly provided by the user but also created by a tool\n(such as a tool that browses a web page and returns a screenshot to the LLM).\n\nTo work around this, I fake a message from the user (because unlike what the\ndocumentation says, GPT-4o does not support images in \"assistant\" or \"system\"\nmessages either) in addition to the tool result, and make sure to mention the\nsame filename in both so that the LLM will associate the results.\n\nThis is the API for a tool to return an image:\n\n```go\nvar rb tools.ResultBuilder\nrb.AddImage(screenshotPath)\nreturn rb.Success(\n    \"Take screenshot\",\n    map[string]any{\"screenshotFileName\": filepath.Base(screenshotPath)},\n)\n```\n\nNote that for now the tool result itself points out this workaround.\n\n### The `writer`, `serif`, and `spinner` packages\n\nPart of having fun with this project was giving the command line tool a bit more\npersonality. Partially, by making it unnecessarily sarcastic and bleak, but also\nby making it type character by character with a serif font which makes it stand\nout on the command line. The formatting is done in a very simple way using the\n`serif` package. It was built to do the same thing those Twitter font generators\ndo, but with some additional support for international letters (ç, ü, and so on)\nand numbers. It also supports italic, bold, and italic+bold variations.\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n\n    \"github.com/blixt/first-aid/serif\"\n)\n\nfunc main() {\n    fmt.Println(serif.Format(\"Étoiles dans l’été, rêves enchantés.\"))\n    // Same as:\n    fmt.Println(\"𝙴́𝚝𝚘𝚒𝚕𝚎𝚜 𝚍𝚊𝚗𝚜 𝚕’𝚎́𝚝𝚎́, 𝚛𝚎̂𝚟𝚎𝚜 𝚎𝚗𝚌𝚑𝚊𝚗𝚝𝚎́𝚜.\")\n}\n```\n\nThe `writer` package was built to be used for a block of output that is written\ncharacter by character using the above serif formatting. Over time it also grew\nto support interweaving tasks with an associated label and spinner, where the\nlabel can be updated over time until the task is complete. This allows us to\nmake tool use by the LLM look like just another part of its continuous stream of\noutput, much like the UI of ChatGPT.\n\nFor ease of use with `fmt`, it implements `io.Writer`:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"time\"\n\n    \"github.com/blixt/first-aid/writer\"\n)\n\nfunc main() {\n    w := writer.New()\n    go func() {\n        defer w.Done()\n        fmt.Fprintln(w, \"Let me just think about that for a few seconds...\")\n        fmt.Fprintln(w, \"\")\n        w.SetTask(\"Thinking...\") // Starts a spinner on the current line\n        time.Sleep(4*time.Second)\n        w.SetTask(\"\") // This resets the current line to be empty\n        fmt.Fprintln(w, \"✅ Done thinking!\")\n        fmt.Fprintln(w, \"\")\n        fmt.Fprintln(w, \"Wait, what were we doing?\")\n    }()\n    w.StartAndWait()\n}\n```\n\nThe speed increases if the unwritten content gets too long.\n\n### The `llms` package\n\n\u003e [!TIP]\n\u003e This code has been moved into a new repository: https://github.com/blixt/go-llms\n\nProbably the least interesting package, it just implements a loop of sending\nmessages to an LLM, and if the LLM returns tool calls, call the LLM once more\nwith the results of those tool calls.\n\n```go\nfunc main() {\n    model := openai.New(os.Getenv(\"OPENAI_API_KEY\"), \"gpt-4o\")\n    ai := llms.New(\n        model,\n        mypkg.ListFiles,\n        mypkg.RunPowerShellCmd,\n        mypkg.RunPython,\n    )\n\n    // System prompt is dynamic so it can always be up-to-date.\n    ai.SystemPrompt = func() content.Content {\n        return content.Textf(\"You're a helpful bot. The time is %s.\", time.Now().Format(time.RFC1123))\n    }\n\n    // Chat returns a channel of updates.\n    for update := range ai.Chat(\"Give me a random number\") {\n        switch update := update.(type) {\n        case llms.ErrorUpdate:\n            panic(update.Error)\n        case llms.TextUpdate:\n            // Received for each chunk of text from the LLM.\n            fmt.Print(update.Text)\n        case llms.ToolStartUpdate:\n            // Received the moment the LLM streams that it intends to use a tool.\n            fmt.Printf(\"(%s: \", update.Tool.Label())\n        case llms.ToolDoneUpdate:\n            // Received after the LLM finished sending arguments and the tool ran.\n            fmt.Printf(\"%s)\\n\", update.Result.Label())\n        }\n    }\n}\n```\n\nExample output:\n\n```text\n(Run Python: `import random` (+1 line))\nHere's a random number for you: **48**.\n```\n\nIf you want to use Google’s Gemini 1.5 Pro instead, it’s easy:\n\n```go\nmodel := google.New(\"gemini-1.5-pro-001\").\n    WithGeminiAPI(os.Getenv(\"GOOGLE_API_KEY\"))\n```\n\nYou can use `WithVertexAI(…)` instead if you have a project set up for it.\n\n## A quote from the tool itself\n\nI asked the tool to update this README with its thoughts:\n\n\u003e There's nothing like a command line tool with a sarcastic AI to make you\n\u003e question all your life choices. Enjoy automating the mundane, because who\n\u003e wouldn't want their computer mocking them while getting things done? Cheers to\n\u003e that.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblixt%2Ffirst-aid","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fblixt%2Ffirst-aid","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblixt%2Ffirst-aid/lists"}