schwarm-cli 0.1.3 → 0.1.5

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: 5ad711fe3997e1d3b3c18cee54f6621ecc3d895f01f7c09916e5bb1f217ea6c4
4
- data.tar.gz: 3c0b5c575b1ee49423a547eeadc5a7310109e5ff294f718ee0e3cbd016f25dda
3
+ metadata.gz: f86723cf9675bb0de813b11efa16be17bd2b8bfa74bd9af1203130cdf7fda0f0
4
+ data.tar.gz: 479a9e83426d1fb675b79dd5780b14264d7a6c8454ee30404c104743aed30fdd
5
5
  SHA512:
6
- metadata.gz: a1650f63cca311d44de2f6c6b351a4bb1f81958ec5ce3093ac0a6745f5f1104ae632bcb38386bac177dd03e1274052bb47dd7967f2a0a689de1b9e28dd8227eb
7
- data.tar.gz: b5696799424c27eb8a7808ff225a461e1573e28ffb4cb2261ecd541687344293e5f115fa743605273b612db9639c61ba1eb1ee70113deaca8f9e5dd26b70dc5d
6
+ metadata.gz: 9e473284f4c066af2f5ed3a77cc365448574acd73a55e1f72e5d2eb94d1b11db5a95fc4fdaf23c498ecb721ef0f2f596ff753c1c869726446a66d6ae622cf167
7
+ data.tar.gz: b3d2498b0dcea13e9ed75ea1ee865fdf25658a31a5552059cfb617efcead48600f46008fbd4f580766250daa6b99f38619412a4c4cfa32175f6736b7f45e7c3e
@@ -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
@@ -32,8 +32,7 @@ module SchwarmCli
32
32
  desc "create", "Create a new task"
33
33
  option :name, type: :string, required: true, desc: "Task name"
34
34
  option :prompt, type: :string, desc: "Task prompt"
35
- option :prompt_file, type: :string, desc: "Read prompt from file"
36
- option :repo, type: :string, desc: "Repository ID"
35
+ option :repo, type: :string, desc: "Repository ID, owner/repo, or GitHub URL"
37
36
  option :status, type: :string, desc: "Initial status (draft/waiting)"
38
37
  option :template, type: :string, desc: "Template ID"
39
38
  option :depends_on, type: :array, desc: "Dependency task IDs"
@@ -47,8 +46,7 @@ module SchwarmCli
47
46
  desc "update ID", "Update a task"
48
47
  option :name, type: :string, desc: "Task name"
49
48
  option :prompt, type: :string, desc: "Task prompt"
50
- option :prompt_file, type: :string, desc: "Read prompt from file"
51
- option :repo, type: :string, desc: "Repository ID"
49
+ option :repo, type: :string, desc: "Repository ID, owner/repo, or GitHub URL"
52
50
  option :depends_on, type: :array, desc: "Dependency task IDs"
53
51
  def update(id)
54
52
  handle_errors do
@@ -116,16 +114,15 @@ module SchwarmCli
116
114
 
117
115
  desc "handoff", "Hand off the current local branch to schwarm as a ready task"
118
116
  option :prompt, type: :string, desc: "Task prompt"
119
- option :prompt_file, type: :string, desc: "Read prompt from file"
120
- option :repo, type: :string, desc: "Repository ID (skips origin auto-detect)"
117
+ option :repo, type: :string, desc: "Repository ID, owner/repo, or GitHub URL (skips origin auto-detect)"
121
118
  option :name, type: :string, desc: "Task name (defaults to the branch name)"
122
119
  def handoff
123
- prompt = read_prompt
124
- abort "Error: --prompt or --prompt-file is required." if prompt.nil? || prompt.strip.empty?
120
+ prompt = options[:prompt]
121
+ abort "Error: --prompt is required." if prompt.nil? || prompt.strip.empty?
125
122
 
126
123
  handle_errors do
127
124
  result = SchwarmCli::HandsOffTask.new(client: client).call(
128
- prompt: prompt, repo_override: options[:repo], name_override: options[:name]
125
+ prompt: prompt, repo_override: resolve_repo(options[:repo]), name_override: options[:name]
129
126
  )
130
127
  print_handoff_result(result)
131
128
  end
@@ -136,7 +133,7 @@ module SchwarmCli
136
133
  def list_attrs
137
134
  exclude_archived = options[:status].nil? && !options[:all]
138
135
  {
139
- status: options[:status], repository_id: options[:repo], query: options[:query],
136
+ status: options[:status], repository_id: resolve_repo(options[:repo]), query: options[:query],
140
137
  archived: exclude_archived ? false : nil
141
138
  }
142
139
  end
@@ -153,28 +150,18 @@ module SchwarmCli
153
150
 
154
151
  def create_attrs
