rails_console_ai 0.29.0 → 0.31.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/README.md +65 -0
  4. data/app/controllers/rails_console_ai/agent_versions_controller.rb +29 -0
  5. data/app/controllers/rails_console_ai/agents_controller.rb +176 -0
  6. data/app/controllers/rails_console_ai/application_controller.rb +5 -0
  7. data/app/controllers/rails_console_ai/memories_controller.rb +136 -0
  8. data/app/controllers/rails_console_ai/memory_versions_controller.rb +29 -0
  9. data/app/controllers/rails_console_ai/skill_versions_controller.rb +29 -0
  10. data/app/controllers/rails_console_ai/skills_controller.rb +171 -0
  11. data/app/helpers/rails_console_ai/diff_helper.rb +114 -0
  12. data/app/models/rails_console_ai/agent.rb +143 -0
  13. data/app/models/rails_console_ai/agent_version.rb +34 -0
  14. data/app/models/rails_console_ai/memory.rb +103 -0
  15. data/app/models/rails_console_ai/memory_version.rb +31 -0
  16. data/app/models/rails_console_ai/session.rb +1 -1
  17. data/app/models/rails_console_ai/skill.rb +148 -0
  18. data/app/models/rails_console_ai/skill_version.rb +33 -0
  19. data/app/views/layouts/rails_console_ai/application.html.erb +78 -1
  20. data/app/views/rails_console_ai/agent_versions/index.html.erb +28 -0
  21. data/app/views/rails_console_ai/agent_versions/show.html.erb +25 -0
  22. data/app/views/rails_console_ai/agents/_form.html.erb +40 -0
  23. data/app/views/rails_console_ai/agents/diff.html.erb +15 -0
  24. data/app/views/rails_console_ai/agents/edit.html.erb +7 -0
  25. data/app/views/rails_console_ai/agents/index.html.erb +80 -0
  26. data/app/views/rails_console_ai/agents/new.html.erb +8 -0
  27. data/app/views/rails_console_ai/agents/show.html.erb +108 -0
  28. data/app/views/rails_console_ai/memories/_form.html.erb +29 -0
  29. data/app/views/rails_console_ai/memories/diff.html.erb +15 -0
  30. data/app/views/rails_console_ai/memories/edit.html.erb +7 -0
  31. data/app/views/rails_console_ai/memories/index.html.erb +67 -0
  32. data/app/views/rails_console_ai/memories/new.html.erb +8 -0
  33. data/app/views/rails_console_ai/memories/show.html.erb +65 -0
  34. data/app/views/rails_console_ai/memory_versions/index.html.erb +26 -0
  35. data/app/views/rails_console_ai/memory_versions/show.html.erb +21 -0
  36. data/app/views/rails_console_ai/skill_versions/index.html.erb +28 -0
  37. data/app/views/rails_console_ai/skill_versions/show.html.erb +23 -0
  38. data/app/views/rails_console_ai/skills/_form.html.erb +43 -0
  39. data/app/views/rails_console_ai/skills/diff.html.erb +15 -0
  40. data/app/views/rails_console_ai/skills/edit.html.erb +7 -0
  41. data/app/views/rails_console_ai/skills/index.html.erb +79 -0
  42. data/app/views/rails_console_ai/skills/new.html.erb +8 -0
  43. data/app/views/rails_console_ai/skills/show.html.erb +94 -0
  44. data/config/routes.rb +39 -0
  45. data/lib/rails_console_ai/agent_loader.rb +139 -43
  46. data/lib/rails_console_ai/agent_runner.rb +209 -0
  47. data/lib/rails_console_ai/channel/api.rb +139 -0
  48. data/lib/rails_console_ai/conversation_engine.rb +19 -13
  49. data/lib/rails_console_ai/session_logger.rb +10 -0
  50. data/lib/rails_console_ai/skill_loader.rb +130 -29
  51. data/lib/rails_console_ai/storage/database_storage.rb +195 -0
  52. data/lib/rails_console_ai/tools/memory_tools.rb +110 -32
  53. data/lib/rails_console_ai/tools/registry.rb +99 -8
  54. data/lib/rails_console_ai/version.rb +1 -1
  55. data/lib/rails_console_ai.rb +240 -0
  56. data/lib/tasks/rails_console_ai.rake +7 -0
  57. metadata +55 -1
