schwarm-cli 0.1.8 → 0.1.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ce993c4ccb8c15a7a2743ef5d28c8f2c06e3513dad637c23ad0f7929e3dc8b5
4
- data.tar.gz: add9e02c84c8b0bb05d96c34c0b07b68210669449bb0c428cda290b4559137a4
3
+ metadata.gz: 6eb34f4bf3e7d00ae361c8f3e2e92543b70a3cf607bca49c2621db6308ff7347
4
+ data.tar.gz: '081ef605e386eef7aa68d0f41646a16c270cf5760bfeb1b5f7270f4b3b5ecf7e'
5
5
  SHA512:
6
- metadata.gz: 54bde32d06880389d77bf624a431c4f702f58fefd6d97067f0f4b6e042ab4bd6fba7e64f3399a16e19e0a4fb1473580c68f45998ae8248cb9ca8a08d3c5ea327
7
- data.tar.gz: f2a4b9dd8b1b2061c3dc7d9512b2889e260abce55c640b8f5638caded2c790c0db5f03753a895a418a9075c54a41dc69050f1af09b89750e167e187036742cf2
6
+ metadata.gz: 259bfe442c01d5d6a7906fcbf474927013374d8283f7083552b5e30137603e94ae94d2f92c3211c173807236d37b188de25e45594179c108fb49562af521a903
7
+ data.tar.gz: 118a0a5418bd88cc5f17e6f06bd80221830fdc2d8b5a4e9a83276894cd64779bdc1b9015012f5e5f1068ee1805800260f8f9dbafbde9327247bf1853cd78b0d6
@@ -3,8 +3,8 @@
3
3
  module SchwarmCli
4
4
  class Client
5
5
  class SecretFiles < Resource
6
- def list(repository_id: nil, page: nil, per_page: nil)
7
- params = { repository_id:, page:, per_page: }.compact
6
+ def list(repository_id: nil, query: nil, page: nil, per_page: nil)
7
+ params = { repository_id:, q: query, page:, per_page: }.compact
8
8
  get("/api/v2/secret_files", params).body
9
9
  end
10
10
 
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "faraday/multipart"
4
+
3
5
  module SchwarmCli
4
6
  class Client
7
+ # The skill_files endpoint expects path as a top-level form param plus a
8
+ # multipart-attached `file` (Active Storage), NOT a JSON body wrapped in
9
+ # `{skill_file: {...}}`. Build a multipart form for create/update.
5
10
  class SkillFiles < Resource
6
11
  def list(skill_id:, page: nil, per_page: nil)
7
12
  params = { page:, per_page: }.compact
@@ -12,18 +17,34 @@ module SchwarmCli
12
17
  get("/api/v2/skills/#{skill_id}/files/#{id}").body
13
18
  end
14
19
 
15
- def create(skill_id:, **attributes)
16
- post("/api/v2/skills/#{skill_id}/files", { skill_file: attributes }).body
20
+ def create(skill_id:, path:, content:)
21
+ post("/api/v2/skills/#{skill_id}/files", multipart_body(path:, content:)).body
17
22
  end
18
23
 
19
- def update(skill_id:, id:, **attributes)
20
- patch("/api/v2/skills/#{skill_id}/files/#{id}", { skill_file: attributes }).body
24
+ def update(skill_id:, id:, path: nil, content: nil)
25
+ body = {}
26
+ body[:path] = path if path
27
+ body[:file] = file_part(path:, content:) if content
28
+ patch("/api/v2/skills/#{skill_id}/files/#{id}", body).body
21
29
  end
22
30
 
23
31
  def destroy(skill_id:, id:)
24
32
  delete("/api/v2/skills/#{skill_id}/files/#{id}")
25
33
  nil
26
34
  end
35
+
36
+ private
37
+
38
+ def multipart_body(path:, content:)
39
+ { path:, file: file_part(path:, content:) }
40
+ end
41
+
42
+ # Active Storage needs a real IO with a filename. Wrap the in-memory
43
+ # content in a StringIO and hand it to Faraday::Multipart::FilePart.
44
+ def file_part(path:, content:)
45
+ io = StringIO.new(content.to_s)
46
+ Faraday::Multipart::FilePart.new(io, "application/octet-stream", File.basename(path.to_s))
47
+ end
27
48
  end
28
49
  end
29
50
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "faraday"
4
+ require "faraday/multipart"
4
5
  require "faraday/net_http_persistent"
5
6
  require "faraday/retry"
6
7
 
