hunkify 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ba6dab53be5bab36d05e9535bc0590a858638840987a0a3c9ee37253279c1fa
4
- data.tar.gz: 30d6ac6b5f76e1ec04d04d2815f733c9bd514393d9cbe5823b0670107a0bf875
3
+ metadata.gz: 488c3d31406ccddec198dfa1f48e26ae6fe88ac4830e4dc3152ed8378a718a07
4
+ data.tar.gz: ea034c42d3dac0e85ef205243ed9da7875f280b3c756e42bfa37afb8dbe99fd8
5
5
  SHA512:
6
- metadata.gz: ea9993ade4e70d1fa2a2d88a3ce3cc0769209685bc54102a286601768c15f31e8706dccc5e1f61fa919b4bf886421df2ba7940e7a8277a494e9d2e2a93d65f29
7
- data.tar.gz: 07c1b7fdff6146ff9425921b46cc0ba8ae004dd4faa94a54688dd0b16169be61d574aea71b4ef7b0c5d749684e721612eaf6f003530e571895022ef1d0233967
6
+ metadata.gz: 44222ddc4ec5685814d177030618a964555fd4562b77cd58bc1c3ea06fd5a989de48fb5fc31fc3b529c1357c28763a57d68908c07556612bbe830b6d9ff2475d
7
+ data.tar.gz: 26a34a3f74cacdaca9b4a2f0a75bbd201266bce7b05ab7bf27c77c615122cab30db0e3f90480738318638fc96aa5a034d72dcba2903c7f995bb16fa53b0dbeba
data/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # hunkify
2
2
 
3
- Split your staged changes into atomic, well-scoped commits — powered by Claude.
3
+ Split your staged changes into atomic, well-scoped commits — powered by GitHub Copilot through OpenCode.
4
4
 
5
- Instead of cramming everything into a single `git commit -m "wip"`, `hunkify` parses your staged diff hunk by hunk, asks Claude to group them by intent, and produces conventional commits (with gitmojis) ready to apply.
5
+ Instead of cramming everything into a single `git commit -m "wip"`, `hunkify` parses your staged diff hunk by hunk, asks OpenCode to group them by intent using GitHub Copilot, and produces conventional commits (with gitmojis) ready to apply.
6
6
 
7
7
  ## Features
8
8
 
9
9
  - **Hunk-level analysis** — each change block is treated independently, so one file can span multiple commits if needed.
10
- - **AI-powered grouping** via Claude Haiku 4.5 — fast, cheap, accurate.
10
+ - **AI-powered grouping** via GitHub Copilot and OpenCode.
11
11
  - **Gitmoji + conventional commits** — `:sparkles: feat(scope): …`, `:bug: fix(scope): …`, etc.
12
12
  - **Interactive review** — confirm, edit, or skip each proposed commit before it's created.
13
13
  - **Free-form context argument** — pass a ticket ID, feature name, or any directive to steer the output.
@@ -16,7 +16,7 @@ Instead of cramming everything into a single `git commit -m "wip"`, `hunkify` pa
16
16
  ## Requirements
17
17
 
18
18
  - Ruby ≥ 2.7
