legionio 1.6.20 → 1.6.21
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 +4 -4
- data/CHANGELOG.md +6 -0
- data/lib/legion/cli/knowledge_command.rb +123 -1
- data/lib/legion/cli/setup_command.rb +6 -6
- data/lib/legion/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d50e1efc9398c0e2d380c7a921e2ccc8b6e6db3a142d4f4a700ff536555a2e4
|
|
4
|
+
data.tar.gz: e44d47e0c4d04362d9a35c6ad6953d0b4a42a5f0f2225dd0420e7f2d1c544ad4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4cc63f869d21abc423f7836a25b91f62a815d01d1972004b85728be0b969e332a5f95c9172ab6177f3759e837b6e40bbb30afba1ecd3a817cd5088fff4c675b
|
|
7
|
+
data.tar.gz: 719f9a8e3d316937be26ed610b7ba86c653317b476b271d821591fecf5ff8157c02788b03df1468c7fe3af19d76ec1a55cf48312d911a502e30873c51d378bb0
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.6.21] - 2026-03-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `legionio knowledge capture transcript` CLI command: ingests Claude Code session transcripts into Apollo knowledge store
|
|
9
|
+
- Stop hook for automatic transcript capture at session end (installed via `legion setup claude-code`)
|
|
10
|
+
|
|
5
11
|
## [1.6.20] - 2026-03-27
|
|
6
12
|
|
|
7
13
|
### Changed
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module CLI
|
|
5
7
|
class MonitorCommand < Thor
|
|
@@ -173,10 +175,130 @@ module Legion
|
|
|
173
175
|
end
|
|
174
176
|
end
|
|
175
177
|
|
|
176
|
-
|
|
178
|
+
desc 'transcript', 'Capture a Claude Code session transcript as knowledge'
|
|
179
|
+
option :session_id, type: :string, desc: 'Session ID (defaults to CLAUDE_SESSION_ID env)'
|
|
180
|
+
option :cwd, type: :string, desc: 'Working directory (defaults to CLAUDE_CWD env)'
|
|
181
|
+
option :max_chunks, type: :numeric, default: 50, desc: 'Max conversation turn chunks to ingest'
|
|
182
|
+
def transcript
|
|
183
|
+
session_id = options[:session_id] || ENV.fetch('CLAUDE_SESSION_ID', nil)
|
|
184
|
+
cwd = options[:cwd] || ENV.fetch('CLAUDE_CWD', nil) || ::Dir.pwd
|
|
185
|
+
|
|
186
|
+
unless session_id
|
|
187
|
+
formatter.warn('No session ID provided (set CLAUDE_SESSION_ID or --session-id)')
|
|
188
|
+
return
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
jsonl_path = resolve_transcript_path(session_id, cwd)
|
|
192
|
+
unless jsonl_path && ::File.exist?(jsonl_path)
|
|
193
|
+
formatter.warn("Transcript not found: #{jsonl_path || 'could not resolve path'}")
|
|
194
|
+
return
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
turns = extract_turns(jsonl_path)
|
|
198
|
+
if turns.empty?
|
|
199
|
+
formatter.warn('No conversation turns found in transcript')
|
|
200
|
+
return
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
repo = `git -C #{Shellwords.escape(cwd)} rev-parse --show-toplevel 2>/dev/null`.strip.split('/').last
|
|
204
|
+
base_tags = ['claude-code', 'transcript', "session:#{session_id}", ::Time.now.strftime('%Y-%m-%d')]
|
|
205
|
+
base_tags << "repo:#{repo}" unless repo.to_s.empty?
|
|
206
|
+
|
|
207
|
+
ingested = 0
|
|
208
|
+
turns.first(options[:max_chunks]).each_with_index do |turn, idx|
|
|
209
|
+
content = format_turn(turn, idx + 1)
|
|
210
|
+
next if content.strip.empty?
|
|
211
|
+
|
|
212
|
+
result = ingest_content(
|
|
213
|
+
content: content,
|
|
214
|
+
tags: base_tags + ["turn:#{idx + 1}"],
|
|
215
|
+
source: "claude-code:#{session_id}:turn-#{idx + 1}"
|
|
216
|
+
)
|
|
217
|
+
ingested += 1 if result[:success]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
out = formatter
|
|
221
|
+
if options[:json]
|
|
222
|
+
out.json(success: true, session_id: session_id, turns: turns.size, ingested: ingested)
|
|
223
|
+
else
|
|
224
|
+
out.success("Captured #{ingested}/#{[turns.size, options[:max_chunks]].min} turns from session #{session_id[0, 8]}")
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
no_commands do # rubocop:disable Metrics/BlockLength
|
|
177
229
|
def formatter
|
|
178
230
|
@formatter ||= Output::Formatter.new(json: options[:json], color: !options[:no_color])
|
|
179
231
|
end
|
|
232
|
+
|
|
233
|
+
def resolve_transcript_path(session_id, cwd)
|
|
234
|
+
project_dir = cwd.gsub('/', '-')
|
|
235
|
+
::File.expand_path("~/.claude/projects/#{project_dir}/#{session_id}.jsonl")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def extract_turns(path)
|
|
239
|
+
turns = []
|
|
240
|
+
current_turn = nil
|
|
241
|
+
|
|
242
|
+
::File.foreach(path) do |line|
|
|
243
|
+
entry = ::JSON.parse(line, symbolize_names: true)
|
|
244
|
+
type = entry[:type]
|
|
245
|
+
|
|
246
|
+
case type
|
|
247
|
+
when 'user'
|
|
248
|
+
turns << current_turn if current_turn
|
|
249
|
+
current_turn = { user: extract_message_text(entry), assistant: +'', timestamp: entry[:timestamp] }
|
|
250
|
+
when 'assistant'
|
|
251
|
+
next unless current_turn
|
|
252
|
+
|
|
253
|
+
text = extract_message_text(entry)
|
|
254
|
+
current_turn[:assistant] << text unless text.empty?
|
|
255
|
+
end
|
|
256
|
+
rescue ::JSON::ParserError
|
|
257
|
+
next
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
turns << current_turn if current_turn
|
|
261
|
+
turns
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def extract_message_text(entry)
|
|
265
|
+
msg = entry[:message]
|
|
266
|
+
return '' unless msg
|
|
267
|
+
|
|
268
|
+
content = msg[:content]
|
|
269
|
+
case content
|
|
270
|
+
when String then content
|
|
271
|
+
when Array
|
|
272
|
+
content.filter_map { |block| block[:text] if block[:type] == 'text' }.join("\n")
|
|
273
|
+
else ''
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def format_turn(turn, number)
|
|
278
|
+
text = "## Turn #{number}\n"
|
|
279
|
+
text << "Timestamp: #{turn[:timestamp]}\n\n" if turn[:timestamp]
|
|
280
|
+
text << "### User\n#{truncate_text(turn[:user], 4096)}\n\n"
|
|
281
|
+
text << "### Assistant\n#{truncate_text(turn[:assistant], 4096)}\n"
|
|
282
|
+
text
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def truncate_text(text, max_bytes)
|
|
286
|
+
return text if text.bytesize <= max_bytes
|
|
287
|
+
|
|
288
|
+
"#{text.byteslice(0, max_bytes - 20)}\n\n[truncated]"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def ingest_content(content:, tags:, source:)
|
|
292
|
+
if defined?(Legion::Extensions::Knowledge::Runners::Ingest)
|
|
293
|
+
Legion::Extensions::Knowledge::Runners::Ingest.ingest_file(
|
|
294
|
+
content: content, tags: tags, source: source
|
|
295
|
+
)
|
|
296
|
+
elsif defined?(Legion::Apollo)
|
|
297
|
+
Legion::Apollo.ingest(content: content, tags: tags, source: source)
|
|
298
|
+
else
|
|
299
|
+
{ success: false, error: 'neither lex-knowledge nor legion-apollo available' }
|
|
300
|
+
end
|
|
301
|
+
end
|
|
180
302
|
end
|
|
181
303
|
end
|
|
182
304
|
|
|
@@ -350,9 +350,9 @@ module Legion
|
|
|
350
350
|
|
|
351
351
|
hooks = existing['hooks'] || {}
|
|
352
352
|
|
|
353
|
-
has_commit
|
|
354
|
-
|
|
355
|
-
if has_commit &&
|
|
353
|
+
has_commit = Array(hooks['PostToolUse']).any? { |h| h['command']&.include?('knowledge capture commit') }
|
|
354
|
+
has_transcript = Array(hooks['Stop']).any? { |h| h['command']&.include?('knowledge capture transcript') }
|
|
355
|
+
if has_commit && has_transcript && !options[:force]
|
|
356
356
|
puts ' Write-back hooks already present (use --force to overwrite)' unless options[:json]
|
|
357
357
|
return
|
|
358
358
|
end
|
|
@@ -368,10 +368,10 @@ module Legion
|
|
|
368
368
|
}
|
|
369
369
|
end
|
|
370
370
|
|
|
371
|
-
unless
|
|
371
|
+
unless has_transcript
|
|
372
372
|
hooks['Stop'] << {
|
|
373
|
-
'command' => 'legionio knowledge capture
|
|
374
|
-
'timeout' =>
|
|
373
|
+
'command' => 'legionio knowledge capture transcript',
|
|
374
|
+
'timeout' => 30_000
|
|
375
375
|
}
|
|
376
376
|
end
|
|
377
377
|
|
data/lib/legion/version.rb
CHANGED