schwarm-cli 0.1.2 → 0.1.4

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: 066e7d9d88f9e9d06311933b23308735d9856580de1f2d2dfc320d90f45f6deb
4
- data.tar.gz: f46de7b54809c3283f14669c0727d308d6e5b8d24066c0e99618c7945b0b044f
3
+ metadata.gz: fda5a7c0a0a68a6e07573a5509cadb8ea7ff600f94a3feabf065317c332a9b40
4
+ data.tar.gz: bfd4c31bca63d5862c9df68514c8ab3bdeefb0a1ebbd944c316aeed4522a626d
5
5
  SHA512:
6
- metadata.gz: 28ae5f67cdd2bbca5a04d1571171252adb36fd7eaccc489f8306038887c52e55c65fea6e348fc5af1ac849f5fd6725be49741d3f68cadfd863897d370fc9ac44
7
- data.tar.gz: ace4db1afda31bcdc3b4555c7167c3259ec2723ec9a86f6750b9c6eb85ef4dcf26cc71f7e1e636b2882d510cb1e006daf277988851758467412811f445a9765c
6
+ metadata.gz: cf2aac68753184cd3ef7fcd28ff24eb75010b84237f2b661e76a00e1582ead6ad07b0789941f9d3c518bd40eadc34bea14d463bc83e7d69dfb62f68f165920a3
7
+ data.tar.gz: 9bdf3cbb26adecc1f40e90d681e9cb722bd2ae477cd7faa1ac258f3bf690cc77aa1374cb2744a8fe9bb6583b7b328b242f81e80f19ab79afa2dedad23c5d6b55
data/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # schwarm-cli
2
+
3
+ Command-line interface for [Schwarm](https://github.com/getdexter/schwarm) — a
4
+ distributed system that orchestrates Claude Code agents working on tasks in
5
+ parallel. Wraps the Schwarm Public API v2 to manage tasks, repositories,
6
+ skills, agents, and more.
7
+
8
+ ## Installation
9
+
10
+ Requires Ruby >= 4.0.
11
+
12
+ ```bash
13
+ gem install schwarm-cli
14
+ ```
15
+
16
+ This installs the `schwarm` executable.
17
+
18
+ ## Configuration
19
+
20
+ Run the interactive configuration once to point the CLI at your Schwarm
21
+ server and save an API key:
22
+
23
+ ```bash
24
+ schwarm configure
25
+ ```
26
+
27
+ You'll be prompted for:
28
+
29
+ - **Server URL** (defaults to `https://schwarm.getdexter.net`)
30
+ - **API Key** — create one from your Schwarm server's API Keys page
31
+
32
+ The credentials are saved to `~/.schwarm/config/<env>.json` and the connection
33
+ is tested before saving.
34
+
35
+ ## Usage
36
+
37
+ List top-level commands:
38
+
39
+ ```bash
40
+ schwarm help
41
+ schwarm tree # full command tree
42
+ schwarm version
43
+ ```
44
+
45
+ Common workflows:
46
+
47
+ ```bash
48
+ schwarm repos list
49
+ schwarm tasks list
50
+ schwarm tasks create --repo <REPO_ID> --name "Fix bug" --prompt "..." --status ready
51
+ schwarm tasks show <TASK_ID>
52
+ schwarm tasks handoff --repo <REPO_ID> --name "Continue WIP"
53
+ ```
54
+
55
+ For the full set of agent-oriented workflows (dispatching tasks, DAGs,
56
+ handoffs, monitoring, messaging), print the bundled skill:
57
+
58
+ ```bash
59
+ schwarm skill
60
+ ```
61
+
62
+ This is the same document AI agents read to learn how to drive the CLI, and
63
+ it's kept in sync with the gem at [`cli/schwarm-skill.md`](schwarm-skill.md).
64
+
65
+ ## Links
66
+
67
+ - [Schwarm repository](https://github.com/getdexter/schwarm)
68
+ - [Source](https://github.com/getdexter/schwarm/tree/main/cli)
@@ -4,7 +4,7 @@ module SchwarmCli
4
4
  class Client
5
5
  class UserMessages < Resource
6
6
  def create(task_id:, content:)
7
- post("/api/v2/tasks/#{task_id}/messages", { user_message: { content: } }).body
7
+ post("/api/v2/tasks/#{task_id}/messages", { message: { content: } }).body
8
8
  end
9
9
  end
10
10
  end
@@ -4,13 +4,13 @@ module SchwarmCli
4
4
  module Commands
5
5
  class Agents < Base
6
6
  desc "list", "List repository agents"
7
- option :repo, type: :string, desc: "Filter by repository ID"
7
+ option :repo, type: :string, desc: "Filter by repository ID, owner/repo, or GitHub URL"
8
8
  option :query, type: :string, desc: "Search by name"
9
9
  pagination_options
10
10
  def list
11
11
  handle_errors do
12
12
  data = fetch_paged do |page_params|
13
- client.agents.list(repository_id: options[:repo], query: options[:query], **page_params)
13
+ client.agents.list(repository_id: resolve_repo(options[:repo]), query: options[:query], **page_params)
14
14
  end
15
15
  output_list(data, columns: [%w[ID id], %w[NAME name], %w[REPO github_repository_name],
16
16
  %w[ENABLED enabled], %w[SCHEDULE schedule]])
@@ -31,15 +31,12 @@ module SchwarmCli
31
31
 
32
32
  desc "create", "Create a repository agent"
33
33
  option :name, type: :string, required: true, desc: "Agent name"
34
- option :repo, type: :string, required: true, desc: "Repository ID"
34
+ option :repo, type: :string, required: true, desc: "Repository ID, owner/repo, or GitHub URL"
35
35
  option :prompt, type: :string, required: true, desc: "Agent prompt"
36
36
  option :schedule, type: :string, desc: "Cron schedule"
37
37
  def create
38
38
  handle_errors do
39
- attrs = { name: options[:name], github_repository_id: options[:repo], prompt: options[:prompt] }
40
- attrs[:schedule] = options[:schedule] if options[:schedule]
41
-
42
- data = client.agents.create(**attrs)
39
+ data = client.agents.create(**create_attrs)
43
40
  output_record(data, fields: { "ID" => "id", "Name" => "name", "Enabled" => "enabled" })
44
41
  end
45
42
  end
@@ -83,6 +80,15 @@ module SchwarmCli
83
80
 
84
81
  private
85
82
 
83
+ def create_attrs
84
+ attrs = {
85
+ name: options[:name], github_repository_id: resolve_repo(options[:repo]),
86
+ prompt: options[:prompt]
87
+ }
88
+ attrs[:schedule] = options[:schedule] if options[:schedule]
89
+ attrs
90
+ end
91
+
86
92
  def update_attrs
87
93
  {}.tap do |attrs|
88
94
  attrs[:name] = options[:name] if options[:name]
@@ -25,6 +25,13 @@ module SchwarmCli
25
25
  @client ||= SchwarmCli::Client.new(url: options[:url], api_key: options[:token])
26
26
  end
27
27
 
28
+ # Resolves --repo input (ID, owner/repo, or GitHub URL) to a schwarm repo ID.
29
+ def resolve_repo(ref)
30
+ SchwarmCli::RepositoryResolver.new(client: client).resolve(ref)
31
+ rescue SchwarmCli::RepositoryResolver::ResolutionError => e
32
+ abort "Error: #{e.message}"
33
+ end
34
+
28
35
  def output_list(data, columns:)
29
36
  if options[:json]
30
37
  Formatters::Json.format(data)
@@ -4,13 +4,13 @@ module SchwarmCli
4
4
  module Commands
5
5
  class Recurring < Base
6
6
  desc "list", "List recurring tasks"
7
- option :repo, type: :string, desc: "Filter by repository ID"
7
+ option :repo, type: :string, desc: "Filter by repository ID, owner/repo, or GitHub URL"
8
8
  option :query, type: :string, desc: "Search by name"
9
9
  pagination_options
10
10
  def list
11
11
  handle_errors do
12
12
  data = fetch_paged do |page_params|
13
- client.recurring.list(repository_id: options[:repo], query: options[:query], **page_params)
13
+ client.recurring.list(repository_id: resolve_repo(options[:repo]), query: options[:query], **page_params)
14
14
  end
15
15
  output_list(data, columns: [%w[ID id], %w[NAME name], %w[REPO github_repository_name],
16
16
  %w[ENABLED enabled], %w[SCHEDULE schedule]])
@@ -31,13 +31,13 @@ module SchwarmCli
31
31
 
32
32
  desc "create", "Create a recurring task"
33
33
  option :name, type: :string, required: true, desc: "Task name"
34
- option :repo, type: :string, required: true, desc: "Repository ID"
34
+ option :repo, type: :string, required: true, desc: "Repository ID, owner/repo, or GitHub URL"
35
35
  option :prompt, type: :string, required: true, desc: "Task prompt"
36
36
  option :schedule, type: :string, required: true, desc: "Cron schedule"
37
37
  def create
38
38
  handle_errors do
39
39
  attrs = {
40
- name: options[:name], github_repository_id: options[:repo],
40
+ name: options[:name], github_repository_id: resolve_repo(options[:repo]),
41
41
  prompt: options[:prompt], schedule: options[:schedule]
42
42
  }
43
43
 
@@ -4,13 +4,15 @@ module SchwarmCli
4
4
  module Commands
5
5
  class RepoSkills < Base
6
6
  desc "list", "List repository-skill associations"
7
- option :repo, type: :string, desc: "Filter by repository ID"
7
+ option :repo, type: :string, desc: "Filter by repository ID, owner/repo, or GitHub URL"
8
8
  option :skill, type: :string, desc: "Filter by skill ID"
9
9
  pagination_options
10
10
  def list
11
11
  handle_errors do
12
12
  data = fetch_paged do |page_params|
13
- client.repo_skills.list(repository_id: options[:repo], skill_id: options[:skill], **page_params)
13
+ client.repo_skills.list(
14
+ repository_id: resolve_repo(options[:repo]), skill_id: options[:skill], **page_params
15
+ )
14
16
  end
15
17
  output_list(data, columns: [%w[ID id], %w[REPO github_repository_name],
16
18
  %w[SKILL skill_name]])
@@ -29,12 +31,12 @@ module SchwarmCli
29
31
  end
30
32
 
31
33
  desc "create", "Associate a skill with a repository"
32
- option :repo, type: :string, required: true, desc: "Repository ID"
34
+ option :repo, type: :string, required: true, desc: "Repository ID, owner/repo, or GitHub URL"
33
35
  option :skill, type: :string, required: true, desc: "Skill ID"
34
36
  def create
35
37
  handle_errors do
36
38
  data = client.repo_skills.create(
37
- github_repository_id: options[:repo], skill_id: options[:skill]
39
+ github_repository_id: resolve_repo(options[:repo]), skill_id: options[:skill]
38
40
  )
39
41
  output_record(data, fields: { "ID" => "id", "Repository" => "github_repository_id",
40
42
  "Skill" => "skill_id" })
@@ -4,12 +4,12 @@ module SchwarmCli
4
4
  module Commands
5
5
  class Secrets < Base
6
6
  desc "list", "List secret files"
7
- option :repo, type: :string, desc: "Filter by repository ID"
7
+ option :repo, type: :string, desc: "Filter by repository ID, owner/repo, or GitHub URL"
8
8
  pagination_options
9
9
  def list
10
10
  handle_errors do
11
11
  data = fetch_paged do |page_params|
12
- client.secrets.list(repository_id: options[:repo], **page_params)
12
+ client.secrets.list(repository_id: resolve_repo(options[:repo]), **page_params)
13
13
  end
14
14
  output_list(data, columns: [%w[ID id], %w[NAME name], %w[REPO github_repository_name],
15
15
  %w[PATH path]])
@@ -29,14 +29,17 @@ module SchwarmCli
29
29
 
30
30
  desc "create", "Create a secret file"
31
31
  option :name, type: :string, required: true, desc: "Secret name"
32
- option :repo, type: :string, required: true, desc: "Repository ID"
32
+ option :repo, type: :string, required: true, desc: "Repository ID, owner/repo, or GitHub URL"
33
33
  option :path, type: :string, required: true, desc: "File path in workspace"
34
34
  option :content, type: :string, desc: "Secret content"
35
35
  option :file, type: :string, desc: "Read content from file"
36
36
  def create
37
37
  handle_errors do
38
38
  content = read_content
39
- attrs = { name: options[:name], github_repository_id: options[:repo], path: options[:path], content: }
39
+ attrs = {
40
+ name: options[:name], github_repository_id: resolve_repo(options[:repo]),
41
+ path: options[:path], content:
42
+ }
40
43
 
41
44
  data = client.secrets.create(**attrs)
42
45
  output_record(data, fields: { "ID" => "id", "Name" => "name", "Path" => "path" })
@@ -5,7 +5,7 @@ module SchwarmCli
5
5
  class Tasks < Base
6
6
  desc "list", "List tasks (excludes archived by default)"
7
7
  option :status, type: :string, desc: "Filter by status"
8
- option :repo, type: :string, desc: "Filter by repository ID"
8
+ option :repo, type: :string, desc: "Filter by repository ID, owner/repo, or GitHub URL"
9
9
  option :query, type: :string, desc: "Search by name"
10
10
  pagination_options
11
11
  def list
@@ -33,7 +33,7 @@ module SchwarmCli
33
33
  option :name, type: :string, required: true, desc: "Task name"
34
34
  option :prompt, type: :string, desc: "Task prompt"
35
35
  option :prompt_file, type: :string, desc: "Read prompt from file"
36
- option :repo, type: :string, desc: "Repository ID"
36
+ option :repo, type: :string, desc: "Repository ID, owner/repo, or GitHub URL"
37
37
  option :status, type: :string, desc: "Initial status (draft/waiting)"
38
38
  option :template, type: :string, desc: "Template ID"
39
39
  option :depends_on, type: :array, desc: "Dependency task IDs"
@@ -48,7 +48,7 @@ module SchwarmCli
48
48
  option :name, type: :string, desc: "Task name"
49
49
  option :prompt, type: :string, desc: "Task prompt"
50
50
  option :prompt_file, type: :string, desc: "Read prompt from file"
51
- option :repo, type: :string, desc: "Repository ID"
51
+ option :repo, type: :string, desc: "Repository ID, owner/repo, or GitHub URL"
52
52
  option :depends_on, type: :array, desc: "Dependency task IDs"
53
53
  def update(id)
54
54
  handle_errors do
@@ -117,7 +117,7 @@ module SchwarmCli
117
117
  desc "handoff", "Hand off the current local branch to schwarm as a ready task"
118
118
  option :prompt, type: :string, desc: "Task prompt"
119
119
  option :prompt_file, type: :string, desc: "Read prompt from file"
120
- option :repo, type: :string, desc: "Repository ID (skips origin auto-detect)"
120
+ option :repo, type: :string, desc: "Repository ID, owner/repo, or GitHub URL (skips origin auto-detect)"
121
121
  option :name, type: :string, desc: "Task name (defaults to the branch name)"
122
122
  def handoff
123
123
  prompt = read_prompt
@@ -125,7 +125,7 @@ module SchwarmCli
125
125
 
126
126
  handle_errors do
127
127
  result = SchwarmCli::HandsOffTask.new(client: client).call(
128
- prompt: prompt, repo_override: options[:repo], name_override: options[:name]
128
+ prompt: prompt, repo_override: resolve_repo(options[:repo]), name_override: options[:name]
129
129
  )
130
130
  print_handoff_result(result)
131
131
  end
@@ -136,7 +136,7 @@ module SchwarmCli
136
136
  def list_attrs
137
137
  exclude_archived = options[:status].nil? && !options[:all]
138
138
  {
139
- status: options[:status], repository_id: options[:repo], query: options[:query],
139
+ status: options[:status], repository_id: resolve_repo(options[:repo]), query: options[:query],
140
140
  archived: exclude_archived ? false : nil
141
141
  }
142
142
  end
@@ -154,14 +154,14 @@ module SchwarmCli
154
154
  def create_attrs
155
155
  {
156
156
  name: options[:name], prompt: read_prompt,
157
- github_repository_id: options[:repo], status: options[:status],
157
+ github_repository_id: resolve_repo(options[:repo]), status: options[:status],
158
158
  task_template_id: options[:template], dependency_ids: options[:depends_on]
159
159
  }.compact
160
160
  end
161
161
 
162
162
  def update_attrs
163
163
  {
164
- name: options[:name], github_repository_id: options[:repo],
164
+ name: options[:name], github_repository_id: resolve_repo(options[:repo]),
165
165
  dependency_ids: options[:depends_on]
166
166
  }.compact.tap do |attrs|
167
167
  attrs[:prompt] = read_prompt if options[:prompt] || options[:prompt_file]
@@ -84,28 +84,17 @@ module SchwarmCli
84
84
  end
85
85
 
86
86
  def resolve_repository(origin:, override:)
87
- if override
88
- begin
89
- return @client.repositories.find(override)["data"]
90
- rescue Faraday::ResourceNotFound
91
- return nil
92
- end
93
- end
94
-
95
- slug = origin_slug(origin)
96
- response = @client.repositories.list(query: slug)
97
- Array(response["data"]).find { |r| normalize_url(r["github_url"]) == normalize_url(origin) }
98
- end
87
+ return find_by_id(override) if override
99
88
 
100
- def origin_slug(url)
101
- # Accept both https://github.com/org/repo[.git] and git@github.com:org/repo.git
102
- url.sub(%r{\Ahttps?://github\.com/}, "").sub(/\Agit@github\.com:/, "").sub(/\.git\z/, "")
89
+ RepositoryResolver.new(client: @client).resolve_record(origin)
90
+ rescue RepositoryResolver::ResolutionError
91
+ nil
103
92
  end
104
93
 
105
- def normalize_url(url)
106
- return nil if url.nil?
107
-
108
- url.sub(/\.git\z/, "").sub(/\Agit@github\.com:/, "https://github.com/")
94
+ def find_by_id(id)
95
+ @client.repositories.find(id)["data"]
96
+ rescue Faraday::ResourceNotFound
97
+ nil
109
98
  end
110
99
 
111
100
  def create_task(repo:, branch:, prompt:, name_override:)
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchwarmCli
4
+ # Turns a user-provided `--repo` reference into a schwarm repository ID.
5
+ #
6
+ # Accepted inputs:
7
+ # - a schwarm repository ID (returned as-is)
8
+ # - "owner/repo"
9
+ # - "https://github.com/owner/repo[.git]"
10
+ # - "git@github.com:owner/repo[.git]"
11
+ class RepositoryResolver
12
+ class ResolutionError < StandardError; end
13
+
14
+ SLUG_PATTERNS = [
15
+ %r{\Ahttps?://github\.com/([^/\s]+/[^/\s]+?)(?:\.git)?/?\z},
16
+ %r{\Agit@github\.com:([^/\s:]+/[^/\s:]+?)(?:\.git)?\z},
17
+ %r{\A([^/\s:]+/[^/\s:]+?)(?:\.git)?\z}
18
+ ].freeze
19
+
20
+ def initialize(client:)
21
+ @client = client
22
+ end
23
+
24
+ def resolve(ref)
25
+ return nil if blank?(ref)
26
+
27
+ record = resolve_record(ref)
28
+ record ? record["id"] : ref
29
+ end
30
+
31
+ # Like `resolve`, but returns the full repo record when the ref is looked up
32
+ # via slug/URL. Returns nil when `ref` is blank or already a bare ID (caller
33
+ # should `find` by id directly in that case).
34
+ def resolve_record(ref)
35
+ return nil if blank?(ref)
36
+
37
+ slug = extract_slug(ref.strip)
38
+ slug ? lookup_by_slug(slug, ref) : nil
39
+ end
40
+
41
+ private
42
+
43
+ def blank?(ref)
44
+ ref.nil? || ref.to_s.strip.empty?
45
+ end
46
+
47
+ def extract_slug(ref)
48
+ SLUG_PATTERNS.filter_map { |pat| ref.match(pat)&.[](1) }.first
49
+ end
50
+
51
+ def lookup_by_slug(slug, original)
52
+ response = @client.repositories.list(query: slug)
53
+ match = Array(response["data"]).find { |r| slug_from_url(r["url"]) == slug }
54
+ raise ResolutionError, "no schwarm repository matches `#{original}`" if match.nil?
55
+
56
+ match
57
+ end
58
+
59
+ def slug_from_url(url)
60
+ return nil if url.nil?
61
+
62
+ url.sub(%r{\Ahttps?://github\.com/}, "").sub(/\Agit@github\.com:/, "").sub(/\.git\z/, "")
63
+ end
64
+ end
65
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SchwarmCli
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.4"
5
5
  end
data/lib/schwarm_cli.rb CHANGED
@@ -4,6 +4,7 @@ require_relative "schwarm_cli/version"
4
4
  require_relative "schwarm_cli/config"
5
5
  require_relative "schwarm_cli/client"
6
6
  require_relative "schwarm_cli/git"
7
+ require_relative "schwarm_cli/repository_resolver"
7
8
  require_relative "schwarm_cli/hands_off_task"
8
9
  require_relative "schwarm_cli/formatters/json"
9
10
  require_relative "schwarm_cli/formatters/table"
data/schwarm-skill.md CHANGED
@@ -97,8 +97,9 @@ Tasks left as `draft` will never run.
97
97
  **When:** You've dispatched tasks and want to check on them.
98
98
 
99
99
  **Steps:**
100
- 1. List active tasks:
100
+ 1. List active tasks (archived excluded by default):
101
101
  ```bash
102
+ schwarm tasks list
102
103
  schwarm tasks list --status claimed
103
104
  schwarm tasks list --status ready
104
105
  schwarm tasks list --status error
@@ -224,3 +225,11 @@ for tasks in a repository.
224
225
  - When building a DAG, create all tasks as drafts first, then start them all.
225
226
  - For long prompts, write them to a file and use `--prompt-file`.
226
227
  - Run `schwarm <command> help` for full flag documentation on any command.
228
+ - `schwarm tasks list` excludes archived tasks by default. Use `--all` to
229
+ include them or `--status archived` to see only archived.
230
+ - `schwarm sessions list` shows only active sessions (pending/running) by
231
+ default. Use `--all` to include terminal states.
232
+ - All list commands support `--limit N` and `--page N` for pagination. The
233
+ footer shows "showing N of M · page X of Y".
234
+ - Run `schwarm skill` to print this document — useful for piping into context
235
+ or refreshing your knowledge of the available workflows.
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.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vincent Garrigues
@@ -86,6 +86,7 @@ executables:
86
86
  extensions: []
87
87
  extra_rdoc_files: []
88
88
  files:
89
+ - README.md
89
90
  - bin/schwarm
90
91
  - lib/schwarm_cli.rb
91
92
  - lib/schwarm_cli/client.rb
@@ -121,6 +122,7 @@ files:
121
122
  - lib/schwarm_cli/formatters/table.rb
122
123
  - lib/schwarm_cli/git.rb
123
124
  - lib/schwarm_cli/hands_off_task.rb
125
+ - lib/schwarm_cli/repository_resolver.rb
124
126
  - lib/schwarm_cli/version.rb
125
127
  - schwarm-skill.md
126
128
  homepage: https://github.com/getdexter/schwarm