summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md94
-rw-r--r--PLAN.md26
-rwxr-xr-xgosbin0 -> 10792292 bytes
-rwxr-xr-xgoscbin0 -> 10792292 bytes
-rw-r--r--gosdir/db/platforms/noop/testing.share:noop.extracted.txt.20250517-174027.posted1
-rw-r--r--gosdir/db/trashbin/testing.share:noop.extracted.txt.20250517-174017.trash1
-rw-r--r--hexai-lsp.log100
-rw-r--r--internal/platforms/linkedin/linkedin.go27
-rw-r--r--internal/platforms/linkedin/oauth2/oauth2.go8
-rw-r--r--internal/platforms/linkedin/preview.go6
-rw-r--r--internal/platforms/linkedin/testdata/fuzz/FuzzLinkedInURLExtract/5ffd3a9d584973772
-rw-r--r--internal/platforms/linkedin/testdata/fuzz/FuzzLinkedInURLExtract/771e938e4458e9832
-rw-r--r--internal/platforms/linkedin/testdata/fuzz/FuzzLinkedInURLExtract/b0118fa98fb2891d2
-rw-r--r--internal/version.go2
-rw-r--r--run.sh10
15 files changed, 275 insertions, 6 deletions
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..1d01ea4
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,94 @@
+# AGENTS.md
+
+## Project Overview
+
+Gos (Go Social Media) is a Go-based command-line tool for scheduling and managing social media posts to Mastodon and LinkedIn. It serves as a replacement for Buffer.com, allowing users to queue and schedule posts from the terminal.
+
+## Project Structure
+
+```
+gos/
+├── cmd/
+│ ├── gos/ # Main gos binary
+│ └── gosc/ # Gos Composer binary (quick post composition)
+├── internal/
+│ ├── colour/ # Terminal color utilities
+│ ├── config/ # Configuration management
+│ ├── entry/ # Post entry handling
+│ ├── oi/ # Output/input utilities
+│ ├── platforms/ # Social media platform implementations
+│ │ ├── linkedin/
+│ │ ├── mastodon/
+│ │ ├── noop/
+│ │ └── platform.go
+│ ├── prompt/ # User prompts
+│ ├── queue/ # Message queue management
+│ ├── schedule/ # Posting schedule logic
+│ ├── summary/ # Gemini gemtext summary generation
+│ ├── table/ # Table output formatting
+│ ├── tags/ # Share tag parsing
+│ ├── timestamp/ # Timestamp utilities
+│ ├── main.go
+│ ├── run.go
+│ └── version.go
+├── docs/ # Documentation and images
+├── examples/ # Example files
+└── gosdir/ # Example gos directory structure
+```
+
+## Build System
+
+This project uses [Mage](https://magefile.org/) for build automation.
+
+### Commands
+
+| Command | Description |
+|---------|-------------|
+| `mage` or `mage build` | Build `gos` and `gosc` binaries |
+| `mage install` | Build and install binaries to `~/go/bin` |
+| `mage clean` | Remove built binaries |
+| `mage test` | Run all tests |
+| `mage fuzz` | Run fuzz tests (10s) |
+| `mage lint` | Run golangci-lint |
+| `mage vet` | Run `go vet` |
+| `mage dev` | Run tests, vet, lint, then build with race detector |
+| `mage devInstall` | Install `gopls` and `golangci-lint` |
+
+### Before Committing
+
+Run `mage dev` to ensure tests pass, vet and lint checks succeed, and the build completes with race detection.
+
+## Testing
+
+- Tests use the standard Go testing framework
+- Test files follow the `*_test.go` naming convention
+- Run tests: `mage test` or `go test -v ./...`
+- Run fuzz tests: `mage fuzz` or `go test ./internal/entry/ -fuzz=FuzzExtractURLs -fuzztime=10s`
+
+## Code Conventions
+
+- **Go version**: 1.23+
+- **Module path**: `codeberg.org/snonux/gos`
+- **Package layout**: Internal packages under `internal/`, commands under `cmd/`
+- **Error handling**: Standard Go error handling patterns
+- **Dependencies**: Minimal external dependencies (fatih/color, golang.org/x packages)
+
+### Go Coding Practices
+
+Follow the practices defined in `/home/paul/git/conf/snippets/go/go-projects/go-projects.md`:
+
+## Linting
+
+Uses `golangci-lint`. Install with:
+```bash
+go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
+```
+
+Or run `mage devInstall`.
+
+## Key Dependencies
+
+- `github.com/fatih/color` - Terminal colors
+- `golang.org/x/oauth2` - OAuth2 for LinkedIn
+- `golang.org/x/net` - HTML parsing for LinkedIn previews
+- `github.com/magefile/mage` - Build automation
diff --git a/PLAN.md b/PLAN.md
new file mode 100644
index 0000000..26caa90
--- /dev/null
+++ b/PLAN.md
@@ -0,0 +1,26 @@
+# Plan
+
+- Add a new flag `--stats` which would only print out the stats for all social networks, but do nothing else.
+
+## Implementation Outline for `--stats` flag: (ALL DONE)
+
+1. **Modify `internal/main.go`**: (DONE)
+ * **Define the `--stats` flag**: Add `statsOnly := flag.Bool("stats", false, "Print statistics for all social networks and exit")` near other flag definitions.
+ * **Populate `config.Args`**: Assign `*statsOnly` to `args.StatsOnly` after `flag.Parse()`.
+ * **Conditional logic**: After `flag.Parse()` and before calling `run(ctx, args)`, add a check:
+ ```go
+ if args.StatsOnly {
+ // Call the new function to print all stats
+ schedule.PrintAllStats(args)
+ return // Exit after printing stats
+ }
+ ```
+
+2. **Modify `internal/config/args.go`**: (DONE)
+ * Add `StatsOnly bool` to the `Args` struct.
+
+3. **Modify `internal/schedule/stats.go`**: (DONE)
+ * **Create a new public function `PrintAllStats(args config.Args)`**:
+ * This function will iterate through `args.Platforms`.
+ * For each platform, it will call `newStats` to gather the statistics.
+ * Then, it will call `stats.RenderTable` to display the statistics for that platform. \ No newline at end of file
diff --git a/gos b/gos
new file mode 100755
index 0000000..e2420c7
--- /dev/null
+++ b/gos
Binary files differ
diff --git a/gosc b/gosc
new file mode 100755
index 0000000..9678007
--- /dev/null
+++ b/gosc
Binary files differ
diff --git a/gosdir/db/platforms/noop/testing.share:noop.extracted.txt.20250517-174027.posted b/gosdir/db/platforms/noop/testing.share:noop.extracted.txt.20250517-174027.posted
new file mode 100644
index 0000000..cb9d77f
--- /dev/null
+++ b/gosdir/db/platforms/noop/testing.share:noop.extracted.txt.20250517-174027.posted
@@ -0,0 +1 @@
+fxoobarbaz #testing \ No newline at end of file
diff --git a/gosdir/db/trashbin/testing.share:noop.extracted.txt.20250517-174017.trash b/gosdir/db/trashbin/testing.share:noop.extracted.txt.20250517-174017.trash
new file mode 100644
index 0000000..cb9d77f
--- /dev/null
+++ b/gosdir/db/trashbin/testing.share:noop.extracted.txt.20250517-174017.trash
@@ -0,0 +1 @@
+fxoobarbaz #testing \ No newline at end of file
diff --git a/hexai-lsp.log b/hexai-lsp.log
new file mode 100644
index 0000000..21cb1bb
--- /dev/null
+++ b/hexai-lsp.log
@@ -0,0 +1,100 @@
+2025/09/04 15:40:17 hexai-lsp llm/openai msg[1] role=user size=339 preview=Provide the next likely code to insert at the cursor.
+File: file:///home/paul/git/hexai/PROJECTSTATU…
+2025/09/04 15:40:17 hexai-lsp llm/openai msg[2] role=user size=3342 preview=Additional context:
+# Ideas
+
+## Code quality
+
+* [/] TODO's in the code to be addressed
+* [/] No more…
+2025/09/04 15:40:17 hexai-lsp llm/openai POST https://api.openai.com/v1/chat/completions
+2025/09/04 15:40:18 hexai-lsp lsp completion trigger kind=1 char="" uri=file:///home/paul/git/hexai/PROJECTSTATUS.md line=15 char=75
+2025/09/04 15:40:18 hexai-lsp lsp completion ctx uri=file:///home/paul/git/hexai/PROJECTSTATUS.md line=15 char=75 above="" current="* [ ] Modify the LLM triggers to be more consistenc. E.g. use >>text here>" below="* [X] Include unit test coverage reports" function=""
+2025/09/04 15:40:18 hexai-lsp llm/openai success choice=0 finish=stop size=22 preview=nt with other triggers duration=631.487672ms
+2025/09/04 15:40:18 hexai-lsp lsp llm stats reqs=22 avg_sent=3915 avg_recv=71 sent_total=86132 recv_total=1431 rpm=3.06 sent_per_min=11993 recv_per_min=199
+2025/09/04 15:40:18 hexai-lsp lsp completion llm=requesting model=gpt-4.1
+2025/09/04 15:40:18 hexai-lsp llm/openai chat start model=gpt-4.1 temp=0.20 max_tokens=4000 stop=0 messages=3
+2025/09/04 15:40:18 hexai-lsp llm/openai msg[0] role=system size=306 preview=You are a terse code completion engine. Return only the code to insert, no surrounding prose or back…
+2025/09/04 15:40:18 hexai-lsp llm/openai msg[1] role=user size=340 preview=Provide the next likely code to insert at the cursor.
+File: file:///home/paul/git/hexai/PROJECTSTATU…
+2025/09/04 15:40:18 hexai-lsp llm/openai msg[2] role=user size=3343 preview=Additional context:
+# Ideas
+
+## Code quality
+
+* [/] TODO's in the code to be addressed
+* [/] No more…
+2025/09/04 15:40:18 hexai-lsp llm/openai POST https://api.openai.com/v1/chat/completions
+2025/09/04 15:40:18 hexai-lsp lsp completion trigger kind=1 char="" uri=file:///home/paul/git/hexai/PROJECTSTATUS.md line=15 char=78
+2025/09/04 15:40:18 hexai-lsp lsp completion ctx uri=file:///home/paul/git/hexai/PROJECTSTATUS.md line=15 char=78 above="" current="* [ ] Modify the LLM triggers to be more consistenc. E.g. use >>text here> or" below="* [X] Include unit test coverage reports" function=""
+2025/09/04 15:40:19 hexai-lsp lsp completion llm=requesting model=gpt-4.1
+2025/09/04 15:40:19 hexai-lsp llm/openai chat start model=gpt-4.1 temp=0.20 max_tokens=4000 stop=0 messages=3
+2025/09/04 15:40:19 hexai-lsp llm/openai msg[0] role=system size=306 preview=You are a terse code completion engine. Return only the code to insert, no surrounding prose or back…
+2025/09/04 15:40:19 hexai-lsp llm/openai msg[1] role=user size=343 preview=Provide the next likely code to insert at the cursor.
+File: file:///home/paul/git/hexai/PROJECTSTATU…
+2025/09/04 15:40:19 hexai-lsp llm/openai msg[2] role=user size=3346 preview=Additional context:
+# Ideas
+
+## Code quality
+
+* [/] TODO's in the code to be addressed
+* [/] No more…
+2025/09/04 15:40:19 hexai-lsp llm/openai POST https://api.openai.com/v1/chat/completions
+2025/09/04 15:40:19 hexai-lsp lsp completion trigger kind=1 char="" uri=file:///home/paul/git/hexai/PROJECTSTATUS.md line=15 char=79
+2025/09/04 15:40:19 hexai-lsp lsp completion ctx uri=file:///home/paul/git/hexai/PROJECTSTATUS.md line=15 char=79 above="" current="* [ ] Modify the LLM triggers to be more consistenc. E.g. use >>text here> or >" below="* [X] Include unit test coverage reports" function=""
+2025/09/04 15:40:19 hexai-lsp llm/openai success choice=0 finish=stop size=91 preview=* [ ] Audit all LLM trigger points for consistency and document the standard trigger format duration=1.796370662s
+2025/09/04 15:40:19 hexai-lsp lsp llm stats reqs=24 avg_sent=3921 avg_recv=72 sent_total=94124 recv_total=1522 rpm=3.33 sent_per_min=13077 recv_per_min=211
+2025/09/04 15:40:19 hexai-lsp lsp completion llm=requesting model=gpt-4.1
+2025/09/04 15:40:19 hexai-lsp llm/openai chat start model=gpt-4.1 temp=0.20 max_tokens=4000 stop=0 messages=3
+2025/09/04 15:40:19 hexai-lsp llm/openai msg[0] role=system size=306 preview=You are a terse code completion engine. Return only the code to insert, no surrounding prose or back…
+2025/09/04 15:40:19 hexai-lsp llm/openai msg[1] role=user size=344 preview=Provide the next likely code to insert at the cursor.
+File: file:///home/paul/git/hexai/PROJECTSTATU…
+2025/09/04 15:40:19 hexai-lsp llm/openai msg[2] role=user size=3347 preview=Additional context:
+# Ideas
+
+## Code quality
+
+* [/] TODO's in the code to be addressed
+* [/] No more…
+2025/09/04 15:40:19 hexai-lsp llm/openai POST https://api.openai.com/v1/chat/completions
+2025/09/04 15:40:19 hexai-lsp llm/openai success choice=0 finish=stop size=45 preview=>text here> throughout for inline completions duration=807.205456ms
+2025/09/04 15:40:19 hexai-lsp llm/openai success choice=0 finish=stop size=93 preview=* [ ] Standardize all LLM triggers to use a consistent delimiter format, e.g. `>>text here>>` duration=1.36528963s
+2025/09/04 15:40:19 hexai-lsp lsp llm stats reqs=24 avg_sent=3921 avg_recv=71 sent_total=94124 recv_total=1567 rpm=3.33 sent_per_min=13062 recv_per_min=217
+2025/09/04 15:40:19 hexai-lsp lsp llm stats reqs=24 avg_sent=3921 avg_recv=72 sent_total=94124 recv_total=1660 rpm=3.33 sent_per_min=13062 recv_per_min=230
+2025/09/04 15:40:20 hexai-lsp llm/openai success choice=0 finish=stop size=81 preview=* [ ] Audit all trigger patterns for consistency and document the standard format duration=666.442236ms
+2025/09/04 15:40:20 hexai-lsp lsp llm stats reqs=24 avg_sent=3921 avg_recv=72 sent_total=94124 recv_total=1741 rpm=3.33 sent_per_min=13056 recv_per_min=242
+2025/09/04 15:46:43 hexai-lsp lsp llm enabled provider=openai model=gpt-4.1
+2025/09/04 15:46:43 hexai-lsp lsp client initialized
+2025/09/04 15:46:44 hexai-lsp lsp completion trigger kind=1 char="" uri=file:///home/paul/Notes/Listen/Urlaub.md line=0 char=1
+2025/09/04 15:46:44 hexai-lsp lsp completion ctx uri=file:///home/paul/Notes/Listen/Urlaub.md line=0 char=1 above="" current="#" below="" function=""
+2025/09/04 15:46:45 hexai-lsp lsp completion llm=requesting model=gpt-4.1
+2025/09/04 15:46:45 hexai-lsp llm/openai chat start model=gpt-4.1 temp=0.20 max_tokens=4000 stop=0 messages=3
+2025/09/04 15:46:45 hexai-lsp llm/openai msg[0] role=system size=306 preview=You are a terse code completion engine. Return only the code to insert, no surrounding prose or back…
+2025/09/04 15:46:45 hexai-lsp llm/openai msg[1] role=user size=221 preview=Provide the next likely code to insert at the cursor.
+File: file:///home/paul/Notes/Listen/Urlaub.md…
+2025/09/04 15:46:45 hexai-lsp llm/openai msg[2] role=user size=22 preview=Additional context:
+#
+
+2025/09/04 15:46:45 hexai-lsp llm/openai POST https://api.openai.com/v1/chat/completions
+2025/09/04 15:46:46 hexai-lsp llm/openai success choice=0 finish=stop size=6 preview=Urlaub duration=970.760029ms
+2025/09/04 15:46:46 hexai-lsp lsp llm stats reqs=1 avg_sent=549 avg_recv=6 sent_total=549 recv_total=6 rpm=27.74 sent_per_min=15228 recv_per_min=166
+2025/09/04 15:46:59 hexai-lsp lsp llm enabled provider=openai model=gpt-4.1
+2025/09/04 15:46:59 hexai-lsp lsp client initialized
+2025/09/04 15:47:02 hexai-lsp lsp completion trigger kind=1 char="" uri=file:///home/paul/Notes/Listen/Vacation.md line=0 char=0
+2025/09/04 15:47:02 hexai-lsp lsp completion ctx uri=file:///home/paul/Notes/Listen/Vacation.md line=0 char=0 above="" current="" below="" function=""
+2025/09/04 15:47:02 hexai-lsp lsp completion llm=requesting model=gpt-4.1
+2025/09/04 15:47:02 hexai-lsp llm/openai chat start model=gpt-4.1 temp=0.20 max_tokens=4000 stop=0 messages=3
+2025/09/04 15:47:02 hexai-lsp llm/openai msg[0] role=system size=306 preview=You are a terse code completion engine. Return only the code to insert, no surrounding prose or back…
+2025/09/04 15:47:02 hexai-lsp llm/openai msg[1] role=user size=222 preview=Provide the next likely code to insert at the cursor.
+File: file:///home/paul/Notes/Listen/Vacation.…
+2025/09/04 15:47:02 hexai-lsp llm/openai msg[2] role=user size=21 preview=Additional context:
+
+
+2025/09/04 15:47:02 hexai-lsp llm/openai POST https://api.openai.com/v1/chat/completions
+2025/09/04 15:47:04 hexai-lsp llm/openai success choice=0 finish=stop size=126 preview=- Book flights
+- Reserve hotel
+- Create itinerary
+- Pack essentials
+- Arrange transportation
+- Set v… duration=1.350494698s
+2025/09/04 15:47:04 hexai-lsp lsp llm stats reqs=1 avg_sent=549 avg_recv=126 sent_total=549 recv_total=126 rpm=14.55 sent_per_min=7987 recv_per_min=1833
diff --git a/internal/platforms/linkedin/linkedin.go b/internal/platforms/linkedin/linkedin.go
index 11291fb..16688d5 100644
--- a/internal/platforms/linkedin/linkedin.go
+++ b/internal/platforms/linkedin/linkedin.go
@@ -27,9 +27,10 @@ func addCommonHeaders(req *http.Request, accessToken, liVersion string) {
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-RestLi-Protocol-Version", "2.0.0")
- if liVersion != "" {
- req.Header.Set("LinkedIn-Version", liVersion)
+ if liVersion == "" {
+ liVersion = "202601" // Default to latest stable version
}
+ req.Header.Set("LinkedIn-Version", liVersion)
}
func Post(ctx context.Context, args config.Args, sizeLimit int, en entry.Entry) error {
@@ -147,7 +148,7 @@ func postMessageToLinkedInAPI(ctx context.Context, personID, accessToken, conten
} else if resp.StatusCode == http.StatusUpgradeRequired {
// 426 often indicates a non-active LinkedIn-Version header.
// Provide a clear hint to configure a valid version.
- err = fmt.Errorf("%w; LinkedIn API version likely inactive. Set an active 'LinkedInVersion' in config (e.g. 202502) or remove to use default. Response: %s", err, string(body))
+ err = fmt.Errorf("%w; LinkedIn API version likely inactive. Set an active 'LinkedInVersion' in config (e.g. 202601) or remove to use default. Response: %s", err, string(body))
}
}
return err
@@ -191,6 +192,24 @@ func initializeImageUpload(ctx context.Context, personURN, accessToken string, l
}
defer resp.Body.Close()
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", "", fmt.Errorf("error reading response: %w", err)
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ err := fmt.Errorf("image upload initialization failed. Status: %s\n%s\n",
+ resp.Status, string(body))
+ if resp.StatusCode == http.StatusUnauthorized {
+ err = errors.Join(err, errUnauthorized)
+ } else if resp.StatusCode == http.StatusUpgradeRequired {
+ // 426 often indicates a non-active LinkedIn-Version header.
+ // Provide a clear hint to configure a valid version.
+ err = fmt.Errorf("%w; LinkedIn API version likely inactive. Set an active 'LinkedInVersion' in config (e.g. 202601) or remove to use default. Response: %s", err, string(body))
+ }
+ return "", "", err
+ }
+
type InitializeUploadResponse struct {
Value struct {
UploadURL string `json:"uploadUrl"`
@@ -198,7 +217,7 @@ func initializeImageUpload(ctx context.Context, personURN, accessToken string, l
} `json:"value"`
}
var response InitializeUploadResponse
- if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
+ if err := json.Unmarshal(body, &response); err != nil {
return "", "", fmt.Errorf("error decoding response: %w", err)
}
diff --git a/internal/platforms/linkedin/oauth2/oauth2.go b/internal/platforms/linkedin/oauth2/oauth2.go
index 8fe25ab..45cb9b7 100644
--- a/internal/platforms/linkedin/oauth2/oauth2.go
+++ b/internal/platforms/linkedin/oauth2/oauth2.go
@@ -93,7 +93,13 @@ func oauthCallbackHandler(w http.ResponseWriter, r *http.Request) {
func LinkedInCreds(ctx context.Context, args config.Args) (string, string, error) {
conf := args.Config
if conf.LinkedInAccessToken != "" && conf.LinkedInPersonID != "" {
- return conf.LinkedInPersonID, conf.LinkedInAccessToken, nil
+ // Validate cached token before using it
+ token := &oauth2.Token{AccessToken: conf.LinkedInAccessToken}
+ if _, err := getOauthPersonID(token); err == nil {
+ return conf.LinkedInPersonID, conf.LinkedInAccessToken, nil
+ }
+ // Cached token is invalid, clear it to trigger re-auth
+ conf.LinkedInAccessToken = ""
}
oauthConfig = &oauth2.Config{
diff --git a/internal/platforms/linkedin/preview.go b/internal/platforms/linkedin/preview.go
index a583c7a..125a4bf 100644
--- a/internal/platforms/linkedin/preview.go
+++ b/internal/platforms/linkedin/preview.go
@@ -73,6 +73,12 @@ func (p preview) Thumbnail() (string, bool) {
}
func (p preview) DownloadImage(destPath string) (string, error) {
+ // Skip data URIs - they can't be downloaded and don't provide meaningful images
+ if u, err := url.Parse(p.thumbnailURL); err == nil && u.Scheme == "data" {
+ colour.Infoln("Skipping data URI image, using article metadata instead")
+ return "", nil
+ }
+
if err := oi.EnsureDir(destPath); err != nil {
return "", err
}
diff --git a/internal/platforms/linkedin/testdata/fuzz/FuzzLinkedInURLExtract/5ffd3a9d58497377 b/internal/platforms/linkedin/testdata/fuzz/FuzzLinkedInURLExtract/5ffd3a9d58497377
new file mode 100644
index 0000000..2eecaf6
--- /dev/null
+++ b/internal/platforms/linkedin/testdata/fuzz/FuzzLinkedInURLExtract/5ffd3a9d58497377
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("\n\n\n")
diff --git a/internal/platforms/linkedin/testdata/fuzz/FuzzLinkedInURLExtract/771e938e4458e983 b/internal/platforms/linkedin/testdata/fuzz/FuzzLinkedInURLExtract/771e938e4458e983
new file mode 100644
index 0000000..ee3f339
--- /dev/null
+++ b/internal/platforms/linkedin/testdata/fuzz/FuzzLinkedInURLExtract/771e938e4458e983
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("0")
diff --git a/internal/platforms/linkedin/testdata/fuzz/FuzzLinkedInURLExtract/b0118fa98fb2891d b/internal/platforms/linkedin/testdata/fuzz/FuzzLinkedInURLExtract/b0118fa98fb2891d
new file mode 100644
index 0000000..190e0f4
--- /dev/null
+++ b/internal/platforms/linkedin/testdata/fuzz/FuzzLinkedInURLExtract/b0118fa98fb2891d
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("0 0")
diff --git a/internal/version.go b/internal/version.go
index f54de26..3980280 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -6,7 +6,7 @@ import (
"codeberg.org/snonux/gos/internal/table"
)
-const versionStr = "v1.2.3"
+const versionStr = "v1.2.4"
func printVersion() {
table.New().
diff --git a/run.sh b/run.sh
new file mode 100644
index 0000000..693cdd4
--- /dev/null
+++ b/run.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+# Pull the pre-built image
+docker pull ghcr.io/zeeno-atl/claude-code:latest
+
+# Run with current directory
+docker run -it --rm -v "$(pwd):/app" ghcr.io/zeeno-atl/claude-code:latest
+
+# Run with API key
+docker run -it --rm -v "$(pwd):/app" -e ANTHROPIC_API_KEY="your_api_key" ghcr.io/zeeno-atl/claude-code:latest