@@ -62,6 +63,7 @@ module SchwarmCli
62
63
 
63
64
  def conn
64
65
  @conn ||= Faraday.new(url:, headers:, **faraday_options) do |f|
66
+ f.request :multipart
65
67
  f.request :json
66
68
  f.request :authorization, "Bearer", api_key
67
69
  f.request :retry, retry_options
@@ -38,8 +38,8 @@ module SchwarmCli
38
38
  desc: "Agent ID to subscribe to this repository"
39
39
  option :additional_instructions, type: :string,
40
40
  desc: "Per-repository additional instructions appended to the shared prompt"
41
- option :enabled, type: :boolean, default: true,
42
- desc: "Whether the subscription is active (default: true)"
41
+ option :enabled, type: :boolean, default: false,
42
+ desc: "Whether the subscription is active (default: false; toggle to enable)"
43
43
  def create
44
44
  handle_errors do
45
45
  data = client.subscriptions.create(**create_attrs)
@@ -17,7 +17,7 @@ module SchwarmCli
17
17
  client.agents.list(query: options[:query], **page_params)
18
18
  end
19
19
  output_list(data, columns: [%w[ID id], %w[NAME name], %w[ENABLED enabled],
20
- %w[SCHEDULE schedule]])
20
+ %w[SCHEDULE cron_expression]])
21
21
  end
22
22
  end
23
23
 
@@ -25,25 +25,25 @@ module SchwarmCli
25
25
  def show(id)
26
26
  handle_errors do
27
27
  data = client.agents.find(id)
28
- output_record(data, fields: {
29
- "ID" => "id", "Name" => "name", "Enabled" => "enabled",
30
- "Schedule" => "schedule", "Prompt" => "prompt",
31
- "Created" => "created_at", "Updated" => "updated_at"
32
- })
28
+ output_record(data, fields: agent_fields)
33
29
  end
34
30
  end
35
31
 
36
32
  desc "create", "Create an agent"
37
33
  option :name, type: :string, required: true, desc: "Agent name"
38
34
  option :prompt, type: :string, required: true, desc: "Agent prompt"
39
- option :schedule, type: :string, desc: "Cron schedule"
35
+ option :schedule, type: :string, required: true, desc: "Cron schedule (e.g. \"0 0 * * *\")"
36
+ option :enabled, type: :boolean, default: false,
37
+ desc: "Whether the agent runs (default: false; toggle to enable)"
40
38
  def create
41
39
  handle_errors do
42
- attrs = { name: options[:name], prompt: options[:prompt] }
43
- attrs[:schedule] = options[:schedule] if options[:schedule]
40
+ attrs = {
41
+ name: options[:name], prompt: options[:prompt],
42
+ cron_expression: options[:schedule], enabled: options[:enabled]
43
+ }
44
44
 
45
45
  data = client.agents.create(**attrs)
46
- output_record(data, fields: { "ID" => "id", "Name" => "name", "Enabled" => "enabled" })
46
+ output_record(data, fields: agent_fields)
47
47
  end
48
48
  end
49
49
 
@@ -54,7 +54,7 @@ module SchwarmCli
54
54
  def update(id)
55
55
  handle_errors do
56
56
  data = client.agents.update(id, **update_attrs)
57
- output_record(data, fields: { "ID" => "id", "Name" => "name", "Enabled" => "enabled" })
57
+ output_record(data, fields: agent_fields)
58
58
  end
59
59
  end
60
60
 
@@ -81,9 +81,17 @@ module SchwarmCli
81
81
  {}.tap do |attrs|
82
82
  attrs[:name] = options[:name] if options[:name]
83
83
  attrs[:prompt] = options[:prompt] if options[:prompt]
84
- attrs[:schedule] = options[:schedule] if options[:schedule]
84
+ attrs[:cron_expression] = options[:schedule] if options[:schedule]
85
85
  end
86
86
  end
87
+
88
+ def agent_fields
89
+ {
90
+ "ID" => "id", "Name" => "name", "Enabled" => "enabled",
91
+ "Schedule" => "cron_expression", "Prompt" => "prompt",
92
+ "Created" => "created_at", "Updated" => "updated_at"
93
+ }
94
+ end
87
95
  end
88
96
  end
89
97
  end
@@ -5,12 +5,39 @@ require "thor"
5
5
  module SchwarmCli
6
6
  module Commands
7
7
  class Base < Thor