19
- - An [Anthropic API key](https://console.anthropic.com/)
19
+ - [OpenCode](https://opencode.ai/) authenticated with GitHub Copilot
20
20
 
21
21
  ## Installation
22
22
 
@@ -32,10 +32,10 @@ gem "hunkify"
32
32
 
33
33
  ## Setup
34
34
 
35
- Export your Anthropic API key (add to your `~/.zshrc` or `~/.bashrc`):
35
+ Authenticate OpenCode with GitHub Copilot:
36
36
 
37
37
  ```bash
38
- export ANTHROPIC_API_KEY=sk-ant-...
38
+ opencode providers login
39
39
  ```
40
40
 
41
41
  ## Usage
@@ -56,7 +56,7 @@ hunkify "focus on refactoring"
56
56
  ### Workflow
57
57
 
58
58
  1. `hunkify` parses your staged diff into hunks.
59
- 2. Claude proposes a grouping into logical commits.
59
+ 2. OpenCode asks GitHub Copilot to propose a grouping into logical commits.
60
60
  3. For each proposed commit, you can **confirm**, **edit the message**, **skip**, or **quit**.
61
61
  4. After a final confirmation, the plan is applied — each commit is created via `git apply --cached`.
62
62
 
data/lib/hunkify/cli.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require_relative "color"
4
4
  require_relative "diff_parser"
5
5
  require_relative "git"
6
- require_relative "anthropic_api"
6
+ require_relative "opencode"
7
7
  require_relative "ui"
8
8
 
9
9
  module Hunkify
@@ -43,11 +43,11 @@ module Hunkify
43
43
 
44
44
  hunks_by_id = hunks.each_with_object({}) { |h, acc| acc[h.id] = h }
45
45
 
46
- puts Color.dim(" Analyzing and grouping via Claude #{AnthropicAPI::MODEL}...")
46
+ puts Color.dim(" Analyzing and grouping via OpenCode #{OpenCode::MODEL}...")
47
47
  puts
48
48
 
49
49
  begin
50
- result = AnthropicAPI.group_hunks(hunks, context: context)
50
+ result = OpenCode.group_hunks(hunks, context: context)
51
51
  rescue JSON::ParserError
52
52
  puts Color.red(" ✗ Invalid AI response (malformed JSON). Try again.")
53
53
  exit 1
@@ -1,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "net/http"
4
3
  require "json"
5
- require "uri"
4
+ require "open3"
6
5
 
7
6
  module Hunkify
8
- module AnthropicAPI
9
- API_URL = "https://api.anthropic.com/v1/messages"
10
- MODEL = "claude-haiku-4-5-20251001"
7
+ module OpenCode
8
+ MODEL = "github-copilot/gpt-5.6-terra"
11
9
 
12
10
  SYSTEM_PROMPT = <<~PROMPT
13
11
  You are a Git expert. You are given a list of hunks (blocks of modifications)
@@ -20,11 +18,11 @@ module Hunkify
20
18
  - STRONGLY prefer fine-grained, atomic commits over large bundled ones.
21
19
  When in doubt, SPLIT rather than merge.
22
20
  - Heuristics to split:
23
- * Different modules/components/features different commits
24
- * Core logic vs. tests different commits (one feat commit + one test commit)
25
- * Core logic vs. docs different commits
26
- * Core logic vs. config/build files different commits
27
- * Unrelated fixes bundled with a feature separate them
21
+ * Different modules/components/features -> different commits
22
+ * Core logic vs. tests -> different commits (one feat commit + one test commit)
23
+ * Core logic vs. docs -> different commits
24
+ * Core logic vs. config/build files -> different commits
25
+ * Unrelated fixes bundled with a feature -> separate them
28
26
  * Each file introducing a new, independent capability usually deserves
29
27
  its own commit
30
28
  - Only bundle hunks together when they genuinely cannot be reviewed or
@@ -39,11 +37,6 @@ module Hunkify
39
37
  "message": ":sparkles: feat(scope): description in English",
40
38
  "hunk_ids": [1, 3, 5],
41
39
  "reasoning": "brief explanation of the grouping"
42
- },
43
- {
44
- "message": ":bug: fix(scope): description in English",
45
- "hunk_ids": [2, 4],
46
- "reasoning": "brief explanation"
47
40
  }
48
41
  ]
49
42
  }
@@ -65,53 +58,6 @@ module Hunkify
65
58
  RESPOND ONLY WITH THE JSON. No markdown, no explanation.
66
59
  PROMPT
67
60
 
