rails_console_ai 0.30.0 → 0.32.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -26
  3. data/README.md +33 -11
  4. data/app/controllers/rails_console_ai/agent_versions_controller.rb +2 -9
  5. data/app/controllers/rails_console_ai/agents_controller.rb +25 -48
  6. data/app/controllers/rails_console_ai/memories_controller.rb +35 -38
  7. data/app/controllers/rails_console_ai/memory_versions_controller.rb +2 -6
  8. data/app/controllers/rails_console_ai/sessions_controller.rb +1 -1
  9. data/app/controllers/rails_console_ai/skill_versions_controller.rb +2 -8
  10. data/app/controllers/rails_console_ai/skills_controller.rb +19 -48
  11. data/app/models/rails_console_ai/agent.rb +33 -65
  12. data/app/models/rails_console_ai/agent_version.rb +8 -20
  13. data/app/models/rails_console_ai/memory.rb +67 -23
  14. data/app/models/rails_console_ai/memory_version.rb +5 -20
  15. data/app/models/rails_console_ai/skill.rb +55 -105
  16. data/app/models/rails_console_ai/skill_version.rb +7 -28
  17. data/app/views/rails_console_ai/agents/_form.html.erb +9 -34
  18. data/app/views/rails_console_ai/agents/diff.html.erb +1 -5
  19. data/app/views/rails_console_ai/agents/new.html.erb +0 -16
  20. data/app/views/rails_console_ai/memories/_form.html.erb +6 -13
  21. data/app/views/rails_console_ai/memories/diff.html.erb +1 -5
  22. data/app/views/rails_console_ai/memories/index.html.erb +13 -1
  23. data/app/views/rails_console_ai/memories/new.html.erb +0 -15
  24. data/app/views/rails_console_ai/memories/show.html.erb +23 -0
  25. data/app/views/rails_console_ai/skills/_form.html.erb +7 -29
  26. data/app/views/rails_console_ai/skills/diff.html.erb +1 -8
  27. data/app/views/rails_console_ai/skills/new.html.erb +0 -17
  28. data/config/routes.rb +3 -3
  29. data/lib/generators/rails_console_ai/templates/initializer.rb +5 -5
  30. data/lib/rails_console_ai/agent_loader.rb +18 -10
  31. data/lib/rails_console_ai/agent_runner.rb +66 -4
  32. data/lib/rails_console_ai/channel/console.rb +1 -1
  33. data/lib/rails_console_ai/configuration.rb +51 -33
  34. data/lib/rails_console_ai/conversation_engine.rb +3 -3
  35. data/lib/rails_console_ai/safety_guards.rb +36 -0
  36. data/lib/rails_console_ai/session_logger.rb +4 -0
  37. data/lib/rails_console_ai/skill_loader.rb +18 -9
  38. data/lib/rails_console_ai/slack_bot.rb +2 -2
  39. data/lib/rails_console_ai/storage/database_storage.rb +14 -20
  40. data/lib/rails_console_ai/sub_agent.rb +1 -1
  41. data/lib/rails_console_ai/tools/memory_tools.rb +37 -6
  42. data/lib/rails_console_ai/tools/registry.rb +18 -6
  43. data/lib/rails_console_ai/version.rb +1 -1
  44. data/lib/rails_console_ai.rb +92 -71
  45. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7455c7e1d55abd75c69bfa99621f1f83ec53a5faa2313e3095b3d80b88ec9938
4
- data.tar.gz: 0a12cdef783d5f101d0c45b7fce9876e54bf31b1874fc4178a5d2103064ecb0a
3
+ metadata.gz: 7eab1fd8d3a5c8c7a228206f0c723bfa845fa2d344471e0251659658c0c5e571
4
+ data.tar.gz: c607b6cfa9e6834612513f0c75dc1303386f79bcdd96bba203a4fb5d0549f32b
5
5
  SHA512:
