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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 97fdae7d1b162b5a4cae8ca5e7fcb1aba0fae6bf25e03361a4c54eccb7416272
4
+ data.tar.gz: d0373fbe16f0381c29762c5863cfb19a28c4d66bd243fe388a59db5a1b349efd
5
+ SHA512:
6
+ metadata.gz: ffbd60efe40c52e1c1226be2352faae6cfbf54eb07711bb4618c4377deb761a0feb6890d8a2a34e8d34149d9d2051b9c386265e9bcfbded591a0f24c7a82ce87
7
+ data.tar.gz: 14cc267c251c5641f361ccf2a0907e17839982793aab811cef1d06e69a44dfdded7948408e1228fc36efa58478a01c37543a78c2d13cd83a4be7e4e6949e9579
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # Memoflow
2
+
3
+ Memoflow is a lightweight Ruby gem that captures repository context for AI coding assistants. It stores commit metadata, task notes, and problem statements in compressed and encrypted records with a small local query API and CLI.
4
+
5
+ ## What it captures
6
+
7
+ - Git commit metadata: sha, author, timestamp, subject, body, changed files
8
+ - Task and session records with active-task linkage to captured commits
9
+ - Optional task annotations and problem statements
10
+ - GitHub/GitLab pull request metadata when available from local environment
11
+ - Repository metadata for retrieval and AI session warm-up
12
+ - Compact hashed vectors for semantic-style retrieval without an external service
13
+
14
+ ## Installation
15
+
16
+ Add the gem to your project:
17
+
18
+ ```ruby
19
+ gem "memoflow"
20
+ ```
21
+
22
+ Then configure it with one initializer:
23
+
24
+ ```ruby
25
+ Memoflow.configure do |config|
26
+ config.encryption_key = ENV.fetch("MEMOFLOW_KEY")
27
+ end
28
+ ```
29
+
30
+ If you do not want a Rails initializer, the CLI can operate with `MEMOFLOW_KEY` alone.
31
+
32
+ Optional embedding provider:
33
+
34
+ ```ruby
35
+ Memoflow.configure do |config|
36
+ config.encryption_key = ENV.fetch("MEMOFLOW_KEY")
37
+ config.embedding_command = ["ruby", "script/embedder.rb"]
38
+ config.embedding_timeout = 5
39
+ end
40
+ ```
41
+
42
+ The embedding command receives JSON on stdin:
43
+
44
+ ```json
45
+ {"text":"your text here"}
46
+ ```
47
+
48
+ It must print either:
49
+
50
+ ```json
51
+ [0.1, 0.2, 0.3]
52
+ ```
53
+
54
+ or:
55
+
56
+ ```json
57
+ {"embedding":[0.1, 0.2, 0.3]}
58
+ ```
59
+
60
+ Optional external storage:
61
+
62
+ ```ruby
63
+ Memoflow.configure do |config|
64
+ config.encryption_key = ENV.fetch("MEMOFLOW_KEY")
65
+ config.storage_policy = :external
66
+ config.storage_path = File.expand_path("~/secure-memories/my_app")
67
+ end
68
+ ```
69
+
70
+ ## Quick start
71
+
72
+ Initialize storage in the repository:
73
+
74
+ ```bash
75
+ bundle exec memoflow init
76
+ bundle exec memoflow install-hook
77
+ bundle exec memoflow task start "Fix webhook retry handling"
78
+ bundle exec memoflow task note "Sentry shows duplicate retries after timeout"
79
+ bundle exec memoflow capture --last
80
+ bundle exec memoflow annotate "Problem statement: fix webhook retries"
81
+ bundle exec memoflow query "webhook retries"
82
+ ```
83
+
84
+ Rails initializer example:
85
+
86
+ ```ruby
87
+ # config/initializers/memoflow.rb
88
+ Memoflow.configure do |config|
89
+ config.encryption_key = ENV.fetch("MEMOFLOW_KEY")
90
+ config.storage_policy = :repo
91
+ end
92
+ ```
93
+
94
+ ## Storage design
95
+
96
+ - Records are stored under `.memoflow` by default
97
+ - Each record is JSON, compressed with `Zlib::Deflate`, then encrypted with `AES-256-GCM`
98
+ - Storage location is configurable and can live inside or outside the repository
99
+ - Active task/session state is also encrypted on disk so commit capture can link work automatically
100
+
101
+ ## AI integration
102
+
103
+ Use the Ruby API:
104
+
105
+ ```ruby
106
+ client = Memoflow.client(repo_path: Dir.pwd)
107
+ client.capture_last_commit
108
+ client.annotate("Investigating flaky job retries", tags: ["task"])
109
+
110
+ context = client.query("job retries", limit: 5)
111
+ context.each do |entry|
112
+ puts "#{entry[:type]} #{entry[:timestamp]} #{entry[:summary]}"
113
+ end
114
+ ```
115
+
116
+ Build a compact session packet:
117
+
118
+ ```ruby
119
+ packet = client.context_packet(query: "continue work on retries")
120
+ puts packet
121
+ ```
122
+
123
+ Expose the data to an AI assistant over local HTTP:
124
+
125
+ ```bash
126
+ bundle exec memoflow serve 4599
127
+ curl "http://127.0.0.1:4599/query?q=retry"
128
+ curl "http://127.0.0.1:4599/context?q=retry"
129
+ ```
130
+
131
+ Retrieval ranking is task-aware and weights matches in:
132
+
133
+ - active task linkage
134
+ - commit subject and summaries
135
+ - task titles/descriptions
136
+ - changed file paths
137
+ - PR titles and bodies
138
+ - recent activity
139
+
140
+ It also blends in a compact vector similarity score built from hashed token embeddings stored with each record.
141
+ If `embedding_command` is configured, Memoflow uses that provider and falls back to local hashed vectors on any failure.
142
+
143
+ If local provider CLIs are installed, Memoflow will also try:
144
+
145
+ - `gh pr view --json number,title,body`
146
+ - `glab mr view --output json`
147
+
148
+ This is optional and only runs locally.
149
+
150
+ ## Backup and retention
151
+
152
+ Export the encrypted store to a portable bundle:
153
+
154
+ ```bash
155
+ bundle exec memoflow export tmp/memoflow.bundle
156
+ bundle exec memoflow import tmp/memoflow.bundle
157
+ ```
158
+
159
+ Prune older records or cap store size:
160
+
161
+ ```bash
162
+ bundle exec memoflow prune --days 30
163
+ bundle exec memoflow prune --max 500
164
+ ```
165
+
166
+ ## Hook strategy
167
+
168
+ `memoflow install-hook` installs:
169
+
170
+ - `post-commit` to capture the newest commit
171
+ - `prepare-commit-msg` to append a saved task/problem note into the commit message as `Problem-Statement: ...`
172
+
173
+ Use `memoflow task note "..."` before committing to persist a one-off problem statement into `.git/MEMOFLOW_TASK_NOTE`.
174
+
175
+ ## Security notes
176
+
177
+ - Set `MEMOFLOW_KEY` to a project-specific secret
178
+ - The key is not written to disk
179
+ - Records are authenticated with GCM tags
180
+ - Query output is decrypted only when requested
181
+ - The local HTTP server binds to `127.0.0.1` by default
182
+ - PR metadata is captured only from local git/CI environment, not from network calls
183
+ - Export bundles are encrypted with the same configured key
184
+ - Provider embeddings are optional; the default path stays fully local and offline
185
+
186
+ ## Development
187
+
188
+ ```bash
189
+ bundle exec rake test
190
+ ```
191
+
192
+ ## Release checklist
193
+
194
+ ```bash
195
+ ruby -Ilib:test test/memoflow_test.rb
196
+ ruby -Ilib bin/memoflow init
197
+ gem build memoflow.gemspec
198
+ ```
data/bin/memoflow ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require_relative "../lib/memoflow"
6
+
7
+ Memoflow::CLI.start(ARGV)
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+
6
+ input = JSON.parse($stdin.read)
7
+ text = input.fetch("text", "")
8
+ tokens = text.downcase.scan(/[a-z0-9_]+/)
9
+
10
+ embedding = Array.new(8, 0.0)
11
+ tokens.each_with_index do |token, index|
12
+ embedding[index % embedding.length] += token.length
13
+ end
14
+
15
+ puts JSON.generate(embedding: embedding)
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Memoflow
4
+ class CLI
5
+ class << self
6
+ def start(argv)
7
+ command = argv.shift
8
+
9
+ case command
10
+ when "init" then init
11
+ when "capture" then capture(argv)
12
+ when "annotate" then annotate(argv)
13
+ when "query" then query(argv)
14
+ when "task" then task(argv)
15
+ when "context" then context(argv)
16
+ when "serve" then serve(argv)
17
+ when "export" then export_bundle(argv)
18
+ when "import" then import_bundle(argv)
19
+ when "prune" then prune(argv)
20
+ when "install-hook" then install_hook
21
+ else
22
+ puts usage
23
+ exit(command ? 1 : 0)
24
+ end
25
+ rescue Memoflow::Error => e
26
+ warn("memoflow: #{e.message}")
27
+ exit(1)
28
+ end
29
+
30
+ private
31
+
32
+ def client
33
+ Memoflow.client(repo_path: Dir.pwd)
34
+ end
35
+
36
+ def init
37
+ client.init!
38
+ puts JSON.pretty_generate(
39
+ storage: Memoflow.configuration.resolved_storage_path(Dir.pwd).to_s,
40
+ embedding_mode: client.embedding_mode
41
+ )
42
+ end
43
+
44
+ def capture(argv)
45
+ sha = argv[0] if argv[0] && !argv[0].start_with?("--")
46
+ sha = client.last_commit_sha if argv.include?("--last") || sha.nil?
47
+ record = client.capture_commit(sha)
48
+ puts JSON.pretty_generate(record)
49
+ end
50
+
51
+ def annotate(argv)
52
+ text = argv.join(" ").strip
53
+ raise Error, "annotation text is required" if text.empty?
54
+
55
+ record = client.annotate(text)
56
+ puts JSON.pretty_generate(record)
57
+ end
58
+
59
+ def query(argv)
60
+ term = argv.join(" ").strip
61
+ puts JSON.pretty_generate(client.query(term.empty? ? nil : term))
62
+ end
63
+
64
+ def install_hook
65
+ path = HookInstaller.new(repo_path: Dir.pwd).install!
66
+ puts "installed #{path}"
67
+ end
68
+
69
+ def task(argv)
70
+ subcommand = argv.shift
71
+ case subcommand
72
+ when "start"
73
+ title = argv.join(" ").strip
74
+ raise Error, "task title is required" if title.empty?
75
+
76
+ puts JSON.pretty_generate(client.start_task(title))
77
+ when "finish"
78
+ id = argv.shift
79
+ puts JSON.pretty_generate(client.finish_task(id))
80
+ when "resume"
81
+ id = argv.shift
82
+ raise Error, "task id is required" if id.to_s.empty?
83
+
84
+ puts JSON.pretty_generate(client.resume_task(id))
85
+ when "list"
86
+ puts JSON.pretty_generate(client.tasks)
87
+ when "current"
88
+ puts JSON.pretty_generate(client.current_task)
89
+ when "note"
90
+ text = argv.join(" ").strip
91
+ raise Error, "task note is required" if text.empty?
92
+
93
+ File.write(File.join(".git", "MEMOFLOW_TASK_NOTE"), text)
94
+ puts "saved task note"
95
+ else
96
+ raise Error, "unknown task command"
97
+ end
98
+ end
99
+
100
+ def context(argv)
101
+ term = argv.join(" ").strip
102
+ puts client.context_packet(query: term.empty? ? nil : term)
103
+ end
104
+
105
+ def serve(argv)
106
+ port = argv[0] ? Integer(argv[0]) : Memoflow.configuration.server_port
107
+ server = Server.new(
108
+ client: client,
109
+ host: Memoflow.configuration.server_host,
110
+ port: port
111
+ )
112
+ puts "memoflow listening on http://#{Memoflow.configuration.server_host}:#{port}"
113
+ server.start
114
+ end
115
+
116
+ def export_bundle(argv)
117
+ path = argv.shift
118
+ raise Error, "export path is required" if path.to_s.empty?
119
+
120
+ puts client.export_bundle(path)
121
+ end
122
+
123
+ def import_bundle(argv)
124
+ path = argv.shift
125
+ raise Error, "import path is required" if path.to_s.empty?
126
+
127
+ client.import_bundle(path)
128
+ puts "imported #{path}"
129
+ end
130
+
131
+ def prune(argv)
132
+ keep_days = nil
133
+ max_records = nil
134
+ until argv.empty?
135
+ flag = argv.shift
136
+ case flag
137
+ when "--days" then keep_days = Integer(argv.shift)
138
+ when "--max" then max_records = Integer(argv.shift)
139
+ else
140
+ raise Error, "unknown prune option #{flag}"
141
+ end
142
+ end
143
+
144
+ removed = client.prune!(keep_days: keep_days, max_records: max_records)
145
+ puts JSON.pretty_generate(removed: removed, count: removed.length)
146
+ end
147
+
148
+ def usage
149
+ <<~TEXT
150
+ Usage:
151
+ memoflow init
152
+ memoflow capture [SHA|--last]
153
+ memoflow annotate TEXT
154
+ memoflow query [TERM]
155
+ memoflow context [TERM]
156
+ memoflow task start TITLE
157
+ memoflow task finish [TASK_ID]
158
+ memoflow task resume TASK_ID
159
+ memoflow task list
160
+ memoflow task current
161
+ memoflow task note TEXT
162
+ memoflow serve [PORT]
163
+ memoflow export PATH
164
+ memoflow import PATH
165
+ memoflow prune [--days N] [--max N]
166
+ memoflow install-hook
167
+ TEXT
168
+ end
169
+ end
170
+ end
171
+ end