rails_console_ai 0.28.0 → 0.30.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +48 -0
  4. data/app/controllers/rails_console_ai/agent_versions_controller.rb +36 -0
  5. data/app/controllers/rails_console_ai/agents_controller.rb +199 -0
  6. data/app/controllers/rails_console_ai/application_controller.rb +5 -0
  7. data/app/controllers/rails_console_ai/memories_controller.rb +159 -0
  8. data/app/controllers/rails_console_ai/memory_versions_controller.rb +33 -0
  9. data/app/controllers/rails_console_ai/skill_versions_controller.rb +35 -0
  10. data/app/controllers/rails_console_ai/skills_controller.rb +200 -0
  11. data/app/helpers/rails_console_ai/diff_helper.rb +114 -0
  12. data/app/models/rails_console_ai/agent.rb +175 -0
  13. data/app/models/rails_console_ai/agent_version.rb +46 -0
  14. data/app/models/rails_console_ai/memory.rb +98 -0
  15. data/app/models/rails_console_ai/memory_version.rb +46 -0
  16. data/app/models/rails_console_ai/session.rb +1 -1
  17. data/app/models/rails_console_ai/skill.rb +198 -0
  18. data/app/models/rails_console_ai/skill_version.rb +54 -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 +65 -0
  23. data/app/views/rails_console_ai/agents/diff.html.erb +19 -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 +24 -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 +36 -0
  29. data/app/views/rails_console_ai/memories/diff.html.erb +19 -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 +23 -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 +65 -0
  39. data/app/views/rails_console_ai/skills/diff.html.erb +22 -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 +25 -0
  43. data/app/views/rails_console_ai/skills/show.html.erb +94 -0
  44. data/config/routes.rb +42 -0
  45. data/lib/rails_console_ai/agent_loader.rb +131 -43
  46. data/lib/rails_console_ai/agent_runner.rb +158 -0
  47. data/lib/rails_console_ai/channel/api.rb +139 -0
  48. data/lib/rails_console_ai/channel/slack.rb +33 -0
  49. data/lib/rails_console_ai/channel/sub_agent.rb +12 -0
  50. data/lib/rails_console_ai/conversation_engine.rb +50 -13
  51. data/lib/rails_console_ai/session_logger.rb +6 -0
  52. data/lib/rails_console_ai/skill_loader.rb +119 -27
  53. data/lib/rails_console_ai/slack_bot.rb +8 -0
  54. data/lib/rails_console_ai/storage/database_storage.rb +201 -0
  55. data/lib/rails_console_ai/sub_agent.rb +25 -0
  56. data/lib/rails_console_ai/tools/memory_tools.rb +102 -32
  57. data/lib/rails_console_ai/tools/registry.rb +99 -8
  58. data/lib/rails_console_ai/version.rb +1 -1
  59. data/lib/rails_console_ai.rb +256 -0
  60. data/lib/tasks/rails_console_ai.rake +7 -0
  61. metadata +55 -1