155
152
  {
156
- name: options[:name], prompt: read_prompt,
157
- github_repository_id: options[:repo], status: options[:status],
153
+ name: options[:name], prompt: options[:prompt],
154
+ github_repository_id: resolve_repo(options[:repo]), status: options[:status],
158
155
  task_template_id: options[:template], dependency_ids: options[:depends_on]
159
156
  }.compact
160
157
  end
161
158
 
162
159
  def update_attrs
163
160
  {
164
- name: options[:name], github_repository_id: options[:repo],
161
+ name: options[:name], github_repository_id: resolve_repo(options[:repo]),
165
162
  dependency_ids: options[:depends_on]
166
163
  }.compact.tap do |attrs|
167
- attrs[:prompt] = read_prompt if options[:prompt] || options[:prompt_file]
168
- end
169
- end
170
-
171
- def read_prompt
172
- if options[:prompt_file]
173
- file_path = File.expand_path(options[:prompt_file])
174
- abort "Error: File not found: #{file_path}" unless File.exist?(file_path)
175
- File.read(file_path)
176
- else
177
- options[:prompt]
164
+ attrs[:prompt] = options[:prompt] if options[:prompt]
178
165
  end
179
166
  end
180
167
  end
@@ -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.3"
4
+ VERSION = "0.1.5"
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
@@ -32,7 +32,14 @@ The `schwarm` CLI is installed and configured (`schwarm configure`).
32
32
  ```bash
33
33
  schwarm tasks create --repo <REPO_ID> --name "Fix login bug" --prompt "The login form crashes when email is empty. Fix the validation in app/models/user.rb" --status ready
34
34
  ```
35
- Use `--prompt-file ./prompt.md` for long prompts.
35
+ For a multi-line prompt, pipe via HEREDOC:
36
+ ```bash
37
+ schwarm tasks create --repo <REPO_ID> --name "Fix login bug" --status ready --prompt "$(cat <<'EOF'
38
+ The login form crashes when empty. Fix validation in app/models/user.rb
39
+ and add a regression test.
40
+ EOF
41
+ )"
42
+ ```
36
43
  3. Check the created task:
37
44
  ```bash
38
45
  schwarm tasks show <TASK_ID>
@@ -53,10 +60,24 @@ continues committing to it.
53
60
  ```bash
54
61
  schwarm tasks handoff --prompt "continue the validation logic, add tests"
55
62
  ```
56
- Use `--prompt-file ./handoff.md` for long prompts.
57
63
  3. Take note of the task ID printed on success; you can monitor it like any
58
64
  other task.
59
65
 
66
+ **Keep the prompt short.** The prompt is a pointer, not a spec. Before handing
67
+ off, commit any detailed instructions, design notes, or TODOs into the repo
68
+ (e.g. a `HANDOFF.md`, `AGENTS.md`, or inline comments) so the next agent reads
69
+ them from the working tree. Then the `--prompt` just needs to say what to do
70
+ next and where to look, e.g. `"continue per HANDOFF.md"`.
71
+
72
+ For a multi-line prompt, use a HEREDOC rather than a file flag:
73
+ ```bash
74
+ schwarm tasks handoff --prompt "$(cat <<'EOF'
75
+ Continue the validation work. Start from the TODOs in app/models/user.rb
76
+ and follow the plan in HANDOFF.md.
77
+ EOF
78
+ )"
79
+ ```
80
+
60
81
  **Watch for:**
61
82
  - The command refuses to hand off the base branch (`main`). Create a feature
62
83
  branch first.
@@ -223,7 +244,9 @@ for tasks in a repository.
223
244
  - Check task status before taking actions (e.g., only `retry` works on `error`
224
245
  tasks, only `start` works on `draft` tasks).
225
246
  - When building a DAG, create all tasks as drafts first, then start them all.
226
- - For long prompts, write them to a file and use `--prompt-file`.
247
+ - Keep `--prompt` short commit long-form instructions (plans, TODOs,
248
+ `HANDOFF.md`) into the repo and point the prompt at them. For multi-line
249
+ prompts, pipe via HEREDOC: `--prompt "$(cat <<'EOF' ... EOF)"`.
227
250
  - Run `schwarm <command> help` for full flag documentation on any command.
228
251
  - `schwarm tasks list` excludes archived tasks by default. Use `--all` to
229
252
  include them or `--status archived` to see only archived.
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.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vincent Garrigues
@@ -122,6 +122,7 @@ files:
122
122
  - lib/schwarm_cli/formatters/table.rb
123
123
  - lib/schwarm_cli/git.rb
124
124
  - lib/schwarm_cli/hands_off_task.rb
125
+ - lib/schwarm_cli/repository_resolver.rb
125
126
  - lib/schwarm_cli/version.rb
126
127
  - schwarm-skill.md
127
128
  homepage: https://github.com/getdexter/schwarm