vibecode 0.0.1

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.
@@ -0,0 +1,111 @@
1
+ require "httparty"
2
+ require "json"
3
+
4
+ module Vibecode
5
+ class OllamaClient
6
+ include HTTParty
7
+ base_uri "http://localhost:11434"
8
+
9
+ def initialize
10
+ @headers = { "Content-Type" => "application/json" }
11
+ end
12
+
13
+ # -----------------------------
14
+ # Chat with Model
15
+ # -----------------------------
16
+ def chat(model, system_prompt, user_input)
17
+ body = {
18
+ model: model,
19
+ stream: false,
20
+ messages: [
21
+ { role: "system", content: system_prompt },
22
+ { role: "user", content: user_input }
23
+ ]
24
+ }
25
+
26
+ response = self.class.post("/api/chat", headers: @headers, body: body.to_json)
27
+
28
+ unless response.success?
29
+ return "Error talking to Ollama: #{response.code} #{response.body}"
30
+ end
31
+
32
+ parsed = JSON.parse(response.body)
33
+ parsed.dig("message", "content") || "(No response from model)"
34
+ rescue Errno::ECONNREFUSED
35
+ "Cannot connect to Ollama. Is it running? Try: `ollama serve`"
36
+ rescue => e
37
+ "Ollama error: #{e.message}"
38
+ end
39
+
40
+ # -----------------------------
41
+ # List Installed Models
42
+ # -----------------------------
43
+ def list_models
44
+ response = self.class.get("/api/tags")
45
+
46
+ return [] unless response.success?
47
+
48
+ parsed = JSON.parse(response.body)
49
+ parsed.fetch("models", []).map { |m| m["name"] }
50
+ rescue
51
+ []
52
+ end
53
+
54
+ def model_installed?(model_name)
55
+ list_models.include?(model_name)
56
+ end
57
+
58
+ # -----------------------------
59
+ # Pull Model
60
+ # -----------------------------
61
+ def pull_model(model_name)
62
+ uri = URI("#{self.class.base_uri}/api/pull")
63
+
64
+ req = Net::HTTP::Post.new(uri, @headers)
65
+ req.body = { name: model_name, stream: true }.to_json
66
+
67
+ Net::HTTP.start(uri.hostname, uri.port) do |http|
68
+ http.request(req) do |res|
69
+ unless res.is_a?(Net::HTTPSuccess)
70
+ puts "Failed to start model pull"
71
+ return false
72
+ end
73
+
74
+ res.read_body do |chunk|
75
+ begin
76
+ data = JSON.parse(chunk)
77
+ show_pull_progress(data)
78
+ rescue JSON::ParserError
79
+ # ignore incomplete chunks
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ true
86
+ rescue => e
87
+ puts "Error pulling model: #{e.message}"
88
+ false
89
+ end
90
+
91
+ def show_pull_progress(data)
92
+ if data["status"]
93
+ print "\r#{data['status'].ljust(60)}"
94
+ elsif data["completed"] && data["total"]
95
+ percent = (data["completed"].to_f / data["total"] * 100).round(1)
96
+ print "\rDownloading... #{percent}%".ljust(60)
97
+ end
98
+ end
99
+
100
+ # -----------------------------
101
+ # Health Check
102
+ # -----------------------------
103
+ def server_alive?
104
+ response = self.class.get("/")
105
+ response.code == 200 || response.code == 404
106
+ rescue
107
+ false
108
+ end
109
+ end
110
+ end
111
+
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vibecode
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,225 @@
1
+ require "fileutils"
2
+ require "pathname"
3
+ require "tty-prompt"
4
+ require "pastel"
5
+ require "diffy"
6
+ require "open3"
7
+
8
+ module Vibecode
9
+ class Workspace
10
+ attr_reader :root
11
+
12
+ def initialize(root = Dir.pwd)
13
+ @root = File.expand_path(root)
14
+ @prompt = TTY::Prompt.new
15
+ @pastel = Pastel.new
16
+ end
17
+
18
+ # --------------------------------------------------
19
+ # File Reading
20
+ # --------------------------------------------------
21
+
22
+ def read_file(path)
23
+ full_path = safe_path(path)
24
+ return error("File does not exist: #{path}") unless File.exist?(full_path)
25
+
26
+ File.read(full_path)
27
+ rescue => e
28
+ error("Failed to read file: #{e.message}")
29
+ end
30
+
31
+ def file_exists?(path)
32
+ full_path = safe_path(path)
33
+ File.exist?(full_path)
34
+ rescue
35
+ false
36
+ end
37
+
38
+ def list_files(limit: 200)
39
+ files = Dir.glob("**/*", base: @root)
40
+ .reject { |f| File.directory?(File.join(@root, f)) }
41
+ .first(limit)
42
+
43
+ files.join("\n")
44
+ end
45
+
46
+ # --------------------------------------------------
47
+ # File Writing
48
+ # --------------------------------------------------
49
+
50
+ def diff_for(path, new_content)
51
+ full_path = safe_path(path)
52
+ old_content = File.exist?(full_path) ? File.read(full_path) : ""
53
+ Diffy::Diff.new(old_content, new_content, context: 3).to_s(:color)
54
+ rescue => e
55
+ error("Failed to diff file: #{e.message}")
56
+ ""
57
+ end
58
+
59
+ def write_file(path, new_content, show_diff: true)
60
+ full_path = safe_path(path)
61
+
62
+ if show_diff
63
+ diff = diff_for(path, new_content)
64
+ puts @pastel.yellow("\nProposed changes to #{path}:\n")
65
+ puts diff.empty? ? @pastel.dim("(No changes)") : diff
66
+ end
67
+
68
+ FileUtils.mkdir_p(File.dirname(full_path))
69
+ File.write(full_path, new_content)
70
+
71
+ puts @pastel.green("Updated #{path}")
72
+ true
73
+ rescue => e
74
+ error("Failed to write file: #{e.message}")
75
+ end
76
+
77
+ # --------------------------------------------------
78
+ # Ruby Execution
79
+ # --------------------------------------------------
80
+
81
+ def ruby_executable_content?(content)
82
+ return false if content.nil? || content.strip.empty?
83
+
84
+ return true if content.match?(/if\s+__FILE__\s*==\s*\$0/)
85
+ return true if content.match?(/\bputs\b/)
86
+
87
+ line = last_significant_line(content)
88
+ return false unless line
89
+
90
+ stripped = line.strip
91
+ return false if stripped.match?(/^(def|class|module|end)\b/)
92
+
93
+ stripped.match?(/[A-Za-z_]\w*(\s*\(|\b)/)
94
+ end
95
+
96
+ def run_ruby(path)
97
+ full_path = safe_path(path)
98
+ return {
99
+ stdout: "",
100
+ stderr: "File does not exist: #{path}",
101
+ status: nil,
102
+ command: "ruby #{path}",
103
+ skipped: true
104
+ } unless File.exist?(full_path)
105
+
106
+ content = File.read(full_path)
107
+ unless ruby_executable_content?(content)
108
+ return {
109
+ stdout: "",
110
+ stderr: "",
111
+ status: nil,
112
+ command: "ruby #{path}",
113
+ skipped: true
114
+ }
115
+ end
116
+
117
+ stdout, stderr, status = Open3.capture3("ruby #{full_path}", chdir: @root)
118
+ {
119
+ stdout: stdout,
120
+ stderr: stderr,
121
+ status: status,
122
+ command: "ruby #{path}",
123
+ skipped: false
124
+ }
125
+ rescue => e
126
+ {
127
+ stdout: "",
128
+ stderr: e.message,
129
+ status: nil,
130
+ command: "ruby #{path}",
131
+ skipped: false
132
+ }
133
+ end
134
+
135
+ # --------------------------------------------------
136
+ # Filename Suggestions
137
+ # --------------------------------------------------
138
+
139
+ def suggest_filename(task_description)
140
+ prompt = task_description.to_s.downcase
141
+
142
+ return "hello_world.rb" if prompt.include?("hello world")
143
+ return "greet.rb" if prompt.match?(/\bgreet\b/)
144
+
145
+ preferred = %w[
146
+ greet hello world user server client parser json api http config file data
147
+ ]
148
+ stopwords = %w[
149
+ a an the to for of and in on with from into is are be create build make write
150
+ ruby method function class module script program app code that this
151
+ ]
152
+
153
+ words = prompt.scan(/[a-z0-9]+/)
154
+ words = words.reject { |w| stopwords.include?(w) }
155
+
156
+ if words.include?("hello") && words.include?("world")
157
+ return "hello_world.rb"
158
+ end
159
+
160
+ picked = []
161
+ preferred.each do |w|
162
+ picked << w if words.include?(w)
163
+ break if picked.size >= 3
164
+ end
165
+
166
+ if picked.empty?
167
+ words.each do |w|
168
+ picked << w
169
+ break if picked.size >= 3
170
+ end
171
+ end
172
+
173
+ return "main.rb" if picked.empty?
174
+
175
+ "#{picked.uniq.first(3).join("_")}.rb"
176
+ end
177
+
178
+ # --------------------------------------------------
179
+ # Directory Tree Snapshot
180
+ # --------------------------------------------------
181
+
182
+ def tree(max_depth: 3)
183
+ output = []
184
+
185
+ Dir.glob("**/*", base: @root).each do |path|
186
+ depth = path.count(File::SEPARATOR)
187
+ next if depth > max_depth
188
+ next if path.start_with?(".git")
189
+
190
+ output << path
191
+ end
192
+
193
+ output.sort.join("\n")
194
+ end
195
+
196
+ # --------------------------------------------------
197
+ # Safety Helpers
198
+ # --------------------------------------------------
199
+
200
+ private
201
+
202
+ def last_significant_line(content)
203
+ content.lines.reverse_each do |line|
204
+ stripped = line.strip
205
+ next if stripped.empty?
206
+ next if stripped.start_with?("#")
207
+ return line
208
+ end
209
+ nil
210
+ end
211
+
212
+ def safe_path(path)
213
+ expanded = File.expand_path(path, @root)
214
+ unless expanded.start_with?(@root)
215
+ raise "Access outside project root is not allowed"
216
+ end
217
+ expanded
218
+ end
219
+
220
+ def error(message)
221
+ puts @pastel.red(message)
222
+ nil
223
+ end
224
+ end
225
+ end
data/lib/vibecode.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "vibecode/version"
4
+ require_relative "vibecode/cli"
5
+ require_relative "vibecode/ollama_client"
6
+ require_relative "vibecode/workspace"
7
+ require_relative "vibecode/git"
8
+ require_relative "vibecode/agent"
9
+
10
+ module Vibecode
11
+ class Error < StandardError; end
12
+ # Your code goes here...
13
+ end
data/sig/vibecode.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Vibecode
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+ require "vibecode"
5
+
6
+ require "minitest/autorun"
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class TestVibecode < Minitest::Test
6
+ def test_that_it_has_a_version_number
7
+ refute_nil ::Vibecode::VERSION
8
+ end
9
+
10
+ def test_it_does_something_useful
11
+ assert false
12
+ end
13
+ end
data/vibecode.gemspec ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/vibecode/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "vibecode"
7
+ spec.version = Vibecode::VERSION
8
+ spec.authors = ["hackliteracy"]
9
+ spec.email = ["hackliteracy@gmail.com"]
10
+
11
+ spec.summary = "A local-first “Codex-style” CLI but powered by Ollama"
12
+ spec.description = "Local AI coding agent with abilities like: File editing with diffs, Git command approvals, Model switching, Repo awareness. All on your machine available offline"
13
+ spec.homepage = "https://github.com/ktamulonis/vibecode"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.0"
16
+
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org/"
18
+
19
+ spec.metadata["source_code_uri"] = "https://github.com/ktamulonis/vibecode"
20
+ spec.metadata["changelog_uri"] = "https://github.com/ktamulonis/vibecode/blob/main/CHANGELOG.md"
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(__dir__) { `git ls-files -z`.split("\x0") }
25
+ spec.bindir = "exe"
26
+ spec.executables = ["vibecode"]
27
+
28
+ spec.require_paths = ["lib"]
29
+
30
+ # Uncomment to register a new dependency of your gem
31
+ spec.add_dependency "tty-prompt", "~> 0.23"
32
+ spec.add_dependency "tty-spinner", "~> 0.9"
33
+ spec.add_dependency "pastel", "~> 0.8"
34
+ spec.add_dependency "httparty", "~> 0.21"
35
+ spec.add_dependency "diffy", "~> 3.4"
36
+
37
+ # For more information and examples about making a new gem, check out our
38
+ # guide at: https://bundler.io/guides/creating_gem.html
39
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vibecode
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - hackliteracy
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-02-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: tty-prompt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.23'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.23'
27
+ - !ruby/object:Gem::Dependency
28
+ name: tty-spinner
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.9'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.9'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pastel
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: httparty
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.21'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.21'
69
+ - !ruby/object:Gem::Dependency
70
+ name: diffy
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.4'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.4'
83
+ description: 'Local AI coding agent with abilities like: File editing with diffs,
84
+ Git command approvals, Model switching, Repo awareness. All on your machine available
85
+ offline'
86
+ email:
87
+ - hackliteracy@gmail.com
88
+ executables:
89
+ - vibecode
90
+ extensions: []
91
+ extra_rdoc_files: []
92
+ files:
93
+ - ".gitignore"
94
+ - CHANGELOG.md
95
+ - Gemfile
96
+ - Gemfile.lock
97
+ - LICENSE.txt
98
+ - README.md
99
+ - Rakefile
100
+ - bin/console
101
+ - bin/setup
102
+ - exe/vibecode
103
+ - lib/vibecode.rb
104
+ - lib/vibecode/agent.rb
105
+ - lib/vibecode/cli.rb
106
+ - lib/vibecode/git.rb
107
+ - lib/vibecode/ollama_client.rb
108
+ - lib/vibecode/version.rb
109
+ - lib/vibecode/workspace.rb
110
+ - sig/vibecode.rbs
111
+ - test/test_helper.rb
112
+ - test/test_vibecode.rb
113
+ - vibecode.gemspec
114
+ homepage: https://github.com/ktamulonis/vibecode
115
+ licenses:
116
+ - MIT
117
+ metadata:
118
+ allowed_push_host: https://rubygems.org/
119
+ source_code_uri: https://github.com/ktamulonis/vibecode
120
+ changelog_uri: https://github.com/ktamulonis/vibecode/blob/main/CHANGELOG.md
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '3.0'
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ requirements: []
136
+ rubygems_version: 3.5.22
137
+ signing_key:
138
+ specification_version: 4
139
+ summary: A local-first “Codex-style” CLI but powered by Ollama
140
+ test_files: []