ego 0.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 +7 -0
- data/.gitignore +22 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Guardfile +14 -0
- data/LICENSE.txt +674 -0
- data/README.md +31 -0
- data/Rakefile +6 -0
- data/bin/ego +5 -0
- data/ego.gemspec +32 -0
- data/lib/ego.rb +5 -0
- data/lib/ego/filesystem.rb +31 -0
- data/lib/ego/formatter.rb +31 -0
- data/lib/ego/handler.rb +76 -0
- data/lib/ego/handler/default.rb +15 -0
- data/lib/ego/handler/echo.rb +10 -0
- data/lib/ego/handler/greet.rb +17 -0
- data/lib/ego/handler/handlers.rb +14 -0
- data/lib/ego/handler/self.rb +9 -0
- data/lib/ego/listener.rb +24 -0
- data/lib/ego/options.rb +52 -0
- data/lib/ego/robot.rb +35 -0
- data/lib/ego/runner.rb +42 -0
- data/lib/ego/version.rb +3 -0
- data/spec/ego/options_spec.rb +65 -0
- data/spec/ego/robot_spec.rb +43 -0
- data/spec/spec_helper.rb +23 -0
- metadata +148 -0
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Ego
|
2
|
+
|
3
|
+
Ego is a personal command-line assistant that provides a flexible, natural
|
4
|
+
language interface (sort of) for interacting with other programs. Think of
|
5
|
+
it as a single-user IRC bot that can be extended with handlers for various
|
6
|
+
natural-language queries.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
$ gem install ego
|
11
|
+
|
12
|
+
## Usage
|
13
|
+
|
14
|
+
$ ego what can you do?
|
15
|
+
|
16
|
+
## License
|
17
|
+
|
18
|
+
Copyright (C) 2016 Noah Frederick
|
19
|
+
|
20
|
+
This program is free software: you can redistribute it and/or modify
|
21
|
+
it under the terms of the GNU General Public License as published by
|
22
|
+
the Free Software Foundation, either version 3 of the License, or
|
23
|
+
(at your option) any later version.
|
24
|
+
|
25
|
+
This program is distributed in the hope that it will be useful,
|
26
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
27
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
28
|
+
GNU General Public License for more details.
|
29
|
+
|
30
|
+
You should have received a copy of the GNU General Public License
|
31
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
data/Rakefile
ADDED
data/bin/ego
ADDED
data/ego.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'ego/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "ego"
|
8
|
+
spec.version = Ego::VERSION
|
9
|
+
spec.authors = ["Noah Frederick"]
|
10
|
+
spec.email = ["acc.rubygems@noahfrederick.com"]
|
11
|
+
spec.summary = %q{An extensible personal command-line assistant}
|
12
|
+
spec.description = <<-EOF
|
13
|
+
Ego is a personal command-line assistant that provides a flexible, natural
|
14
|
+
language interface (sort of) for interacting with other programs. Think of
|
15
|
+
it as a single-user IRC bot that can be extended with handlers for various
|
16
|
+
natural-language queries.
|
17
|
+
EOF
|
18
|
+
spec.homepage = "https://github.com/noahfrederick/ego"
|
19
|
+
spec.license = "GPL-3.0+"
|
20
|
+
|
21
|
+
spec.files = `git ls-files -z`.split("\x0")
|
22
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
23
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
24
|
+
spec.require_paths = ["lib"]
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
27
|
+
spec.add_development_dependency "rake"
|
28
|
+
spec.add_development_dependency "rspec", "~> 3.4"
|
29
|
+
spec.add_development_dependency "guard-rspec"
|
30
|
+
|
31
|
+
spec.add_runtime_dependency "colorize", "~> 0.7"
|
32
|
+
end
|
data/lib/ego.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Ego::Filesystem
|
2
|
+
HANDLER_GLOB = 'handler/*.rb'
|
3
|
+
|
4
|
+
BASENAME = 'ego'
|
5
|
+
|
6
|
+
XDG_CACHE_HOME = ENV['XDG_CACHE_HOME'] || File.expand_path('~/.cache')
|
7
|
+
XDG_CONFIG_HOME = ENV['XDG_CONFIG_HOME'] || File.expand_path('~/.config')
|
8
|
+
XDG_DATA_HOME = ENV['XDG_DATA_HOME'] || File.expand_path('~/.local/share')
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def cache path = ''
|
13
|
+
File.join XDG_CACHE_HOME, BASENAME, path
|
14
|
+
end
|
15
|
+
|
16
|
+
def config path = ''
|
17
|
+
File.join XDG_CONFIG_HOME, BASENAME, path
|
18
|
+
end
|
19
|
+
|
20
|
+
def data path = ''
|
21
|
+
File.join XDG_DATA_HOME, BASENAME, path
|
22
|
+
end
|
23
|
+
|
24
|
+
def builtin_handlers
|
25
|
+
Dir[File.expand_path(HANDLER_GLOB, __dir__)]
|
26
|
+
end
|
27
|
+
|
28
|
+
def user_handlers
|
29
|
+
Dir[File.expand_path(HANDLER_GLOB, config)]
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
|
3
|
+
module Ego
|
4
|
+
class Formatter
|
5
|
+
def initialize
|
6
|
+
String.disable_colorization = !STDOUT.isatty
|
7
|
+
end
|
8
|
+
|
9
|
+
def puts message
|
10
|
+
STDOUT.puts message
|
11
|
+
end
|
12
|
+
|
13
|
+
def robot_respond message
|
14
|
+
STDOUT.puts message.yellow
|
15
|
+
end
|
16
|
+
|
17
|
+
def robot_action message
|
18
|
+
STDOUT.puts "*#{message}*".magenta
|
19
|
+
end
|
20
|
+
|
21
|
+
def debug message
|
22
|
+
STDERR.puts message
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.print_handlers handlers
|
26
|
+
handlers.keys.sort.each do |key|
|
27
|
+
STDOUT.puts "- #{handlers[key]}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/ego/handler.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
require_relative 'listener'
|
2
|
+
require_relative 'formatter'
|
3
|
+
|
4
|
+
module Ego
|
5
|
+
class Handler
|
6
|
+
@@handlers = {}
|
7
|
+
@@listeners = []
|
8
|
+
|
9
|
+
attr_reader :name
|
10
|
+
attr_accessor :description
|
11
|
+
|
12
|
+
def initialize name
|
13
|
+
@name = name
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
"#{@description}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def listen pattern, priority: 5, &parser
|
21
|
+
unless block_given?
|
22
|
+
parser = Proc.new { |matches| matches }
|
23
|
+
end
|
24
|
+
@@listeners << Ego::Listener.new(pattern, priority, parser, @name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def run robot = nil, params = nil, &action
|
28
|
+
if block_given?
|
29
|
+
@action = action
|
30
|
+
end
|
31
|
+
|
32
|
+
if robot.nil?
|
33
|
+
return
|
34
|
+
elsif @action.arity == 1
|
35
|
+
@action.call(robot)
|
36
|
+
else
|
37
|
+
@action.call(robot, params)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.register name: nil
|
42
|
+
if name.nil?
|
43
|
+
handler_path = caller_locations(1, 1)[0].absolute_path
|
44
|
+
name = File.basename(handler_path, '.*')
|
45
|
+
end
|
46
|
+
|
47
|
+
handler = Ego::Handler.new(name)
|
48
|
+
yield handler
|
49
|
+
|
50
|
+
@@handlers[handler.name] = handler
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.has handler_name
|
54
|
+
@@handlers.has_key? handler_name
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.load handler_names
|
58
|
+
handler_names.each do |path|
|
59
|
+
handler = File.basename(path, '.*')
|
60
|
+
require path unless has(handler)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.dispatch robot, query
|
65
|
+
@@listeners.sort.reverse_each do |listener|
|
66
|
+
if params = listener.match(query)
|
67
|
+
return @@handlers[listener.handler].run(robot, params)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.handlers
|
73
|
+
@@handlers
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
Ego::Handler.register do |handler|
|
2
|
+
handler.description = 'let you know when I don\'t understand something'
|
3
|
+
|
4
|
+
handler.listen /(.*)/, priority: 0
|
5
|
+
|
6
|
+
handler.run do |robot, params|
|
7
|
+
robot.respond %Q{I don't understand "#{params[0]}".}
|
8
|
+
|
9
|
+
STDERR.puts <<-EOF
|
10
|
+
Perhaps add a handler to #{Ego::Filesystem.config 'handler/{}.rb'}:
|
11
|
+
|
12
|
+
...
|
13
|
+
EOF
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
Ego::Handler.register do |handler|
|
2
|
+
handler.description = 'greet you'
|
3
|
+
|
4
|
+
handler.listen /^(hello|salve|ave|hi|hey|ciao|hej)/i, priority: 3
|
5
|
+
|
6
|
+
handler.run do |robot|
|
7
|
+
robot.respond [
|
8
|
+
'Hello.',
|
9
|
+
'Salve tu.',
|
10
|
+
'Ave.',
|
11
|
+
'Hi.',
|
12
|
+
'Hey.',
|
13
|
+
'Ciao.',
|
14
|
+
'Hej.',
|
15
|
+
].sample
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative '../handler'
|
2
|
+
|
3
|
+
Ego::Handler.register do |handler|
|
4
|
+
handler.description = 'tell you what I can do'
|
5
|
+
|
6
|
+
handler.listen /^(show me|show|tell me|list)\s+(handlers|what you can do|what (?:you are|you're) able to do|what you do|what (?:queries )?you (?:can )?understand)$/i
|
7
|
+
handler.listen /^what (?:can you|are you able to|do you) (?:do|handle|understand)\??$/i
|
8
|
+
|
9
|
+
handler.run do |robot|
|
10
|
+
robot.respond 'I know how to...'
|
11
|
+
|
12
|
+
Ego::Formatter.print_handlers Ego::Handler.handlers
|
13
|
+
end
|
14
|
+
end
|
data/lib/ego/listener.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Ego
|
2
|
+
class Listener
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
attr_accessor :pattern, :priority, :parser, :handler
|
6
|
+
|
7
|
+
def initialize pattern, priority, parser, handler
|
8
|
+
@pattern = pattern
|
9
|
+
@priority = priority
|
10
|
+
@parser = parser
|
11
|
+
@handler = handler
|
12
|
+
end
|
13
|
+
|
14
|
+
def <=> other
|
15
|
+
@priority <=> other.priority
|
16
|
+
end
|
17
|
+
|
18
|
+
def match query
|
19
|
+
return false unless matches = @pattern.match(query)
|
20
|
+
|
21
|
+
@parser.call matches
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/ego/options.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Ego
|
4
|
+
class Options
|
5
|
+
|
6
|
+
attr_reader :mode,
|
7
|
+
:robot_name,
|
8
|
+
:verbose,
|
9
|
+
:query,
|
10
|
+
:usage,
|
11
|
+
:usage_error
|
12
|
+
|
13
|
+
def initialize(argv)
|
14
|
+
@mode = :interpret
|
15
|
+
@verbose = false
|
16
|
+
parse(argv)
|
17
|
+
@query = argv.join(" ")
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def parse(argv)
|
23
|
+
OptionParser.new do |opts|
|
24
|
+
@robot_name = opts.program_name.capitalize
|
25
|
+
opts.banner = "Usage: #{opts.program_name} [ options ] query..."
|
26
|
+
|
27
|
+
opts.on("-v", "--version", "Print version number") do
|
28
|
+
@mode = :version
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on("-V", "--verbose", "Include debugging info in output") do
|
32
|
+
@verbose = true
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on("-h", "--help", "Show this message") do
|
36
|
+
@mode = :help
|
37
|
+
end
|
38
|
+
|
39
|
+
begin
|
40
|
+
argv = ["-h"] if argv.empty?
|
41
|
+
opts.parse!(argv)
|
42
|
+
rescue OptionParser::ParseError => e
|
43
|
+
@usage_error = e.message
|
44
|
+
@mode = :help
|
45
|
+
ensure
|
46
|
+
@usage = opts
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
data/lib/ego/robot.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module Ego
|
2
|
+
class Robot
|
3
|
+
attr_reader :name, :options
|
4
|
+
|
5
|
+
def initialize options, formatter
|
6
|
+
@name = options.robot_name
|
7
|
+
@options = options
|
8
|
+
@formatter = formatter
|
9
|
+
end
|
10
|
+
|
11
|
+
def respond message
|
12
|
+
@formatter.robot_respond message
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def it message
|
17
|
+
@formatter.robot_action message
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
def debug message
|
22
|
+
return unless @options.verbose
|
23
|
+
@formatter.debug message
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
def stop
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
def continue
|
32
|
+
false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/ego/runner.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative 'version'
|
2
|
+
require_relative 'options'
|
3
|
+
require_relative 'filesystem'
|
4
|
+
require_relative 'robot'
|
5
|
+
require_relative 'handler'
|
6
|
+
|
7
|
+
module Ego
|
8
|
+
# The Ego::Runner class, given an array of arguments, initializes the
|
9
|
+
# required objects and executes the request.
|
10
|
+
class Runner
|
11
|
+
# Takes an array of arguments and parses them into options:
|
12
|
+
#
|
13
|
+
# runner = Ego::Runner.new(ARGV)
|
14
|
+
#
|
15
|
+
def initialize(argv)
|
16
|
+
@options = Options.new(argv)
|
17
|
+
@formatter = Ego::Formatter.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Run the appropriate action based on the arguments provided to
|
21
|
+
# #initialize.
|
22
|
+
def run
|
23
|
+
case @options.mode
|
24
|
+
when :help
|
25
|
+
if @options.usage_error
|
26
|
+
STDERR.puts @options.usage_error, "\n"
|
27
|
+
end
|
28
|
+
|
29
|
+
@formatter.puts @options.usage
|
30
|
+
|
31
|
+
exit(-1) if @options.usage_error
|
32
|
+
when :version
|
33
|
+
@formatter.puts "ego v#{Ego::VERSION}"
|
34
|
+
else
|
35
|
+
robot = Ego::Robot.new(@options, @formatter)
|
36
|
+
Ego::Handler.load Ego::Filesystem.user_handlers
|
37
|
+
Ego::Handler.load Ego::Filesystem.builtin_handlers
|
38
|
+
Ego::Handler.dispatch robot, @options.query
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|