6
- metadata.gz: 7433c944553f7c683b61e58653e76138e389e41ff29dff45c28fb344913a50514d2a58783fca2c916ff6f86a46a4d71cbad47803625e8078aa25895a2b12a057
7
- data.tar.gz: 6cb61503f513cf4864fd918e95719f167733fc94ef001bb61290f6e0b262be0b2a3d83a936904ea7100096103c6c5c8145f74f5e99536088e45ad3aeb90d5a99
6
+ metadata.gz: 1acd49dcf3370eaab8a8fb8ef77c7dad4746fcb0e0eb2f78935b53c870228a9b53a2cef8dc82608ed94f5f2642de905070fe9e241e9a98d8f408653535ce30d7
7
+ data.tar.gz: bd7ec6fcf50788b45f47f6a7b1948a74fb83cf4ff7fba62687eb2d6e8c75743972f4e83525d87f57096c1a6cd114611a69c5b6e2299a88dea6b02063cdf09adc
data/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.32.0]
6
+
7
+ - Switch default models to Claude Sonnet 5 and Opus 4.8, and rework pricing to match models by family so dated snapshots and Bedrock IDs are covered
8
+ - Add an abort mechanism for background sub-agents
9
+ - Require human approval before AI-proposed memory changes take effect
10
+ - Keep `explore_output` focused on exploring a previous command's output instead of spinning off into database reads
11
+ - Fix loading `ApplicationController` under the classic autoloader
12
+
13
+ ## [0.31.0]
14
+
15
+ - Simplify DB-backed skills, sub-agents, and memories to a single content field per record, replacing the separate frontmatter/body columns and streamlining the models, controllers, and forms
16
+ - Add options for running agents
17
+ - Fix the `/script/release` script
18
+
5
19
  ## [0.30.0]
6
20
 
7
21
  - Add background agent support — new `run_agent` mechanism to launch agents asynchronously alongside an HTTP API channel
@@ -16,32 +30,6 @@ All notable changes to this project will be documented in this file.
16
30
  - Harden `Skill` / `Agent` model accessors to return safe defaults when newly added columns are not yet present
17
31
  - Fix built-in agent `.md` files with UTF-8 characters failing to load under US-ASCII locales
18
32
 
