aia 1.0.0 → 1.1.1
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 +66 -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/examples/common.sh +7 -0
- data/examples/prompts_dir/project_summary +2 -2
- data/lib/aia/chat_loop.rb +61 -0
- data/lib/aia/chat_processor_service.rb +18 -1
- data/lib/aia/config/cli_parser.rb +49 -11
- data/lib/aia/config/defaults.yml +17 -2
- data/lib/aia/config/validator.rb +64 -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/examples/common.sh
CHANGED
|
@@ -18,3 +18,10 @@ while IFS= read -r var; do
|
|
|
18
18
|
done < <(env | grep '^AIA_' | cut -d= -f1)
|
|
19
19
|
|
|
20
20
|
CONFIG="aia_config.yml"
|
|
21
|
+
|
|
22
|
+
# When running from the develop branch, prefer the local bin/aia over any
|
|
23
|
+
# installed gem so that uncommitted working-tree changes are exercised.
|
|
24
|
+
LOCAL_AIA="${SCRIPT_DIR}/../bin/aia"
|
|
25
|
+
if [[ -x "${LOCAL_AIA}" ]]; then
|
|
26
|
+
export PATH="${SCRIPT_DIR}/../bin:${PATH}"
|
|
27
|
+
fi
|
|
@@ -4,9 +4,9 @@ temperature: 0.3
|
|
|
4
4
|
---
|
|
5
5
|
Summarize this project based on the following information:
|
|
6
6
|
|
|
7
|
-
Gemspec name: <%= `grep 'spec.name'
|
|
7
|
+
Gemspec name: <%= `grep 'spec.name' ../*.gemspec 2>/dev/null`.strip %>
|
|
8
8
|
Ruby version: <%= RUBY_VERSION %>
|
|
9
9
|
Current directory: $(pwd)
|
|
10
|
-
Files in lib/: $(ls
|
|
10
|
+
Files in lib/: $(ls ../lib/aia/*.rb 2>/dev/null | wc -l | tr -d ' ')
|
|
11
11
|
|
|
12
12
|
Provide a three-sentence overview.
|
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,63 @@ 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
|
+
|
|
52
|
+
# fetch_role applies the role file's metadata (including any model: key) to
|
|
53
|
+
# AIA.config. Preserve the user's chosen model so the chat session keeps
|
|
54
|
+
# using the model from the CLI, not one embedded in the role file.
|
|
55
|
+
saved_models = AIA.config.models
|
|
56
|
+
role_parsed = prompt_handler.fetch_role(role)
|
|
57
|
+
AIA.config.models = saved_models
|
|
58
|
+
|
|
59
|
+
return if role_parsed.nil?
|
|
60
|
+
|
|
61
|
+
role_content = role_parsed.to_s
|
|
62
|
+
return if role_content.nil? || role_content.strip.empty?
|
|
63
|
+
|
|
64
|
+
return unless AIA.client.respond_to?(:chats)
|
|
65
|
+
|
|
66
|
+
system_msg = RubyLLM::Message.new(role: :system, content: role_content)
|
|
67
|
+
|
|
68
|
+
AIA.client.chats.each_value do |chat|
|
|
69
|
+
next if chat.messages.any? { |m| m.role == :system }
|
|
70
|
+
chat.add_message(system_msg)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def process_skill_context
|
|
75
|
+
skills = AIA.config.prompts.skills
|
|
76
|
+
return if skills.nil? || skills.empty?
|
|
77
|
+
|
|
78
|
+
skills_dir = AIA.config.skills.dir
|
|
79
|
+
bodies = Array(skills).filter_map do |skill_name|
|
|
80
|
+
skill_name = skill_name.to_s.strip
|
|
81
|
+
next if skill_name.empty?
|
|
82
|
+
|
|
83
|
+
skill_path = find_skill_dir(skill_name, skills_dir)
|
|
84
|
+
next unless skill_path
|
|
85
|
+
|
|
86
|
+
if File.file?(skill_path)
|
|
87
|
+
skill_body(File.read(skill_path))
|
|
88
|
+
else
|
|
89
|
+
md = File.join(skill_path, 'SKILL.md')
|
|
90
|
+
skill_body(File.read(md)) if File.exist?(md)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
return if bodies.empty?
|
|
95
|
+
|
|
96
|
+
skill_content = bodies.join("\n\n")
|
|
97
|
+
response_data = @chat_processor.process_prompt(skill_content)
|
|
98
|
+
content = response_data.is_a?(Hash) ? response_data[:content] : response_data
|
|
99
|
+
@chat_processor.output_response(content)
|
|
100
|
+
@ui_presenter.display_separator
|
|
101
|
+
end
|
|
102
|
+
|
|
42
103
|
def process_initial_context(skip_context_files)
|
|
43
104
|
return if skip_context_files || !AIA.config.context_files || AIA.config.context_files.empty?
|
|
44
105
|
|
|
@@ -98,7 +98,24 @@ 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") or a
|
|
103
|
+
# provider-prefixed name (e.g. "ollama/qwen3").
|
|
104
|
+
#
|
|
105
|
+
# The adapter strips provider prefixes when creating the RubyLLM chat
|
|
106
|
+
# (e.g. "ollama/qwen3" → model: "qwen3"), so client_model has no prefix.
|
|
107
|
+
# Strip the prefix from model_name before comparing so that
|
|
108
|
+
# "qwen3".include?("qwen3") → true (was: "qwen3".include?("ollama/qwen3") → false).
|
|
109
|
+
comparable_name = model_name.sub(%r{\A[^/]+/}, '')
|
|
110
|
+
unless client_model.downcase.include?(comparable_name.downcase)
|
|
111
|
+
# Never replace the adapter when conversation history exists — doing so
|
|
112
|
+
# destroys all prior context. Role/prompt files can change AIA.config.models
|
|
113
|
+
# via front matter metadata, but that must not evict an active chat session.
|
|
114
|
+
if AIA.client.respond_to?(:chats) &&
|
|
115
|
+
AIA.client.chats.values.any? { |chat| chat.messages.any? }
|
|
116
|
+
warn "Warning: Model config changed to '#{model_name}' but keeping '#{client_model}' to preserve conversation history."
|
|
117
|
+
return
|
|
118
|
+
end
|
|
102
119
|
AIA.client = AIA.client.class.new
|
|
103
120
|
end
|
|
104
121
|
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
|
#
|
|
@@ -23,6 +24,10 @@ module AIA
|
|
|
23
24
|
stdin_content = process_stdin_content
|
|
24
25
|
config.stdin_content = stdin_content if stdin_content && !stdin_content.strip.empty?
|
|
25
26
|
|
|
27
|
+
# Require tool files early so AIA::Directive subclasses defined in them
|
|
28
|
+
# are registered into PM.directives before PromptHandler.new is called.
|
|
29
|
+
require_tool_files(config)
|
|
30
|
+
|
|
26
31
|
# Process arguments and validate
|
|
27
32
|
process_prompt_id_from_args(config, remaining_args)
|
|
28
33
|
validate_and_set_context_files(config, remaining_args)
|
|
@@ -31,6 +36,7 @@ module AIA
|
|
|
31
36
|
handle_dump_config(config)
|
|
32
37
|
handle_mcp_list(config)
|
|
33
38
|
handle_list_tools(config)
|
|
39
|
+
handle_list_skills(config)
|
|
34
40
|
handle_completion_script(config)
|
|
35
41
|
validate_required_prompt_id(config)
|
|
36
42
|
process_role_configuration(config)
|
|
@@ -128,14 +134,18 @@ module AIA
|
|
|
128
134
|
return if role.nil? || role.empty?
|
|
129
135
|
|
|
130
136
|
roles_prefix = config.prompts.roles_prefix
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
end
|
|
137
|
+
|
|
138
|
+
unless AIA::SkillUtils.path_based_id?(role) || roles_prefix.nil? || roles_prefix.empty? || role.start_with?(roles_prefix)
|
|
139
|
+
config.prompts.role = "#{roles_prefix}/#{role}"
|
|
140
|
+
role = config.prompts.role
|
|
136
141
|
end
|
|
137
142
|
|
|
138
|
-
config.prompts.roles_dir ||= File.join(config.prompts.dir, roles_prefix)
|
|
143
|
+
config.prompts.roles_dir ||= File.join(config.prompts.dir, roles_prefix.to_s)
|
|
144
|
+
|
|
145
|
+
# In chat-only mode (no prompt_id), leave the role configured so ChatLoop
|
|
146
|
+
# can inject it as initial context. Promoting it to prompt_id would cause
|
|
147
|
+
# PM to receive the role path as a literal string rather than file content.
|
|
148
|
+
return if config.flags&.chat == true
|
|
139
149
|
|
|
140
150
|
if config.prompt_id.nil? || config.prompt_id.empty?
|
|
141
151
|
unless role.nil? || role.empty?
|
|
@@ -230,6 +240,40 @@ module AIA
|
|
|
230
240
|
exit 0
|
|
231
241
|
end
|
|
232
242
|
|
|
243
|
+
def handle_list_skills(config)
|
|
244
|
+
return unless config.list_skills
|
|
245
|
+
|
|
246
|
+
skills_dir = AIA.config.skills.dir
|
|
247
|
+
|
|
248
|
+
unless Dir.exist?(skills_dir)
|
|
249
|
+
$stderr.puts "No skills directory found at #{skills_dir}"
|
|
250
|
+
exit 0
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
skill_dirs = Dir.glob("*/SKILL.md", base: skills_dir).map { |f| File.dirname(f) }.sort
|
|
254
|
+
|
|
255
|
+
if skill_dirs.empty?
|
|
256
|
+
$stderr.puts "No skills found in #{skills_dir}"
|
|
257
|
+
exit 0
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
skill_dirs.each do |skill_name|
|
|
261
|
+
skill_md = File.join(skills_dir, skill_name, 'SKILL.md')
|
|
262
|
+
fm = AIA::SkillUtils.parse_front_matter(skill_md)
|
|
263
|
+
|
|
264
|
+
puts "## #{skill_name}"
|
|
265
|
+
puts
|
|
266
|
+
puts "| Key | Value |"
|
|
267
|
+
puts "|-----|-------|"
|
|
268
|
+
fm.each do |key, value|
|
|
269
|
+
puts "| #{key} | #{value} |"
|
|
270
|
+
end
|
|
271
|
+
puts
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
exit 0
|
|
275
|
+
end
|
|
276
|
+
|
|
233
277
|
def list_tools_terminal(local_tools, mcp_tool_groups)
|
|
234
278
|
width = (ENV['COLUMNS'] || 80).to_i - 4
|
|
235
279
|
indent = ' '
|
|
@@ -321,6 +365,19 @@ module AIA
|
|
|
321
365
|
!Array(config.mcp_use).empty? || !Array(config.mcp_skip).empty?
|
|
322
366
|
end
|
|
323
367
|
|
|
368
|
+
def require_tool_files(config)
|
|
369
|
+
Array(config.tools&.paths).each do |path|
|
|
370
|
+
expanded = File.expand_path(path)
|
|
371
|
+
if File.exist?(expanded)
|
|
372
|
+
require expanded
|
|
373
|
+
else
|
|
374
|
+
warn "Warning: Tool file not found: #{path}"
|
|
375
|
+
end
|
|
376
|
+
rescue LoadError, StandardError => e
|
|
377
|
+
warn "Warning: Failed to load tool '#{path}': #{e.message}"
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
324
381
|
def load_local_tools(config)
|
|
325
382
|
# Load required libraries (with gem activation and lazy-load triggering)
|
|
326
383
|
Array(config.require_libs).each do |lib|
|
|
@@ -518,6 +575,7 @@ module AIA
|
|
|
518
575
|
File.write(file, content)
|
|
519
576
|
puts "Config successfully dumped to #{file}"
|
|
520
577
|
end
|
|
578
|
+
|
|
521
579
|
end
|
|
522
580
|
end
|
|
523
581
|
end
|