8
- class_option :json, type: :boolean, default: false, desc: "Output as JSON"
8
+ # No `default: false` on :json we need to distinguish "not provided"
9
+ # (nil) from "explicitly negated" (--no-json => false) so nested
10
+ # subcommands can opt out of an inherited --json (see options below).
11
+ class_option :json, type: :boolean, desc: "Output as JSON"
9
12
  class_option :url, type: :string, desc: "Schwarm server URL (overrides config)"
10
13
  class_option :token, type: :string, desc: "API key (overrides config)"
11
14
 
12
15
  def self.exit_on_failure? = true
13
16
 
17
+ # Thor parses class_options at each subcommand level independently, so
18
+ # `--json` at a doubly-nested invocation (`agents subscriptions list
19
+ # --json`) is consumed by the outer class and never reaches the inner.
20
+ # Merge parent_options for global flags (json, url, token) so they
21
+ # propagate transparently into nested subcommands.
22
+ #
23
+ # We can't use Hash#merge with a block because Thor returns a
24
+ # HashWithIndifferentAccess whose `merge` ignores the block argument
25
+ # (lib/thor/core_ext/hash_with_indifferent_access.rb:53). Walk the
26
+ # inherited keys explicitly so an explicit child value (including
27
+ # `false` from --no-json) wins over an inherited parent value.
28
+ INHERITED_OPTIONS = %w[json url token].freeze
29
+
30
+ no_commands do
31
+ def options
32
+ merged = super.dup
33
+ parent = parent_options || {}
34
+ INHERITED_OPTIONS.each do |key|
35
+ merged[key] = parent[key] if merged[key].nil? && !parent[key].nil?
36
+ end
37
+ merged
38
+ end
39
+ end
40
+
14
41
  DEFAULT_LIST_LIMIT = 20
15
42
 
16
43
  # Declares --limit, --page, --all options for the next method.
@@ -21,11 +21,7 @@ module SchwarmCli
21
21
  def show(id)
22
22
  handle_errors do
23
23
  data = client.recurring.find(id)
24
- output_record(data, fields: {
25
- "ID" => "id", "Name" => "name", "Repository" => "github_repository_id",
26
- "Enabled" => "enabled", "Schedule" => "cron_expression", "Prompt" => "prompt",
27
- "Created" => "created_at", "Updated" => "updated_at"
28
- })
24
+ output_record(data, fields: recurring_fields)
29
25
  end
30
26
  end
31
27
 
@@ -34,15 +30,12 @@ module SchwarmCli
34
30
  option :repo, type: :string, required: true, desc: "Repository ID, owner/repo, or GitHub URL"
35
31
  option :prompt, type: :string, required: true, desc: "Task prompt"
36
32
  option :schedule, type: :string, required: true, desc: "Cron schedule"
33
+ option :enabled, type: :boolean, default: false,
34
+ desc: "Whether the recurring task fires (default: false; toggle to enable)"
37
35
  def create
38
36
  handle_errors do
39
- attrs = {
40
- name: options[:name], github_repository_id: resolve_repo(options[:repo]),
41
- prompt: options[:prompt], cron_expression: options[:schedule]
42
- }
43
-
44
- data = client.recurring.create(**attrs)
45
- output_record(data, fields: { "ID" => "id", "Name" => "name", "Schedule" => "cron_expression" })
37
+ data = client.recurring.create(**create_attrs)
38
+ output_record(data, fields: recurring_fields)
46
39
  end
47
40
  end
48
41
 
@@ -53,7 +46,7 @@ module SchwarmCli
53
46
  def update(id)
54
47
  handle_errors do
55
48
  data = client.recurring.update(id, **update_attrs)
56
- output_record(data, fields: { "ID" => "id", "Name" => "name", "Schedule" => "cron_expression" })
49
+ output_record(data, fields: recurring_fields)
57
50
  end
58
51
  end
59
52
 
@@ -76,6 +69,20 @@ module SchwarmCli
76
69
 
77
70
  private
78
71
 
72
+ def create_attrs
73
+ {
74
+ name: options[:name], github_repository_id: resolve_repo(options[:repo]),
75
+ prompt: options[:prompt], cron_expression: options[:schedule],
76
+ enabled: options[:enabled]
77
+ }
78
+ end
79
+
80
+ def recurring_fields
81
+ { "ID" => "id", "Name" => "name", "Repository" => "github_repository_id",
82
+ "Enabled" => "enabled", "Schedule" => "cron_expression", "Prompt" => "prompt",
83
+ "Created" => "created_at", "Updated" => "updated_at" }
84
+ end
85
+
79
86
  def update_attrs