19
- ## [Unreleased]
20
-
21
- - New skill / memory / agent pages now have a "Paste a .md file" textarea at the top. Paste the contents of a Markdown file with YAML frontmatter (same format used by `.rails_console_ai/{skills,memories,agents}/*.md` and the gem's built-in agents), click "Parse pasted content ↓", and the form fields below are prefilled from the parse. You still click Create to actually save, so the normal proposed-status + version-row flow applies — and you can tweak the parsed content before saving. Useful for moving an existing on-disk record into the versioned DB store
22
- - `RailsConsoleAi::SkillLoader.parse`, `RailsConsoleAi::AgentLoader.parse`, and `RailsConsoleAi::Tools::MemoryTools.parse` are now public class methods. They return a parsed frontmatter+body hash, or `nil` on malformed input
23
-
24
- - Usage tracking for DB-backed skills, memories, and agents. New `use_count` and `last_used_at` columns are bumped atomically (one SQL `UPDATE … SET use_count = use_count + 1, last_used_at = NOW()`, no callbacks, no `updated_at` change) when:
25
- - `activate_skill` resolves a DB skill
26
- - `recall_memory` resolves a DB memory, or `recall_memories` returns a DB memory in its result set
27
- - `delegate_task` resolves a DB sub-agent
28
- File / built-in records have no DB row so they're not tracked (the web UI shows `—` for them). System-prompt summary inclusion does **not** count — counters only move when the AI actively invokes/loads the record. Surfaced in the index tables (with a "Sort: most used" toggle) and on each show page (Times activated / Times recalled / Times invoked, plus Last used)
29
- - Bugfix: `RailsConsoleAi.migrate!` now always re-runs the `setup_*_tables!` methods so column-add probes execute on upgrades. Previously, when the base table already existed, the outer `unless table_exists?` guard skipped column probes entirely, leaving new columns un-added and causing `NameError: undefined local variable or method 'status'` from `Skill#proposed?` on host apps that hadn't fully migrated
30
- - Bugfix: `Skill` / `Agent` model accessors for `status` / `approved_by` / `approved_at` / `use_count` / `last_used_at` are now defensive — they return safe defaults via `has_attribute?` rather than raising NameError when the column hasn't been added yet. The `status` inclusion validation is also gated on the column being present
31
-
32
- - Sub-agents can now be stored in the database, versioned, and approved through the web UI — same workflow as skills. DB-backed agents start as `proposed` and are invisible to `delegate_task` until a human clicks Approve at `/rails_console_ai/agents`; editing an approved agent reverts it to proposed. Built-in (gem-shipped) and file-based agents are pre-approved and continue working unchanged
33
- - New AI tools `save_agent` and `delete_agent` — the AI can now draft a sub-agent definition (lands as proposed, awaits human approval) and request deletion. `delegate_task` now emits a specific "awaiting human approval" error when the AI references a proposed DB agent
34
- - New web UI section at `/rails_console_ai/agents` with three-source listing (DB / FILE / BUILTIN badges) plus the full skills-style CRUD + version history + diff + restore + approve. Built-in agents are read-only with a "Create DB override" link that prefills the new-agent form
35
- - `ai_db_setup` / `ai_db_migrate` now also create `rails_console_ai_agents` and `rails_console_ai_agent_versions` tables idempotently
36
- - Fix: built-in agent .md files containing UTF-8 (em-dashes, smart quotes) now load correctly. `safe_load_builtin_agents` was using `File.read`'s locale-default encoding, which silently swallowed `Encoding::CompatibilityError` on US-ASCII locales
37
-
38
- - Skills and memories can now be stored in the database (in addition to the existing on-disk `.rails_console_ai/skills` and `.rails_console_ai/memories` files). DB-backed records are versioned — every save creates a `SkillVersion` / `MemoryVersion` row with `edited_by` and an optional change note
39
- - DB-backed skills start in a **proposed** state and cannot be activated by the AI until a human approves them in the web UI. Editing an approved skill reverts it to proposed. File-backed skills are unaffected (already git-tracked, considered pre-approved). Memories are not gated
40
- - `SkillLoader#load_all_skills` and `MemoryTools#load_all_memories` now return the union of DB and file records (DB wins on name collision). `SkillLoader#find_skill` / `skill_summaries` filter out proposed skills so the AI never sees them
41
- - `save_skill` / `save_memory` AI tools accept an optional `target` parameter (`"db"` default, `"file"` to write to disk) plus `change_note`. When the AI saves a DB skill, the tool response tells it the skill is awaiting human approval
42
- - New web UI sections at `/rails_console_ai/skills` and `/rails_console_ai/memories` provide list, view, create, edit, delete, version history, side-by-side diff, and restore. Skills also have an Approve button and PROPOSED / APPROVED badges. File-sourced records are surfaced but read-only in the UI
43
- - `ai_db_setup` / `ai_db_migrate` now create the new `rails_console_ai_skills`, `rails_console_ai_skill_versions`, `rails_console_ai_memories`, and `rails_console_ai_memory_versions` tables idempotently, plus the `status` / `approved_by` / `approved_at` columns on skills
44
-
45
33
  ## [0.29.0]
46
34
 
47
35
  - Allow steering Slack conversations mid-run by sending follow-up messages that are folded in as user guidance at the next tool-loop boundary
data/README.md CHANGED
@@ -291,7 +291,7 @@ RailsConsoleAi.configure do |config|
291
291
  end
292
292
  ```
293
293
 
294
- Default model: `claude-sonnet-4-6`. Thinking model: `claude-opus-4-6`. Prompt caching is enabled automatically.
294
+ Default model: `claude-sonnet-5`. Thinking model: `claude-opus-4-8`. Prompt caching is enabled automatically.
295
295
 
296
296
  ### OpenAI
297
297
 
@@ -317,8 +317,8 @@ gem 'aws-sdk-bedrockruntime'
317
317
  RailsConsoleAi.configure do |config|
318
318
  config.provider = :bedrock
319
319
  config.bedrock_region = 'us-east-1'
320
- # config.model = 'us.anthropic.claude-sonnet-4-6' # default
321
- # config.thinking_model = 'us.anthropic.claude-opus-4-6-v1' # default
320
+ # config.model = 'us.anthropic.claude-sonnet-5' # default
321
+ # config.thinking_model = 'us.anthropic.claude-opus-4-8' # default
322
322
  end
323
323
  ```
324
324
 
@@ -365,25 +365,25 @@ Before adopting a new Claude model, smoke-test it against the Anthropic or Bedro
365
365
 
366
366
  ```bash
367
367
  # Anthropic — provider inferred from the `claude-` prefix
368
- ANTHROPIC_API_KEY=sk-ant-... bin/smoke_model.rb --model claude-opus-4-7
368
+ ANTHROPIC_API_KEY=sk-ant-... bin/smoke_model.rb --model claude-opus-4-8
369
369
 
370
370
  # Bedrock — provider inferred from the regional `us.anthropic.` prefix.
371
371
  # Requires the aws-sdk-bedrockruntime gem and AWS credentials in the environment.
372
- bin/smoke_model.rb --model us.anthropic.claude-opus-4-7
372
+ bin/smoke_model.rb --model us.anthropic.claude-opus-4-8
373
373
 
374
374
  # Bedrock in another region
375
- bin/smoke_model.rb --model eu.anthropic.claude-opus-4-7 --region eu-west-1
375
+ bin/smoke_model.rb --model eu.anthropic.claude-opus-4-8 --region eu-west-1
376
376
 
377
377
  # Subset of checks, e.g. when iterating on cache behavior
378
- bin/smoke_model.rb --model claude-sonnet-4-6 --checks cache
378
+ bin/smoke_model.rb --model claude-sonnet-5 --checks cache
379
379
 
380
380
  # Force a provider when the model ID is ambiguous
381
- bin/smoke_model.rb --provider anthropic --model claude-opus-4-7
381
+ bin/smoke_model.rb --provider anthropic --model claude-opus-4-8
382
382
  ```
383
383
 
384
384
  `DEBUG=1` enables the providers' raw request/response logging.
385
385
 
386
- If the model rejects a parameter the gem sends by default (e.g. opus-4-7 deprecated `temperature`), add the model ID to `Configuration::MODELS_WITHOUT_TEMPERATURE` in `lib/rails_console_ai/configuration.rb` so the providers omit the field.
386
+ Pricing, default max tokens, and parameter support (e.g. which families reject `temperature`) are keyed by model family in `Configuration::MODEL_FAMILIES` (`lib/rails_console_ai/configuration.rb`). Family matching is by substring, so one entry covers bare IDs, dated snapshots, and Bedrock inference profiles (`us.` / `global.` prefixes). When adopting a new model family, add an entry there and smoke-test it.
387
387
 
388
388
  ## Configuration
389
389
 
@@ -513,14 +513,36 @@ id = RailsConsoleAi.run_agent("How many users signed up yesterday?", name: 'dail
513
513
  # => 4821 (Integer session id, returned immediately)
514
514
 
515
515
  RailsConsoleAi.check_agent(id)
516
- # => 'queued' | 'running' | 'ready' | 'failed' | nil
516
+ # => 'queued' | 'running' | 'ready' | 'failed' | 'aborted' | nil
517
517
 
518
518
  RailsConsoleAi.get_agent_response(id)
519
519
  # => { status: 'ready', result: "1,432 users signed up yesterday.\n", error: nil }
520
+
521
+ RailsConsoleAi.abort_agent(id)
522
+ # => true (aborted) | false (already finished, or unknown id)
520
523
  ```
521
524
 
525
+ `abort_agent` cancels a run: a queued run is never picked up, and a run already executing keeps going but its result is discarded when it completes -- the session stays `status='aborted'`.
526
+
522
527
  `run_agent` enqueues a row in the sessions table with `mode='agent_api'` and `status='queued'`. A separate long-running rake task picks them up and runs each in its own thread using the same engine that powers `ai "..."` in the console.
523
528
 
529
+ ### Per-run options
530
+
531
+ `run_agent` accepts two extra keyword arguments to tune individual runs:
532
+
533
+ ```ruby
534
+ RailsConsoleAi.run_agent(
535
+ "Trace why nightly billing is double-charging some accounts",
536
+ use_thinking_model: true, # run on the thinking-tier model (e.g. Opus)
537
+ max_wall_clock_seconds: 1800 # hard kill after 30 minutes; pass nil for no cap
538
+ )
539
+ ```
540
+
541
+ - `use_thinking_model:` (default `false`) — switches the run to `config.thinking_model` (or the provider default thinking model) for the duration of the agent. Useful for harder, multi-step problems.
542
+ - `max_wall_clock_seconds:` (default `600`) — hard ceiling on wall-clock time. If the run exceeds the cap, the worker thread is killed and the session is marked `status='failed'` with `error_message: "exceeded max_wall_clock_seconds (Ns)"`. Pass `nil` to opt out of any cap.
543
+
544
+ These (along with any future per-run options) are stored in a JSON `options` column on the session row, so they survive the handoff to the background runner.
545
+
524
546
  ### Running the background runner
525
547
 
526
548
  ```bash
@@ -535,7 +557,7 @@ Run it separately from your web server, alongside (or instead of) the Slack bot.
535
557
 
536
558
  ### Requirements
537
559
 
538
- - `RailsConsoleAi.setup!` must have been run so the sessions table has the `status`, `result`, and `error_message` columns. `ai_db_setup` (or `ai_db_migrate` on existing installs) handles this.
560
+ - `RailsConsoleAi.setup!` must have been run so the sessions table has the `status`, `result`, `error_message`, and `options` columns. `ai_db_setup` (or `ai_db_migrate` on existing installs) handles this.
539
561
  - `session_logging` must be enabled (it is by default).
540
562
 
541
563
  ### Behavior notes
@@ -1,5 +1,5 @@
1
1
  module RailsConsoleAi
2
- class AgentVersionsController < ApplicationController
2
+ class AgentVersionsController < RailsConsoleAi::ApplicationController
3
3
  before_action :load_agent
4
4
 
5
5
  def index
@@ -13,14 +13,7 @@ module RailsConsoleAi
13
13
  def restore
14
14
  version = @agent.versions.find(params[:id])
15
15
  @agent.update_with_version!(
16
- {
17
- name: version.name,
18
- description: version.description,
19
- body: version.body,
20
- max_rounds: version.max_rounds,
21
- model: version.model,
22
- tools: Array(version.tools)
23
- },
16
+ { content: version.content },
24
17
  edited_by: params[:edited_by].presence || 'web',
25
18
  change_note: "Restored from version ##{version.id}"
26
19
  )
@@ -1,7 +1,7 @@
1
1
  require 'rails_console_ai/agent_loader'
2
2
 
3
3
  module RailsConsoleAi
4
- class AgentsController < ApplicationController
4
+ class AgentsController < RailsConsoleAi::ApplicationController
5
5
  before_action :load_agent, only: [:show, :edit, :update, :destroy, :approve]
6
6
 
7
7
  def index
@@ -32,14 +32,17 @@ module RailsConsoleAi
32
32
  a['source'] == :builtin && a['name'].to_s.downcase == params[:from_builtin].to_s.downcase
33
33
  }
34
34
  if builtin
35
- @agent.name = builtin['name']
36
- @agent.description = builtin['description']
37
- @agent.body = builtin['body']
38
- @agent.max_rounds = builtin['max_rounds']
39
- @agent.model = builtin['model']
40
- @agent.tools = Array(builtin['tools'])
35
+ @agent.content = AgentLoader.dump(
36
+ name: builtin['name'],
37
+ description: builtin['description'],
38
+ body: builtin['body'],
39
+ max_rounds: builtin['max_rounds'],
40
+ model: builtin['model'],
41
+ tools: Array(builtin['tools'])
42
+ )
41
43
  end
42
44
  end
45
+ @agent.content ||= new_agent_template
43
46
  end
44
47
 
45
48
  def create
@@ -57,34 +60,6 @@ module RailsConsoleAi
57
60
  end
58
61
  end
59
62
 
60
- # POST /agents/import — parse a pasted .md blob and re-render `new` with fields prefilled.
61
- def import
62
- content = params[:content].to_s
63
- if content.strip.empty?
64
- redirect_to new_agent_path, alert: 'Nothing to parse — paste the .md content into the box first.'
65
- return
66
- end
67
-
68
- parsed = AgentLoader.parse(content)
69
- if parsed.nil? || parsed['name'].to_s.strip.empty?
70
- redirect_to new_agent_path,
71
- alert: 'Could not parse. Expected YAML frontmatter (between `---` lines) with at least a `name` field, followed by the agent body.'
72
- return
73
- end
74
-
75
- @agent = Agent.new(
76
- name: parsed['name'],
77
- description: parsed['description'],
78
- body: parsed['body'],
79
- max_rounds: parsed['max_rounds'],
80
- model: parsed['model']
81
- )
82
- @agent.tools = Array(parsed['tools'])
83
-
84
- flash.now[:notice] = "Parsed \"#{parsed['name']}\" from pasted content. Review the fields below and click Create agent to save to the DB."
85
- render :new
86
- end
87
-
88
63
  def edit
89
64
  redirect_to agents_path, alert: read_only_message and return unless @agent.is_a?(RailsConsoleAi::Agent)
90
65
  end
@@ -136,9 +111,8 @@ module RailsConsoleAi
136
111
  @agent = Agent.find(params[:agent_id])
137
112
  @from = @agent.versions.find(params[:from])
138
113
  @to = params[:to].present? ? @agent.versions.find(params[:to]) : nil
139
- @to_label = @to ? "Version ##{@to.id}" : 'Current'
140
- @to_body = @to ? @to.body : @agent.body
141
- @to_tools = @to ? Array(@to.tools) : Array(@agent.tools)
114
+ @to_label = @to ? "Version ##{@to.id}" : 'Current'
115
+ @to_content = @to ? @to.content : @agent.content
142
116
  end
143
117
 
144
118
  private
@@ -163,22 +137,25 @@ module RailsConsoleAi
163
137
  end
164
138
 
165
139
  def agent_params
166
- {
167
- name: params.require(:agent)[:name],
168
- description: params[:agent][:description],
169
- body: params[:agent][:body],
170
- max_rounds: params[:agent][:max_rounds].presence&.to_i,
171
- model: params[:agent][:model].presence,
172
- tools: split_lines(params[:agent][:tools])
173
- }
140
+ { content: params.require(:agent)[:content].to_s }
174
141
  end
175
142
 
176
143
  def edited_by_param
177
144
  params[:edited_by].presence || 'web'
178
145
  end
179
146
 
180
- def split_lines(str)
181
- str.to_s.split(/[\r\n,]+/).map(&:strip).reject(&:empty?)
147
+ def new_agent_template
148
+ <<~MD
149
+ ---
150
+ name:
151
+ description:
152
+ max_rounds:
153
+ model:
154
+ tools: []
155
+ ---
156
+
157
+ Persona, strategy, rules…
158
+ MD
182
159
  end
183
160
 
184
161
  def slugify(name)
@@ -1,8 +1,8 @@
1
1
  require 'rails_console_ai/tools/memory_tools'
2
2
 
3
3
  module RailsConsoleAi
4
- class MemoriesController < ApplicationController
5
- before_action :load_memory, only: [:show, :edit, :update, :destroy]
4
+ class MemoriesController < RailsConsoleAi::ApplicationController
5
+ before_action :load_memory, only: [:show, :edit, :update, :destroy, :approve]
6
6
 
7
7
  def index
8
8
  @memories = Tools::MemoryTools.new.load_all_memories
@@ -25,32 +25,7 @@ module RailsConsoleAi
25
25
  end
26
26
 
27
27
  def new
28
- @memory = Memory.new
29
- end
30
-
31
- # POST /memories/import — parse a pasted .md blob and re-render `new` with fields prefilled.
32
- def import
33
- content = params[:content].to_s
34
- if content.strip.empty?
35
- redirect_to new_memory_path, alert: 'Nothing to parse — paste the .md content into the box first.'
36
- return
37
- end
38
-
39
- parsed = Tools::MemoryTools.parse(content)
40
- if parsed.nil? || parsed['name'].to_s.strip.empty?
41
- redirect_to new_memory_path,
42
- alert: 'Could not parse. Expected YAML frontmatter (between `---` lines) with at least a `name` field, followed by the memory body.'
43
- return
44
- end
45
-
46
- @memory = Memory.new(
47
- name: parsed['name'],
48
- description: parsed['description']
49
- )
50
- @memory.tags = Array(parsed['tags'])
51
-
52
- flash.now[:notice] = "Parsed \"#{parsed['name']}\" from pasted content. Review the fields below and click Create memory to save to the DB."
53
- render :new
28
+ @memory = Memory.new(content: new_memory_template)
54
29
  end
55
30
 
56
31
  def create
@@ -98,13 +73,32 @@ module RailsConsoleAi
98
73
  end
99
74
  end
100
75
 
76
+ def approve
77
+ redirect_to memories_path, alert: file_memory_message and return unless @memory.is_a?(RailsConsoleAi::Memory)
78
+
79
+ approver = params[:approved_by].presence ||
80
+ (request.respond_to?(:remote_user) && request.remote_user.presence) ||
81
+ 'web'
82
+
83
+ if @memory.approved?
84
+ redirect_to memory_path(@memory), notice: 'Memory is already approved.'
85
+ return
86
+ end
87
+
88
+ begin
89
+ @memory.approve!(approved_by: approver)
90
+ redirect_to memory_path(@memory), notice: "Approved by #{approver}. The AI can now recall this memory."
91
+ rescue ArgumentError, ActiveRecord::RecordInvalid => e
92
+ redirect_to memory_path(@memory), alert: "Could not approve: #{e.message}"
93
+ end
94
+ end
95
+
101
96
  def diff
102
97
  @memory = Memory.find(params[:memory_id])
103
98
  @from = @memory.versions.find(params[:from])
104
99
  @to = params[:to].present? ? @memory.versions.find(params[:to]) : nil
105
- @to_label = @to ? "Version ##{@to.id}" : 'Current'
106
- @to_description = @to ? @to.description : @memory.description
107
- @to_tags = @to ? Array(@to.tags) : Array(@memory.tags)
100
+ @to_label = @to ? "Version ##{@to.id}" : 'Current'
101
+ @to_content = @to ? @to.content : @memory.content
108
102
  end
109
103
 
110
104
  private
@@ -130,19 +124,22 @@ module RailsConsoleAi
130
124
  end
131
125
 
132
126
  def memory_params
133
- {
134
- name: params.require(:memory)[:name],
135
- description: params[:memory][:description],
136
- tags: split_csv(params[:memory][:tags])
137
- }
127
+ { content: params.require(:memory)[:content].to_s }
138
128
  end
139
129
 
140
130
  def edited_by_param
141
131
  params[:edited_by].presence || 'web'
142
132
  end
143
133
 
144
- def split_csv(str)
145
- str.to_s.split(',').map(&:strip).reject(&:empty?)
134
+ def new_memory_template
135
+ <<~MD
136
+ ---
137
+ name:
138
+ tags: []
139
+ ---
140
+
141
+ The fact or pattern you're persisting.
142
+ MD
146
143
  end
147
144
 
148
145
  def slugify(name)
@@ -1,5 +1,5 @@
1
1
  module RailsConsoleAi
2
- class MemoryVersionsController < ApplicationController
2
+ class MemoryVersionsController < RailsConsoleAi::ApplicationController
3
3
  before_action :load_memory
4
4
 
5
5
  def index
@@ -13,11 +13,7 @@ module RailsConsoleAi
13
13
  def restore
14
14
  version = @memory.versions.find(params[:id])
15
15
  @memory.update_with_version!(
16
- {
17
- name: version.name,
18
- description: version.description,
19
- tags: Array(version.tags)
20
- },
16
+ { content: version.content },
21
17
  edited_by: params[:edited_by].presence || 'web',
22
18
  change_note: "Restored from version ##{version.id}"
23
19
  )
@@ -1,5 +1,5 @@
1
1
  module RailsConsoleAi
2
- class SessionsController < ApplicationController
2
+ class SessionsController < RailsConsoleAi::ApplicationController
3
3
  PER_PAGE = 50
4
4
 
5
5
  def index
@@ -1,5 +1,5 @@
1
1
  module RailsConsoleAi
2
- class SkillVersionsController < ApplicationController
2
+ class SkillVersionsController < RailsConsoleAi::ApplicationController
3
3
  before_action :load_skill
4
4
 
5
5
  def index
@@ -13,13 +13,7 @@ module RailsConsoleAi
13
13
  def restore
14
14
  version = @skill.versions.find(params[:id])
15
15
  @skill.update_with_version!(
16
- {
17
- name: version.name,
18
- description: version.description,
19
- body: version.body,
20
- tags: Array(version.tags),
21
- bypass_guards_for_methods: Array(version.bypass_guards_for_methods)
22
- },
16
+ { content: version.content },
23
17
  edited_by: params[:edited_by].presence || 'web',
24
18
  change_note: "Restored from version ##{version.id}"
25
19
  )
@@ -1,7 +1,7 @@
1
1
  require 'rails_console_ai/skill_loader'
2
2
 
3
3
  module RailsConsoleAi
4
- class SkillsController < ApplicationController
4
+ class SkillsController < RailsConsoleAi::ApplicationController
5
5
  before_action :load_skill, only: [:show, :edit, :update, :destroy, :approve]
6
6
 
7
7
  def index
@@ -26,37 +26,7 @@ module RailsConsoleAi
26
26
  end
27
27
 
28
28
  def new
29
- @skill = Skill.new
30
- end
31
-
32
- # POST /skills/import — accepts a pasted .md blob in params[:content], parses
33
- # YAML frontmatter + body, and re-renders `new` with the fields pre-populated.
34
- # The user reviews + clicks Create skill to actually persist (normal proposed-
35
- # status + version-row flow applies).
36
- def import
37
- content = params[:content].to_s
38
- if content.strip.empty?
39
- redirect_to new_skill_path, alert: 'Nothing to parse — paste the .md content into the box first.'
40
- return
41
- end
42
-
43
- parsed = SkillLoader.parse(content)
44
- if parsed.nil? || parsed['name'].to_s.strip.empty?
45
- redirect_to new_skill_path,
46
- alert: 'Could not parse. Expected YAML frontmatter (between `---` lines) with at least a `name` field, followed by a markdown body.'
47
- return
48
- end
49
-
50
- @skill = Skill.new(
51
- name: parsed['name'],
52
- description: parsed['description'],
53
- body: parsed['body']
54
- )
55
- @skill.tags = Array(parsed['tags'])
56
- @skill.bypass_guards_for_methods = Array(parsed['bypass_guards_for_methods'])
57
-
58
- flash.now[:notice] = "Parsed \"#{parsed['name']}\" from pasted content. Review the fields below and click Create skill to save to the DB."
59
- render :new
29
+ @skill = Skill.new(content: new_skill_template)
60
30
  end
61
31
 
62
32
  def create
@@ -130,10 +100,8 @@ module RailsConsoleAi
130
100
  @from = @skill.versions.find(params[:from])
131
101
  @to = params[:to].present? ? @skill.versions.find(params[:to]) : nil
132
102
  # If `to` is omitted, diff against the current skill.
133
- @to_label = @to ? "Version ##{@to.id}" : 'Current'
134
- @to_body = @to ? @to.body : @skill.body
135
- @to_tags = @to ? Array(@to.tags) : Array(@skill.tags)
136
- @to_bypass = @to ? Array(@to.bypass_guards_for_methods) : Array(@skill.bypass_guards_for_methods)
103
+ @to_label = @to ? "Version ##{@to.id}" : 'Current'
104
+ @to_content = @to ? @to.content : @skill.content
137
105
  end
138
106
 
139
107
  private
@@ -165,25 +133,28 @@ module RailsConsoleAi
165
133
  end
166
134
 
167
135
  def skill_params
168
- {
169
- name: params.require(:skill)[:name],
170
- description: params[:skill][:description],
171
- body: params[:skill][:body],
172
- tags: split_csv(params[:skill][:tags]),
173
- bypass_guards_for_methods: split_lines(params[:skill][:bypass_guards_for_methods])
174
- }
136
+ { content: params.require(:skill)[:content].to_s }
175
137
  end
176
138
 
177
139
  def edited_by_param
178
140
  params[:edited_by].presence || 'web'
179
141
  end
180
142
 
181
- def split_csv(str)
182
- str.to_s.split(',').map(&:strip).reject(&:empty?)
183
- end
143
+ def new_skill_template
144
+ <<~MD
145
+ ---
146
+ name:
147
+ description:
148
+ tags: []
149
+ bypass_guards_for_methods: []
150
+ ---
151
+
152
+ ## When to use
153
+
154
+ ## Recipe
184
155
 
185
- def split_lines(str)
186
- str.to_s.split(/[\r\n]+/).map(&:strip).reject(&:empty?)
156
+ ## Notes
157
+ MD
187
158
  end
188
159
 
189
160
  def slugify(name)