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
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,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)
|
data/lib/memoflow/cli.rb
ADDED
|
@@ -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
|