vsm 0.1.0 → 0.2.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/README.md +144 -0
- data/Rakefile +1 -5
- data/examples/01_echo_tool.rb +5 -24
- data/examples/02_openai_streaming.rb +3 -3
- data/examples/02b_anthropic_streaming.rb +1 -4
- data/examples/03b_anthropic_tools.rb +1 -4
- data/examples/05_mcp_server_and_chattty.rb +63 -0
- data/examples/06_mcp_mount_reflection.rb +45 -0
- data/examples/07_connect_claude_mcp.rb +78 -0
- data/examples/08_custom_chattty.rb +63 -0
- data/examples/09_mcp_with_llm_calls.rb +49 -0
- data/examples/10_meta_read_only.rb +56 -0
- data/exe/vsm +17 -0
- data/lib/vsm/async_channel.rb +26 -3
- data/lib/vsm/capsule.rb +2 -0
- data/lib/vsm/cli.rb +78 -0
- data/lib/vsm/dsl.rb +41 -11
- data/lib/vsm/dsl_mcp.rb +36 -0
- data/lib/vsm/generator/new_project.rb +154 -0
- data/lib/vsm/generator/templates/Gemfile.erb +9 -0
- data/lib/vsm/generator/templates/README_md.erb +40 -0
- data/lib/vsm/generator/templates/Rakefile.erb +5 -0
- data/lib/vsm/generator/templates/bin_console.erb +11 -0
- data/lib/vsm/generator/templates/bin_setup.erb +7 -0
- data/lib/vsm/generator/templates/exe_name.erb +34 -0
- data/lib/vsm/generator/templates/gemspec.erb +24 -0
- data/lib/vsm/generator/templates/gitignore.erb +10 -0
- data/lib/vsm/generator/templates/lib_name_rb.erb +9 -0
- data/lib/vsm/generator/templates/lib_organism_rb.erb +44 -0
- data/lib/vsm/generator/templates/lib_ports_chat_tty_rb.erb +12 -0
- data/lib/vsm/generator/templates/lib_tools_read_file_rb.erb +32 -0
- data/lib/vsm/generator/templates/lib_version_rb.erb +6 -0
- data/lib/vsm/mcp/client.rb +80 -0
- data/lib/vsm/mcp/jsonrpc.rb +92 -0
- data/lib/vsm/mcp/remote_tool_capsule.rb +35 -0
- data/lib/vsm/meta/snapshot_builder.rb +121 -0
- data/lib/vsm/meta/snapshot_cache.rb +25 -0
- data/lib/vsm/meta/support.rb +35 -0
- data/lib/vsm/meta/tools.rb +498 -0
- data/lib/vsm/meta.rb +59 -0
- data/lib/vsm/ports/chat_tty.rb +112 -0
- data/lib/vsm/ports/mcp/server_stdio.rb +101 -0
- data/lib/vsm/roles/intelligence.rb +6 -2
- data/lib/vsm/version.rb +1 -1
- data/lib/vsm.rb +10 -0
- data/mcp_update.md +162 -0
- metadata +38 -18
- data/.rubocop.yml +0 -8
data/lib/vsm/async_channel.rb
CHANGED
@@ -11,11 +11,34 @@ module VSM
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def emit(message)
|
14
|
-
|
15
|
-
|
14
|
+
begin
|
15
|
+
@queue.enqueue(message)
|
16
|
+
rescue StandardError
|
17
|
+
# If no async scheduler is available in this thread, best-effort enqueue later.
|
18
|
+
end
|
19
|
+
@subs.each do |blk|
|
20
|
+
begin
|
21
|
+
Async { blk.call(message) }
|
22
|
+
rescue StandardError
|
23
|
+
# Fallback when no Async task is active in this thread
|
24
|
+
begin
|
25
|
+
blk.call(message)
|
26
|
+
rescue StandardError
|
27
|
+
# ignore subscriber errors
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
16
31
|
end
|
17
32
|
|
18
33
|
def pop = @queue.dequeue
|
19
|
-
|
34
|
+
|
35
|
+
def subscribe(&blk)
|
36
|
+
@subs << blk
|
37
|
+
blk
|
38
|
+
end
|
39
|
+
|
40
|
+
def unsubscribe(subscriber)
|
41
|
+
@subs.delete(subscriber)
|
42
|
+
end
|
20
43
|
end
|
21
44
|
end
|
data/lib/vsm/capsule.rb
CHANGED
@@ -11,6 +11,8 @@ module VSM
|
|
11
11
|
ctx = { operations_children: children.transform_keys(&:to_s) }
|
12
12
|
@bus = AsyncChannel.new(context: ctx)
|
13
13
|
@homeostat = Homeostat.new
|
14
|
+
# Inject bus into children that accept it, to enable richer observability
|
15
|
+
@children.each_value { |c| c.bus = @bus if c.respond_to?(:bus=) }
|
14
16
|
wire_observers!
|
15
17
|
end
|
16
18
|
|
data/lib/vsm/cli.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require_relative 'generator/new_project'
|
5
|
+
|
6
|
+
module VSM
|
7
|
+
class CLI
|
8
|
+
def self.start(argv = ARGV)
|
9
|
+
new.run(argv)
|
10
|
+
end
|
11
|
+
|
12
|
+
def run(argv)
|
13
|
+
cmd = argv.shift
|
14
|
+
case cmd
|
15
|
+
when 'new'
|
16
|
+
run_new(argv)
|
17
|
+
when nil, '-h', '--help', 'help'
|
18
|
+
puts help_text
|
19
|
+
else
|
20
|
+
warn "Unknown command: #{cmd}\n"
|
21
|
+
puts help_text
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def run_new(argv)
|
29
|
+
opts = {
|
30
|
+
path: nil,
|
31
|
+
git: false,
|
32
|
+
bundle: false,
|
33
|
+
provider: 'openai',
|
34
|
+
model: nil,
|
35
|
+
force: false
|
36
|
+
}
|
37
|
+
parser = OptionParser.new do |o|
|
38
|
+
o.banner = "Usage: vsm new <name> [options]"
|
39
|
+
o.on('--path PATH', 'Target directory (default: ./<name>)') { |v| opts[:path] = v }
|
40
|
+
o.on('--git', 'Run git init and initial commit') { opts[:git] = true }
|
41
|
+
o.on('--bundle', 'Run bundle install after generation') { opts[:bundle] = true }
|
42
|
+
o.on('--with-llm PROVIDER', %w[openai anthropic gemini], 'LLM provider: openai (default), anthropic, or gemini') { |v| opts[:provider] = v }
|
43
|
+
o.on('--model NAME', 'Default model name') { |v| opts[:model] = v }
|
44
|
+
o.on('--force', 'Overwrite existing directory') { opts[:force] = true }
|
45
|
+
o.on('-h', '--help', 'Show help') { puts o; exit 0 }
|
46
|
+
end
|
47
|
+
|
48
|
+
name = nil
|
49
|
+
begin
|
50
|
+
parser.order!(argv)
|
51
|
+
name = argv.shift
|
52
|
+
rescue OptionParser::ParseError => e
|
53
|
+
warn e.message
|
54
|
+
puts parser
|
55
|
+
exit 1
|
56
|
+
end
|
57
|
+
|
58
|
+
unless name && !name.strip.empty?
|
59
|
+
warn 'Please provide a project name, e.g., vsm new my_app'
|
60
|
+
puts parser
|
61
|
+
exit 1
|
62
|
+
end
|
63
|
+
|
64
|
+
VSM::Generator::NewProject.run(name: name, **opts)
|
65
|
+
end
|
66
|
+
|
67
|
+
def help_text
|
68
|
+
<<~TXT
|
69
|
+
VSM CLI
|
70
|
+
|
71
|
+
Commands:
|
72
|
+
vsm new <name> [options] Create a new VSM app skeleton
|
73
|
+
|
74
|
+
Run `vsm new --help` for options.
|
75
|
+
TXT
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/vsm/dsl.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "meta"
|
2
4
|
module VSM
|
3
5
|
module DSL
|
4
6
|
class Builder
|
@@ -6,40 +8,69 @@ module VSM
|
|
6
8
|
@name = name
|
7
9
|
@roles = {}
|
8
10
|
@children = {}
|
11
|
+
@after_build = []
|
9
12
|
end
|
10
13
|
|
11
|
-
def identity(klass: VSM::Identity, args: {}) = (
|
12
|
-
def governance(klass: VSM::Governance, args: {}) = (
|
13
|
-
def coordination(klass: VSM::Coordination, args: {}) = (
|
14
|
-
def intelligence(klass: VSM::Intelligence, args: {}) = (
|
14
|
+
def identity(klass: VSM::Identity, args: {}) = assign_role(:identity, klass, args)
|
15
|
+
def governance(klass: VSM::Governance, args: {}) = assign_role(:governance, klass, args)
|
16
|
+
def coordination(klass: VSM::Coordination, args: {}) = assign_role(:coordination, klass, args)
|
17
|
+
def intelligence(klass: VSM::Intelligence, args: {}) = assign_role(:intelligence, klass, args)
|
15
18
|
def operations(klass: VSM::Operations, args: {}, &blk)
|
16
|
-
@roles[:operations] = klass
|
19
|
+
@roles[:operations] = instantiate(klass, args)
|
17
20
|
if blk
|
18
|
-
builder = ChildrenBuilder.new
|
21
|
+
builder = ChildrenBuilder.new(self)
|
19
22
|
builder.instance_eval(&blk)
|
20
23
|
@children.merge!(builder.result)
|
21
24
|
end
|
22
25
|
end
|
23
26
|
|
24
|
-
def monitoring(klass: VSM::Monitoring, args: {}) = (
|
27
|
+
def monitoring(klass: VSM::Monitoring, args: {}) = assign_role(:monitoring, klass, args)
|
25
28
|
|
26
29
|
def build
|
27
30
|
# Inject governance into tool capsules if they accept it
|
28
31
|
@children.each_value do |child|
|
29
32
|
child.governance = @roles[:governance] if child.respond_to?(:governance=)
|
30
33
|
end
|
31
|
-
VSM::Capsule.new(name: @name, roles: @roles, children: @children)
|
34
|
+
capsule = VSM::Capsule.new(name: @name, roles: @roles, children: @children)
|
35
|
+
@after_build.each { _1.call(capsule) }
|
36
|
+
capsule
|
32
37
|
end
|
33
38
|
|
34
39
|
class ChildrenBuilder
|
35
|
-
def initialize
|
40
|
+
def initialize(parent)
|
41
|
+
@parent = parent
|
42
|
+
@children = {}
|
43
|
+
end
|
36
44
|
def capsule(name, klass:, args: {})
|
37
|
-
|
45
|
+
instance = klass.new(**args)
|
46
|
+
VSM::Meta::Support.record_constructor_args(instance, args)
|
47
|
+
@children[name.to_s] = instance
|
48
|
+
end
|
49
|
+
def meta_tools(prefix: "", only: nil, except: nil)
|
50
|
+
@parent.__send__(:after_build) do |capsule|
|
51
|
+
VSM::Meta.attach!(capsule, prefix: prefix, only: only, except: except)
|
52
|
+
end
|
53
|
+
result
|
38
54
|
end
|
39
55
|
def result = @children
|
40
56
|
def method_missing(*) = result
|
41
57
|
def respond_to_missing?(*) = true
|
42
58
|
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def after_build(&block)
|
63
|
+
@after_build << block if block
|
64
|
+
end
|
65
|
+
|
66
|
+
def assign_role(key, klass, args)
|
67
|
+
@roles[key] = instantiate(klass, args)
|
68
|
+
end
|
69
|
+
|
70
|
+
def instantiate(klass, args)
|
71
|
+
instance = klass.new(**args)
|
72
|
+
VSM::Meta::Support.record_constructor_args(instance, args)
|
73
|
+
end
|
43
74
|
end
|
44
75
|
|
45
76
|
def self.define(name, &blk)
|
@@ -47,4 +78,3 @@ module VSM
|
|
47
78
|
end
|
48
79
|
end
|
49
80
|
end
|
50
|
-
|
data/lib/vsm/dsl_mcp.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative "dsl"
|
3
|
+
require_relative "mcp/client"
|
4
|
+
require_relative "mcp/remote_tool_capsule"
|
5
|
+
|
6
|
+
module VSM
|
7
|
+
module DSL
|
8
|
+
class Builder
|
9
|
+
class ChildrenBuilder
|
10
|
+
# Reflect tools from a remote MCP server and add them as tool capsules.
|
11
|
+
# Options:
|
12
|
+
# include: Array<String> whitelist of tool names
|
13
|
+
# exclude: Array<String> blacklist of tool names
|
14
|
+
# prefix: String prefix for local names to avoid collisions
|
15
|
+
# env: Hash environment passed to the server process
|
16
|
+
# cwd: Working directory for spawning the process
|
17
|
+
#
|
18
|
+
# Example:
|
19
|
+
# mcp_server :smith, cmd: "smith-server --stdio", include: %w[search read], prefix: "smith_"
|
20
|
+
def mcp_server(name, cmd:, env: {}, include: nil, exclude: nil, prefix: nil, cwd: nil)
|
21
|
+
client = VSM::MCP::Client.new(cmd: cmd, env: env, cwd: cwd, name: name.to_s).start
|
22
|
+
tools = client.list_tools
|
23
|
+
tools.each do |t|
|
24
|
+
tool_name = t[:name]
|
25
|
+
next if include && !Array(include).include?(tool_name)
|
26
|
+
next if exclude && Array(exclude).include?(tool_name)
|
27
|
+
local_name = [prefix, tool_name].compact.join
|
28
|
+
capsule = VSM::MCP::RemoteToolCapsule.new(client: client, remote_name: tool_name, descriptor: t)
|
29
|
+
@children[local_name] = capsule
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'erb'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'pathname'
|
6
|
+
require_relative '../version'
|
7
|
+
|
8
|
+
module VSM
|
9
|
+
module Generator
|
10
|
+
class NewProject
|
11
|
+
TemplateRoot = File.expand_path('templates', __dir__)
|
12
|
+
|
13
|
+
def self.run(name:, path: nil, git: false, bundle: false, provider: 'openai', model: nil, force: false)
|
14
|
+
new(name: name, path: path, git: git, bundle: bundle, provider: provider, model: model, force: force).run
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(name:, path:, git:, bundle:, provider:, model:, force:)
|
18
|
+
@input_name = name
|
19
|
+
@target_dir = File.expand_path(path || name)
|
20
|
+
@git = git
|
21
|
+
@bundle = bundle
|
22
|
+
@provider = provider
|
23
|
+
@model = model
|
24
|
+
@force = force
|
25
|
+
end
|
26
|
+
|
27
|
+
def run
|
28
|
+
prepare_target_dir!
|
29
|
+
|
30
|
+
# Create directory tree
|
31
|
+
mkdirs(
|
32
|
+
'exe',
|
33
|
+
'bin',
|
34
|
+
"lib/#{lib_name}",
|
35
|
+
"lib/#{lib_name}/ports",
|
36
|
+
"lib/#{lib_name}/tools"
|
37
|
+
)
|
38
|
+
|
39
|
+
# Render files
|
40
|
+
write('README.md', render('README_md.erb'))
|
41
|
+
write('.gitignore', render('gitignore.erb'))
|
42
|
+
write('Gemfile', render('Gemfile.erb'))
|
43
|
+
write('Rakefile', render('Rakefile.erb'))
|
44
|
+
write("#{lib_name}.gemspec", render('gemspec.erb'))
|
45
|
+
|
46
|
+
write("exe/#{exe_name}", render('exe_name.erb'), mode: 0o755)
|
47
|
+
write('bin/console', render('bin_console.erb'), mode: 0o755)
|
48
|
+
write('bin/setup', render('bin_setup.erb'), mode: 0o755)
|
49
|
+
|
50
|
+
write("lib/#{lib_name}.rb", render('lib_name_rb.erb'))
|
51
|
+
write("lib/#{lib_name}/version.rb", render('lib_version_rb.erb'))
|
52
|
+
write("lib/#{lib_name}/organism.rb", render('lib_organism_rb.erb'))
|
53
|
+
write("lib/#{lib_name}/ports/chat_tty.rb", render('lib_ports_chat_tty_rb.erb'))
|
54
|
+
write("lib/#{lib_name}/tools/read_file.rb", render('lib_tools_read_file_rb.erb'))
|
55
|
+
|
56
|
+
post_steps
|
57
|
+
|
58
|
+
puts <<~DONE
|
59
|
+
|
60
|
+
Created #{module_name} in #{@target_dir}
|
61
|
+
|
62
|
+
Next steps:
|
63
|
+
cd #{relative_target}
|
64
|
+
bundle install
|
65
|
+
bundle exec exe/#{exe_name}
|
66
|
+
|
67
|
+
Add tools in lib/#{lib_name}/tools and customize banner in lib/#{lib_name}/ports/chat_tty.rb.
|
68
|
+
DONE
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def mkdirs(*dirs)
|
74
|
+
dirs.each { |d| FileUtils.mkdir_p(File.join(@target_dir, d)) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def write(rel, content, mode: nil)
|
78
|
+
full = File.join(@target_dir, rel)
|
79
|
+
FileUtils.mkdir_p(File.dirname(full))
|
80
|
+
File.write(full, content)
|
81
|
+
File.chmod(mode, full) if mode
|
82
|
+
end
|
83
|
+
|
84
|
+
def render(template_name)
|
85
|
+
template_path = File.join(TemplateRoot, template_name)
|
86
|
+
erb = ERB.new(File.read(template_path), trim_mode: '-')
|
87
|
+
erb.result(binding)
|
88
|
+
end
|
89
|
+
|
90
|
+
def post_steps
|
91
|
+
Dir.chdir(@target_dir) do
|
92
|
+
if @git
|
93
|
+
system('git', 'init')
|
94
|
+
system('git', 'add', '-A')
|
95
|
+
system('git', 'commit', '-m', 'init')
|
96
|
+
end
|
97
|
+
if @bundle
|
98
|
+
system('bundle', 'install')
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def prepare_target_dir!
|
104
|
+
if Dir.exist?(@target_dir)
|
105
|
+
if !@force && !(Dir.children(@target_dir) - %w[. ..]).empty?
|
106
|
+
raise "Target directory already exists and is not empty: #{@target_dir} (use --force to overwrite)"
|
107
|
+
end
|
108
|
+
else
|
109
|
+
FileUtils.mkdir_p(@target_dir)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# --- Template helpers (available via binding) ---
|
114
|
+
|
115
|
+
def module_name
|
116
|
+
@module_name ||= @input_name.split(/[-_]/).map { |p| p.gsub(/[^a-zA-Z0-9]/, '').capitalize }.join
|
117
|
+
end
|
118
|
+
|
119
|
+
def lib_name
|
120
|
+
@lib_name ||= @input_name.downcase.gsub('-', '_')
|
121
|
+
end
|
122
|
+
|
123
|
+
def exe_name
|
124
|
+
@exe_name ||= @input_name.downcase.gsub('_', '-')
|
125
|
+
end
|
126
|
+
|
127
|
+
def env_prefix
|
128
|
+
@env_prefix ||= @input_name.gsub('-', '_').upcase
|
129
|
+
end
|
130
|
+
|
131
|
+
def vsm_version_constraint
|
132
|
+
parts = Vsm::VERSION.split('.')
|
133
|
+
"~> #{parts[0]}.#{parts[1]}"
|
134
|
+
end
|
135
|
+
|
136
|
+
def provider
|
137
|
+
(@provider || 'openai').downcase
|
138
|
+
end
|
139
|
+
|
140
|
+
def default_model
|
141
|
+
return @model if @model && !@model.empty?
|
142
|
+
case provider
|
143
|
+
when 'anthropic' then 'claude-3-5-sonnet-latest'
|
144
|
+
when 'gemini' then 'gemini-2.0-flash'
|
145
|
+
else 'gpt-4o-mini'
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def relative_target
|
150
|
+
Pathname.new(@target_dir).relative_path_from(Pathname.new(Dir.pwd)).to_s rescue @target_dir
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# <%= module_name %>
|
2
|
+
|
3
|
+
A minimal VSM app scaffold. Starts a capsule with a ChatTTY interface, an LLM-backed intelligence (OpenAI by default), and a `read_file` tool.
|
4
|
+
|
5
|
+
## Quickstart
|
6
|
+
|
7
|
+
```bash
|
8
|
+
bundle install
|
9
|
+
OPENAI_API_KEY=... bundle exec exe/<%= exe_name %>
|
10
|
+
```
|
11
|
+
|
12
|
+
Ask the assistant questions, or request reading a file, e.g.:
|
13
|
+
|
14
|
+
```
|
15
|
+
read README.md
|
16
|
+
```
|
17
|
+
|
18
|
+
You can customize the banner and prompt in `lib/<%= lib_name %>/ports/chat_tty.rb` and add tools under `lib/<%= lib_name %>/tools`.
|
19
|
+
|
20
|
+
## LLM Configuration
|
21
|
+
|
22
|
+
This scaffold includes LLM wiring. Configure provider via env vars (or choose at generation time):
|
23
|
+
|
24
|
+
- `<%= env_prefix %>_PROVIDER` — `openai` (default), `anthropic`, or `gemini`
|
25
|
+
- `<%= env_prefix %>_MODEL` — defaults to `<%= default_model %>` if not set
|
26
|
+
- API key env var depends on provider:
|
27
|
+
- `OPENAI_API_KEY`
|
28
|
+
- `ANTHROPIC_API_KEY`
|
29
|
+
- `GEMINI_API_KEY`
|
30
|
+
|
31
|
+
Run:
|
32
|
+
|
33
|
+
```bash
|
34
|
+
<%= env_prefix %>_PROVIDER=<%= provider %> <%= env_prefix %>_MODEL=<%= default_model %> \
|
35
|
+
OPENAI_API_KEY=... bundle exec exe/<%= exe_name %>
|
36
|
+
```
|
37
|
+
|
38
|
+
## Lens (optional)
|
39
|
+
|
40
|
+
Set `VSM_LENS=1` to launch the Lens UI and print its URL. You can change `VSM_LENS_PORT` and provide `VSM_LENS_TOKEN`.
|
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup" if File.exist?(File.expand_path("../Gemfile", __dir__))
|
5
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
6
|
+
require "<%= lib_name %>"
|
7
|
+
|
8
|
+
puts "Starting console with <%= module_name %> loaded"
|
9
|
+
require 'irb'
|
10
|
+
IRB.start
|
11
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Keep CLI independent of any project's Bundler context.
|
5
|
+
ENV.delete('BUNDLE_GEMFILE')
|
6
|
+
ENV.delete('BUNDLE_BIN_PATH')
|
7
|
+
if (rubyopt = ENV['RUBYOPT'])
|
8
|
+
ENV['RUBYOPT'] = rubyopt.split.reject { |x| x.include?('bundler/setup') }.join(' ')
|
9
|
+
end
|
10
|
+
ENV.delete('RUBYGEMS_GEMDEPS')
|
11
|
+
|
12
|
+
$stdout.sync = true
|
13
|
+
$stderr.sync = true
|
14
|
+
|
15
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
16
|
+
require "vsm"
|
17
|
+
require "<%= lib_name %>"
|
18
|
+
|
19
|
+
capsule = <%= module_name %>::Organism.build
|
20
|
+
|
21
|
+
hub = nil
|
22
|
+
if ENV["VSM_LENS"] == "1"
|
23
|
+
hub = VSM::Lens.attach!(
|
24
|
+
capsule,
|
25
|
+
host: "127.0.0.1",
|
26
|
+
port: (ENV["VSM_LENS_PORT"] || 9292).to_i,
|
27
|
+
token: ENV["VSM_LENS_TOKEN"]
|
28
|
+
)
|
29
|
+
puts "Lens: http://127.0.0.1:#{ENV['VSM_LENS_PORT'] || 9292}"
|
30
|
+
end
|
31
|
+
|
32
|
+
port = <%= module_name %>::Ports::ChatTTY.new(capsule: capsule)
|
33
|
+
VSM::Runtime.start(capsule, ports: [port])
|
34
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/<%= lib_name %>/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "<%= lib_name %>"
|
7
|
+
spec.version = <%= module_name %>::VERSION
|
8
|
+
spec.authors = ["Your Name"]
|
9
|
+
spec.email = ["you@example.com"]
|
10
|
+
|
11
|
+
spec.summary = "VSM app scaffold"
|
12
|
+
spec.description = "A minimal VSM-based agent app with ChatTTY and sample tools."
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.required_ruby_version = ">= 3.2"
|
16
|
+
|
17
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
18
|
+
Dir["{bin,exe,lib}/**/*", "README.md", "LICENSE.txt", "Rakefile", "Gemfile"].select { |f| File.file?(f) }
|
19
|
+
end
|
20
|
+
spec.bindir = "exe"
|
21
|
+
spec.executables = ["<%= exe_name %>"]
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
require_relative "tools/read_file"
|
6
|
+
|
7
|
+
module <%= module_name %>
|
8
|
+
module Organism
|
9
|
+
def self.build
|
10
|
+
# Provider selection via env (default injected at generation time)
|
11
|
+
provider = (ENV["<%= env_prefix %>_PROVIDER"] || "<%= provider %>").downcase
|
12
|
+
driver =
|
13
|
+
case provider
|
14
|
+
when "anthropic"
|
15
|
+
VSM::Drivers::Anthropic::AsyncDriver.new(
|
16
|
+
api_key: ENV.fetch("ANTHROPIC_API_KEY"),
|
17
|
+
model: ENV["<%= env_prefix %>_MODEL"] || "<%= default_model %>"
|
18
|
+
)
|
19
|
+
when "gemini"
|
20
|
+
VSM::Drivers::Gemini::AsyncDriver.new(
|
21
|
+
api_key: ENV.fetch("GEMINI_API_KEY"),
|
22
|
+
model: ENV["<%= env_prefix %>_MODEL"] || "<%= default_model %>"
|
23
|
+
)
|
24
|
+
else
|
25
|
+
VSM::Drivers::OpenAI::AsyncDriver.new(
|
26
|
+
api_key: ENV.fetch("OPENAI_API_KEY"),
|
27
|
+
model: ENV["<%= env_prefix %>_MODEL"] || "<%= default_model %>"
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
VSM::DSL.define(:<%= lib_name %>) do
|
32
|
+
identity klass: VSM::Identity, args: { identity: "<%= lib_name %>", invariants: [] }
|
33
|
+
governance klass: VSM::Governance
|
34
|
+
coordination klass: VSM::Coordination
|
35
|
+
intelligence klass: VSM::Intelligence, args: { driver: driver, system_prompt: "You are a helpful assistant. Use tools when helpful." }
|
36
|
+
monitoring klass: VSM::Monitoring
|
37
|
+
|
38
|
+
operations do
|
39
|
+
capsule :read_file, klass: <%= module_name %>::Tools::ReadFile
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module <%= module_name %>
|
4
|
+
module Tools
|
5
|
+
class ReadFile < VSM::ToolCapsule
|
6
|
+
tool_name "read_file"
|
7
|
+
tool_description "Read the contents of a UTF-8 text file at a relative path within the current workspace."
|
8
|
+
tool_schema({
|
9
|
+
type: "object",
|
10
|
+
properties: {
|
11
|
+
path: { type: "string", description: "Relative path to a text file (UTF-8)." }
|
12
|
+
},
|
13
|
+
required: ["path"]
|
14
|
+
})
|
15
|
+
|
16
|
+
def run(args)
|
17
|
+
rel = args["path"].to_s
|
18
|
+
raise "path required" if rel.strip.empty?
|
19
|
+
root = Dir.pwd
|
20
|
+
full = File.expand_path(File.join(root, rel))
|
21
|
+
# Prevent escaping outside workspace root
|
22
|
+
unless full.start_with?(root + File::SEPARATOR) || full == root
|
23
|
+
raise "outside workspace"
|
24
|
+
end
|
25
|
+
File.read(full, mode: "r:UTF-8")
|
26
|
+
rescue Errno::ENOENT
|
27
|
+
raise "file not found: #{rel}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|