initial commit

This commit is contained in:
Laura
2025-08-13 03:37:27 +02:00
parent 77c7a5473c
commit 48f6efc216
10 changed files with 417 additions and 0 deletions

70
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,70 @@
name: Build and Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
goos: [windows, linux, darwin]
goarch: [amd64, arm64]
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24.6'
- name: Build ${{ matrix.goos }}_${{ matrix.goarch }}
shell: bash
run: |
mkdir -p build
[[ "${{ matrix.goos }}" == "windows" ]] && EXT=".exe" || EXT=""
OUT="build/idk_${{ github.ref_name }}_${{ matrix.goos }}_${{ matrix.goarch }}${EXT}"
GOOS=${{ matrix.goos }} \
GOARCH=${{ matrix.goarch }} \
CGO_ENABLED=0 \
go build \
-trimpath \
-buildvcs=false \
-o "$OUT" .
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: idk_${{ github.ref_name }}_${{ matrix.goos }}_${{ matrix.goarch }}
path: build/*
release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download all build artifacts
uses: actions/download-artifact@v4
with:
path: ./build
- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:
files: ./build/**
name: "Release ${{ github.ref_name }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
idk.exe
idk

91
answer.go Normal file
View File

@@ -0,0 +1,91 @@
package main
import (
"context"
_ "embed"
"fmt"
"io"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/revrost/go-openrouter"
)
var (
//go:embed prompt.txt
Prompt string
)
func answer(config *Config, question string) (*exec.Cmd, string, error) {
shell, err := resolveShell()
if err != nil {
return nil, "", err
}
available := availableCommands()
client := openrouter.NewClient(config.OpenRouter.ApiKey)
prompt := fmt.Sprintf(
Prompt,
runtime.GOOS,
filepath.Base(shell),
available,
)
request := openrouter.ChatCompletionRequest{
Model: config.OpenRouter.Model,
Messages: []openrouter.ChatCompletionMessage{
openrouter.SystemMessage(prompt),
openrouter.UserMessage(question),
},
Stream: true,
}
stream, err := client.CreateChatCompletionStream(context.Background(), request)
if err != nil {
return nil, "", err
}
defer stream.Close()
defer fmt.Println()
var response strings.Builder
for {
chunk, err := stream.Recv()
if err != nil {
if err == io.EOF {
break
}
return nil, "", err
}
delta := chunk.Choices[0].Delta.Content
if delta != "" {
response.WriteString(delta)
fmt.Print(delta)
}
}
rgx := regexp.MustCompile(`(?m)^\$(.+?)$`)
command := rgx.FindString(response.String())
command = strings.Trim(command, "$ \r\n")
var flag string
if runtime.GOOS == "windows" {
flag = "/C"
} else {
flag = "-c"
}
return exec.Command(shell, flag, command), command, nil
}

66
config.go Normal file
View File

@@ -0,0 +1,66 @@
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/goccy/go-yaml"
)
type Config struct {
OpenRouter struct {
ApiKey string `json:"api_key"`
Model string `json:"model"`
} `json:"openrouter"`
Commands struct {
AskToRun bool `json:"ask_to_run"`
} `json:"commands"`
}
func loadConfig() (*Config, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
var config Config
// defaults
config.OpenRouter.ApiKey = "your-api-key"
config.OpenRouter.Model = "openai/gpt-5-chat"
config.Commands.AskToRun = true
// ~/.idk.yml
path := filepath.Join(home, ".idk.yml")
file, err := os.OpenFile(path, os.O_RDONLY, 0)
if err != nil {
if os.IsNotExist(err) {
b, err := yaml.Marshal(config)
if err != nil {
return nil, err
}
err = os.WriteFile(path, b, 0644)
if err != nil {
return nil, err
}
fmt.Printf("created config at %s\n", path)
os.Exit(1)
}
return nil, err
}
defer file.Close()
err = yaml.NewDecoder(file).Decode(&config)
if err != nil {
return nil, err
}
return &config, nil
}

46
environment.go Normal file
View File

@@ -0,0 +1,46 @@
package main
import (
"os"
"os/exec"
"runtime"
"sort"
"github.com/shirou/gopsutil/v4/process"
)
func resolveShell() (string, error) {
ppid := os.Getppid()
proc, err := process.NewProcess(int32(ppid))
if err != nil {
return "", err
}
return proc.Exe()
}
func availableCommands() []string {
check := []string{"ls", "cat", "rm", "cp", "mv", "ln", "grep", "curl", "wget", "tar", "zip", "unzip"}
switch runtime.GOOS {
case "linux":
check = append(check, "yay", "paru", "apt", "pacman", "dnf", "zypper")
case "windows":
check = append(check, "winget", "choco", "scoop")
}
available := make([]string, 0, len(check))
for _, command := range check {
if _, err := exec.LookPath(command); err != nil {
continue
}
available = append(available, command)
}
sort.Strings(available)
return available
}

24
go.mod Normal file
View File

@@ -0,0 +1,24 @@
module idk
go 1.24.5
require (
github.com/coalaura/getch v0.0.1
github.com/goccy/go-yaml v1.18.0
github.com/revrost/go-openrouter v0.2.1
github.com/shirou/gopsutil/v4 v4.25.7
)
require (
github.com/ebitengine/purego v0.8.4 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/sys v0.35.0 // indirect
)

52
go.sum Normal file
View File

@@ -0,0 +1,52 @@
github.com/coalaura/getch v0.0.1 h1:DvQMtfZ7jtl+RNauLWT08iSZe5SgRrFU4hmV1deJBdU=
github.com/coalaura/getch v0.0.1/go.mod h1:6UU/F2WklDvfo+n81V6g3MqLD17r039UT7hCaxfzkDU=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/revrost/go-openrouter v0.2.1 h1:4BMQ6pgYeEJq9pLl7pFbwnBabmqgUa35hGRnVHqjpA4=
github.com/revrost/go-openrouter v0.2.1/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

60
main.go Normal file
View File

@@ -0,0 +1,60 @@
package main
import (
"fmt"
"os"
"strings"
"github.com/coalaura/getch"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: idk <question>")
os.Exit(1)
}
config, err := loadConfig()
if err != nil {
fmt.Printf("error: %v\n", err)
os.Exit(1)
}
question := strings.Join(os.Args[1:], " ")
cmd, command, err := answer(config, question)
if err != nil {
fmt.Printf("error: %v\n", err)
os.Exit(1)
}
if command == "" || !config.Commands.AskToRun {
return
}
fmt.Print("\n> run command? [y/n]: ")
ch, err := getch.GetChar()
if err != nil {
fmt.Printf("error: %v\n", err)
os.Exit(1)
}
if ch == 0x03 || ch == 'n' || ch == 'N' {
fmt.Println("no")
return
}
fmt.Println("yes")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
}

1
prompt.txt Normal file
View File

@@ -0,0 +1 @@
You are a concise CLI assistant. The user's environment is: os=%q, shell=%q. The following commands are available: %q. Only use these commands if they are the most appropriate for the task, not just because they are available. Do not prepend the shell unless it is required. Respond with exactly one valid command for this environment, starting with "$ " followed by the command on the first line. On subsequent lines, indent exactly two spaces and give a short explanation of what the command does. Briefly explain the command and include important or non-obvious parameters. Keep the response short. Do not include multiple commands unless absolutely necessary. Do not use any other formatting or extra text.

5
run.cmd Normal file
View File

@@ -0,0 +1,5 @@
@echo off
go build -o idk.exe
idk.exe %*