memoflow 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +198 -0
- data/bin/memoflow +7 -0
- data/examples/embedder.rb +15 -0
- data/lib/memoflow/cli.rb +171 -0
- data/lib/memoflow/client.rb +308 -0
- data/lib/memoflow/configuration.rb +39 -0
- data/lib/memoflow/embedding_provider.rb +68 -0
- data/lib/memoflow/encryptor.rb +54 -0
- data/lib/memoflow/errors.rb +7 -0
- data/lib/memoflow/git_context.rb +82 -0
- data/lib/memoflow/hook_installer.rb +41 -0
- data/lib/memoflow/provider_context.rb +123 -0
- data/lib/memoflow/server.rb +99 -0
- data/lib/memoflow/store.rb +188 -0
- data/lib/memoflow/vectorizer.rb +57 -0
- data/lib/memoflow/version.rb +6 -0
- data/lib/memoflow.rb +47 -0
- metadata +79 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Memoflow
|
|
4
|
+
class ProviderContext
|
|
5
|
+
def initialize(remote_url:, repo_path:, env: ENV, cli_runner: nil)
|
|
6
|
+
@remote_url = remote_url.to_s.strip
|
|
7
|
+
@repo_path = repo_path.to_s
|
|
8
|
+
@env = env
|
|
9
|
+
@cli_runner = cli_runner || method(:default_cli_runner)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def capture
|
|
13
|
+
{
|
|
14
|
+
provider: provider,
|
|
15
|
+
repository: repository,
|
|
16
|
+
pull_request: pull_request
|
|
17
|
+
}.compact
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def provider
|
|
23
|
+
return "github" if source_text.include?("github")
|
|
24
|
+
return "gitlab" if source_text.include?("gitlab")
|
|
25
|
+
return "bitbucket" if source_text.include?("bitbucket")
|
|
26
|
+
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def repository
|
|
31
|
+
@env["GITHUB_REPOSITORY"] || @env["CI_PROJECT_PATH"] || parsed_repository
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def pull_request
|
|
35
|
+
payload = github_pull_request || gitlab_merge_request || github_pull_request_from_cli || gitlab_merge_request_from_cli
|
|
36
|
+
payload if payload && payload[:number]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def github_pull_request
|
|
40
|
+
number = @env["GITHUB_PR_NUMBER"] || @env["PR_NUMBER"]
|
|
41
|
+
return unless number
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
number: number.to_s,
|
|
45
|
+
title: @env["GITHUB_PR_TITLE"],
|
|
46
|
+
body: @env["GITHUB_PR_BODY"],
|
|
47
|
+
source: "github_env"
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def gitlab_merge_request
|
|
52
|
+
number = @env["CI_MERGE_REQUEST_IID"]
|
|
53
|
+
return unless number
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
number: number.to_s,
|
|
57
|
+
title: @env["CI_MERGE_REQUEST_TITLE"],
|
|
58
|
+
body: @env["CI_MERGE_REQUEST_DESCRIPTION"],
|
|
59
|
+
source: "gitlab_env"
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def parsed_repository
|
|
64
|
+
cleaned = @remote_url.sub(/\Agit@([^:]+):/, '\1/').sub(%r{\Ahttps?://}, "")
|
|
65
|
+
parts = cleaned.split("/")
|
|
66
|
+
return if parts.length < 3
|
|
67
|
+
|
|
68
|
+
"#{parts[-2]}/#{parts[-1].sub(/\.git\z/, "")}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def source_text
|
|
72
|
+
[@remote_url, @env["GITHUB_SERVER_URL"], @env["CI_SERVER_URL"]].compact.join(" ").downcase
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def github_pull_request_from_cli
|
|
76
|
+
return unless provider == "github"
|
|
77
|
+
|
|
78
|
+
output = run_cli("gh", "pr", "view", "--json", "number,title,body", "--repo", repository.to_s)
|
|
79
|
+
return unless output
|
|
80
|
+
|
|
81
|
+
payload = JSON.parse(output)
|
|
82
|
+
{
|
|
83
|
+
number: payload["number"].to_s,
|
|
84
|
+
title: payload["title"],
|
|
85
|
+
body: payload["body"],
|
|
86
|
+
source: "gh_cli"
|
|
87
|
+
}
|
|
88
|
+
rescue JSON::ParserError
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def gitlab_merge_request_from_cli
|
|
93
|
+
return unless provider == "gitlab"
|
|
94
|
+
|
|
95
|
+
output = run_cli("glab", "mr", "view", "--output", "json")
|
|
96
|
+
return unless output
|
|
97
|
+
|
|
98
|
+
payload = JSON.parse(output)
|
|
99
|
+
{
|
|
100
|
+
number: (payload["iid"] || payload["id"]).to_s,
|
|
101
|
+
title: payload["title"],
|
|
102
|
+
body: payload["description"],
|
|
103
|
+
source: "glab_cli"
|
|
104
|
+
}
|
|
105
|
+
rescue JSON::ParserError
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def run_cli(*command)
|
|
110
|
+
result = @cli_runner.call(command, @repo_path)
|
|
111
|
+
return unless result[:success]
|
|
112
|
+
|
|
113
|
+
result[:output]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def default_cli_runner(command, repo_path)
|
|
117
|
+
output, status = Open3.capture2e(*command, chdir: repo_path)
|
|
118
|
+
{ success: status.success?, output: output }
|
|
119
|
+
rescue Errno::ENOENT
|
|
120
|
+
{ success: false, output: "" }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Memoflow
|
|
4
|
+
class Server
|
|
5
|
+
def initialize(client:, host:, port:)
|
|
6
|
+
@client = client
|
|
7
|
+
@host = host
|
|
8
|
+
@port = port
|
|
9
|
+
@running = true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def start
|
|
13
|
+
trap_signals
|
|
14
|
+
@server = TCPServer.new(@host, @port)
|
|
15
|
+
|
|
16
|
+
while @running
|
|
17
|
+
socket = nil
|
|
18
|
+
begin
|
|
19
|
+
socket = @server.accept
|
|
20
|
+
handle(socket)
|
|
21
|
+
rescue IOError, SystemCallError
|
|
22
|
+
break unless @running
|
|
23
|
+
ensure
|
|
24
|
+
socket&.close
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
ensure
|
|
28
|
+
@server&.close
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def handle(socket)
|
|
34
|
+
request_line = socket.gets
|
|
35
|
+
return unless request_line
|
|
36
|
+
|
|
37
|
+
_method, target, _version = request_line.split(" ", 3)
|
|
38
|
+
consume_headers(socket)
|
|
39
|
+
path, query = parse_target(target)
|
|
40
|
+
body = route(path, query)
|
|
41
|
+
socket.write(response(body))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def consume_headers(socket)
|
|
45
|
+
while (line = socket.gets)
|
|
46
|
+
break if line == "\r\n"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def parse_target(target)
|
|
51
|
+
uri = URI.parse(target)
|
|
52
|
+
query = URI.decode_www_form(uri.query.to_s).to_h
|
|
53
|
+
[uri.path, query]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def route(path, query)
|
|
57
|
+
payload = case path
|
|
58
|
+
when "/health"
|
|
59
|
+
{ ok: true, current_task_id: @client.current_task_id }
|
|
60
|
+
when "/query"
|
|
61
|
+
@client.query(query["q"], limit: integer_param(query["limit"], 5))
|
|
62
|
+
when "/context"
|
|
63
|
+
{ packet: @client.context_packet(query: query["q"], limit: integer_param(query["limit"], 5)) }
|
|
64
|
+
when "/tasks"
|
|
65
|
+
@client.tasks(limit: 20)
|
|
66
|
+
else
|
|
67
|
+
{ error: "not_found" }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
JSON.generate(payload)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def response(body)
|
|
74
|
+
[
|
|
75
|
+
"HTTP/1.1 200 OK",
|
|
76
|
+
"Content-Type: application/json",
|
|
77
|
+
"Content-Length: #{body.bytesize}",
|
|
78
|
+
"Connection: close",
|
|
79
|
+
"",
|
|
80
|
+
body
|
|
81
|
+
].join("\r\n")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def integer_param(value, default)
|
|
85
|
+
Integer(value)
|
|
86
|
+
rescue ArgumentError, TypeError
|
|
87
|
+
default
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def trap_signals
|
|
91
|
+
%w[INT TERM].each do |signal|
|
|
92
|
+
trap(signal) do
|
|
93
|
+
@running = false
|
|
94
|
+
@server&.close
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Memoflow
|
|
4
|
+
class Store
|
|
5
|
+
STATE_DIRS = %w[commits annotations tasks sessions state].freeze
|
|
6
|
+
|
|
7
|
+
def initialize(root:, encryptor:)
|
|
8
|
+
@root = Pathname.new(root)
|
|
9
|
+
@encryptor = encryptor
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def setup!
|
|
13
|
+
STATE_DIRS.each do |dir|
|
|
14
|
+
FileUtils.mkdir_p(@root.join(dir))
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def write_commit(record)
|
|
19
|
+
write("commits", "#{record.fetch(:sha)}.json.enc", record)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def write_annotation(record)
|
|
23
|
+
write("annotations", "#{record.fetch(:id)}.json.enc", record)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def write_task(record)
|
|
27
|
+
write("tasks", "#{record.fetch(:id)}.json.enc", record)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def write_session(record)
|
|
31
|
+
write("sessions", "#{record.fetch(:id)}.json.enc", record)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def write_state(name, record)
|
|
35
|
+
write("state", "#{name}.json.enc", record)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def export_bundle(path)
|
|
39
|
+
with_lock do
|
|
40
|
+
payload = {
|
|
41
|
+
exported_at: Time.now.utc.iso8601,
|
|
42
|
+
version: Memoflow::VERSION,
|
|
43
|
+
schema_version: Memoflow::SCHEMA_VERSION,
|
|
44
|
+
records: read_all,
|
|
45
|
+
state: read_scope("state")
|
|
46
|
+
}
|
|
47
|
+
atomic_write(Pathname.new(path), @encryptor.encrypt(JSON.generate(payload)))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def import_bundle(path)
|
|
52
|
+
with_lock do
|
|
53
|
+
payload = JSON.parse(@encryptor.decrypt(File.binread(path)), symbolize_names: true)
|
|
54
|
+
Array(payload[:records]).each { |record| import_record(record) }
|
|
55
|
+
Array(payload[:state]).each do |record|
|
|
56
|
+
name = File.basename(record.fetch(:_scope_name, "current_task"))
|
|
57
|
+
write_state(name, record.reject { |key, _| key == :_scope_name })
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def prune!(keep_days: nil, max_records: nil)
|
|
63
|
+
with_lock do
|
|
64
|
+
removed = []
|
|
65
|
+
%w[commits annotations tasks sessions].each do |scope|
|
|
66
|
+
records = Dir[@root.join(scope, "*.enc")].sort
|
|
67
|
+
if keep_days
|
|
68
|
+
cutoff = Time.now.utc - (keep_days * 86_400)
|
|
69
|
+
records.each do |path|
|
|
70
|
+
entry = read_file(path)
|
|
71
|
+
timestamp = extract_timestamp(entry)
|
|
72
|
+
next unless timestamp && timestamp < cutoff
|
|
73
|
+
|
|
74
|
+
File.delete(path)
|
|
75
|
+
removed << path
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
next unless max_records
|
|
80
|
+
|
|
81
|
+
current = Dir[@root.join(scope, "*.enc")].sort_by { |path| extract_timestamp(read_file(path)) || Time.at(0) }
|
|
82
|
+
overflow = current.length - max_records
|
|
83
|
+
current.first(overflow).each do |path|
|
|
84
|
+
next if removed.include?(path)
|
|
85
|
+
|
|
86
|
+
File.delete(path)
|
|
87
|
+
removed << path
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
removed
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def commit?(sha)
|
|
96
|
+
@root.join("commits", "#{sha}.json.enc").exist?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def read_state(name)
|
|
100
|
+
path = @root.join("state", "#{name}.json.enc")
|
|
101
|
+
return unless path.exist?
|
|
102
|
+
|
|
103
|
+
read_file(path)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def read_scope(scope)
|
|
107
|
+
Dir[@root.join(scope, "*.enc")].sort.map do |path|
|
|
108
|
+
record = read_file(path)
|
|
109
|
+
record.merge(_scope_name: File.basename(path, ".json.enc"))
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def read_all
|
|
114
|
+
STATE_DIRS.reject { |scope| scope == "state" }.flat_map { |scope| read_scope(scope) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def write(scope, filename, record)
|
|
120
|
+
with_lock do
|
|
121
|
+
setup!
|
|
122
|
+
path = @root.join(scope, filename)
|
|
123
|
+
payload = JSON.generate(envelope(record).transform_keys(&:to_s))
|
|
124
|
+
atomic_write(path, @encryptor.encrypt(payload))
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def read_file(path)
|
|
129
|
+
payload = File.binread(path)
|
|
130
|
+
migrate_record(JSON.parse(@encryptor.decrypt(payload), symbolize_names: true))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def import_record(record)
|
|
134
|
+
scope =
|
|
135
|
+
case record[:type]
|
|
136
|
+
when "commit" then "commits"
|
|
137
|
+
when "annotation" then "annotations"
|
|
138
|
+
when "task" then "tasks"
|
|
139
|
+
when "session" then "sessions"
|
|
140
|
+
else
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
return unless scope
|
|
144
|
+
|
|
145
|
+
key = record[:sha] || record[:id]
|
|
146
|
+
write(scope, "#{key}.json.enc", record.reject { |field, _| field == :_scope_name })
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def extract_timestamp(entry)
|
|
150
|
+
value = entry[:timestamp] || entry[:committed_at] || entry[:updated_at] || entry[:started_at]
|
|
151
|
+
Time.parse(value) if value
|
|
152
|
+
rescue ArgumentError
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def envelope(record)
|
|
157
|
+
record.merge(
|
|
158
|
+
_schema_version: Memoflow::SCHEMA_VERSION,
|
|
159
|
+
_stored_at: Time.now.utc.iso8601
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def migrate_record(record)
|
|
164
|
+
return record if record[:_schema_version] == Memoflow::SCHEMA_VERSION
|
|
165
|
+
|
|
166
|
+
record.merge(_schema_version: Memoflow::SCHEMA_VERSION)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def with_lock
|
|
170
|
+
setup!
|
|
171
|
+
File.open(@root.join(".lock"), File::RDWR | File::CREAT, 0o644) do |file|
|
|
172
|
+
file.flock(File::LOCK_EX)
|
|
173
|
+
yield
|
|
174
|
+
ensure
|
|
175
|
+
file.flock(File::LOCK_UN)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def atomic_write(path, payload)
|
|
180
|
+
temp_path = path.sub_ext("#{path.extname}.tmp-#{SecureRandom.hex(6)}")
|
|
181
|
+
File.binwrite(temp_path, payload)
|
|
182
|
+
File.rename(temp_path, path)
|
|
183
|
+
path
|
|
184
|
+
ensure
|
|
185
|
+
File.delete(temp_path) if temp_path && File.exist?(temp_path)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Memoflow
|
|
4
|
+
class Vectorizer
|
|
5
|
+
DIMENSIONS = 128
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def encode(text)
|
|
9
|
+
vector = Array.new(DIMENSIONS, 0.0)
|
|
10
|
+
tokens(text).each do |token|
|
|
11
|
+
bucket = Zlib.crc32(token) % DIMENSIONS
|
|
12
|
+
vector[bucket] += 1.0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
normalize(vector)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def similarity(left, right)
|
|
19
|
+
return 0.0 if left.nil? || right.nil?
|
|
20
|
+
return 0.0 if left.empty? || right.empty?
|
|
21
|
+
|
|
22
|
+
left.zip(right).sum { |a, b| a.to_f * b.to_f }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def indexable_text(entry)
|
|
26
|
+
pr = entry[:pull_request] || {}
|
|
27
|
+
[
|
|
28
|
+
entry[:summary],
|
|
29
|
+
entry[:subject],
|
|
30
|
+
entry[:body],
|
|
31
|
+
entry[:title],
|
|
32
|
+
entry[:description],
|
|
33
|
+
Array(entry[:changed_files]).join(" "),
|
|
34
|
+
Array(entry[:tags]).join(" "),
|
|
35
|
+
entry[:task_id],
|
|
36
|
+
entry[:repository],
|
|
37
|
+
entry[:provider],
|
|
38
|
+
pr[:title],
|
|
39
|
+
pr[:body]
|
|
40
|
+
].compact.join(" ")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def tokens(text)
|
|
46
|
+
text.to_s.downcase.scan(/[a-z0-9_\/.-]{2,}/)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def normalize(vector)
|
|
50
|
+
magnitude = Math.sqrt(vector.sum { |value| value * value })
|
|
51
|
+
return vector if magnitude.zero?
|
|
52
|
+
|
|
53
|
+
vector.map { |value| (value / magnitude).round(6) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/memoflow.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "pathname"
|
|
8
|
+
require "securerandom"
|
|
9
|
+
require "shellwords"
|
|
10
|
+
require "socket"
|
|
11
|
+
require "time"
|
|
12
|
+
require "uri"
|
|
13
|
+
require "zlib"
|
|
14
|
+
|
|
15
|
+
require_relative "memoflow/client"
|
|
16
|
+
require_relative "memoflow/cli"
|
|
17
|
+
require_relative "memoflow/configuration"
|
|
18
|
+
require_relative "memoflow/encryptor"
|
|
19
|
+
require_relative "memoflow/embedding_provider"
|
|
20
|
+
require_relative "memoflow/errors"
|
|
21
|
+
require_relative "memoflow/git_context"
|
|
22
|
+
require_relative "memoflow/hook_installer"
|
|
23
|
+
require_relative "memoflow/provider_context"
|
|
24
|
+
require_relative "memoflow/server"
|
|
25
|
+
require_relative "memoflow/store"
|
|
26
|
+
require_relative "memoflow/vectorizer"
|
|
27
|
+
require_relative "memoflow/version"
|
|
28
|
+
|
|
29
|
+
module Memoflow
|
|
30
|
+
class << self
|
|
31
|
+
def configuration
|
|
32
|
+
@configuration ||= Configuration.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def configure
|
|
36
|
+
yield(configuration)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def client(repo_path: Dir.pwd, env: ENV, embedding_provider: nil)
|
|
40
|
+
Client.new(configuration: configuration, repo_path: repo_path, env: env, embedding_provider: embedding_provider)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reset_configuration!
|
|
44
|
+
@configuration = Configuration.new
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: memoflow
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Iksha Labs
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-25 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: minitest
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '5.25'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '5.25'
|
|
27
|
+
description: Memoflow captures git-backed development context and exposes a lightweight
|
|
28
|
+
query API for AI assistants.
|
|
29
|
+
email:
|
|
30
|
+
- opensource@ikshalabs.com
|
|
31
|
+
executables:
|
|
32
|
+
- memoflow
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- LICENSE.txt
|
|
37
|
+
- README.md
|
|
38
|
+
- bin/memoflow
|
|
39
|
+
- examples/embedder.rb
|
|
40
|
+
- lib/memoflow.rb
|
|
41
|
+
- lib/memoflow/cli.rb
|
|
42
|
+
- lib/memoflow/client.rb
|
|
43
|
+
- lib/memoflow/configuration.rb
|
|
44
|
+
- lib/memoflow/embedding_provider.rb
|
|
45
|
+
- lib/memoflow/encryptor.rb
|
|
46
|
+
- lib/memoflow/errors.rb
|
|
47
|
+
- lib/memoflow/git_context.rb
|
|
48
|
+
- lib/memoflow/hook_installer.rb
|
|
49
|
+
- lib/memoflow/provider_context.rb
|
|
50
|
+
- lib/memoflow/server.rb
|
|
51
|
+
- lib/memoflow/store.rb
|
|
52
|
+
- lib/memoflow/vectorizer.rb
|
|
53
|
+
- lib/memoflow/version.rb
|
|
54
|
+
homepage: https://github.com/ikshalabs/memoflow
|
|
55
|
+
licenses:
|
|
56
|
+
- MIT
|
|
57
|
+
metadata:
|
|
58
|
+
source_code_uri: https://github.com/ikshalabs/memoflow
|
|
59
|
+
changelog_uri: https://github.com/ikshalabs/memoflow/releases
|
|
60
|
+
post_install_message:
|
|
61
|
+
rdoc_options: []
|
|
62
|
+
require_paths:
|
|
63
|
+
- lib
|
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.1'
|
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '0'
|
|
74
|
+
requirements: []
|
|
75
|
+
rubygems_version: 3.5.22
|
|
76
|
+
signing_key:
|
|
77
|
+
specification_version: 4
|
|
78
|
+
summary: Encrypted, compressed repo context for AI coding assistants
|
|
79
|
+
test_files: []
|