68
- def self.group_hunks(hunks, context: nil)
69
- api_key = ENV["ANTHROPIC_API_KEY"]
70
- raise "ANTHROPIC_API_KEY missing! Add it to your ~/.zshrc or ~/.bashrc" if api_key.nil? || api_key.empty?
71
-
72
- user_ctx = context && !context.empty? ? "\nUser context: #{context}" : ""
73
- hunks_summary = hunks.map(&:to_summary).join("\n\n---\n\n")
74
- user_message = "#{user_ctx}\n\nHere are the hunks to group:\n\n#{hunks_summary}"
75
-
76
- uri = URI(API_URL)
77
- http = Net::HTTP.new(uri.host, uri.port)
78
- http.use_ssl = true
79
- http.read_timeout = 30
80
-
81
- request = Net::HTTP::Post.new(uri.path)
82
- request["Content-Type"] = "application/json"
83
- request["x-api-key"] = api_key
84
- request["anthropic-version"] = "2023-06-01"
85
- request.body = JSON.generate({
86
- model: MODEL,
87
- max_tokens: 1024,
88
- system: SYSTEM_PROMPT,
89
- messages: [{role: "user", content: user_message}]
90
- })
91
-
92
- response = http.request(request)
93
- body = JSON.parse(response.body)
94
-
95
- raise "API Error #{response.code}: #{body["error"]&.dig("message")}" unless response.code == "200"
96
-
97
- raw = body.dig("content", 0, "text")&.strip
98
-
99
- if ENV["HUNKIFY_DEBUG"]
100
- warn "\n--- RAW AI RESPONSE ---\n#{raw}\n-----------------------\n"
101
- end
102
-
103
- cleaned = raw
104
- .gsub(/\A```(?:json)?\s*/i, "")
105
- .gsub(/\s*```\z/, "")
106
- .strip
107
-
108
- if (match = cleaned.match(/(\{.+\})/m))
109
- cleaned = match[1]
110
- end
111
-
112
- JSON.parse(cleaned)
113
- end
114
-
115
61
  SUGGEST_SYSTEM_PROMPT = <<~PROMPT
116
62
  You are a Git expert. You are given one or more hunks that the user wants
117
63
  to bundle into a single commit. Produce ONE conventional commit message
@@ -132,35 +78,36 @@ module Hunkify
132
78
  RESPOND ONLY WITH THE MESSAGE. No markdown, no quotes, no explanation.
133
79
  PROMPT
134
80
 
135
- def self.suggest_message(hunks, context: nil)
136
- api_key = ENV["ANTHROPIC_API_KEY"]
137
- raise "ANTHROPIC_API_KEY missing!" if api_key.nil? || api_key.empty?
81
+ def self.group_hunks(hunks, context: nil)
82
+ user_ctx = context && !context.empty? ? "\nUser context: #{context}" : ""
83
+ summary = hunks.map(&:to_summary).join("\n\n---\n\n")
84
+ raw = ask(SYSTEM_PROMPT, "#{user_ctx}\n\nHere are the hunks to group:\n\n#{summary}")
85
+ cleaned = raw.gsub(/\A```(?:json)?\s*/i, "").gsub(/\s*```\z/, "").strip
86
+ cleaned = Regexp.last_match(1) if cleaned.match(/(\{.+\})/m)
87
+ JSON.parse(cleaned)
88
+ end
138
89
 
90
+ def self.suggest_message(hunks, context: nil)
139
91
  user_ctx = context && !context.empty? ? "\nUser context: #{context}" : ""
140
92
  summary = hunks.map(&:to_summary).join("\n\n---\n\n")
141
- user_message = "#{user_ctx}\n\nHunks to bundle into a single commit:\n\n#{summary}"
142
-
143
- uri = URI(API_URL)
144
- http = Net::HTTP.new(uri.host, uri.port)
145
- http.use_ssl = true
146
- http.read_timeout = 30
147
-
148
- request = Net::HTTP::Post.new(uri.path)
149
- request["Content-Type"] = "application/json"
150
- request["x-api-key"] = api_key
151
- request["anthropic-version"] = "2023-06-01"
152
- request.body = JSON.generate({
153
- model: MODEL,
154
- max_tokens: 128,
155
- system: SUGGEST_SYSTEM_PROMPT,
156
- messages: [{role: "user", content: user_message}]
157
- })
158
-
159
- response = http.request(request)
160
- body = JSON.parse(response.body)
161
- raise "API Error #{response.code}: #{body["error"]&.dig("message")}" unless response.code == "200"
162
-
163
- body.dig("content", 0, "text").to_s.strip.lines.first.to_s.strip
93
+ ask(SUGGEST_SYSTEM_PROMPT, "#{user_ctx}\n\nHunks to bundle into a single commit:\n\n#{summary}").lines.first.to_s.strip
94
+ end
95
+
96
+ def self.ask(system_prompt, user_message)
97
+ output, status = Open3.capture2e("opencode", "run", "--model", MODEL, "--format", "json", "#{system_prompt}\n\n#{user_message}")
98
+ raise "OpenCode failed: #{output.strip}" unless status.success?
99
+
100
+ raw = output.lines.filter_map do |line|
101
+ event = JSON.parse(line)
102
+ event.dig("part", "text") if event["type"] == "text"
103
+ rescue JSON::ParserError
104
+ nil
105
+ end.join.strip
106
+
107
+ raise "OpenCode returned no text response" if raw.empty?
108
+
109
+ warn "\n--- RAW AI RESPONSE ---\n#{raw}\n-----------------------\n" if ENV["HUNKIFY_DEBUG"]
110
+ raw
164
111
  end
