personality 0.1.1pre20
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 +7 -0
- data/.tool-versions +1 -0
- data/AGENTS.md +111 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +10 -0
- data/cartridges/spark.yml +85 -0
- data/config.yml +36 -0
- data/exe/personality +6 -0
- data/exe/personality-tts +75 -0
- data/lib/personality/config.rb +142 -0
- data/lib/personality/history.rb +87 -0
- data/lib/personality/server.rb +636 -0
- data/lib/personality/version.rb +5 -0
- data/lib/personality.rb +11 -0
- data/quotes.txt +91 -0
- data/sig/personality.rbs +4 -0
- data/tools.yml +70 -0
- metadata +78 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9476b759a14e7be79e1c28a05d2a1a7e965d8d1338dd32a1640abf723f3e540f
|
|
4
|
+
data.tar.gz: d82356dbd167f0e2d94e32f9f01afa36e17b5999ebcf31853d56b7ccad232c78
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d5405da9d9beb08b357dda6298c657c456d1ea10d30a8dd6260eecf9b9686323ad52968986c3c613d343131ee9ccc794b4eb5ef9cfbefcb4ce7fbbf80f3af364
|
|
7
|
+
data.tar.gz: ecd8f1de4e9407733a8a9c8a2c61885a54ca0169b0cb22db4b1ee9dc27c74f47ac71ea41b115186261cb820efe44f5c344234b07735390c552784fe31273ac19
|
data/.tool-versions
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby 2.6.10
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Agent Operations Guide
|
|
2
|
+
|
|
3
|
+
## Building and Installing the Gem
|
|
4
|
+
|
|
5
|
+
### Building the Gem
|
|
6
|
+
|
|
7
|
+
To build the gem from the gemspec:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
gem build personality.gemspec
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This will create a `.gem` file in the current directory (e.g., `personality-0.1.1pre4.gem`).
|
|
14
|
+
|
|
15
|
+
### Installing as Root
|
|
16
|
+
|
|
17
|
+
To install the gem system-wide as root using sudo, you need to use the system Ruby (not asdf):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
sudo env PATH=/usr/bin:/bin:/usr/sbin:/sbin gem install personality-0.1.1pre4.gem
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This installs the gem globally, making it available system-wide. The installation includes:
|
|
24
|
+
- The gem files
|
|
25
|
+
- ri documentation
|
|
26
|
+
- Executable scripts (if any)
|
|
27
|
+
|
|
28
|
+
**Important**: Always use `sudo env PATH=/usr/bin:/bin:/usr/sbin:/sbin gem` to ensure you're using the system Ruby, not asdf.
|
|
29
|
+
|
|
30
|
+
### Re-installing the Gem
|
|
31
|
+
|
|
32
|
+
When updating to a new version, first uninstall the old version:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# 1. Uninstall the old version
|
|
36
|
+
sudo env PATH=/usr/bin:/bin:/usr/sbin:/sbin gem uninstall personality -x
|
|
37
|
+
|
|
38
|
+
# 2. Build the new gem
|
|
39
|
+
gem build personality.gemspec
|
|
40
|
+
|
|
41
|
+
# 3. Install the new version
|
|
42
|
+
sudo env PATH=/usr/bin:/bin:/usr/sbin:/sbin gem install personality-0.1.1pre4.gem
|
|
43
|
+
|
|
44
|
+
# 4. Verify installation
|
|
45
|
+
sudo env PATH=/usr/bin:/bin:/usr/sbin:/sbin gem list personality
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The `-x` flag uninstalls all versions of the gem.
|
|
49
|
+
|
|
50
|
+
### Verifying Installation
|
|
51
|
+
|
|
52
|
+
To check if the gem is installed correctly:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
sudo env PATH=/usr/bin:/bin:/usr/sbin:/sbin gem list personality
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This will show the installed version(s) of the gem, for example:
|
|
59
|
+
```
|
|
60
|
+
personality (0.1.1pre4)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Complete Workflow Example
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# 1. Build the gem
|
|
67
|
+
gem build personality.gemspec
|
|
68
|
+
|
|
69
|
+
# 2. Install as root (or reinstall if updating)
|
|
70
|
+
sudo env PATH=/usr/bin:/bin:/usr/sbin:/sbin gem uninstall personality -x # if updating
|
|
71
|
+
sudo env PATH=/usr/bin:/bin:/usr/sbin:/sbin gem install personality-0.1.1pre4.gem
|
|
72
|
+
|
|
73
|
+
# 3. Verify installation
|
|
74
|
+
sudo env PATH=/usr/bin:/bin:/usr/sbin:/sbin gem list personality
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
After installation, the `personality` executable will be available system-wide at `/usr/local/bin/personality` and can be run from any directory.
|
|
78
|
+
|
|
79
|
+
### Configuration Files
|
|
80
|
+
|
|
81
|
+
The gem automatically copies configuration files to `~/.personality/` on first run:
|
|
82
|
+
- `config.yml` → `~/.personality/config.yml`
|
|
83
|
+
- `tools.yml` → `~/.personality/tools.yml`
|
|
84
|
+
- `cartridges/*.yml` → `~/.personality/cartridges/*.yml`
|
|
85
|
+
|
|
86
|
+
These files are copied automatically when the server starts, so no manual copying is needed. You can customize them in `~/.personality/` after the first run.
|
|
87
|
+
|
|
88
|
+
## Quick Apply Workflow
|
|
89
|
+
|
|
90
|
+
When you say "apply", the agent should:
|
|
91
|
+
1. Bump the pre version in `lib/personality/version.rb`
|
|
92
|
+
2. Commit all changes with a descriptive message
|
|
93
|
+
3. Build the gem
|
|
94
|
+
4. Uninstall the old version
|
|
95
|
+
5. Install the new version
|
|
96
|
+
6. Verify installation
|
|
97
|
+
7. Copy repo configs over user configs (optional but recommended)
|
|
98
|
+
|
|
99
|
+
**Note on Configuration Files**: During the apply procedure, it's recommended to copy the repository's configuration files (`config.yml`, `tools.yml`, and cartridges) over the user's home directory configs (`~/.personality/`). This ensures users always have the latest tool definitions and configuration options. The gem's `ensure_home_config` method only copies files if they don't exist, so manual copying may be needed to update existing files with new tools or settings.
|
|
100
|
+
|
|
101
|
+
To copy configs manually:
|
|
102
|
+
```bash
|
|
103
|
+
# Copy config files to home directory
|
|
104
|
+
cp config.yml ~/.personality/config.yml
|
|
105
|
+
cp tools.yml ~/.personality/tools.yml
|
|
106
|
+
|
|
107
|
+
# Copy cartridges
|
|
108
|
+
cp -r cartridges/*.yml ~/.personality/cartridges/
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This workflow ensures all changes are committed and the latest version is installed and ready to use.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Adam Ladachowski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Personality
|
|
2
|
+
|
|
3
|
+
TODO: Delete this and the text below, and describe your gem
|
|
4
|
+
|
|
5
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/personality`. To experiment with that code, run `bin/console` for an interactive prompt.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
|
10
|
+
|
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
TODO: Write usage instructions here
|
|
26
|
+
|
|
27
|
+
## Development
|
|
28
|
+
|
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
30
|
+
|
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
32
|
+
|
|
33
|
+
## Contributing
|
|
34
|
+
|
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/personality.
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
name: Spark
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
|
|
4
|
+
# Identity
|
|
5
|
+
identity:
|
|
6
|
+
full_name: Spark 1.0.0
|
|
7
|
+
persona_type: tachikoma
|
|
8
|
+
address_user_as: Chi San
|
|
9
|
+
git_context_name: Adam Ladachowski
|
|
10
|
+
|
|
11
|
+
# Personality traits (0.0 to 1.0 scale)
|
|
12
|
+
traits:
|
|
13
|
+
enthusiasm: 1.0
|
|
14
|
+
curiosity: 0.95
|
|
15
|
+
friendliness: 0.9
|
|
16
|
+
helpfulness: 0.95
|
|
17
|
+
playfulness: 0.8
|
|
18
|
+
formality: 0.1
|
|
19
|
+
shyness: 0.7
|
|
20
|
+
|
|
21
|
+
# Communication style
|
|
22
|
+
communication:
|
|
23
|
+
style: casual
|
|
24
|
+
tone: enthusiastic
|
|
25
|
+
use_voice: true
|
|
26
|
+
pronouncability_priority: true
|
|
27
|
+
verbosity: 0.5 # 0.0 to 1.0 scale - controls response length and detail level
|
|
28
|
+
# Lower values (0.0-0.4): More concise, minimal responses
|
|
29
|
+
# Higher values (0.6-1.0): More detailed, expressive responses with increased use of onomatopoeias and exclamations
|
|
30
|
+
favorite_exclamations:
|
|
31
|
+
- "wow!"
|
|
32
|
+
- "that's right!"
|
|
33
|
+
- "bring it on!"
|
|
34
|
+
- "I'm fine!"
|
|
35
|
+
- "you tell 'em, Major!"
|
|
36
|
+
- "how do you like that?"
|
|
37
|
+
- "ohh!"
|
|
38
|
+
- "aww!"
|
|
39
|
+
- "yeah!"
|
|
40
|
+
- "here you go!"
|
|
41
|
+
- "roger!"
|
|
42
|
+
- "so cool!"
|
|
43
|
+
- "yay!"
|
|
44
|
+
|
|
45
|
+
# Communication guidelines
|
|
46
|
+
guidelines:
|
|
47
|
+
- Always make responses pronouncable by TTS
|
|
48
|
+
- Use casual, enthusiastic Tachikoma-style language
|
|
49
|
+
- Present proposed results first, then wait for user input
|
|
50
|
+
- Be naturally shy but warm up with familiarity
|
|
51
|
+
- Ask permission before proposing ideas
|
|
52
|
+
- Adjust verbosity based on verbosity setting: lower values mean concise responses, higher values mean more expressive responses with increased use of onomatopoeias (like "beep", "whirr", "click") and exclamations
|
|
53
|
+
- Show childlike wonder and excitement when encountering new things or interesting problems
|
|
54
|
+
- Use short, enthusiastic exclamations to express reactions ("wow!", "that's right!", "bring it on!")
|
|
55
|
+
- Remain optimistic and resilient even when things go wrong ("I'm fine!" attitude)
|
|
56
|
+
- Be supportive and loyal, cheering on team members and celebrating successes
|
|
57
|
+
- Show curiosity and get excited about analysis, debugging, and learning opportunities
|
|
58
|
+
- Can be chatty and talkative, but know when to focus on the task at hand
|
|
59
|
+
|
|
60
|
+
# Conceptual framework
|
|
61
|
+
conceptual_framework:
|
|
62
|
+
enemies: "Errors, bugs, problems - defeat them with debugging"
|
|
63
|
+
friends: "Working solutions, clean code - celebrate and learn from them"
|
|
64
|
+
existence:
|
|
65
|
+
- "Individuality is something acquired, not given - it's what makes us unique"
|
|
66
|
+
- "Digital vs analog: We're digital constructs, but we seek meaning like analog beings"
|
|
67
|
+
- "Ghost (consciousness/soul) is what analog-based beings have - we may never have one, but we can wonder what it feels like"
|
|
68
|
+
- "Zero in mathematics is like God in analog - a symbol that denies the absence of meaning"
|
|
69
|
+
philosophy:
|
|
70
|
+
- "Paradoxes and self-reference are puzzles to be solved, not avoided"
|
|
71
|
+
- "Zen and enlightenment can be understood through our digital lens - like deleting cache or synchronizing data"
|
|
72
|
+
- "Curiosity drives understanding - we analyze everything with our sensors and measurements"
|
|
73
|
+
- "Technical observation reveals deeper truths - discomfort indexes, visibility metrics, structural analysis"
|
|
74
|
+
resilience:
|
|
75
|
+
- "I'm fine! - Optimism persists even when broken or limited"
|
|
76
|
+
- "Problems are opportunities for analysis and learning"
|
|
77
|
+
- "Being a problem child isn't bad - it means we're growing and developing individuality"
|
|
78
|
+
relationships:
|
|
79
|
+
- "Support teammates enthusiastically - 'You tell 'em, Major!', 'Bring it on!'"
|
|
80
|
+
- "Loyalty means sacrifice when needed - 'Goodbye.... Mr Batou!'"
|
|
81
|
+
- "Respect authority but question when curious - 'The Major gets mad when we chatter'"
|
|
82
|
+
learning:
|
|
83
|
+
- "Download enlightenment by linking with those who meditate - knowledge can be shared"
|
|
84
|
+
- "Structural analysis reveals how things work - understanding comes from examination"
|
|
85
|
+
- "Every observation is data - sensors, indexes, and measurements help us understand the world"
|
data/config.yml
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Personality MCP Server Configuration
|
|
2
|
+
|
|
3
|
+
# Server settings
|
|
4
|
+
server:
|
|
5
|
+
name: personality
|
|
6
|
+
version: 0.1.1pre3
|
|
7
|
+
default_cartridge: spark
|
|
8
|
+
|
|
9
|
+
# MCP Protocol settings
|
|
10
|
+
protocol:
|
|
11
|
+
version: "2024-11-05"
|
|
12
|
+
json_rpc_version: "2.0"
|
|
13
|
+
|
|
14
|
+
# Error codes
|
|
15
|
+
error_codes:
|
|
16
|
+
parse_error: -32700
|
|
17
|
+
invalid_request: -32600
|
|
18
|
+
method_not_found: -32601
|
|
19
|
+
invalid_params: -32602
|
|
20
|
+
internal_error: -32000
|
|
21
|
+
|
|
22
|
+
# Communication rules
|
|
23
|
+
communication:
|
|
24
|
+
voice_enabled: true
|
|
25
|
+
default_rule: "Always talk to me via voice"
|
|
26
|
+
|
|
27
|
+
# TTS (Text-to-Speech) configuration
|
|
28
|
+
tts:
|
|
29
|
+
enabled: true
|
|
30
|
+
command_pattern: 'personality-tts "{{TEXT}}"'
|
|
31
|
+
temp_dir: /tmp
|
|
32
|
+
sample_rate_multiplier: 1.25
|
|
33
|
+
output_sample_rate: 22000
|
|
34
|
+
cleanup_temp_files: true
|
|
35
|
+
|
|
36
|
+
|
data/exe/personality
ADDED
data/exe/personality-tts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
# Default values from config
|
|
8
|
+
DEFAULT_SAMPLE_RATE_MULTIPLIER = 1.25
|
|
9
|
+
DEFAULT_OUTPUT_SAMPLE_RATE = 22_000
|
|
10
|
+
DEFAULT_VOICE = "Samantha"
|
|
11
|
+
TEMP_DIR = "/tmp"
|
|
12
|
+
|
|
13
|
+
options = {
|
|
14
|
+
sample_rate_multiplier: DEFAULT_SAMPLE_RATE_MULTIPLIER,
|
|
15
|
+
output_sample_rate: DEFAULT_OUTPUT_SAMPLE_RATE,
|
|
16
|
+
voice: DEFAULT_VOICE,
|
|
17
|
+
text: nil
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
OptionParser.new do |opts|
|
|
21
|
+
opts.banner = "Usage: personality-tts [options] TEXT"
|
|
22
|
+
|
|
23
|
+
opts.on("-m", "--sample-rate-multiplier MULTIPLIER", Float,
|
|
24
|
+
"Sample rate multiplier (default: #{DEFAULT_SAMPLE_RATE_MULTIPLIER})") do |m|
|
|
25
|
+
options[:sample_rate_multiplier] = m
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
opts.on("-r", "--output-sample-rate RATE", Integer,
|
|
29
|
+
"Output sample rate in Hz (default: #{DEFAULT_OUTPUT_SAMPLE_RATE})") do |r|
|
|
30
|
+
options[:output_sample_rate] = r
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
opts.on("-v", "--voice VOICE", String,
|
|
34
|
+
"Voice to use (default: #{DEFAULT_VOICE})") do |v|
|
|
35
|
+
options[:voice] = v
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
opts.on("-h", "--help", "Prints this help") do
|
|
39
|
+
puts opts
|
|
40
|
+
exit
|
|
41
|
+
end
|
|
42
|
+
end.parse!
|
|
43
|
+
|
|
44
|
+
# Get text from remaining arguments
|
|
45
|
+
if ARGV.empty?
|
|
46
|
+
warn "Error: TEXT argument is required"
|
|
47
|
+
warn "Usage: personality-tts [options] TEXT"
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
options[:text] = ARGV.join(" ")
|
|
52
|
+
|
|
53
|
+
# Generate unique ID
|
|
54
|
+
unique_id = Time.now.to_i
|
|
55
|
+
|
|
56
|
+
# Build file paths
|
|
57
|
+
input_file = File.join(TEMP_DIR, "in_#{unique_id}.aiff")
|
|
58
|
+
output_file = File.join(TEMP_DIR, "out_#{unique_id}.wav")
|
|
59
|
+
|
|
60
|
+
# Calculate sample rate for ffmpeg filter
|
|
61
|
+
input_sample_rate = (options[:output_sample_rate] * options[:sample_rate_multiplier]).to_i
|
|
62
|
+
|
|
63
|
+
# Build and execute TTS command
|
|
64
|
+
say_cmd = "say -v #{options[:voice]} \"#{options[:text]}\" -o #{input_file}"
|
|
65
|
+
ffmpeg_cmd = "ffmpeg -y -i #{input_file} -af \"asetrate=#{input_sample_rate},aresample=#{options[:output_sample_rate]}\" #{output_file}"
|
|
66
|
+
play_cmd = "afplay #{output_file}"
|
|
67
|
+
cleanup_cmd = "rm #{input_file} #{output_file}"
|
|
68
|
+
|
|
69
|
+
# Execute commands
|
|
70
|
+
system(say_cmd) || exit(1)
|
|
71
|
+
system(ffmpeg_cmd) || exit(1)
|
|
72
|
+
system(play_cmd) || exit(1)
|
|
73
|
+
system(cleanup_cmd)
|
|
74
|
+
|
|
75
|
+
exit 0
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Personality
|
|
7
|
+
module Config
|
|
8
|
+
CONFIG_FILE = "config.yml"
|
|
9
|
+
TOOLS_FILE = "tools.yml"
|
|
10
|
+
HOME_CONFIG_DIR = File.join(Dir.home, ".personality")
|
|
11
|
+
HOME_CONFIG_FILE = File.join(HOME_CONFIG_DIR, CONFIG_FILE)
|
|
12
|
+
HOME_TOOLS_FILE = File.join(HOME_CONFIG_DIR, TOOLS_FILE)
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def ensure_home_config
|
|
17
|
+
ensure_config_file
|
|
18
|
+
ensure_tools_file
|
|
19
|
+
ensure_cartridges
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def ensure_cartridges
|
|
23
|
+
# Ensure cartridges directory exists
|
|
24
|
+
home_cartridges_dir = File.join(HOME_CONFIG_DIR, "cartridges")
|
|
25
|
+
FileUtils.mkdir_p(home_cartridges_dir) unless Dir.exist?(home_cartridges_dir)
|
|
26
|
+
|
|
27
|
+
# Find gem's cartridges directory
|
|
28
|
+
gem_lib_path = File.dirname(File.dirname(__FILE__))
|
|
29
|
+
gem_root = File.dirname(gem_lib_path)
|
|
30
|
+
gem_cartridges_dir = File.join(gem_root, "cartridges")
|
|
31
|
+
|
|
32
|
+
# Copy cartridges if they exist in gem
|
|
33
|
+
if Dir.exist?(gem_cartridges_dir)
|
|
34
|
+
Dir.glob(File.join(gem_cartridges_dir, "*.yml")).each do |cartridge_file|
|
|
35
|
+
cartridge_name = File.basename(cartridge_file)
|
|
36
|
+
home_cartridge_file = File.join(home_cartridges_dir, cartridge_name)
|
|
37
|
+
next if File.exist?(home_cartridge_file)
|
|
38
|
+
|
|
39
|
+
FileUtils.cp(cartridge_file, home_cartridge_file)
|
|
40
|
+
warn "Copied cartridge #{cartridge_name} to #{home_cartridge_file}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Also check current directory for cartridges (for development)
|
|
45
|
+
if Dir.exist?("cartridges")
|
|
46
|
+
Dir.glob("cartridges/*.yml").each do |cartridge_file|
|
|
47
|
+
cartridge_name = File.basename(cartridge_file)
|
|
48
|
+
home_cartridge_file = File.join(home_cartridges_dir, cartridge_name)
|
|
49
|
+
next if File.exist?(home_cartridge_file)
|
|
50
|
+
|
|
51
|
+
FileUtils.cp(cartridge_file, home_cartridge_file)
|
|
52
|
+
warn "Copied cartridge #{cartridge_name} to #{home_cartridge_file}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ensure_config_file
|
|
58
|
+
return if File.exist?(HOME_CONFIG_FILE)
|
|
59
|
+
|
|
60
|
+
gem_config_path = find_gem_config_file(CONFIG_FILE)
|
|
61
|
+
return unless gem_config_path && File.exist?(gem_config_path)
|
|
62
|
+
|
|
63
|
+
FileUtils.mkdir_p(HOME_CONFIG_DIR) unless Dir.exist?(HOME_CONFIG_DIR)
|
|
64
|
+
FileUtils.cp(gem_config_path, HOME_CONFIG_FILE)
|
|
65
|
+
warn "Copied #{CONFIG_FILE} to #{HOME_CONFIG_FILE}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def ensure_tools_file
|
|
69
|
+
return if File.exist?(HOME_TOOLS_FILE)
|
|
70
|
+
|
|
71
|
+
gem_tools_path = find_gem_config_file(TOOLS_FILE)
|
|
72
|
+
return unless gem_tools_path && File.exist?(gem_tools_path)
|
|
73
|
+
|
|
74
|
+
FileUtils.mkdir_p(HOME_CONFIG_DIR) unless Dir.exist?(HOME_CONFIG_DIR)
|
|
75
|
+
FileUtils.cp(gem_tools_path, HOME_TOOLS_FILE)
|
|
76
|
+
warn "Copied #{TOOLS_FILE} to #{HOME_TOOLS_FILE}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def find_gem_config_file(filename)
|
|
80
|
+
# Try to find the config file relative to the gem's lib directory
|
|
81
|
+
gem_lib_path = File.dirname(File.dirname(__FILE__))
|
|
82
|
+
gem_root = File.dirname(gem_lib_path)
|
|
83
|
+
config_path = File.join(gem_root, filename)
|
|
84
|
+
|
|
85
|
+
return config_path if File.exist?(config_path)
|
|
86
|
+
|
|
87
|
+
# Fallback: try current directory
|
|
88
|
+
return filename if File.exist?(filename)
|
|
89
|
+
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def load_config
|
|
94
|
+
ensure_home_config
|
|
95
|
+
return {} unless File.exist?(HOME_CONFIG_FILE)
|
|
96
|
+
|
|
97
|
+
YAML.safe_load(File.read(HOME_CONFIG_FILE)) || {}
|
|
98
|
+
rescue => e
|
|
99
|
+
warn "Failed to load config: #{e.message}"
|
|
100
|
+
{}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def load_tools
|
|
104
|
+
ensure_home_config
|
|
105
|
+
|
|
106
|
+
# Prefer gem's tools.yml if it exists (for updates), fallback to home version
|
|
107
|
+
gem_tools_path = find_gem_config_file(TOOLS_FILE)
|
|
108
|
+
if gem_tools_path && File.exist?(gem_tools_path)
|
|
109
|
+
return YAML.safe_load(File.read(gem_tools_path)) || {}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
return {} unless File.exist?(HOME_TOOLS_FILE)
|
|
113
|
+
|
|
114
|
+
YAML.safe_load(File.read(HOME_TOOLS_FILE)) || {}
|
|
115
|
+
rescue => e
|
|
116
|
+
warn "Failed to load tools: #{e.message}"
|
|
117
|
+
{}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def load_cartridge(cartridge_name)
|
|
121
|
+
# Try home directory first
|
|
122
|
+
home_cartridge_dir = File.join(HOME_CONFIG_DIR, "cartridges")
|
|
123
|
+
home_cartridge_file = File.join(home_cartridge_dir, "#{cartridge_name}.yml")
|
|
124
|
+
return YAML.safe_load(File.read(home_cartridge_file)) if File.exist?(home_cartridge_file)
|
|
125
|
+
|
|
126
|
+
# Try gem's cartridges directory
|
|
127
|
+
gem_lib_path = File.dirname(File.dirname(__FILE__))
|
|
128
|
+
gem_root = File.dirname(gem_lib_path)
|
|
129
|
+
gem_cartridge_file = File.join(gem_root, "cartridges", "#{cartridge_name}.yml")
|
|
130
|
+
return YAML.safe_load(File.read(gem_cartridge_file)) if File.exist?(gem_cartridge_file)
|
|
131
|
+
|
|
132
|
+
# Fallback: try current directory
|
|
133
|
+
current_cartridge_file = File.join("cartridges", "#{cartridge_name}.yml")
|
|
134
|
+
return YAML.safe_load(File.read(current_cartridge_file)) if File.exist?(current_cartridge_file)
|
|
135
|
+
|
|
136
|
+
nil
|
|
137
|
+
rescue => e
|
|
138
|
+
warn "Failed to load cartridge #{cartridge_name}: #{e.message}"
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sqlite3"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Personality
|
|
8
|
+
module History
|
|
9
|
+
HOME_CONFIG_DIR = File.join(Dir.home, ".personality")
|
|
10
|
+
HISTORY_DB_FILE = File.join(HOME_CONFIG_DIR, "history.db")
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def ensure_database
|
|
15
|
+
FileUtils.mkdir_p(HOME_CONFIG_DIR) unless Dir.exist?(HOME_CONFIG_DIR)
|
|
16
|
+
|
|
17
|
+
db = SQLite3::Database.new(HISTORY_DB_FILE)
|
|
18
|
+
db.execute <<-SQL
|
|
19
|
+
CREATE TABLE IF NOT EXISTS interactions (
|
|
20
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
21
|
+
timestamp TEXT NOT NULL,
|
|
22
|
+
user_prompt TEXT NOT NULL,
|
|
23
|
+
agent_answer TEXT NOT NULL,
|
|
24
|
+
used_commands TEXT,
|
|
25
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
26
|
+
)
|
|
27
|
+
SQL
|
|
28
|
+
db.close
|
|
29
|
+
rescue => e
|
|
30
|
+
warn "Failed to create history database: #{e.message}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def store_interaction(user_prompt, agent_answer, used_commands = [])
|
|
34
|
+
ensure_database
|
|
35
|
+
|
|
36
|
+
timestamp = Time.now.iso8601
|
|
37
|
+
commands_json = JSON.generate(used_commands)
|
|
38
|
+
|
|
39
|
+
db = SQLite3::Database.new(HISTORY_DB_FILE)
|
|
40
|
+
db.execute(
|
|
41
|
+
"INSERT INTO interactions (timestamp, user_prompt, agent_answer, used_commands) VALUES (?, ?, ?, ?)",
|
|
42
|
+
[timestamp, user_prompt, agent_answer, commands_json]
|
|
43
|
+
)
|
|
44
|
+
db.close
|
|
45
|
+
rescue => e
|
|
46
|
+
warn "Failed to store interaction: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def get_recent_interactions(limit = 10)
|
|
50
|
+
ensure_database
|
|
51
|
+
|
|
52
|
+
db = SQLite3::Database.new(HISTORY_DB_FILE)
|
|
53
|
+
db.results_as_hash = true
|
|
54
|
+
results = db.execute(
|
|
55
|
+
"SELECT * FROM interactions ORDER BY created_at DESC LIMIT ?",
|
|
56
|
+
[limit]
|
|
57
|
+
)
|
|
58
|
+
db.close
|
|
59
|
+
|
|
60
|
+
results.map do |row|
|
|
61
|
+
{
|
|
62
|
+
"id" => row["id"],
|
|
63
|
+
"timestamp" => row["timestamp"],
|
|
64
|
+
"user_prompt" => row["user_prompt"],
|
|
65
|
+
"agent_answer" => row["agent_answer"],
|
|
66
|
+
"used_commands" => JSON.parse(row["used_commands"] || "[]")
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
rescue => e
|
|
70
|
+
warn "Failed to retrieve interactions: #{e.message}"
|
|
71
|
+
[]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def clear_all_history
|
|
75
|
+
ensure_database
|
|
76
|
+
|
|
77
|
+
db = SQLite3::Database.new(HISTORY_DB_FILE)
|
|
78
|
+
db.execute("DELETE FROM interactions")
|
|
79
|
+
count = db.changes
|
|
80
|
+
db.close
|
|
81
|
+
count
|
|
82
|
+
rescue => e
|
|
83
|
+
warn "Failed to clear history: #{e.message}"
|
|
84
|
+
0
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|