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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +144 -0
  3. data/Rakefile +1 -5
  4. data/examples/01_echo_tool.rb +5 -24
  5. data/examples/02_openai_streaming.rb +3 -3
  6. data/examples/02b_anthropic_streaming.rb +1 -4
  7. data/examples/03b_anthropic_tools.rb +1 -4
  8. data/examples/05_mcp_server_and_chattty.rb +63 -0
  9. data/examples/06_mcp_mount_reflection.rb +45 -0
  10. data/examples/07_connect_claude_mcp.rb +78 -0
  11. data/examples/08_custom_chattty.rb +63 -0
  12. data/examples/09_mcp_with_llm_calls.rb +49 -0
  13. data/examples/10_meta_read_only.rb +56 -0
  14. data/exe/vsm +17 -0
  15. data/lib/vsm/async_channel.rb +26 -3
  16. data/lib/vsm/capsule.rb +2 -0
  17. data/lib/vsm/cli.rb +78 -0
  18. data/lib/vsm/dsl.rb +41 -11
  19. data/lib/vsm/dsl_mcp.rb +36 -0
  20. data/lib/vsm/generator/new_project.rb +154 -0
  21. data/lib/vsm/generator/templates/Gemfile.erb +9 -0
  22. data/lib/vsm/generator/templates/README_md.erb +40 -0
  23. data/lib/vsm/generator/templates/Rakefile.erb +5 -0
  24. data/lib/vsm/generator/templates/bin_console.erb +11 -0
  25. data/lib/vsm/generator/templates/bin_setup.erb +7 -0
  26. data/lib/vsm/generator/templates/exe_name.erb +34 -0
  27. data/lib/vsm/generator/templates/gemspec.erb +24 -0
  28. data/lib/vsm/generator/templates/gitignore.erb +10 -0
  29. data/lib/vsm/generator/templates/lib_name_rb.erb +9 -0
  30. data/lib/vsm/generator/templates/lib_organism_rb.erb +44 -0
  31. data/lib/vsm/generator/templates/lib_ports_chat_tty_rb.erb +12 -0
  32. data/lib/vsm/generator/templates/lib_tools_read_file_rb.erb +32 -0
  33. data/lib/vsm/generator/templates/lib_version_rb.erb +6 -0
  34. data/lib/vsm/mcp/client.rb +80 -0
  35. data/lib/vsm/mcp/jsonrpc.rb +92 -0
  36. data/lib/vsm/mcp/remote_tool_capsule.rb +35 -0
  37. data/lib/vsm/meta/snapshot_builder.rb +121 -0
  38. data/lib/vsm/meta/snapshot_cache.rb +25 -0
  39. data/lib/vsm/meta/support.rb +35 -0
  40. data/lib/vsm/meta/tools.rb +498 -0
  41. data/lib/vsm/meta.rb +59 -0
  42. data/lib/vsm/ports/chat_tty.rb +112 -0
  43. data/lib/vsm/ports/mcp/server_stdio.rb +101 -0
  44. data/lib/vsm/roles/intelligence.rb +6 -2
  45. data/lib/vsm/version.rb +1 -1
  46. data/lib/vsm.rb +10 -0
  47. data/mcp_update.md +162 -0
  48. metadata +38 -18
  49. data/.rubocop.yml +0 -8
@@ -11,11 +11,34 @@ module VSM
11
11
  end
12
12
 
13
13
  def emit(message)
14
- @queue.enqueue(message)
15
- @subs.each { |blk| Async { blk.call(message) } }
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
- def subscribe(&blk) = @subs << blk
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: {}) = (@roles[:identity] = klass.new(**args))
12
- def governance(klass: VSM::Governance, args: {}) = (@roles[:governance] = klass.new(**args))
13
- def coordination(klass: VSM::Coordination, args: {}) = (@roles[:coordination] = klass.new(**args))
14
- def intelligence(klass: VSM::Intelligence, args: {}) = (@roles[:intelligence] = klass.new(**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.new(**args)
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: {}) = (@roles[:monitoring] = klass.new(**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; @children = {}; end
40
+ def initialize(parent)
41
+ @parent = parent
42
+ @children = {}
43
+ end
36
44
  def capsule(name, klass:, args: {})
37
- @children[name.to_s] = klass.new(**args)
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
-
@@ -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,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "vsm", "<%= vsm_version_constraint %>"
4
+
5
+ group :development do
6
+ gem "rake"
7
+ gem "rspec"
8
+ end
9
+
@@ -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,5 @@
1
+ require "bundler/gem_tasks"
2
+ task :default do
3
+ sh "bundle exec rspec" if File.exist?("spec")
4
+ end
5
+
@@ -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,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ bundle install
5
+
6
+ echo "OK"
7
+
@@ -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,10 @@
1
+ .bundle/
2
+ vendor/bundle/
3
+ pkg/
4
+ .vsm.log.jsonl
5
+ *.gem
6
+ .DS_Store
7
+ /.ruby-version
8
+ /.ruby-gemset
9
+ /.env
10
+
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "vsm"
4
+ require_relative "<%= lib_name %>/organism"
5
+ require_relative "<%= lib_name %>/ports/chat_tty"
6
+
7
+ module <%= module_name %>
8
+ end
9
+
@@ -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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= module_name %>
4
+ module Ports
5
+ class ChatTTY < VSM::Ports::ChatTTY
6
+ def banner(io)
7
+ io.puts "\e[96m<%= lib_name %>\e[0m — Ctrl-C to exit"
8
+ end
9
+ end
10
+ end
11
+ end
12
+
@@ -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
+
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= module_name %>
4
+ VERSION = "0.1.0"
5
+ end
6
+