aia 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/CHANGELOG.md +51 -0
- data/README.md +172 -15
- data/docs/cli-reference.md +92 -1
- data/docs/configuration.md +22 -0
- data/docs/directives-reference.md +35 -15
- data/docs/index.md +36 -62
- data/docs/prompt_management.md +88 -1
- data/lib/aia/chat_loop.rb +54 -0
- data/lib/aia/chat_processor_service.rb +4 -1
- data/lib/aia/config/cli_parser.rb +49 -11
- data/lib/aia/config/defaults.yml +17 -2
- data/lib/aia/config/validator.rb +47 -6
- data/lib/aia/config.rb +29 -3
- data/lib/aia/directive.rb +29 -0
- data/lib/aia/directives/model_directives.rb +28 -27
- data/lib/aia/directives/web_and_file_directives.rb +75 -41
- data/lib/aia/prompt_handler.rb +26 -1
- data/lib/aia/prompt_pipeline.rb +45 -1
- data/lib/aia/skill_utils.rb +61 -0
- data/lib/aia.rb +1 -0
- metadata +4 -3
data/docs/index.md
CHANGED
|
@@ -23,80 +23,48 @@ Welcome to AIA, your powerful CLI tool for dynamic prompt management and AI inte
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
-
!!!
|
|
26
|
+
!!! tip "🚀 New: AI Assistant Scheduler (AIAS)"
|
|
27
27
|
|
|
28
|
-
**
|
|
28
|
+
**Schedule and automate your AIA prompts!** AIAS is a new Ruby gem that lets you run AIA prompts on a cron-like schedule — perfect for recurring AI tasks, automated reports, and timed workflows.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
**[View AIAS on GitHub →](https://github.com/madbomber/aias)**
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- **File extension** — `.txt` is now `.md`
|
|
35
|
-
- **Parameters** — `[PLACEHOLDER]` is now `<%= placeholder %>`
|
|
36
|
-
- **Metadata** — Separate `.json` history files are now YAML front matter inside the `.md` file
|
|
37
|
-
- **Directive embedding** — `//config`, `//pipeline`, etc. are now YAML front matter keys
|
|
38
|
-
|
|
39
|
-
**Before (v0.11.2):**
|
|
40
|
-
|
|
41
|
-
# ~/.prompts/my_prompt.txt
|
|
42
|
-
//config temperature 0.7
|
|
43
|
-
//pipeline next_prompt, another_prompt
|
|
44
|
-
Tell me about [TOPIC] in [LANGUAGE]
|
|
45
|
-
|
|
46
|
-
**After (v1.0.0):**
|
|
47
|
-
|
|
48
|
-
---
|
|
49
|
-
temperature: 0.7
|
|
50
|
-
pipeline: [next_prompt, another_prompt]
|
|
51
|
-
parameters:
|
|
52
|
-
topic: null
|
|
53
|
-
language: null
|
|
54
|
-
---
|
|
55
|
-
Tell me about <%= topic %> in <%= language %>
|
|
56
|
-
|
|
57
|
-
**Run the migration tool:**
|
|
58
|
-
|
|
59
|
-
# Back up first (recommended)
|
|
60
|
-
cp -r ~/.prompts ~/.prompts.backup
|
|
61
|
-
|
|
62
|
-
# Migrate all prompts
|
|
63
|
-
migrate_prompts --verbose ~/.prompts
|
|
64
|
-
|
|
65
|
-
# Review flagged files (*.txt-review), then recover them
|
|
66
|
-
migrate_prompts --reprocess ~/.prompts
|
|
67
|
-
|
|
68
|
-
The `migrate_prompts` script is a standalone program in `bin/`. It handles placeholder conversion, directive migration, history file merging, and flags files with code fences for manual review. See [Migration Guide](guides/migrate-prompts.md) for details.
|
|
32
|
+
---
|
|
69
33
|
|
|
70
|
-
!!!
|
|
34
|
+
!!! tip "🎭 Roles: Give Your Robot a Personality"
|
|
71
35
|
|
|
72
|
-
-
|
|
73
|
-
- **`--regex` deprecated** — Parameter extraction uses ERB (`<%= param %>`) instead of regex. The flag prints a warning and has no effect.
|
|
74
|
-
- **`--terse` deprecated** — No longer supported. The flag prints a warning and has no effect.
|
|
75
|
-
- **`--metrics` renamed to `--tokens`** — Use `--tokens` to display token usage. `--cost` implies `--tokens`.
|
|
36
|
+
Roles are plain-text prompt files that define how your AI thinks, talks, and interacts with you. Drop one in `~/.prompts/roles/` and your robot instantly becomes someone new.
|
|
76
37
|
|
|
77
|
-
|
|
38
|
+
**For fun:**
|
|
39
|
+
```bash
|
|
40
|
+
aia --chat --role pirate # Arrr, matey!
|
|
41
|
+
aia --chat --role nyc_cabbie # opinions about EVERYTHING
|
|
42
|
+
aia --chat --role stoned_hacker # solves it anyway, dude
|
|
43
|
+
```
|
|
78
44
|
|
|
79
|
-
|
|
45
|
+
**For serious work:**
|
|
46
|
+
```bash
|
|
47
|
+
# Explains quantum physics to your 7-year-old
|
|
48
|
+
aia --chat --role first_grade_teacher
|
|
80
49
|
|
|
81
|
-
|
|
50
|
+
# Three expert robots on the same design doc, simultaneously
|
|
51
|
+
aia --model gpt-4o=architect,claude=security,gemini=performance design.md
|
|
52
|
+
```
|
|
82
53
|
|
|
83
|
-
|
|
54
|
+
Assign a different role to each model and get multiple expert perspectives in one command. **[Full Roles Guide →](guides/models.md)**
|
|
84
55
|
|
|
85
|
-
|
|
86
|
-
|---|---|
|
|
87
|
-
| `AIA_PROMPTS_DIR` | `AIA_PROMPTS__DIR` |
|
|
88
|
-
| `AIA_OUT_FILE` | `AIA_OUTPUT__FILE` |
|
|
89
|
-
| `AIA_VERBOSE` | `AIA_FLAGS__VERBOSE` |
|
|
90
|
-
| `AIA_DEBUG` | `AIA_FLAGS__DEBUG` |
|
|
91
|
-
| `AIA_CHAT` | `AIA_FLAGS__CHAT` |
|
|
92
|
-
| `AIA_TEMPERATURE` | `AIA_LLM__TEMPERATURE` |
|
|
93
|
-
| `AIA_MARKDOWN` | `AIA_OUTPUT__MARKDOWN` |
|
|
56
|
+
!!! tip "🎓 Skills: Teach Your Robot Your Process"
|
|
94
57
|
|
|
95
|
-
|
|
58
|
+
Skills are structured instruction sets that tell your robot *exactly how* to approach a task — your workflow, your standards, every single time. Each skill is a directory with a `SKILL.md` file in `~/.prompts/skills/`.
|
|
96
59
|
|
|
97
|
-
|
|
60
|
+
```bash
|
|
61
|
+
aia -s code-quality my_prompt # one skill
|
|
62
|
+
aia -s code-quality,security-review my_prompt # stack them
|
|
63
|
+
aia --chat --role senior_dev -s code-quality # role + skill
|
|
64
|
+
/skill code-quality # add mid-chat
|
|
65
|
+
```
|
|
98
66
|
|
|
99
|
-
|
|
67
|
+
Combine roles and skills: a pirate who follows your code review process, a first-grade teacher who uses your step-by-step explanation method, or multiple robots each with their own role and skill set — all from the command line. **[Full Skills Guide →](directives-reference.md)**
|
|
100
68
|
|
|
101
69
|
---
|
|
102
70
|
|
|
@@ -105,9 +73,15 @@ Welcome to AIA, your powerful CLI tool for dynamic prompt management and AI inte
|
|
|
105
73
|
### 🚀 Dynamic Prompt Management
|
|
106
74
|
- **Hierarchical Configuration**: Embedded directives > CLI args > environment variables > config files > defaults
|
|
107
75
|
- **Prompt Sequences and Workflows**: Chain prompts together for complex AI workflows
|
|
108
|
-
- **Role-based Prompts**: Use predefined roles to context your AI interactions
|
|
109
76
|
- **Fuzzy Search**: Find prompts quickly with fuzzy matching (requires `fzf`)
|
|
110
77
|
|
|
78
|
+
### 🎭 Roles & 🎓 Skills
|
|
79
|
+
- **Robot Personalities**: Give any robot a voice — fun personas like a pirate or NYC cabbie, or professional ones like a senior architect or first-grade teacher
|
|
80
|
+
- **Per-Model Roles**: Assign a different role to each model in a multi-model session — `--model gpt-4o=architect,claude=security`
|
|
81
|
+
- **Reusable Skill Sets**: Encode your exact workflow once in a `SKILL.md` file; apply it to any prompt with `-s skill-name`
|
|
82
|
+
- **Role + Skill Combos**: A robot can be *who you want* (role) and *know exactly what to do* (skill) simultaneously
|
|
83
|
+
- **Mid-Chat Skills**: Add a skill to a running chat session with `/skill skill-name`
|
|
84
|
+
|
|
111
85
|
### 🔧 Powerful Integration
|
|
112
86
|
- **Shell Integration**: Execute shell commands directly within prompts
|
|
113
87
|
- **Ruby (ERB) Processing**: Use Ruby code in your prompts for dynamic content
|
data/docs/prompt_management.md
CHANGED
|
@@ -8,10 +8,15 @@ AIA provides sophisticated prompt management capabilities through the PM gem, en
|
|
|
8
8
|
```
|
|
9
9
|
~/.prompts/
|
|
10
10
|
├── README.md # Documentation for your prompt collection
|
|
11
|
-
├── roles/ # Role
|
|
11
|
+
├── roles/ # Role definitions (LLM personality/persona)
|
|
12
12
|
│ ├── assistant.md
|
|
13
13
|
│ ├── code_expert.md
|
|
14
14
|
│ └── teacher.md
|
|
15
|
+
├── skills/ # Skill definitions (task instructions)
|
|
16
|
+
│ ├── code-review/
|
|
17
|
+
│ │ └── SKILL.md # YAML front matter + instruction body
|
|
18
|
+
│ └── summarizer/
|
|
19
|
+
│ └── SKILL.md
|
|
15
20
|
├── development/ # Development-related prompts
|
|
16
21
|
│ ├── code_review.md
|
|
17
22
|
│ ├── debug_help.md
|
|
@@ -292,6 +297,88 @@ Current Task:
|
|
|
292
297
|
Please provide guidance consistent with the project architecture and your role as <%= role %>.
|
|
293
298
|
```
|
|
294
299
|
|
|
300
|
+
## Skills
|
|
301
|
+
|
|
302
|
+
### Roles vs Skills
|
|
303
|
+
|
|
304
|
+
These two concepts work together but serve distinct purposes:
|
|
305
|
+
|
|
306
|
+
| Concept | Defines | Loaded from | Injected as |
|
|
307
|
+
|---------|---------|-------------|-------------|
|
|
308
|
+
| **Role** | LLM *personality* — who the model is | `~/.prompts/roles/<id>.md` | First, before skills and prompt |
|
|
309
|
+
| **Skill** | Task *instructions* — how to approach the work | `~/.prompts/skills/<name>/SKILL.md` | After role, before user prompt |
|
|
310
|
+
|
|
311
|
+
A **role** sets the persona: "You are a senior Ruby developer with deep expertise in performance optimization."
|
|
312
|
+
|
|
313
|
+
A **skill** provides procedural guidance for that persona to follow when executing the user's request: "When reviewing code, always check for: N+1 queries, missing indexes, memory leaks, and security vulnerabilities. Present findings as a prioritized list."
|
|
314
|
+
|
|
315
|
+
The assembled prompt order is:
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
1. Role content ← WHO the LLM is (personality)
|
|
319
|
+
2. Skill content(s) ← HOW to approach the task (instructions)
|
|
320
|
+
3. User prompt ← WHAT to do (request)
|
|
321
|
+
4. Context files ← supporting material
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Skill File Format
|
|
325
|
+
|
|
326
|
+
Each skill lives in its own subdirectory under `~/.prompts/skills/`. The subdirectory must contain a `SKILL.md` file with YAML front matter followed by the skill instruction body:
|
|
327
|
+
|
|
328
|
+
```markdown
|
|
329
|
+
---
|
|
330
|
+
name: code-review
|
|
331
|
+
description: Thorough code review focusing on correctness, security, and maintainability.
|
|
332
|
+
user-invocable: true
|
|
333
|
+
argument-hint: ["file or topic to review"]
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
When reviewing code, systematically check:
|
|
337
|
+
|
|
338
|
+
1. **Correctness** — Does the logic match the stated intent? Are edge cases handled?
|
|
339
|
+
2. **Security** — Are there injection risks, unsafe deserialization, or exposed secrets?
|
|
340
|
+
3. **Performance** — Are there N+1 queries, unbounded loops, or unnecessary allocations?
|
|
341
|
+
4. **Maintainability** — Is the code readable? Are names clear? Is complexity justified?
|
|
342
|
+
|
|
343
|
+
Present findings as a prioritized list with file:line references where applicable.
|
|
344
|
+
Always suggest a concrete fix, not just identification of the problem.
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
The YAML front matter is metadata only. Only the body (everything after the closing `---`) is injected into the prompt.
|
|
348
|
+
|
|
349
|
+
### Using Skills
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
# Prepend a skill before the user prompt
|
|
353
|
+
aia --skill code-review review_prompt my_code.rb
|
|
354
|
+
|
|
355
|
+
# Combine role + skill for maximum context
|
|
356
|
+
aia --role ruby_expert --skill code-review review_prompt my_code.rb
|
|
357
|
+
|
|
358
|
+
# Multiple skills (applied in order)
|
|
359
|
+
aia --skill code-review --skill security-audit review_prompt my_code.rb
|
|
360
|
+
aia -s code-review,security-audit review_prompt my_code.rb
|
|
361
|
+
|
|
362
|
+
# List available skills
|
|
363
|
+
aia --list-skills
|
|
364
|
+
|
|
365
|
+
# Use a skill from within a chat session
|
|
366
|
+
/skill code-review
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Skills in Chat Mode
|
|
370
|
+
|
|
371
|
+
In chat mode, use the `/skill` directive to inject a skill at any point in the conversation:
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
> /skill summarizer
|
|
375
|
+
[Skill "summarizer" instructions are injected into the next message context]
|
|
376
|
+
|
|
377
|
+
> Please summarize the discussion so far.
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
The `/skill` directive injects only the body content of `SKILL.md` — the YAML front matter is never sent to the LLM.
|
|
381
|
+
|
|
295
382
|
## Prompt Workflows and Pipelines
|
|
296
383
|
|
|
297
384
|
### Simple Workflows
|
data/lib/aia/chat_loop.rb
CHANGED
|
@@ -6,6 +6,8 @@ require "pm"
|
|
|
6
6
|
|
|
7
7
|
module AIA
|
|
8
8
|
class ChatLoop
|
|
9
|
+
include AIA::SkillUtils
|
|
10
|
+
|
|
9
11
|
def initialize(chat_processor, ui_presenter, directive_processor)
|
|
10
12
|
@chat_processor = chat_processor
|
|
11
13
|
@ui_presenter = ui_presenter
|
|
@@ -15,6 +17,8 @@ module AIA
|
|
|
15
17
|
# Start the interactive chat session
|
|
16
18
|
def start(skip_context_files: false)
|
|
17
19
|
setup_session
|
|
20
|
+
process_role_context
|
|
21
|
+
process_skill_context
|
|
18
22
|
process_initial_context(skip_context_files)
|
|
19
23
|
handle_piped_input
|
|
20
24
|
run_loop
|
|
@@ -39,6 +43,56 @@ module AIA
|
|
|
39
43
|
Signal.trap("INT") { exit }
|
|
40
44
|
end
|
|
41
45
|
|
|
46
|
+
def process_role_context
|
|
47
|
+
role = AIA.config.prompts.role
|
|
48
|
+
return if role.nil? || role.empty?
|
|
49
|
+
|
|
50
|
+
prompt_handler = AIA::PromptHandler.new
|
|
51
|
+
role_parsed = prompt_handler.fetch_role(role)
|
|
52
|
+
return if role_parsed.nil?
|
|
53
|
+
|
|
54
|
+
role_content = role_parsed.to_s
|
|
55
|
+
return if role_content.nil? || role_content.strip.empty?
|
|
56
|
+
|
|
57
|
+
return unless AIA.client.respond_to?(:chats)
|
|
58
|
+
|
|
59
|
+
system_msg = RubyLLM::Message.new(role: :system, content: role_content)
|
|
60
|
+
|
|
61
|
+
AIA.client.chats.each_value do |chat|
|
|
62
|
+
next if chat.messages.any? { |m| m.role == :system }
|
|
63
|
+
chat.add_message(system_msg)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def process_skill_context
|
|
68
|
+
skills = AIA.config.prompts.skills
|
|
69
|
+
return if skills.nil? || skills.empty?
|
|
70
|
+
|
|
71
|
+
skills_dir = AIA.config.skills.dir
|
|
72
|
+
bodies = Array(skills).filter_map do |skill_name|
|
|
73
|
+
skill_name = skill_name.to_s.strip
|
|
74
|
+
next if skill_name.empty?
|
|
75
|
+
|
|
76
|
+
skill_path = find_skill_dir(skill_name, skills_dir)
|
|
77
|
+
next unless skill_path
|
|
78
|
+
|
|
79
|
+
if File.file?(skill_path)
|
|
80
|
+
skill_body(File.read(skill_path))
|
|
81
|
+
else
|
|
82
|
+
md = File.join(skill_path, 'SKILL.md')
|
|
83
|
+
skill_body(File.read(md)) if File.exist?(md)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
return if bodies.empty?
|
|
88
|
+
|
|
89
|
+
skill_content = bodies.join("\n\n")
|
|
90
|
+
response_data = @chat_processor.process_prompt(skill_content)
|
|
91
|
+
content = response_data.is_a?(Hash) ? response_data[:content] : response_data
|
|
92
|
+
@chat_processor.output_response(content)
|
|
93
|
+
@ui_presenter.display_separator
|
|
94
|
+
end
|
|
95
|
+
|
|
42
96
|
def process_initial_context(skip_context_files)
|
|
43
97
|
return if skip_context_files || !AIA.config.context_files || AIA.config.context_files.empty?
|
|
44
98
|
|
|
@@ -98,7 +98,10 @@ module AIA
|
|
|
98
98
|
first_model = models.first
|
|
99
99
|
model_name = first_model.respond_to?(:name) ? first_model.name : first_model.to_s
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
# client_model is the full resolved ID (e.g. "claude-sonnet-4-20250514"),
|
|
102
|
+
# model_name is the configured alias (e.g. "claude-sonnet-4").
|
|
103
|
+
# The alias is always a prefix/substring of the resolved ID, so check that way.
|
|
104
|
+
unless client_model.downcase.include?(model_name.downcase)
|
|
102
105
|
AIA.client = AIA.client.class.new
|
|
103
106
|
end
|
|
104
107
|
end
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
# for the Config class.
|
|
7
7
|
|
|
8
8
|
require 'optparse'
|
|
9
|
+
require 'yaml'
|
|
9
10
|
require_relative 'model_spec'
|
|
11
|
+
require_relative '../skill_utils'
|
|
10
12
|
|
|
11
13
|
module AIA
|
|
12
14
|
module CLIParser
|
|
@@ -166,6 +168,23 @@ module AIA
|
|
|
166
168
|
warn "Warning: --regex is deprecated. PM v1.0.0 uses ERB parameters (<%= param %>)."
|
|
167
169
|
options[:parameter_regex] = pattern
|
|
168
170
|
end
|
|
171
|
+
|
|
172
|
+
opts.on("--skills-dir DIR", "Set directory containing skill subdirectories") do |dir|
|
|
173
|
+
options[:skills_dir] = dir
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
opts.on("--skills-prefix PREFIX", "Set subdirectory name for skill files (default: skills)") do |prefix|
|
|
177
|
+
options[:skills_prefix] = prefix
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
opts.on("-s", "--skill SKILL_IDS", "Prepend skill(s) to prompt (comma-separated IDs or paths)") do |ids|
|
|
181
|
+
options[:skills] ||= []
|
|
182
|
+
options[:skills] += ids.split(',').map(&:strip)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
opts.on("--list-skills", "List available skills and exit") do
|
|
186
|
+
options[:list_skills] = true
|
|
187
|
+
end
|
|
169
188
|
end
|
|
170
189
|
|
|
171
190
|
def setup_ai_parameters(opts, options)
|
|
@@ -410,6 +429,13 @@ module AIA
|
|
|
410
429
|
end
|
|
411
430
|
|
|
412
431
|
def validate_role_exists(role_id)
|
|
432
|
+
if AIA::SkillUtils.path_based_id?(role_id)
|
|
433
|
+
expanded = File.expand_path(role_id)
|
|
434
|
+
expanded += '.md' if File.extname(expanded).empty?
|
|
435
|
+
raise ArgumentError, "Role file not found: #{expanded}" unless File.exist?(expanded)
|
|
436
|
+
return
|
|
437
|
+
end
|
|
438
|
+
|
|
413
439
|
prompts_dir = ENV.fetch('AIA_PROMPTS__DIR', File.join(ENV['HOME'], '.prompts'))
|
|
414
440
|
roles_prefix = ENV.fetch('AIA_PROMPTS__ROLES_PREFIX', 'roles')
|
|
415
441
|
|
|
@@ -442,19 +468,30 @@ module AIA
|
|
|
442
468
|
roles_prefix = ENV.fetch('AIA_PROMPTS__ROLES_PREFIX', 'roles')
|
|
443
469
|
roles_dir = File.join(prompts_dir, roles_prefix)
|
|
444
470
|
|
|
445
|
-
|
|
446
|
-
roles = list_available_role_names(prompts_dir, roles_prefix)
|
|
447
|
-
|
|
448
|
-
if roles.empty?
|
|
449
|
-
puts "No role files found in #{roles_dir}"
|
|
450
|
-
puts "Create .md files in this directory to define roles."
|
|
451
|
-
else
|
|
452
|
-
puts "Available roles in #{roles_dir}:"
|
|
453
|
-
roles.each { |role| puts " - #{role}" }
|
|
454
|
-
end
|
|
455
|
-
else
|
|
471
|
+
unless Dir.exist?(roles_dir)
|
|
456
472
|
puts "No roles directory found at #{roles_dir}"
|
|
457
473
|
puts "Create this directory and add role files to use roles."
|
|
474
|
+
return
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
roles = list_available_role_names(prompts_dir, roles_prefix)
|
|
478
|
+
|
|
479
|
+
if roles.empty?
|
|
480
|
+
puts "No role files found in #{roles_dir}"
|
|
481
|
+
puts "Create .md files in this directory to define roles."
|
|
482
|
+
return
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
roles.each do |role_id|
|
|
486
|
+
role_file = File.join(roles_dir, "#{role_id}.md")
|
|
487
|
+
fm = AIA::SkillUtils.parse_front_matter(role_file)
|
|
488
|
+
|
|
489
|
+
puts "## #{role_id}"
|
|
490
|
+
puts
|
|
491
|
+
puts "| Key | Value |"
|
|
492
|
+
puts "|-----|-------|"
|
|
493
|
+
fm.each { |key, value| puts "| #{key} | #{value} |" }
|
|
494
|
+
puts
|
|
458
495
|
end
|
|
459
496
|
end
|
|
460
497
|
|
|
@@ -464,6 +501,7 @@ module AIA
|
|
|
464
501
|
|
|
465
502
|
Dir.glob("**/*.md", base: roles_dir)
|
|
466
503
|
.map { |f| f.chomp('.md') }
|
|
504
|
+
.reject { |f| f.split('/').any? { |part| part.start_with?('_') } }
|
|
467
505
|
.sort
|
|
468
506
|
end
|
|
469
507
|
|
data/lib/aia/config/defaults.yml
CHANGED
|
@@ -59,12 +59,26 @@ defaults:
|
|
|
59
59
|
prompts:
|
|
60
60
|
dir: ~/.prompts
|
|
61
61
|
extname: .md
|
|
62
|
-
|
|
63
|
-
roles_dir: ~/.prompts/roles
|
|
62
|
+
#
|
|
64
63
|
role: ~
|
|
64
|
+
roles_dir: ~/.prompts/roles
|
|
65
|
+
roles_prefix: roles
|
|
66
|
+
#
|
|
67
|
+
skills: []
|
|
68
|
+
skills_prefix: skills
|
|
69
|
+
#
|
|
70
|
+
tool: ~
|
|
71
|
+
tools_prefix: tools
|
|
72
|
+
#
|
|
65
73
|
system_prompt: ~
|
|
66
74
|
parameter_regex: ~
|
|
67
75
|
|
|
76
|
+
roles:
|
|
77
|
+
dir: ~/.prompts/roles
|
|
78
|
+
|
|
79
|
+
skills:
|
|
80
|
+
dir: ~/.prompts/skills
|
|
81
|
+
|
|
68
82
|
# ---------------------------------------------------------------------------
|
|
69
83
|
# Output Configuration
|
|
70
84
|
# Access: AIA.config.output.file, AIA.config.output.append, etc.
|
|
@@ -112,6 +126,7 @@ defaults:
|
|
|
112
126
|
# Env: AIA_TOOLS__PATHS, AIA_TOOLS__ALLOWED, etc.
|
|
113
127
|
# ---------------------------------------------------------------------------
|
|
114
128
|
tools:
|
|
129
|
+
dir: ~/.prompts/tools
|
|
115
130
|
paths: []
|
|
116
131
|
allowed: ~
|
|
117
132
|
rejected: ~
|
data/lib/aia/config/validator.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'word_wrapper'
|
|
4
4
|
require_relative '../adapter/gem_activator'
|
|
5
|
+
require_relative '../skill_utils'
|
|
5
6
|
|
|
6
7
|
# lib/aia/config/validator.rb
|
|
7
8
|
#
|
|
@@ -31,6 +32,7 @@ module AIA
|
|
|
31
32
|
handle_dump_config(config)
|
|
32
33
|
handle_mcp_list(config)
|
|
33
34
|
handle_list_tools(config)
|
|
35
|
+
handle_list_skills(config)
|
|
34
36
|
handle_completion_script(config)
|
|
35
37
|
validate_required_prompt_id(config)
|
|
36
38
|
process_role_configuration(config)
|
|
@@ -128,14 +130,18 @@ module AIA
|
|
|
128
130
|
return if role.nil? || role.empty?
|
|
129
131
|
|
|
130
132
|
roles_prefix = config.prompts.roles_prefix
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
end
|
|
133
|
+
|
|
134
|
+
unless AIA::SkillUtils.path_based_id?(role) || roles_prefix.nil? || roles_prefix.empty? || role.start_with?(roles_prefix)
|
|
135
|
+
config.prompts.role = "#{roles_prefix}/#{role}"
|
|
136
|
+
role = config.prompts.role
|
|
136
137
|
end
|
|
137
138
|
|
|
138
|
-
config.prompts.roles_dir ||= File.join(config.prompts.dir, roles_prefix)
|
|
139
|
+
config.prompts.roles_dir ||= File.join(config.prompts.dir, roles_prefix.to_s)
|
|
140
|
+
|
|
141
|
+
# In chat-only mode (no prompt_id), leave the role configured so ChatLoop
|
|
142
|
+
# can inject it as initial context. Promoting it to prompt_id would cause
|
|
143
|
+
# PM to receive the role path as a literal string rather than file content.
|
|
144
|
+
return if config.flags&.chat == true
|
|
139
145
|
|
|
140
146
|
if config.prompt_id.nil? || config.prompt_id.empty?
|
|
141
147
|
unless role.nil? || role.empty?
|
|
@@ -230,6 +236,40 @@ module AIA
|
|
|
230
236
|
exit 0
|
|
231
237
|
end
|
|
232
238
|
|
|
239
|
+
def handle_list_skills(config)
|
|
240
|
+
return unless config.list_skills
|
|
241
|
+
|
|
242
|
+
skills_dir = AIA.config.skills.dir
|
|
243
|
+
|
|
244
|
+
unless Dir.exist?(skills_dir)
|
|
245
|
+
$stderr.puts "No skills directory found at #{skills_dir}"
|
|
246
|
+
exit 0
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
skill_dirs = Dir.glob("*/SKILL.md", base: skills_dir).map { |f| File.dirname(f) }.sort
|
|
250
|
+
|
|
251
|
+
if skill_dirs.empty?
|
|
252
|
+
$stderr.puts "No skills found in #{skills_dir}"
|
|
253
|
+
exit 0
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
skill_dirs.each do |skill_name|
|
|
257
|
+
skill_md = File.join(skills_dir, skill_name, 'SKILL.md')
|
|
258
|
+
fm = AIA::SkillUtils.parse_front_matter(skill_md)
|
|
259
|
+
|
|
260
|
+
puts "## #{skill_name}"
|
|
261
|
+
puts
|
|
262
|
+
puts "| Key | Value |"
|
|
263
|
+
puts "|-----|-------|"
|
|
264
|
+
fm.each do |key, value|
|
|
265
|
+
puts "| #{key} | #{value} |"
|
|
266
|
+
end
|
|
267
|
+
puts
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
exit 0
|
|
271
|
+
end
|
|
272
|
+
|
|
233
273
|
def list_tools_terminal(local_tools, mcp_tool_groups)
|
|
234
274
|
width = (ENV['COLUMNS'] || 80).to_i - 4
|
|
235
275
|
indent = ' '
|
|
@@ -518,6 +558,7 @@ module AIA
|
|
|
518
558
|
File.write(file, content)
|
|
519
559
|
puts "Config successfully dumped to #{file}"
|
|
520
560
|
end
|
|
561
|
+
|
|
521
562
|
end
|
|
522
563
|
end
|
|
523
564
|
end
|
data/lib/aia/config.rb
CHANGED
|
@@ -48,7 +48,7 @@ module AIA
|
|
|
48
48
|
# ==========================================================================
|
|
49
49
|
|
|
50
50
|
# Nested section attributes (defined as hashes, converted to ConfigSection)
|
|
51
|
-
attr_config :service, :llm, :prompts, :output, :audio, :image, :embedding,
|
|
51
|
+
attr_config :service, :llm, :prompts, :roles, :skills, :output, :audio, :image, :embedding,
|
|
52
52
|
:tools, :flags, :registry, :paths, :logger
|
|
53
53
|
|
|
54
54
|
# Array/collection attributes
|
|
@@ -56,7 +56,7 @@ module AIA
|
|
|
56
56
|
|
|
57
57
|
# Runtime attributes (not loaded from config files)
|
|
58
58
|
attr_accessor :prompt_id, :stdin_content, :remaining_args, :dump_file,
|
|
59
|
-
:completion, :mcp_list, :list_tools,
|
|
59
|
+
:completion, :mcp_list, :list_tools, :list_skills,
|
|
60
60
|
:executable_prompt_content,
|
|
61
61
|
:tool_names, :loaded_tools,
|
|
62
62
|
:log_level_override, :log_file_override,
|
|
@@ -105,6 +105,8 @@ module AIA
|
|
|
105
105
|
service: config_section_coercion(:service),
|
|
106
106
|
llm: config_section_coercion(:llm),
|
|
107
107
|
prompts: config_section_coercion(:prompts),
|
|
108
|
+
roles: config_section_coercion(:roles),
|
|
109
|
+
skills: config_section_coercion(:skills),
|
|
108
110
|
output: config_section_coercion(:output),
|
|
109
111
|
audio: config_section_coercion(:audio),
|
|
110
112
|
image: config_section_coercion(:image),
|
|
@@ -169,6 +171,9 @@ module AIA
|
|
|
169
171
|
prompts_dir: [:prompts, :dir],
|
|
170
172
|
roles_prefix: [:prompts, :roles_prefix],
|
|
171
173
|
role: [:prompts, :role],
|
|
174
|
+
skills_dir: [:skills, :dir],
|
|
175
|
+
skills_prefix: [:prompts, :skills_prefix],
|
|
176
|
+
skills: [:prompts, :skills],
|
|
172
177
|
parameter_regex: [:prompts, :parameter_regex],
|
|
173
178
|
system_prompt: [:prompts, :system_prompt],
|
|
174
179
|
# output section
|
|
@@ -235,6 +240,8 @@ module AIA
|
|
|
235
240
|
llm: llm.to_h,
|
|
236
241
|
models: models.map(&:to_h),
|
|
237
242
|
prompts: prompts.to_h,
|
|
243
|
+
roles: roles.to_h,
|
|
244
|
+
skills: skills.to_h,
|
|
238
245
|
output: output.to_h,
|
|
239
246
|
audio: audio.to_h,
|
|
240
247
|
image: image.to_h,
|
|
@@ -296,7 +303,7 @@ module AIA
|
|
|
296
303
|
send("#{key}=", Array(value)) if respond_to?("#{key}=")
|
|
297
304
|
when :mcp_servers
|
|
298
305
|
self.mcp_servers = Array(value)
|
|
299
|
-
when :service, :llm, :prompts, :output, :audio, :image, :embedding,
|
|
306
|
+
when :service, :llm, :prompts, :roles, :skills, :output, :audio, :image, :embedding,
|
|
300
307
|
:tools, :flags, :registry, :paths, :logger
|
|
301
308
|
section = send(key)
|
|
302
309
|
if section.is_a?(MywayConfig::ConfigSection) && value.is_a?(Hash)
|
|
@@ -388,6 +395,18 @@ module AIA
|
|
|
388
395
|
if output.history_file
|
|
389
396
|
output.history_file = File.expand_path(output.history_file)
|
|
390
397
|
end
|
|
398
|
+
|
|
399
|
+
if roles.dir
|
|
400
|
+
roles.dir = File.expand_path(roles.dir)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
if skills.dir
|
|
404
|
+
skills.dir = File.expand_path(skills.dir)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
if tools.dir
|
|
408
|
+
tools.dir = File.expand_path(tools.dir)
|
|
409
|
+
end
|
|
391
410
|
end
|
|
392
411
|
|
|
393
412
|
def ensure_arrays
|
|
@@ -401,6 +420,9 @@ module AIA
|
|
|
401
420
|
|
|
402
421
|
# Ensure tools.paths is an array
|
|
403
422
|
tools.paths = [] if tools.paths.nil?
|
|
423
|
+
|
|
424
|
+
# Ensure prompts.skills is an array
|
|
425
|
+
prompts.skills = [] if prompts.respond_to?(:skills) && prompts.skills.nil?
|
|
404
426
|
end
|
|
405
427
|
|
|
406
428
|
# Process MCP JSON files and merge servers into mcp_servers
|
|
@@ -441,6 +463,10 @@ module AIA
|
|
|
441
463
|
registry.send("#{key}=", value) if registry.respond_to?("#{key}=")
|
|
442
464
|
when :paths
|
|
443
465
|
paths.send("#{key}=", value) if paths.respond_to?("#{key}=")
|
|
466
|
+
when :roles
|
|
467
|
+
roles.send("#{key}=", value) if roles.respond_to?("#{key}=")
|
|
468
|
+
when :skills
|
|
469
|
+
skills.send("#{key}=", value) if skills.respond_to?("#{key}=")
|
|
444
470
|
end
|
|
445
471
|
end
|
|
446
472
|
end
|
data/lib/aia/directive.rb
CHANGED
|
@@ -84,5 +84,34 @@ module AIA
|
|
|
84
84
|
""
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# Split args into positive and negative search terms.
|
|
91
|
+
# Tokens prefixed with -, ~, or ! are negative (exclusion) terms.
|
|
92
|
+
# All other tokens (bare or +-prefixed) are positive (inclusion) terms.
|
|
93
|
+
# All terms are downcased.
|
|
94
|
+
#
|
|
95
|
+
# @param args [Array<String>] raw argument tokens
|
|
96
|
+
# @return [Array<Array<String>>] [positive_terms, negative_terms]
|
|
97
|
+
def parse_search_terms(args)
|
|
98
|
+
positive = []
|
|
99
|
+
negative = []
|
|
100
|
+
|
|
101
|
+
Array(args).each do |arg|
|
|
102
|
+
arg.split.each do |token|
|
|
103
|
+
downcased = token.downcase
|
|
104
|
+
if downcased =~ /\A[-~!]/
|
|
105
|
+
negative << downcased[1..]
|
|
106
|
+
elsif downcased.start_with?('+')
|
|
107
|
+
positive << downcased[1..]
|
|
108
|
+
else
|
|
109
|
+
positive << downcased
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
[positive, negative]
|
|
115
|
+
end
|
|
87
116
|
end
|
|
88
117
|
end
|