@@ -0,0 +1,108 @@
1
+ <%
2
+ is_db = @agent.is_a?(RailsConsoleAi::Agent)
3
+ is_builtin = !is_db && @agent['source'] == :builtin
4
+ is_file = !is_db && @agent['source'] == :file
5
+
6
+ name = is_db ? @agent.name : @agent['name']
7
+ description = is_db ? @agent.description : @agent['description']
8
+ body = is_db ? @agent.body : @agent['body']
9
+ max_rounds = is_db ? @agent.max_rounds : @agent['max_rounds']
10
+ model = is_db ? @agent.model : @agent['model']
11
+ tools = is_db ? Array(@agent.tools) : Array(@agent['tools'])
12
+ file_key = is_db ? nil : @agent['file_key']
13
+ %>
14
+
15
+ <%= link_to '← All agents', agents_path, class: 'back-link' %>
16
+
17
+ <% if is_db && !@agent.approved? %>
18
+ <div class="flash flash-alert" style="margin-bottom:16px;">
19
+ <strong>Awaiting approval.</strong> The AI cannot invoke this agent via delegate_task until a human approves it.
20
+ Review the body and tools list below, then click <strong>Approve</strong>.
21
+ </div>
22
+ <% end %>
23
+
24
+ <% if is_builtin %>
25
+ <div class="flash flash-notice" style="margin-bottom:16px;">
26
+ This is a <strong>built-in agent</strong> shipped with the gem. It's read-only here.
27
+ To customize it, <%= link_to 'create a same-named DB override', new_agent_path(from_builtin: name) %> — your DB agent will shadow this one.
28
+ </div>
29
+ <% end %>
30
+
31
+ <div class="meta-card">
32
+ <h2 style="margin-bottom:8px;">
33
+ <%= name %>
34
+ <% if is_db %>
35
+ <span class="badge source-db">DB</span>
36
+ <% if @agent.approved? %>
37
+ <span class="badge status-approved">APPROVED</span>
38
+ <% else %>
39
+ <span class="badge status-proposed">PROPOSED</span>
40
+ <% end %>
41
+ <% elsif is_builtin %>
42
+ <span class="badge source-builtin">BUILTIN</span>
43
+ <% else %>
44
+ <span class="badge source-file">FILE</span>
45
+ <% end %>
46
+ </h2>
47
+ <p class="text-muted"><%= description %></p>
48
+ <div class="meta-grid" style="margin-top:16px;">
49
+ <div class="meta-item"><label>Model</label><span><%= model || '(default)' %></span></div>
50
+ <div class="meta-item"><label>Max rounds</label><span><%= max_rounds || '(default)' %></span></div>
51
+ <div class="meta-item"><label>Tool whitelist</label><span><%= tools.empty? ? '(default set)' : tools.join(', ') %></span></div>
52
+ <% if is_db %>
53
+ <div class="meta-item"><label>Updated</label><span><%= @agent.updated_at.strftime('%Y-%m-%d %H:%M') %></span></div>
54
+ <div class="meta-item"><label>Versions</label><span><%= @agent.versions.count %></span></div>
55
+ <div class="meta-item"><label>Times invoked</label><span><%= @agent.use_count %></span></div>
56
+ <div class="meta-item"><label>Last invoked</label><span><%= @agent.last_used_at&.strftime('%Y-%m-%d %H:%M') || 'never' %></span></div>
57
+ <% if @agent.approved? %>
58
+ <div class="meta-item"><label>Approved by</label><span><%= @agent.approved_by || '—' %></span></div>
59
+ <div class="meta-item"><label>Approved at</label><span><%= @agent.approved_at&.strftime('%Y-%m-%d %H:%M') || '—' %></span></div>
60
+ <% end %>
61
+ <% else %>
62
+ <div class="meta-item"><label>Path</label><span class="mono"><%= file_key %></span></div>
63
+ <% end %>
64
+ </div>
65
+ </div>
66
+
67
+ <div class="btn-bar">
68
+ <% if is_db %>
69
+ <% unless @agent.approved? %>
70
+ <%= form_with url: approve_agent_path(@agent), method: :post, local: true, style: 'display:inline' do %>
71
+ <input type="text" name="approved_by" placeholder="Your name (required)" style="padding:7px 10px; border:1px solid #ccc; border-radius:4px; margin-right:6px;">
72
+ <button type="submit" class="btn btn-danger" data-confirm="Approve this agent? The AI will then be able to invoke it via delegate_task with the configured tool whitelist.">Approve</button>
73
+ <% end %>
74
+ <% end %>
75
+ <%= link_to 'Edit', edit_agent_path(@agent), class: 'btn' %>
76
+ <%= link_to 'Versions', agent_versions_path(@agent), class: 'btn btn-secondary' %>
77
+ <%= button_to 'Delete', agent_path(@agent), method: :delete, class: 'btn btn-danger', form: { style: 'display:inline' }, data: { confirm: 'Delete this agent? Past versions remain in history.' } %>
78
+ <% elsif is_builtin %>
79
+ <%= link_to 'Create DB override', new_agent_path(from_builtin: name), class: 'btn' %>
80
+ <% end %>
81
+ </div>
82
+
83
+ <h3 style="margin-bottom:8px;">Body / instructions</h3>
84
+ <div class="markdown-body"><%= body %></div>
85
+
86
+ <% if is_db && @versions && @versions.any? %>
87
+ <h3 style="margin:24px 0 8px;">Recent versions</h3>
88
+ <div class="versions-list">
89
+ <% @versions.first(5).each do |v| %>
90
+ <div class="ver-row">
91
+ <div>
92
+ <strong>v<%= v.id %></strong>
93
+ <span class="ver-meta">— <%= v.created_at.strftime('%Y-%m-%d %H:%M') %> by <%= v.edited_by || 'unknown' %></span>
94
+ <% if v.status == 'approved' %><span class="badge status-approved">APPROVED</span><% end %>
95
+ <% if v.change_note.present? %><div class="ver-meta"><%= v.change_note %></div><% end %>
96
+ </div>
97
+ <div>
98
+ <%= link_to 'View', agent_version_path(@agent, v) %>
99
+ ·
100
+ <%= link_to 'Diff vs current', diff_agents_path(agent_id: @agent.id, from: v.id) %>
101
+ </div>
102
+ </div>
103
+ <% end %>
104
+ </div>
105
+ <% if @versions.size > 5 %>
106
+ <p style="margin-top:8px;"><%= link_to "See all #{@versions.size} versions →", agent_versions_path(@agent) %></p>
107
+ <% end %>
108
+ <% end %>
@@ -0,0 +1,36 @@
1
+ <%= form_with model: @memory, url: form_url, method: form_method, local: true do |f| %>
2
+ <div class="form-card">
3
+ <div class="form-row">
4
+ <label>Name</label>
5
+ <%= f.text_field :name, value: @memory.name, required: true %>
6
+ </div>
7
+
8
+ <div class="form-row">
9
+ <label>Description</label>
10
+ <%= f.text_area :description, value: @memory.description, rows: 14 %>
11
+ <div class="hint">The fact or pattern you're persisting. Markdown is fine.</div>
12
+ </div>
13
+
14
+ <div class="form-row">
15
+ <label>Tags (comma-separated)</label>
16
+ <%= f.text_field :tags, value: Array(@memory.tags).join(', '), placeholder: 'database, sharding' %>
17
+ </div>
18
+
19
+ <hr style="margin: 20px 0; border: 0; border-top: 1px solid #eee;">
20
+
21
+ <div class="form-row">
22
+ <label>Edited by</label>
23
+ <input type="text" name="edited_by" value="<%= params[:edited_by] %>" placeholder="Your name or handle">
24
+ </div>
25
+
26
+ <div class="form-row">
27
+ <label>Change note (optional)</label>
28
+ <input type="text" name="change_note" value="" placeholder="What did you change and why?">
29
+ </div>
30
+ </div>
31
+
32
+ <div class="btn-bar">
33
+ <%= f.submit submit_label, class: 'btn' %>
34
+ <%= link_to 'Cancel', cancel_path, class: 'btn btn-secondary' %>
35
+ </div>
36
+ <% end %>
@@ -0,0 +1,19 @@
1
+ <%= link_to '← Back to memory', memory_path(@memory), class: 'back-link' %>
2
+
3
+ <h2 style="margin-bottom:8px;">Diff — <%= @memory.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
+ <h3 style="margin:16px 0 6px;">Description</h3>
9
+ <%= render_text_diff(@from.description, @to_description, left_label: "v##{@from.id}", right_label: @to_label) %>
10
+
11
+ <h3 style="margin:24px 0 6px;">Tags</h3>
12
+ <%= render_json_diff(Array(@from.tags), @to_tags, left_label: "v##{@from.id}", right_label: @to_label) %>
13
+
14
+ <div class="btn-bar" style="margin-top:24px;">
15
+ <% if @to.nil? %>
16
+ <%= button_to "Restore v##{@from.id}", restore_memory_version_path(@memory, @from), method: :post, class: 'btn btn-danger', data: { confirm: 'Restoring will overwrite the current memory (a new version row is created).' } %>
17
+ <% end %>
18
+ <%= link_to 'Back', memory_path(@memory), class: 'btn btn-secondary' %>
19
+ </div>
@@ -0,0 +1,7 @@
1
+ <%= link_to '← Back to memory', memory_path(@memory), class: 'back-link' %>
2
+ <h2 style="margin-bottom:16px;">Edit memory</h2>
3
+ <%= render 'form',
4
+ form_url: memory_path(@memory),
5
+ form_method: :patch,
6
+ submit_label: 'Save changes',
7
+ cancel_path: memory_path(@memory) %>
@@ -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,23 @@
1
+ <%= link_to '← All memories', memories_path, class: 'back-link' %>
2
+ <h2 style="margin-bottom:16px;">New memory</h2>
3
+
4
+ <%= form_with url: import_memories_path, method: :post, local: true do %>
5
+ <div class="form-card" style="background:#f8f9fa;">
6
+ <div class="form-row" style="margin-bottom:8px;">
7
+ <label>Paste a .md file (optional)</label>
8
+ <textarea name="content" rows="12" placeholder="---&#10;name: My memory&#10;tags: [database]&#10;---&#10;&#10;The fact or pattern …"></textarea>
9
+ <div class="hint">
10
+ Paste the full contents of a Markdown file (YAML frontmatter + body) — same
11
+ format as <code>.rails_console_ai/memories/*.md</code>. The fields below will
12
+ be prefilled from the paste.
13
+ </div>
14
+ </div>
15
+ <button type="submit" class="btn btn-secondary">Parse pasted content ↓</button>
16
+ </div>
17
+ <% end %>
18
+
19
+ <%= render 'form',
20
+ form_url: memories_path,
21
+ form_method: :post,
22
+ submit_label: 'Create memory',
23
+ 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,65 @@
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>Name</label>
15
+ <%= f.text_field :name, value: @skill.name, required: true %>
16
+ </div>
17
+
18
+ <div class="form-row">
19
+ <label>Description</label>
20
+ <%= f.text_field :description, value: @skill.description, placeholder: 'One-line description of when to use this skill' %>
21
+ </div>
22
+
23
+ <div class="form-row">
24
+ <label>Body (markdown)</label>
25
+ <%= f.text_area :body, value: @skill.body, rows: 18 %>
26
+ <div class="hint">Include "## When to use", "## Recipe" (numbered steps with code blocks), "## Notes".</div>
27
+ </div>
28
+
29
+ <div class="form-row">
30
+ <label>Tags (comma-separated)</label>
31
+ <%= f.text_field :tags, value: Array(@skill.tags).join(', '), placeholder: 'booking-page, admin' %>
32
+ </div>
33
+
34
+ <div class="form-row">
35
+ <label>Bypass guards for methods (one per line)</label>
36
+ <%= f.text_area :bypass_guards_for_methods, value: Array(@skill.bypass_guards_for_methods).join("\n"), rows: 4 %>
37
+ <div class="hint">
38
+ Two forms accepted — anything else is ignored:
39
+ <ul style="margin:6px 0 0 18px; padding:0;">
40
+ <li><code>ClassName#method</code> — instance method, e.g. <code>BookingPage#save!</code></li>
41
+ <li><code>ClassName.method</code> — class (singleton) method, e.g. <code>User.create_admin!</code></li>
42
+ </ul>
43
+ Namespaced classes work: <code>Acme::Booking#destroy</code>. The <code>!</code> is part of the match, so <code>save</code> and <code>save!</code> are different specs. While the skill is active, calls to these methods skip the configured safety guards (DB writes, HTTP mutations, email, etc.) <strong>for that tool-call only</strong>.
44
+ </div>
45
+ </div>
46
+
47
+ <hr style="margin: 20px 0; border: 0; border-top: 1px solid #eee;">
48
+
49
+ <div class="form-row">
50
+ <label>Edited by</label>
51
+ <input type="text" name="edited_by" value="<%= params[:edited_by] || cookies[:rca_edited_by] %>" placeholder="Your name or handle">
52
+ <div class="hint">Recorded with this version. Free text — the gem does not assume authentication beyond the admin login.</div>
53
+ </div>
54
+
55
+ <div class="form-row">
56
+ <label>Change note (optional)</label>
57
+ <input type="text" name="change_note" value="" placeholder="What did you change and why?">
58
+ </div>
59
+ </div>
60
+
61
+ <div class="btn-bar">
62
+ <%= f.submit submit_label, class: 'btn' %>
63
+ <%= link_to 'Cancel', cancel_path, class: 'btn btn-secondary' %>
64
+ </div>
65
+ <% end %>
@@ -0,0 +1,22 @@
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
+ <h3 style="margin:16px 0 6px;">Body</h3>
9
+ <%= render_text_diff(@from.body, @to_body, left_label: "v##{@from.id}", right_label: @to_label) %>
10
+
11
+ <h3 style="margin:24px 0 6px;">Tags</h3>
12
+ <%= render_json_diff(Array(@from.tags), @to_tags, left_label: "v##{@from.id}", right_label: @to_label) %>
13
+
14
+ <h3 style="margin:24px 0 6px;">Bypass guards</h3>
15
+ <%= render_json_diff(Array(@from.bypass_guards_for_methods), @to_bypass, left_label: "v##{@from.id}", right_label: @to_label) %>
16
+
17
+ <div class="btn-bar" style="margin-top:24px;">
18
+ <% if @to.nil? %>
19
+ <%= 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).' } %>
20
+ <% end %>
21
+ <%= link_to 'Back', skill_path(@skill), class: 'btn btn-secondary' %>
22
+ </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,25 @@
1
+ <%= link_to '← All skills', skills_path, class: 'back-link' %>
2
+ <h2 style="margin-bottom:16px;">New skill</h2>
3
+
4
+ <%= form_with url: import_skills_path, method: :post, local: true do %>
5
+ <div class="form-card" style="background:#f8f9fa;">
6
+ <div class="form-row" style="margin-bottom:8px;">
7
+ <label>Paste a .md file (optional)</label>
8
+ <textarea name="content" rows="14" placeholder="---&#10;name: My skill&#10;description: What it does&#10;tags: [admin]&#10;bypass_guards_for_methods: []&#10;---&#10;&#10;## When to use&#10;…&#10;&#10;## Recipe&#10;1. …"></textarea>
9
+ <div class="hint">
10
+ Paste the full contents of a Markdown file (YAML frontmatter between
11
+ <code>---</code> lines, then a markdown body) — same format as
12
+ <code>.rails_console_ai/skills/*.md</code>. The fields below will be
13
+ prefilled from the paste. You can review and tweak before clicking
14
+ <strong>Create skill</strong>.
15
+ </div>
16
+ </div>
17
+ <button type="submit" class="btn btn-secondary">Parse pasted content ↓</button>
18
+ </div>
19
+ <% end %>
20
+
21
+ <%= render 'form',
22
+ form_url: skills_path,
23
+ form_method: :post,
24
+ submit_label: 'Create skill',
25
+ cancel_path: skills_path %>