165
112
  end
166
113
  end
data/lib/hunkify/ui.rb CHANGED
@@ -12,7 +12,7 @@ module Hunkify
12
12
  puts
13
13
  puts " #{Color.magenta("▲")} #{Color.bold("hunkify")} #{Color.dim("v#{version}")}"
14
14
  puts " #{Color.dim("─" * 40)}"
15
- puts " #{Color.dim("Atomic commits, grouped by Claude.")}"
15
+ puts " #{Color.dim("Atomic commits, grouped by GitHub Copilot.")}"
16
16
  puts
17
17
  end
18
18
 
@@ -65,9 +65,9 @@ module Hunkify
65
65
 
66
66
  def prompt_new_message(hunk_ids, hunks_by_id, context:)
67
67
  hunks = hunk_ids.map { |id| hunks_by_id[id] }.compact
68
- print Color.dim(" Asking Claude for a suggestion... ")
68
+ print Color.dim(" Asking OpenCode for a suggestion... ")
69
69
  begin
70
- suggestion = AnthropicAPI.suggest_message(hunks, context: context)
70
+ suggestion = OpenCode.suggest_message(hunks, context: context)
71
71
  rescue => e
72
72
  puts Color.red("failed (#{e.message})")
73
73
  suggestion = nil
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hunkify
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/hunkify.rb CHANGED
@@ -5,7 +5,7 @@ require_relative "hunkify/color"
5
5
  require_relative "hunkify/hunk"
6
6
  require_relative "hunkify/diff_parser"
7
7
  require_relative "hunkify/git"
8
- require_relative "hunkify/anthropic_api"
8
+ require_relative "hunkify/opencode"
9
9
  require_relative "hunkify/ui"
10
10
  require_relative "hunkify/cli"
11
11
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hunkify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom SCHIAVI
@@ -9,8 +9,8 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: hunkify analyzes staged hunks, asks Claude to group them into logical
13
- commits, and applies them via git apply --cached.
12
+ description: hunkify analyzes staged hunks, asks OpenCode with GitHub Copilot to group
13
+ them into logical commits, and applies them via git apply --cached.
14
14
  executables:
15
15
  - hunkify
16
16
  extensions: []
@@ -20,12 +20,12 @@ files:
20
20
  - README.md
21
21
  - bin/hunkify
22
22
  - lib/hunkify.rb
23
- - lib/hunkify/anthropic_api.rb
24
23
  - lib/hunkify/cli.rb
25
24
  - lib/hunkify/color.rb
26
25
  - lib/hunkify/diff_parser.rb
27
26
  - lib/hunkify/git.rb
28
27
  - lib/hunkify/hunk.rb
28
+ - lib/hunkify/opencode.rb
29
29
  - lib/hunkify/ui.rb
30
30
  - lib/hunkify/version.rb
31
31
  homepage: https://github.com/tomschiavi/smartcommit
@@ -51,5 +51,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
51
51
  requirements: []
52
52
  rubygems_version: 4.0.8
53
53
  specification_version: 4
54
- summary: Split staged changes into atomic commits using Claude.
54
+ summary: Split staged changes into atomic commits using GitHub Copilot.
55
55
  test_files: []