80
87
  {}.tap do |attrs|
81
88
  attrs[:name] = options[:name] if options[:name]
@@ -23,10 +23,7 @@ module SchwarmCli
23
23
  def show(id)
24
24
  handle_errors do
25
25
  data = client.repo_skills.find(id)
26
- output_record(data, fields: {
27
- "ID" => "id", "Repository" => "github_repository_id",
28
- "Skill" => "skill_id", "Created" => "created_at"
29
- })
26
+ output_record(data, fields: repo_skill_fields)
30
27
  end
31
28
  end
32
29
 
@@ -38,8 +35,7 @@ module SchwarmCli
38
35
  data = client.repo_skills.create(
39
36
  github_repository_id: resolve_repo(options[:repo]), skill_id: options[:skill]
40
37
  )
41
- output_record(data, fields: { "ID" => "id", "Repository" => "github_repository_id",
42
- "Skill" => "skill_id" })
38
+ output_record(data, fields: repo_skill_fields)
43
39
  end
44
40
  end
45
41
 
@@ -50,6 +46,13 @@ module SchwarmCli
50
46
  puts "Repository-skill association #{id} deleted."
51
47
  end
52
48
  end
49
+
50
+ private
51
+
52
+ def repo_skill_fields
53
+ { "ID" => "id", "Repository" => "github_repository_name",
54
+ "Skill" => "skill_name", "Created" => "created_at" }
55
+ end
53
56
  end
54
57
  end
55
58
  end
@@ -5,11 +5,14 @@ module SchwarmCli
5
5
  class Secrets < Base
6
6
  desc "list", "List secret files"
7
7
  option :repo, type: :string, desc: "Filter by repository ID, owner/repo, or GitHub URL"
8
+ option :query, type: :string, desc: "Search by path"
8
9
  pagination_options
9
10
  def list
10
11
  handle_errors do
11
12
  data = fetch_paged do |page_params|
12
- client.secrets.list(repository_id: resolve_repo(options[:repo]), **page_params)
13
+ client.secrets.list(
14
+ repository_id: resolve_repo(options[:repo]), query: options[:query], **page_params
15
+ )
13
16
  end
14
17
  output_list(data, columns: [%w[ID id], %w[REPO github_repository_name], %w[PATH path]])
15
18
  end
@@ -3,9 +3,8 @@
3
3
  module SchwarmCli
4
4
  module Commands
5
5
  class SkillFilesCmd < Base
6
- class_option :skill, type: :string, required: true, desc: "Skill ID"
7
-
8
6
  desc "list", "List skill files"
7
+ option :skill, type: :string, required: true, desc: "Skill ID"
9
8
  pagination_options
10
9
  def list
11
10
  handle_errors do
@@ -17,39 +16,46 @@ module SchwarmCli
17
16
  end
18
17
 
19
18
  desc "show ID", "Show skill file details"
19
+ option :skill, type: :string, required: true, desc: "Skill ID"
20
20
  def show(id)
21
21
  handle_errors do
22
22
  data = client.skill_files.find(skill_id: options[:skill], id:)
23
- output_record(data, fields: {
24
- "ID" => "id", "Path" => "path", "Content" => "content", "Created" => "created_at"
25
- })
23
+ output_record(data, fields: skill_file_fields)
26
24
  end
27
25
  end
28
26
 
29
27
  desc "create", "Create a skill file"
30
- option :path, type: :string, required: true, desc: "File path"
31
- option :content, type: :string, desc: "File content"
32
- option :file, type: :string, desc: "Read content from file"
28
+ option :skill, type: :string, required: true, desc: "Skill ID"
29
+ option :path, type: :string, required: true, desc: "File path within the skill (e.g. SKILL.md)"
30
+ option :content, type: :string, desc: "File content (mutually exclusive with --file)"
31
+ option :file, type: :string, desc: "Read content from file (mutually exclusive with --content)"
33
32
  def create
34
33
  handle_errors do
35
- content = read_content
36
- data = client.skill_files.create(skill_id: options[:skill], path: options[:path], content:)
37
- output_record(data, fields: { "ID" => "id", "Path" => "path" })
34
+ data = client.skill_files.create(
35
+ skill_id: options[:skill], path: options[:path],
36
+ content: read_content(required: true)
37
+ )
38
+ output_record(data, fields: skill_file_fields)
38
39
  end
39
40
  end
40
41
 
41
42
  desc "update ID", "Update a skill file"