@@ -0,0 +1,67 @@
1
+ <div class="btn-bar">
2
+ <%= link_to 'New memory', new_memory_path, class: 'btn' %>
3
+ <% if @sort == 'used' %>
4
+ <%= link_to 'Sort: alphabetical', memories_path(q: @q), class: 'btn btn-secondary' %>
5
+ <% else %>
6
+ <%= link_to 'Sort: most used', memories_path(q: @q, sort: 'used'), class: 'btn btn-secondary' %>
7
+ <% end %>
8
+ <form method="get" action="<%= memories_path %>" style="margin-left:auto;">
9
+ <input type="text" name="q" value="<%= @q %>" placeholder="Search memories…" style="padding:6px 10px; border:1px solid #ccc; border-radius:4px;">
10
+ <% if @sort.present? %><input type="hidden" name="sort" value="<%= @sort %>"><% end %>
11
+ </form>
12
+ </div>
13
+
14
+ <% if @memories.empty? %>
15
+ <div class="meta-card">
16
+ <p class="text-muted">No memories yet. <%= link_to 'Create one', new_memory_path %> or drop a Markdown file in <code>.rails_console_ai/memories/</code>.</p>
17
+ </div>
18
+ <% else %>
19
+ <table>
20
+ <thead>
21
+ <tr>
22
+ <th>Name</th>
23
+ <th>Description</th>
24
+ <th>Tags</th>
25
+ <th>Source</th>
26
+ <th title="Number of times the AI recalled this memory (DB only)">Uses</th>
27
+ <th>Last used</th>
28
+ <th></th>
29
+ </tr>
30
+ </thead>
31
+ <tbody>
32
+ <% @memories.each do |m| %>
33
+ <% link_id = m['id'] || m['name'] %>
34
+ <tr>
35
+ <td><strong><%= link_to m['name'], memory_path(link_id) %></strong></td>
36
+ <td class="query-cell"><%= m['description'].to_s[0, 140] %></td>
37
+ <td>
38
+ <% Array(m['tags']).each do |t| %><span class="tag"><%= t %></span><% end %>
39
+ </td>
40
+ <td>
41
+ <% if m['source'] == :db %>
42
+ <span class="badge source-db">DB</span>
43
+ <% else %>
44
+ <span class="badge source-file">FILE</span>
45
+ <% end %>
46
+ </td>
47
+ <td>
48
+ <% if m['source'] == :db %>
49
+ <%= m['use_count'] || 0 %>
50
+ <% else %>
51
+ <span class="text-muted">—</span>
52
+ <% end %>
53
+ </td>
54
+ <td class="text-muted" style="font-size:12px;">
55
+ <%= m['last_used_at']&.strftime('%Y-%m-%d %H:%M') || '—' %>
56
+ </td>
57
+ <td>
58
+ <%= link_to 'View', memory_path(link_id) %>
59
+ <% if m['source'] == :db %>
60
+ · <%= link_to 'Edit', edit_memory_path(m['id']) %>
61
+ <% end %>
62
+ </td>
63
+ </tr>
64
+ <% end %>
65
+ </tbody>
66
+ </table>
67
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <%= link_to '← All memories', memories_path, class: 'back-link' %>
2
+ <h2 style="margin-bottom:16px;">New memory</h2>
3
+
4
+ <%= render 'form',
5
+ form_url: memories_path,
6
+ form_method: :post,
7
+ submit_label: 'Create memory',
8
+ cancel_path: memories_path %>
@@ -0,0 +1,65 @@
1
+ <%
2
+ is_db = @memory.is_a?(RailsConsoleAi::Memory)
3
+ name = is_db ? @memory.name : @memory['name']
4
+ description = is_db ? @memory.description : @memory['description']
5
+ tags = is_db ? Array(@memory.tags) : Array(@memory['tags'])
6
+ file_key = is_db ? nil : @memory['file_key']
7
+ %>
8
+
9
+ <%= link_to '← All memories', memories_path, class: 'back-link' %>
10
+
11
+ <div class="meta-card">
12
+ <h2 style="margin-bottom:8px;">
13
+ <%= name %>
14
+ <% if is_db %>
15
+ <span class="badge source-db">DB</span>
16
+ <% else %>
17
+ <span class="badge source-file">FILE</span>
18
+ <% end %>
19
+ </h2>
20
+ <div class="meta-grid" style="margin-top:16px;">
21
+ <div class="meta-item"><label>Tags</label><span><%= tags.empty? ? '—' : tags.join(', ') %></span></div>
22
+ <% if is_db %>
23
+ <div class="meta-item"><label>Updated</label><span><%= @memory.updated_at.strftime('%Y-%m-%d %H:%M') %></span></div>
24
+ <div class="meta-item"><label>Versions</label><span><%= @memory.versions.count %></span></div>
25
+ <div class="meta-item"><label>Times recalled</label><span><%= @memory.use_count %></span></div>
26
+ <div class="meta-item"><label>Last recalled</label><span><%= @memory.last_used_at&.strftime('%Y-%m-%d %H:%M') || 'never' %></span></div>
27
+ <% else %>
28
+ <div class="meta-item"><label>Path</label><span class="mono"><%= file_key %></span></div>
29
+ <% end %>
30
+ </div>
31
+ </div>
32
+
33
+ <div class="btn-bar">
34
+ <% if is_db %>
35
+ <%= link_to 'Edit', edit_memory_path(@memory), class: 'btn' %>
36
+ <%= link_to 'Versions', memory_versions_path(@memory), class: 'btn btn-secondary' %>
37
+ <%= button_to 'Delete', memory_path(@memory), method: :delete, class: 'btn btn-danger', form: { style: 'display:inline' },data: { confirm: 'Delete this memory? Past versions remain in history.' } %>
38
+ <% end %>
39
+ </div>
40
+
41
+ <h3 style="margin-bottom:8px;">Description</h3>
42
+ <div class="markdown-body"><%= description %></div>
43
+
44
+ <% if is_db && @versions && @versions.any? %>
45
+ <h3 style="margin:24px 0 8px;">Recent versions</h3>
46
+ <div class="versions-list">
47
+ <% @versions.first(5).each do |v| %>
48
+ <div class="ver-row">
49
+ <div>
50
+ <strong>v<%= v.id %></strong>
51
+ <span class="ver-meta">— <%= v.created_at.strftime('%Y-%m-%d %H:%M') %> by <%= v.edited_by || 'unknown' %></span>
52
+ <% if v.change_note.present? %><div class="ver-meta"><%= v.change_note %></div><% end %>
53
+ </div>
54
+ <div>
55
+ <%= link_to 'View', memory_version_path(@memory, v) %>
56
+ ·
57
+ <%= link_to 'Diff vs current', diff_memories_path(memory_id: @memory.id, from: v.id) %>
58
+ </div>
59
+ </div>
60
+ <% end %>
61
+ </div>
62
+ <% if @versions.size > 5 %>
63
+ <p style="margin-top:8px;"><%= link_to "See all #{@versions.size} versions →", memory_versions_path(@memory) %></p>
64
+ <% end %>
65
+ <% end %>
@@ -0,0 +1,26 @@
1
+ <%= link_to '← Back to memory', memory_path(@memory), class: 'back-link' %>
2
+
3
+ <h2 style="margin-bottom:16px;">Versions — <%= @memory.name %></h2>
4
+
5
+ <% if @versions.empty? %>
6
+ <div class="meta-card"><p class="text-muted">No versions recorded yet.</p></div>
7
+ <% else %>
8
+ <div class="versions-list">
9
+ <% @versions.each do |v| %>
10
+ <div class="ver-row">
11
+ <div>
12
+ <strong>v<%= v.id %></strong>
13
+ <span class="ver-meta">— <%= v.created_at.strftime('%Y-%m-%d %H:%M:%S') %> by <%= v.edited_by || 'unknown' %></span>
14
+ <% if v.change_note.present? %><div class="ver-meta"><%= v.change_note %></div><% end %>
15
+ </div>
16
+ <div>
17
+ <%= link_to 'View', memory_version_path(@memory, v) %>
18
+ ·
19
+ <%= link_to 'Diff vs current', diff_memories_path(memory_id: @memory.id, from: v.id) %>
20
+ ·
21
+ <%= button_to 'Restore', restore_memory_version_path(@memory, v), method: :post, class: 'btn btn-secondary', form: { style: 'display:inline' },data: { confirm: "Restore version ##{v.id}? A new version row will be created from this snapshot." } %>
22
+ </div>
23
+ </div>
24
+ <% end %>
25
+ </div>
26
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <%= link_to '← All versions', memory_versions_path(@memory), class: 'back-link' %>
2
+
3
+ <div class="meta-card">
4
+ <h2 style="margin-bottom:8px;"><%= @version.name %> <span class="badge source-db">v<%= @version.id %></span></h2>
5
+ <div class="meta-grid" style="margin-top:16px;">
6
+ <div class="meta-item"><label>Edited by</label><span><%= @version.edited_by || '—' %></span></div>
7
+ <div class="meta-item"><label>At</label><span><%= @version.created_at.strftime('%Y-%m-%d %H:%M:%S') %></span></div>
8
+ <div class="meta-item"><label>Tags</label><span><%= Array(@version.tags).join(', ').presence || '—' %></span></div>
9
+ </div>
10
+ <% if @version.change_note.present? %>
11
+ <p style="margin-top:12px;"><strong>Change note:</strong> <%= @version.change_note %></p>
12
+ <% end %>
13
+ </div>
14
+
15
+ <div class="btn-bar">
16
+ <%= button_to "Restore this version", restore_memory_version_path(@memory, @version), method: :post, class: 'btn btn-danger', data: { confirm: "Restore version ##{@version.id}? Current memory will be overwritten." } %>
17
+ <%= link_to 'Diff vs current', diff_memories_path(memory_id: @memory.id, from: @version.id), class: 'btn btn-secondary' %>
18
+ </div>
19
+
20
+ <h3 style="margin-top:24px; margin-bottom:8px;">Description</h3>
21
+ <div class="markdown-body"><%= @version.description %></div>
@@ -0,0 +1,28 @@
1
+ <%= link_to '← Back to skill', skill_path(@skill), class: 'back-link' %>
2
+
3
+ <h2 style="margin-bottom:16px;">Versions — <%= @skill.name %></h2>
4
+
5
+ <% if @versions.empty? %>
6
+ <div class="meta-card"><p class="text-muted">No versions recorded yet.</p></div>
7
+ <% else %>
8
+ <div class="versions-list">
9
+ <% @versions.each do |v| %>
10
+ <div class="ver-row">
11
+ <div>
12
+ <strong>v<%= v.id %></strong>
13
+ <span class="ver-meta">— <%= v.created_at.strftime('%Y-%m-%d %H:%M:%S') %> by <%= v.edited_by || 'unknown' %></span>
14
+ <% if v.status == 'approved' %><span class="badge status-approved">APPROVED</span>
15
+ <% elsif v.status == 'proposed' %><span class="badge status-proposed">PROPOSED</span><% end %>
16
+ <% if v.change_note.present? %><div class="ver-meta"><%= v.change_note %></div><% end %>
17
+ </div>
18
+ <div>
19
+ <%= link_to 'View', skill_version_path(@skill, v) %>
20
+ ·
21
+ <%= link_to 'Diff vs current', diff_skills_path(skill_id: @skill.id, from: v.id) %>
22
+ ·
23
+ <%= button_to 'Restore', restore_skill_version_path(@skill, v), method: :post, class: 'btn btn-secondary', form: { style: 'display:inline' }, data: { confirm: "Restore version ##{v.id}? Restoring reverts the skill to PROPOSED and a new version row will be created." } %>
24
+ </div>
25
+ </div>
26
+ <% end %>
27
+ </div>
28
+ <% end %>
@@ -0,0 +1,23 @@
1
+ <%= link_to '← All versions', skill_versions_path(@skill), class: 'back-link' %>
2
+
3
+ <div class="meta-card">
4
+ <h2 style="margin-bottom:8px;"><%= @version.name %> <span class="badge source-db">v<%= @version.id %></span></h2>
5
+ <p class="text-muted"><%= @version.description %></p>
6
+ <div class="meta-grid" style="margin-top:16px;">
7
+ <div class="meta-item"><label>Edited by</label><span><%= @version.edited_by || '—' %></span></div>
8
+ <div class="meta-item"><label>At</label><span><%= @version.created_at.strftime('%Y-%m-%d %H:%M:%S') %></span></div>
9
+ <div class="meta-item"><label>Tags</label><span><%= Array(@version.tags).join(', ').presence || '—' %></span></div>
10
+ <div class="meta-item"><label>Bypass guards</label><span><%= Array(@version.bypass_guards_for_methods).join(', ').presence || '—' %></span></div>
11
+ </div>
12
+ <% if @version.change_note.present? %>
13
+ <p style="margin-top:12px;"><strong>Change note:</strong> <%= @version.change_note %></p>
14
+ <% end %>
15
+ </div>
16
+
17
+ <div class="btn-bar">
18
+ <%= button_to "Restore this version", restore_skill_version_path(@skill, @version), method: :post, class: 'btn btn-danger', data: { confirm: "Restore version ##{@version.id}? Current skill will be overwritten." } %>
19
+ <%= link_to 'Diff vs current', diff_skills_path(skill_id: @skill.id, from: @version.id), class: 'btn btn-secondary' %>
20
+ </div>
21
+
22
+ <h3 style="margin-top:24px; margin-bottom:8px;">Recipe</h3>
23
+ <div class="markdown-body"><%= @version.body %></div>
@@ -0,0 +1,43 @@
1
+ <% if @skill.persisted? && @skill.respond_to?(:approved?) && @skill.approved? %>
2
+ <div class="flash flash-alert" style="margin-bottom:16px;">
3
+ Heads up: this skill is currently <strong>approved</strong>. Editing it will revert its status to <strong>proposed</strong> and the AI won't be able to activate it again until someone re-approves it.
4
+ </div>
5
+ <% elsif !@skill.persisted? %>
6
+ <div class="flash flash-notice" style="margin-bottom:16px;">
7
+ New DB skills start as <strong>proposed</strong>. After saving, an approver (likely you) needs to click the Approve button on the show page before the AI can activate this skill.
8
+ </div>
9
+ <% end %>
10
+
11
+ <%= form_with model: @skill, url: form_url, method: form_method, local: true do |f| %>
12
+ <div class="form-card">
13
+ <div class="form-row">
14
+ <label>Markdown (frontmatter + body)</label>
15
+ <%= f.text_area :content, value: @skill.content, rows: 28, required: true, style: 'font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px;' %>
16
+ <div class="hint">
17
+ YAML frontmatter between <code>---</code> lines, then the markdown body. Required frontmatter:
18
+ <code>name</code>, <code>description</code>. Optional: <code>tags</code>,
19
+ <code>bypass_guards_for_methods</code> (entries shaped like <code>ClassName#method</code> or
20
+ <code>ClassName.method</code> — while the skill is active, calls to these methods skip the
21
+ configured safety guards for that tool-call only).
22
+ </div>
23
+ </div>
24
+
25
+ <hr style="margin: 20px 0; border: 0; border-top: 1px solid #eee;">
26
+
27
+ <div class="form-row">
28
+ <label>Edited by</label>
29
+ <input type="text" name="edited_by" value="<%= params[:edited_by] || cookies[:rca_edited_by] %>" placeholder="Your name or handle">
30
+ <div class="hint">Recorded with this version. Free text — the gem does not assume authentication beyond the admin login.</div>
31
+ </div>
32
+
33
+ <div class="form-row">
34
+ <label>Change note (optional)</label>
35
+ <input type="text" name="change_note" value="" placeholder="What did you change and why?">
36
+ </div>
37
+ </div>
38
+
39
+ <div class="btn-bar">
40
+ <%= f.submit submit_label, class: 'btn' %>
41
+ <%= link_to 'Cancel', cancel_path, class: 'btn btn-secondary' %>
42
+ </div>
43
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <%= link_to '← Back to skill', skill_path(@skill), class: 'back-link' %>
2
+
3
+ <h2 style="margin-bottom:8px;">Diff — <%= @skill.name %></h2>
4
+ <p class="text-muted" style="margin-bottom:16px;">
5
+ Version #<%= @from.id %> (<%= @from.created_at.strftime('%Y-%m-%d %H:%M') %>) → <%= @to_label %>
6
+ </p>
7
+
8
+ <%= render_text_diff(@from.content, @to_content, left_label: "v##{@from.id}", right_label: @to_label) %>
9
+
10
+ <div class="btn-bar" style="margin-top:24px;">
11
+ <% if @to.nil? %>
12
+ <%= button_to "Restore v##{@from.id}", restore_skill_version_path(@skill, @from), method: :post, class: 'btn btn-danger', data: { confirm: 'Restoring will overwrite the current skill (a new version row is created).' } %>
13
+ <% end %>
14
+ <%= link_to 'Back', skill_path(@skill), class: 'btn btn-secondary' %>
15
+ </div>
@@ -0,0 +1,7 @@
1
+ <%= link_to '← Back to skill', skill_path(@skill), class: 'back-link' %>
2
+ <h2 style="margin-bottom:16px;">Edit skill</h2>
3
+ <%= render 'form',
4
+ form_url: skill_path(@skill),
5
+ form_method: :patch,
6
+ submit_label: 'Save changes',
7
+ cancel_path: skill_path(@skill) %>
@@ -0,0 +1,79 @@
1
+ <div class="btn-bar">
2
+ <%= link_to 'New skill', new_skill_path, class: 'btn' %>
3
+ <% if @sort == 'used' %>
4
+ <%= link_to 'Sort: alphabetical', skills_path(q: @q), class: 'btn btn-secondary' %>
5
+ <% else %>
6
+ <%= link_to 'Sort: most used', skills_path(q: @q, sort: 'used'), class: 'btn btn-secondary' %>
7
+ <% end %>
8
+ <form method="get" action="<%= skills_path %>" style="margin-left:auto;">
9
+ <input type="text" name="q" value="<%= @q %>" placeholder="Search skills…" style="padding:6px 10px; border:1px solid #ccc; border-radius:4px;">
10
+ <% if @sort.present? %><input type="hidden" name="sort" value="<%= @sort %>"><% end %>
11
+ </form>
12
+ </div>
13
+
14
+ <% proposed_count = @skills.count { |s| s['source'] == :db && s['status'] != 'approved' } %>
15
+ <% if proposed_count > 0 %>
16
+ <div class="flash flash-alert" style="margin-bottom:16px;">
17
+ <strong><%= proposed_count %></strong> skill<%= proposed_count == 1 ? '' : 's' %> awaiting approval. The AI cannot activate them until a human approves.
18
+ </div>
19
+ <% end %>
20
+
21
+ <% if @skills.empty? %>
22
+ <div class="meta-card">
23
+ <p class="text-muted">No skills yet. <%= link_to 'Create one', new_skill_path %> or drop a Markdown file in <code>.rails_console_ai/skills/</code>.</p>
24
+ </div>
25
+ <% else %>
26
+ <table>
27
+ <thead>
28
+ <tr>
29
+ <th>Name</th>
30
+ <th>Description</th>
31
+ <th>Tags</th>
32
+ <th>Source / Status</th>
33
+ <th title="Number of times the AI activated this skill (DB only)">Uses</th>
34
+ <th>Last used</th>
35
+ <th></th>
36
+ </tr>
37
+ </thead>
38
+ <tbody>
39
+ <% @skills.each do |s| %>
40
+ <% link_id = s['id'] || s['name'] %>
41
+ <tr>
42
+ <td><strong><%= link_to s['name'], skill_path(link_id) %></strong></td>
43
+ <td class="query-cell"><%= s['description'] %></td>
44
+ <td>
45
+ <% Array(s['tags']).each do |t| %><span class="tag"><%= t %></span><% end %>
46
+ </td>
47
+ <td>
48
+ <% if s['source'] == :db %>
49
+ <span class="badge source-db">DB</span>
50
+ <% if s['status'] == 'approved' %>
51
+ <span class="badge status-approved">APPROVED</span>
52
+ <% else %>
53
+ <span class="badge status-proposed">PROPOSED</span>
54
+ <% end %>
55
+ <% else %>
56
+ <span class="badge source-file">FILE</span>
57
+ <% end %>
58
+ </td>
59
+ <td>
60
+ <% if s['source'] == :db %>
61
+ <%= s['use_count'] || 0 %>
62
+ <% else %>
63
+ <span class="text-muted">—</span>
64
+ <% end %>
65
+ </td>
66
+ <td class="text-muted" style="font-size:12px;">
67
+ <%= s['last_used_at']&.strftime('%Y-%m-%d %H:%M') || '—' %>
68
+ </td>
69
+ <td>
70
+ <%= link_to 'View', skill_path(link_id) %>
71
+ <% if s['source'] == :db %>
72
+ · <%= link_to 'Edit', edit_skill_path(s['id']) %>
73
+ <% end %>
74
+ </td>
75
+ </tr>
76
+ <% end %>
77
+ </tbody>
78
+ </table>
79
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <%= link_to '← All skills', skills_path, class: 'back-link' %>
2
+ <h2 style="margin-bottom:16px;">New skill</h2>
3
+
4
+ <%= render 'form',
5
+ form_url: skills_path,
6
+ form_method: :post,
7
+ submit_label: 'Create skill',
8
+ cancel_path: skills_path %>
@@ -0,0 +1,94 @@
1
+ <%
2
+ # @skill can be either a Skill AR record or a Hash from a file source.
3
+ is_db = @skill.is_a?(RailsConsoleAi::Skill)
4
+ name = is_db ? @skill.name : @skill['name']
5
+ description = is_db ? @skill.description : @skill['description']
6
+ body = is_db ? @skill.body : @skill['body']
7
+ tags = is_db ? Array(@skill.tags) : Array(@skill['tags'])
8
+ bypass = is_db ? Array(@skill.bypass_guards_for_methods) : Array(@skill['bypass_guards_for_methods'])
9
+ file_key = is_db ? nil : @skill['file_key']
10
+ status = is_db ? @skill.status : 'approved' # file skills are implicitly approved
11
+ %>
12
+
13
+ <%= link_to '← All skills', skills_path, class: 'back-link' %>
14
+
15
+ <% if is_db && !@skill.approved? %>
16
+ <div class="flash flash-alert" style="margin-bottom:16px;">
17
+ <strong>Awaiting approval.</strong> The AI cannot activate this skill until a human approves it.
18
+ Review the recipe and bypass-guard list below, then click <strong>Approve</strong>.
19
+ </div>
20
+ <% end %>
21
+
22
+ <div class="meta-card">
23
+ <h2 style="margin-bottom:8px;">
24
+ <%= name %>
25
+ <% if is_db %>
26
+ <span class="badge source-db">DB</span>
27
+ <% if @skill.approved? %>
28
+ <span class="badge status-approved">APPROVED</span>
29
+ <% else %>
30
+ <span class="badge status-proposed">PROPOSED</span>
31
+ <% end %>
32
+ <% else %>
33
+ <span class="badge source-file">FILE</span>
34
+ <% end %>
35
+ </h2>
36
+ <p class="text-muted"><%= description %></p>
37
+ <div class="meta-grid" style="margin-top:16px;">
38
+ <div class="meta-item"><label>Tags</label><span><%= tags.empty? ? '—' : tags.join(', ') %></span></div>
39
+ <div class="meta-item"><label>Bypass guards</label><span><%= bypass.empty? ? '—' : bypass.join(', ') %></span></div>
40
+ <% if is_db %>
41
+ <div class="meta-item"><label>Updated</label><span><%= @skill.updated_at.strftime('%Y-%m-%d %H:%M') %></span></div>
42
+ <div class="meta-item"><label>Versions</label><span><%= @skill.versions.count %></span></div>
43
+ <div class="meta-item"><label>Times activated</label><span><%= @skill.use_count %></span></div>
44
+ <div class="meta-item"><label>Last activated</label><span><%= @skill.last_used_at&.strftime('%Y-%m-%d %H:%M') || 'never' %></span></div>
45
+ <% if @skill.approved? %>
46
+ <div class="meta-item"><label>Approved by</label><span><%= @skill.approved_by || '—' %></span></div>
47
+ <div class="meta-item"><label>Approved at</label><span><%= @skill.approved_at&.strftime('%Y-%m-%d %H:%M') || '—' %></span></div>
48
+ <% end %>
49
+ <% else %>
50
+ <div class="meta-item"><label>Path</label><span class="mono"><%= file_key %></span></div>
51
+ <% end %>
52
+ </div>
53
+ </div>
54
+
55
+ <div class="btn-bar">
56
+ <% if is_db %>
57
+ <% unless @skill.approved? %>
58
+ <%= form_with url: approve_skill_path(@skill), method: :post, local: true, style: 'display:inline' do %>
59
+ <input type="text" name="approved_by" placeholder="Your name (required)" style="padding:7px 10px; border:1px solid #ccc; border-radius:4px; margin-right:6px;">
60
+ <button type="submit" class="btn btn-danger" data-confirm="Approve this skill? The AI will then be able to activate it and use its guard bypasses.">Approve</button>
61
+ <% end %>
62
+ <% end %>
63
+ <%= link_to 'Edit', edit_skill_path(@skill), class: 'btn' %>
64
+ <%= link_to 'Versions', skill_versions_path(@skill), class: 'btn btn-secondary' %>
65
+ <%= button_to 'Delete', skill_path(@skill), method: :delete, class: 'btn btn-danger', form: { style: 'display:inline' }, data: { confirm: 'Delete this skill? Past versions remain in history.' } %>
66
+ <% end %>
67
+ </div>
68
+
69
+ <h3 style="margin-bottom:8px;">Recipe</h3>
70
+ <div class="markdown-body"><%= body %></div>
71
+
72
+ <% if is_db && @versions && @versions.any? %>
73
+ <h3 style="margin:24px 0 8px;">Recent versions</h3>
74
+ <div class="versions-list">
75
+ <% @versions.first(5).each do |v| %>
76
+ <div class="ver-row">
77
+ <div>
78
+ <strong>v<%= v.id %></strong>
79
+ <span class="ver-meta">— <%= v.created_at.strftime('%Y-%m-%d %H:%M') %> by <%= v.edited_by || 'unknown' %></span>
80
+ <% if v.status == 'approved' %><span class="badge status-approved">APPROVED</span><% end %>
81
+ <% if v.change_note.present? %><div class="ver-meta"><%= v.change_note %></div><% end %>
82
+ </div>
83
+ <div>
84
+ <%= link_to 'View', skill_version_path(@skill, v) %>
85
+ ·
86
+ <%= link_to 'Diff vs current', diff_skills_path(skill_id: @skill.id, from: v.id) %>
87
+ </div>
88
+ </div>
89
+ <% end %>
90
+ </div>
91
+ <% if @versions.size > 5 %>
92
+ <p style="margin-top:8px;"><%= link_to "See all #{@versions.size} versions →", skill_versions_path(@skill) %></p>
93
+ <% end %>
94
+ <% end %>
data/config/routes.rb CHANGED
@@ -1,4 +1,43 @@
1
1
  RailsConsoleAi::Engine.routes.draw do
2
2
  root to: 'sessions#index'
3
3
  resources :sessions, only: [:index, :show]
4
+
5
+ resources :skills do
6
+ member do
7
+ post :approve
8
+ end
9
+ collection do
10
+ get :diff
11
+ end
12
+ resources :versions, only: [:index, :show], controller: 'skill_versions' do
13
+ member do
14
+ post :restore
15
+ end
16
+ end
17
+ end
18
+
19
+ resources :memories do
20
+ collection do
21
+ get :diff
22
+ end
23
+ resources :versions, only: [:index, :show], controller: 'memory_versions' do
24
+ member do
25
+ post :restore
26
+ end
27
+ end
28
+ end
29
+
30
+ resources :agents do
31
+ member do
32
+ post :approve
33
+ end
34
+ collection do
35
+ get :diff
36
+ end
37
+ resources :versions, only: [:index, :show], controller: 'agent_versions' do
38
+ member do
39
+ post :restore
40
+ end
41
+ end
42
+ end
4
43
  end