43
+ option :skill, type: :string, required: true, desc: "Skill ID"
42
44
  option :path, type: :string, desc: "File path"
43
- option :content, type: :string, desc: "File content"
44
- option :file, type: :string, desc: "Read content from file"
45
+ option :content, type: :string, desc: "File content (mutually exclusive with --file)"
46
+ option :file, type: :string, desc: "Read content from file (mutually exclusive with --content)"
45
47
  def update(id)
46
48
  handle_errors do
47
- data = client.skill_files.update(skill_id: options[:skill], id:, **update_attrs)
48
- output_record(data, fields: { "ID" => "id", "Path" => "path" })
49
+ data = client.skill_files.update(
50
+ skill_id: options[:skill], id:,
51
+ path: options[:path], content: read_content(required: false)
52
+ )
53
+ output_record(data, fields: skill_file_fields)
49
54
  end
50
55
  end
51
56
 
52
57
  desc "delete ID", "Delete a skill file"
58
+ option :skill, type: :string, required: true, desc: "Skill ID"
53
59
  def delete(id)
54
60
  handle_errors do
55
61
  client.skill_files.destroy(skill_id: options[:skill], id:)
@@ -59,21 +65,23 @@ module SchwarmCli
59
65
 
60
66
  private
61
67
 
62
- def update_attrs
63
- {}.tap do |attrs|
64
- attrs[:path] = options[:path] if options[:path]
65
- attrs[:content] = read_content if options[:content] || options[:file]
66
- end
68
+ def skill_file_fields
69
+ { "ID" => "id", "Path" => "path", "Skill" => "skill_id",
70
+ "Created" => "created_at", "Updated" => "updated_at" }
67
71
  end
68
72
 
69
- def read_content
70
- if options[:file]
71
- file_path = File.expand_path(options[:file])
72
- abort "Error: File not found: #{file_path}" unless File.exist?(file_path)
73
- File.read(file_path)
74
- else
75
- options[:content]
76
- end
73
+ def read_content(required:)
74
+ abort "Error: --content and --file are mutually exclusive." if options[:content] && options[:file]
75
+ return options[:content] if options[:content]
76
+ return read_file(options[:file]) if options[:file]
77
+
78
+ abort "Error: one of --content or --file is required." if required
79
+ end
80
+
81
+ def read_file(path)
82
+ expanded = File.expand_path(path)
83
+ abort "Error: File not found: #{expanded}" unless File.exist?(expanded)
84
+ File.read(expanded)
77
85
  end
78
86
  end
79
87
  end
@@ -19,10 +19,7 @@ module SchwarmCli
19
19
  def show(id)
20
20
  handle_errors do
21
21
  data = client.templates.find(id)
22
- output_record(data, fields: {
23
- "ID" => "id", "Name" => "name", "Description" => "description",
24
- "Prompt" => "prompt_prefix", "Created" => "created_at"
25
- })
22
+ output_record(data, fields: template_fields)
26
23
  end
27
24
  end
28
25
 
@@ -36,7 +33,7 @@ module SchwarmCli
36
33
  attrs[:description] = options[:description] if options[:description]
37
34
 
38
35
  data = client.templates.create(**attrs)
39
- output_record(data, fields: { "ID" => "id", "Name" => "name" })
36
+ output_record(data, fields: template_fields)
40
37
  end
41
38
  end
42
39
 
@@ -47,7 +44,7 @@ module SchwarmCli
47
44
  def update(id)
48
45
  handle_errors do
49
46
  data = client.templates.update(id, **update_attrs)
50
- output_record(data, fields: { "ID" => "id", "Name" => "name" })
47
+ output_record(data, fields: template_fields)
51
48
  end
52
49
  end
53
50
 
@@ -61,6 +58,11 @@ module SchwarmCli
61
58
 
62
59
  private
63
60
 
61
+ def template_fields
62
+ { "ID" => "id", "Name" => "name", "Description" => "description",
63
+ "Prompt" => "prompt_prefix", "Created" => "created_at", "Updated" => "updated_at" }
64
+ end
65
+
64
66
  def update_attrs
65
67
  {}.tap do |attrs|
66
68
  attrs[:name] = options[:name] if options[:name]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SchwarmCli
4
- VERSION = "0.1.8"
4
+ VERSION = "0.1.9"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schwarm-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vincent Garrigues
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: faraday-multipart
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: faraday-net_http_persistent
28
42
  requirement: !ruby/object:Gem::Requirement