schwarm-cli 0.1.5 → 0.1.7
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/lib/schwarm_cli/commands/agents.rb +20 -15
- data/lib/schwarm_cli/commands/base.rb +17 -8
- data/lib/schwarm_cli/commands/main.rb +5 -3
- data/lib/schwarm_cli/commands/recurring.rb +6 -6
- data/lib/schwarm_cli/commands/repos.rb +10 -2
- data/lib/schwarm_cli/commands/secrets.rb +26 -25
- data/lib/schwarm_cli/commands/sessions.rb +2 -1
- data/lib/schwarm_cli/commands/skills.rb +25 -4
- data/lib/schwarm_cli/commands/tasks.rb +11 -5
- data/lib/schwarm_cli/commands/templates.rb +19 -7
- data/lib/schwarm_cli/hands_off_task.rb +21 -8
- data/lib/schwarm_cli/template_resolver.rb +43 -0
- data/lib/schwarm_cli/version.rb +1 -1
- data/lib/schwarm_cli.rb +1 -0
- data/schwarm-skill.md +39 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 601a7ca203a5837a47d4bd0348300054e07fe3577a57c99865a64c282e310d8f
|
|
4
|
+
data.tar.gz: 857487dd8fa4d92e575466ebe1ed63c70fd3b1c744c6b041eb3d4c675c4657fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 899c50f55a6b91d9a8690603de5cbf7dc429b022f18198d205c3679eb6a97911e1fb5afe6bc96cb8ae76f852bb38edf9ca7ef82df245249e5ef1fc5e8188ccc9
|
|
7
|
+
data.tar.gz: 506dd09f7f837606a154cd61f0e2f79e7b231af0daa1e495115ba16b2e7c6fc5290dc101fd0c30ca8370132bc08178e39158295100a68c3a36a0e21e2079ca58
|
|
@@ -22,18 +22,23 @@ module SchwarmCli
|
|
|
22
22
|
handle_errors do
|
|
23
23
|
data = client.agents.find(id)
|
|
24
24
|
output_record(data, fields: {
|
|
25
|
-
"ID" => "id", "Name" => "name", "Repository" => "
|
|
26
|
-
"
|
|
25
|
+
"ID" => "id", "Name" => "name", "Repository" => "github_repository_name",
|
|
26
|
+
"Shared Agent" => "shared_agent_id", "Enabled" => "enabled",
|
|
27
|
+
"Schedule" => "schedule",
|
|
28
|
+
"Additional Instructions" => "additional_instructions",
|
|
27
29
|
"Created" => "created_at", "Updated" => "updated_at"
|
|
28
30
|
})
|
|
29
31
|
end
|
|
30
32
|
end
|
|
31
33
|
|
|
32
|
-
desc "create", "Create a repository agent"
|
|
33
|
-
option :name, type: :string, required: true, desc: "Agent name"
|
|
34
|
+
desc "create", "Create a repository agent (subscribe a shared agent to a repo)"
|
|
34
35
|
option :repo, type: :string, required: true, desc: "Repository ID, owner/repo, or GitHub URL"
|
|
35
|
-
option :
|
|
36
|
-
|
|
36
|
+
option :shared_agent, type: :string, required: true,
|
|
37
|
+
desc: "Shared agent ID to subscribe to this repository"
|
|
38
|
+
option :additional_instructions, type: :string,
|
|
39
|
+
desc: "Per-repository additional instructions appended to the shared prompt"
|
|
40
|
+
option :enabled, type: :boolean, default: true,
|
|
41
|
+
desc: "Whether the agent runs (default: true)"
|
|
37
42
|
def create
|
|
38
43
|
handle_errors do
|
|
39
44
|
data = client.agents.create(**create_attrs)
|
|
@@ -42,9 +47,9 @@ module SchwarmCli
|
|
|
42
47
|
end
|
|
43
48
|
|
|
44
49
|
desc "update ID", "Update a repository agent"
|
|
45
|
-
option :
|
|
46
|
-
|
|
47
|
-
option :
|
|
50
|
+
option :additional_instructions, type: :string,
|
|
51
|
+
desc: "Per-repository additional instructions"
|
|
52
|
+
option :enabled, type: :boolean, desc: "Whether the agent runs"
|
|
48
53
|
def update(id)
|
|
49
54
|
handle_errors do
|
|
50
55
|
data = client.agents.update(id, **update_attrs)
|
|
@@ -82,18 +87,18 @@ module SchwarmCli
|
|
|
82
87
|
|
|
83
88
|
def create_attrs
|
|
84
89
|
attrs = {
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
shared_agent_id: options[:shared_agent],
|
|
91
|
+
github_repository_id: resolve_repo(options[:repo]),
|
|
92
|
+
enabled: options[:enabled]
|
|
87
93
|
}
|
|
88
|
-
attrs[:
|
|
94
|
+
attrs[:additional_instructions] = options[:additional_instructions] if options[:additional_instructions]
|
|
89
95
|
attrs
|
|
90
96
|
end
|
|
91
97
|
|
|
92
98
|
def update_attrs
|
|
93
99
|
{}.tap do |attrs|
|
|
94
|
-
attrs[:
|
|
95
|
-
attrs[:
|
|
96
|
-
attrs[:schedule] = options[:schedule] if options[:schedule]
|
|
100
|
+
attrs[:additional_instructions] = options[:additional_instructions] if options[:additional_instructions]
|
|
101
|
+
attrs[:enabled] = options[:enabled] unless options[:enabled].nil?
|
|
97
102
|
end
|
|
98
103
|
end
|
|
99
104
|
end
|
|
@@ -9,6 +9,8 @@ module SchwarmCli
|
|
|
9
9
|
class_option :url, type: :string, desc: "Schwarm server URL (overrides config)"
|
|
10
10
|
class_option :token, type: :string, desc: "API key (overrides config)"
|
|
11
11
|
|
|
12
|
+
def self.exit_on_failure? = true
|
|
13
|
+
|
|
12
14
|
DEFAULT_LIST_LIMIT = 20
|
|
13
15
|
|
|
14
16
|
# Declares --limit, --page, --all options for the next method.
|
|
@@ -32,6 +34,13 @@ module SchwarmCli
|
|
|
32
34
|
abort "Error: #{e.message}"
|
|
33
35
|
end
|
|
34
36
|
|
|
37
|
+
# Resolves --template input (ID or exact name) to a schwarm template ID.
|
|
38
|
+
def resolve_template(ref)
|
|
39
|
+
SchwarmCli::TemplateResolver.new(client: client).resolve(ref)
|
|
40
|
+
rescue SchwarmCli::TemplateResolver::ResolutionError => e
|
|
41
|
+
abort "Error: #{e.message}"
|
|
42
|
+
end
|
|
43
|
+
|
|
35
44
|
def output_list(data, columns:)
|
|
36
45
|
if options[:json]
|
|
37
46
|
Formatters::Json.format(data)
|
|
@@ -80,12 +89,12 @@ module SchwarmCli
|
|
|
80
89
|
yield
|
|
81
90
|
rescue Faraday::UnauthorizedError
|
|
82
91
|
abort "Error: Unauthorized. Check your API key or run `schwarm configure`."
|
|
83
|
-
rescue Faraday::ResourceNotFound
|
|
84
|
-
abort "Error: Not found."
|
|
92
|
+
rescue Faraday::ResourceNotFound => e
|
|
93
|
+
abort "Error: #{parse_error_body(e, fallback: 'Not found.')}"
|
|
85
94
|
rescue Faraday::UnprocessableEntityError => e
|
|
86
|
-
abort "Error: #{parse_error_body(e)}"
|
|
95
|
+
abort "Error: #{parse_error_body(e, fallback: 'Validation failed')}"
|
|
87
96
|
rescue Faraday::ClientError => e
|
|
88
|
-
abort "Error: #{e.message}"
|
|
97
|
+
abort "Error: #{parse_error_body(e, fallback: e.message)}"
|
|
89
98
|
rescue Faraday::ServerError
|
|
90
99
|
abort "Error: Server error. Try again later."
|
|
91
100
|
rescue Faraday::ConnectionFailed
|
|
@@ -94,14 +103,14 @@ module SchwarmCli
|
|
|
94
103
|
abort "Error: Request timed out."
|
|
95
104
|
end
|
|
96
105
|
|
|
97
|
-
def parse_error_body(error)
|
|
106
|
+
def parse_error_body(error, fallback:)
|
|
98
107
|
body = error.response&.dig(:body)
|
|
99
|
-
return
|
|
108
|
+
return fallback unless body.is_a?(String) && !body.empty?
|
|
100
109
|
|
|
101
110
|
parsed = JSON.parse(body)
|
|
102
|
-
parsed.dig("error", "message") ||
|
|
111
|
+
parsed.dig("error", "message") || fallback
|
|
103
112
|
rescue JSON::ParserError
|
|
104
|
-
|
|
113
|
+
fallback
|
|
105
114
|
end
|
|
106
115
|
end
|
|
107
116
|
end
|
|
@@ -17,6 +17,8 @@ require_relative "configure"
|
|
|
17
17
|
module SchwarmCli
|
|
18
18
|
module Commands
|
|
19
19
|
class Main < Thor
|
|
20
|
+
def self.exit_on_failure? = true
|
|
21
|
+
|
|
20
22
|
desc "tasks SUBCOMMAND", "Manage tasks"
|
|
21
23
|
subcommand "tasks", Tasks
|
|
22
24
|
|
|
@@ -30,13 +32,13 @@ module SchwarmCli
|
|
|
30
32
|
subcommand "skills", SkillsCmd
|
|
31
33
|
|
|
32
34
|
desc "skill-files SUBCOMMAND", "Manage skill files"
|
|
33
|
-
subcommand "
|
|
35
|
+
subcommand "skill_files", SkillFilesCmd
|
|
34
36
|
|
|
35
37
|
desc "agents SUBCOMMAND", "Manage repository agents"
|
|
36
38
|
subcommand "agents", Agents
|
|
37
39
|
|
|
38
40
|
desc "shared-agents SUBCOMMAND", "Manage shared agents"
|
|
39
|
-
subcommand "
|
|
41
|
+
subcommand "shared_agents", SharedAgentsCmd
|
|
40
42
|
|
|
41
43
|
desc "recurring SUBCOMMAND", "Manage recurring tasks"
|
|
42
44
|
subcommand "recurring", Recurring
|
|
@@ -48,7 +50,7 @@ module SchwarmCli
|
|
|
48
50
|
subcommand "sessions", Sessions
|
|
49
51
|
|
|
50
52
|
desc "repo-skills SUBCOMMAND", "Manage repository-skill associations"
|
|
51
|
-
subcommand "
|
|
53
|
+
subcommand "repo_skills", RepoSkills
|
|
52
54
|
|
|
53
55
|
desc "configure", "Set up Schwarm CLI configuration"
|
|
54
56
|
subcommand "configure", Configure
|
|
@@ -13,7 +13,7 @@ module SchwarmCli
|
|
|
13
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
|
-
%w[ENABLED enabled], %w[SCHEDULE
|
|
16
|
+
%w[ENABLED enabled], %w[SCHEDULE cron_expression]])
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -23,7 +23,7 @@ module SchwarmCli
|
|
|
23
23
|
data = client.recurring.find(id)
|
|
24
24
|
output_record(data, fields: {
|
|
25
25
|
"ID" => "id", "Name" => "name", "Repository" => "github_repository_id",
|
|
26
|
-
"Enabled" => "enabled", "Schedule" => "
|
|
26
|
+
"Enabled" => "enabled", "Schedule" => "cron_expression", "Prompt" => "prompt",
|
|
27
27
|
"Created" => "created_at", "Updated" => "updated_at"
|
|
28
28
|
})
|
|
29
29
|
end
|
|
@@ -38,11 +38,11 @@ module SchwarmCli
|
|
|
38
38
|
handle_errors do
|
|
39
39
|
attrs = {
|
|
40
40
|
name: options[:name], github_repository_id: resolve_repo(options[:repo]),
|
|
41
|
-
prompt: options[:prompt],
|
|
41
|
+
prompt: options[:prompt], cron_expression: options[:schedule]
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
data = client.recurring.create(**attrs)
|
|
45
|
-
output_record(data, fields: { "ID" => "id", "Name" => "name", "Schedule" => "
|
|
45
|
+
output_record(data, fields: { "ID" => "id", "Name" => "name", "Schedule" => "cron_expression" })
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -53,7 +53,7 @@ module SchwarmCli
|
|
|
53
53
|
def update(id)
|
|
54
54
|
handle_errors do
|
|
55
55
|
data = client.recurring.update(id, **update_attrs)
|
|
56
|
-
output_record(data, fields: { "ID" => "id", "Name" => "name", "Schedule" => "
|
|
56
|
+
output_record(data, fields: { "ID" => "id", "Name" => "name", "Schedule" => "cron_expression" })
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
|
|
@@ -80,7 +80,7 @@ module SchwarmCli
|
|
|
80
80
|
{}.tap do |attrs|
|
|
81
81
|
attrs[:name] = options[:name] if options[:name]
|
|
82
82
|
attrs[:prompt] = options[:prompt] if options[:prompt]
|
|
83
|
-
attrs[:
|
|
83
|
+
attrs[:cron_expression] = options[:schedule] if options[:schedule]
|
|
84
84
|
end
|
|
85
85
|
end
|
|
86
86
|
end
|
|
@@ -64,7 +64,11 @@ module SchwarmCli
|
|
|
64
64
|
def pause(id)
|
|
65
65
|
handle_errors do
|
|
66
66
|
data = client.repositories.pause(id)
|
|
67
|
-
|
|
67
|
+
if options[:json]
|
|
68
|
+
output_record(data, fields: {})
|
|
69
|
+
else
|
|
70
|
+
puts "Repository #{id} paused (#{data.dig('data', 'status')})."
|
|
71
|
+
end
|
|
68
72
|
end
|
|
69
73
|
end
|
|
70
74
|
|
|
@@ -72,7 +76,11 @@ module SchwarmCli
|
|
|
72
76
|
def resume(id)
|
|
73
77
|
handle_errors do
|
|
74
78
|
data = client.repositories.resume(id)
|
|
75
|
-
|
|
79
|
+
if options[:json]
|
|
80
|
+
output_record(data, fields: {})
|
|
81
|
+
else
|
|
82
|
+
puts "Repository #{id} resumed (#{data.dig('data', 'status')})."
|
|
83
|
+
end
|
|
76
84
|
end
|
|
77
85
|
end
|
|
78
86
|
|
|
@@ -11,50 +11,46 @@ module SchwarmCli
|
|
|
11
11
|
data = fetch_paged do |page_params|
|
|
12
12
|
client.secrets.list(repository_id: resolve_repo(options[:repo]), **page_params)
|
|
13
13
|
end
|
|
14
|
-
output_list(data, columns: [%w[ID id], %w[
|
|
15
|
-
%w[PATH path]])
|
|
14
|
+
output_list(data, columns: [%w[ID id], %w[REPO github_repository_name], %w[PATH path]])
|
|
16
15
|
end
|
|
17
16
|
end
|
|
18
17
|
|
|
19
|
-
desc "show ID", "Show secret file details"
|
|
18
|
+
desc "show ID", "Show secret file details (metadata only — content is never returned)"
|
|
20
19
|
def show(id)
|
|
21
20
|
handle_errors do
|
|
22
21
|
data = client.secrets.find(id)
|
|
23
22
|
output_record(data, fields: {
|
|
24
|
-
"ID" => "id", "
|
|
23
|
+
"ID" => "id", "Repository" => "github_repository_name",
|
|
25
24
|
"Path" => "path", "Created" => "created_at", "Updated" => "updated_at"
|
|
26
25
|
})
|
|
27
26
|
end
|
|
28
27
|
end
|
|
29
28
|
|
|
30
29
|
desc "create", "Create a secret file"
|
|
31
|
-
option :name, type: :string, required: true, desc: "Secret name"
|
|
32
30
|
option :repo, type: :string, required: true, desc: "Repository ID, owner/repo, or GitHub URL"
|
|
33
31
|
option :path, type: :string, required: true, desc: "File path in workspace"
|
|
34
|
-
option :content, type: :string, desc: "Secret content"
|
|
35
|
-
option :file, type: :string, desc: "Read content from file"
|
|
32
|
+
option :content, type: :string, desc: "Secret content (mutually exclusive with --file)"
|
|
33
|
+
option :file, type: :string, desc: "Read content from file (mutually exclusive with --content)"
|
|
36
34
|
def create
|
|
37
35
|
handle_errors do
|
|
38
|
-
content = read_content
|
|
39
36
|
attrs = {
|
|
40
|
-
|
|
41
|
-
path: options[:path], content:
|
|
37
|
+
github_repository_id: resolve_repo(options[:repo]),
|
|
38
|
+
path: options[:path], content: read_content(required: true)
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
data = client.secrets.create(**attrs)
|
|
45
|
-
output_record(data, fields: { "ID" => "id", "
|
|
42
|
+
output_record(data, fields: { "ID" => "id", "Path" => "path" })
|
|
46
43
|
end
|
|
47
44
|
end
|
|
48
45
|
|
|
49
46
|
desc "update ID", "Update a secret file"
|
|
50
|
-
option :name, type: :string, desc: "Secret name"
|
|
51
47
|
option :path, type: :string, desc: "File path in workspace"
|
|
52
|
-
option :content, type: :string, desc: "Secret content"
|
|
53
|
-
option :file, type: :string, desc: "Read content from file"
|
|
48
|
+
option :content, type: :string, desc: "Secret content (mutually exclusive with --file)"
|
|
49
|
+
option :file, type: :string, desc: "Read content from file (mutually exclusive with --content)"
|
|
54
50
|
def update(id)
|
|
55
51
|
handle_errors do
|
|
56
52
|
data = client.secrets.update(id, **update_attrs)
|
|
57
|
-
output_record(data, fields: { "ID" => "id", "
|
|
53
|
+
output_record(data, fields: { "ID" => "id", "Path" => "path" })
|
|
58
54
|
end
|
|
59
55
|
end
|
|
60
56
|
|
|
@@ -69,19 +65,24 @@ module SchwarmCli
|
|
|
69
65
|
private
|
|
70
66
|
|
|
71
67
|
def update_attrs
|
|
72
|
-
{
|
|
73
|
-
|
|
68
|
+
{ path: options[:path] }.compact.tap do |attrs|
|
|
69
|
+
content = read_content(required: false)
|
|
70
|
+
attrs[:content] = content if content
|
|
74
71
|
end
|
|
75
72
|
end
|
|
76
73
|
|
|
77
|
-
def read_content
|
|
78
|
-
if options[:file]
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
def read_content(required:)
|
|
75
|
+
abort "Error: --content and --file are mutually exclusive." if options[:content] && options[:file]
|
|
76
|
+
return options[:content] if options[:content]
|
|
77
|
+
return read_file(options[:file]) if options[:file]
|
|
78
|
+
|
|
79
|
+
abort "Error: one of --content or --file is required." if required
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def read_file(path)
|
|
83
|
+
expanded = File.expand_path(path)
|
|
84
|
+
abort "Error: File not found: #{expanded}" unless File.exist?(expanded)
|
|
85
|
+
File.read(expanded)
|
|
85
86
|
end
|
|
86
87
|
end
|
|
87
88
|
end
|
|
@@ -27,7 +27,8 @@ module SchwarmCli
|
|
|
27
27
|
data = client.sessions.find(id)
|
|
28
28
|
output_record(data, fields: {
|
|
29
29
|
"ID" => "id", "Source" => "source", "Status" => "status",
|
|
30
|
-
"
|
|
30
|
+
"External ID" => "external_id", "Model" => "model_id",
|
|
31
|
+
"Error" => "error_message",
|
|
31
32
|
"Created" => "created_at", "Updated" => "updated_at"
|
|
32
33
|
})
|
|
33
34
|
end
|
|
@@ -21,19 +21,24 @@ module SchwarmCli
|
|
|
21
21
|
data = client.skills.find(id)
|
|
22
22
|
output_record(data, fields: {
|
|
23
23
|
"ID" => "id", "Name" => "name", "Description" => "description",
|
|
24
|
-
"
|
|
24
|
+
"Instructions" => "instructions", "Global" => "global",
|
|
25
|
+
"Created" => "created_at"
|
|
25
26
|
})
|
|
26
27
|
end
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
desc "create", "Create a skill"
|
|
30
31
|
option :name, type: :string, required: true, desc: "Skill name"
|
|
31
|
-
option :description, type: :string, desc: "Skill description"
|
|
32
|
+
option :description, type: :string, required: true, desc: "Skill description"
|
|
33
|
+
option :instructions, type: :string, desc: "Skill instructions (inline)"
|
|
34
|
+
option :instructions_file, type: :string, desc: "Read instructions from file"
|
|
32
35
|
option :global, type: :boolean, default: false, desc: "Make skill global"
|
|
33
36
|
def create
|
|
34
37
|
handle_errors do
|
|
35
|
-
attrs = {
|
|
36
|
-
|
|
38
|
+
attrs = {
|
|
39
|
+
name: options[:name], description: options[:description],
|
|
40
|
+
instructions: read_instructions, global: options[:global]
|
|
41
|
+
}
|
|
37
42
|
|
|
38
43
|
data = client.skills.create(**attrs)
|
|
39
44
|
output_record(data, fields: { "ID" => "id", "Name" => "name", "Global" => "global" })
|
|
@@ -43,6 +48,8 @@ module SchwarmCli
|
|
|
43
48
|
desc "update ID", "Update a skill"
|
|
44
49
|
option :name, type: :string, desc: "Skill name"
|
|
45
50
|
option :description, type: :string, desc: "Skill description"
|
|
51
|
+
option :instructions, type: :string, desc: "Skill instructions (inline)"
|
|
52
|
+
option :instructions_file, type: :string, desc: "Read instructions from file"
|
|
46
53
|
option :global, type: :boolean, desc: "Make skill global"
|
|
47
54
|
def update(id)
|
|
48
55
|
handle_errors do
|
|
@@ -62,10 +69,24 @@ module SchwarmCli
|
|
|
62
69
|
private
|
|
63
70
|
|
|
64
71
|
def update_attrs
|
|
72
|
+
instructions = read_instructions
|
|
65
73
|
{ name: options[:name], description: options[:description] }.compact.tap do |attrs|
|
|
74
|
+
attrs[:instructions] = instructions if instructions
|
|
66
75
|
attrs[:global] = options[:global] unless options[:global].nil?
|
|
67
76
|
end
|
|
68
77
|
end
|
|
78
|
+
|
|
79
|
+
def read_instructions
|
|
80
|
+
if options[:instructions] && options[:instructions_file]
|
|
81
|
+
abort "Error: --instructions and --instructions-file are mutually exclusive."
|
|
82
|
+
elsif options[:instructions_file]
|
|
83
|
+
file_path = File.expand_path(options[:instructions_file])
|
|
84
|
+
abort "Error: File not found: #{file_path}" unless File.exist?(file_path)
|
|
85
|
+
File.read(file_path)
|
|
86
|
+
else
|
|
87
|
+
options[:instructions]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
69
90
|
end
|
|
70
91
|
end
|
|
71
92
|
end
|
|
@@ -34,7 +34,7 @@ module SchwarmCli
|
|
|
34
34
|
option :prompt, type: :string, desc: "Task prompt"
|
|
35
35
|
option :repo, type: :string, desc: "Repository ID, owner/repo, or GitHub URL"
|
|
36
36
|
option :status, type: :string, desc: "Initial status (draft/waiting)"
|
|
37
|
-
option :template, type: :string, desc: "Template ID"
|
|
37
|
+
option :template, type: :string, desc: "Template ID or name"
|
|
38
38
|
option :depends_on, type: :array, desc: "Dependency task IDs"
|
|
39
39
|
def create
|
|
40
40
|
handle_errors do
|
|
@@ -116,14 +116,13 @@ module SchwarmCli
|
|
|
116
116
|
option :prompt, type: :string, desc: "Task prompt"
|
|
117
117
|
option :repo, type: :string, desc: "Repository ID, owner/repo, or GitHub URL (skips origin auto-detect)"
|
|
118
118
|
option :name, type: :string, desc: "Task name (defaults to the branch name)"
|
|
119
|
+
option :template, type: :string, desc: "Task template ID or name"
|
|
119
120
|
def handoff
|
|
120
121
|
prompt = options[:prompt]
|
|
121
122
|
abort "Error: --prompt is required." if prompt.nil? || prompt.strip.empty?
|
|
122
123
|
|
|
123
124
|
handle_errors do
|
|
124
|
-
result = SchwarmCli::HandsOffTask.new(client: client).call(
|
|
125
|
-
prompt: prompt, repo_override: resolve_repo(options[:repo]), name_override: options[:name]
|
|
126
|
-
)
|
|
125
|
+
result = SchwarmCli::HandsOffTask.new(client: client).call(**handoff_attrs(prompt))
|
|
127
126
|
print_handoff_result(result)
|
|
128
127
|
end
|
|
129
128
|
end
|
|
@@ -148,11 +147,18 @@ module SchwarmCli
|
|
|
148
147
|
end
|
|
149
148
|
end
|
|
150
149
|
|
|
150
|
+
def handoff_attrs(prompt)
|
|
151
|
+
{
|
|
152
|
+
prompt: prompt, repo_override: resolve_repo(options[:repo]),
|
|
153
|
+
name_override: options[:name], template_override: options[:template]
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
|
|
151
157
|
def create_attrs
|
|
152
158
|
{
|
|
153
159
|
name: options[:name], prompt: options[:prompt],
|
|
154
160
|
github_repository_id: resolve_repo(options[:repo]), status: options[:status],
|
|
155
|
-
task_template_id: options[:template], dependency_ids: options[:depends_on]
|
|
161
|
+
task_template_id: resolve_template(options[:template]), dependency_ids: options[:depends_on]
|
|
156
162
|
}.compact
|
|
157
163
|
end
|
|
158
164
|
|
|
@@ -20,7 +20,8 @@ module SchwarmCli
|
|
|
20
20
|
handle_errors do
|
|
21
21
|
data = client.templates.find(id)
|
|
22
22
|
output_record(data, fields: {
|
|
23
|
-
"ID" => "id", "Name" => "name", "
|
|
23
|
+
"ID" => "id", "Name" => "name", "Description" => "description",
|
|
24
|
+
"Prompt" => "prompt_prefix", "Created" => "created_at"
|
|
24
25
|
})
|
|
25
26
|
end
|
|
26
27
|
end
|
|
@@ -28,9 +29,13 @@ module SchwarmCli
|
|
|
28
29
|
desc "create", "Create a task template"
|
|
29
30
|
option :name, type: :string, required: true, desc: "Template name"
|
|
30
31
|
option :prompt, type: :string, required: true, desc: "Template prompt"
|
|
32
|
+
option :description, type: :string, desc: "Template description"
|
|
31
33
|
def create
|
|
32
34
|
handle_errors do
|
|
33
|
-
|
|
35
|
+
attrs = { name: options[:name], prompt_prefix: options[:prompt] }
|
|
36
|
+
attrs[:description] = options[:description] if options[:description]
|
|
37
|
+
|
|
38
|
+
data = client.templates.create(**attrs)
|
|
34
39
|
output_record(data, fields: { "ID" => "id", "Name" => "name" })
|
|
35
40
|
end
|
|
36
41
|
end
|
|
@@ -38,13 +43,10 @@ module SchwarmCli
|
|
|
38
43
|
desc "update ID", "Update a task template"
|
|
39
44
|
option :name, type: :string, desc: "Template name"
|
|
40
45
|
option :prompt, type: :string, desc: "Template prompt"
|
|
46
|
+
option :description, type: :string, desc: "Template description"
|
|
41
47
|
def update(id)
|
|
42
48
|
handle_errors do
|
|
43
|
-
|
|
44
|
-
attrs[:name] = options[:name] if options[:name]
|
|
45
|
-
attrs[:prompt] = options[:prompt] if options[:prompt]
|
|
46
|
-
|
|
47
|
-
data = client.templates.update(id, **attrs)
|
|
49
|
+
data = client.templates.update(id, **update_attrs)
|
|
48
50
|
output_record(data, fields: { "ID" => "id", "Name" => "name" })
|
|
49
51
|
end
|
|
50
52
|
end
|
|
@@ -56,6 +58,16 @@ module SchwarmCli
|
|
|
56
58
|
puts "Template #{id} deleted."
|
|
57
59
|
end
|
|
58
60
|
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def update_attrs
|
|
65
|
+
{}.tap do |attrs|
|
|
66
|
+
attrs[:name] = options[:name] if options[:name]
|
|
67
|
+
attrs[:prompt_prefix] = options[:prompt] if options[:prompt]
|
|
68
|
+
attrs[:description] = options[:description] if options[:description]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
59
71
|
end
|
|
60
72
|
end
|
|
61
73
|
end
|
|
@@ -12,14 +12,18 @@ module SchwarmCli
|
|
|
12
12
|
@client = client
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def call(prompt:, repo_override:, name_override:)
|
|
15
|
+
def call(prompt:, repo_override:, name_override:, template_override: nil)
|
|
16
16
|
repo, branch, error = run_preflight(repo_override)
|
|
17
17
|
return error if error
|
|
18
18
|
|
|
19
|
+
template_id, template_error = resolve_template_id(template_override)
|
|
20
|
+
return template_error if template_error
|
|
21
|
+
|
|
19
22
|
push_result = @git.run("push", "-u", "origin", "HEAD")
|
|
20
23
|
return failure(push_result.stderr.strip) unless push_result.success?
|
|
21
24
|
|
|
22
|
-
create_task(repo: repo, branch: branch, prompt: prompt, name_override: name_override
|
|
25
|
+
create_task(repo: repo, branch: branch, prompt: prompt, name_override: name_override,
|
|
26
|
+
template_id: template_id)
|
|
23
27
|
rescue Faraday::Error => e
|
|
24
28
|
failure(parse_api_error(e))
|
|
25
29
|
end
|
|
@@ -97,13 +101,22 @@ module SchwarmCli
|
|
|
97
101
|
nil
|
|
98
102
|
end
|
|
99
103
|
|
|
100
|
-
def
|
|
104
|
+
def resolve_template_id(ref)
|
|
105
|
+
[TemplateResolver.new(client: @client).resolve(ref), nil]
|
|
106
|
+
rescue TemplateResolver::ResolutionError => e
|
|
107
|
+
[nil, failure(e.message)]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def create_task(repo:, branch:, prompt:, name_override:, template_id:)
|
|
101
111
|
response = @client.tasks.create(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
**{
|
|
113
|
+
name: name_override || branch,
|
|
114
|
+
branch_name: branch,
|
|
115
|
+
prompt: prompt,
|
|
116
|
+
status: "ready",
|
|
117
|
+
github_repository_id: repo["id"],
|
|
118
|
+
task_template_id: template_id
|
|
119
|
+
}.compact
|
|
107
120
|
)
|
|
108
121
|
data = response["data"]
|
|
109
122
|
Result.new(
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SchwarmCli
|
|
4
|
+
# Turns a user-provided `--template` reference into a task template ID.
|
|
5
|
+
#
|
|
6
|
+
# Accepted inputs:
|
|
7
|
+
# - a numeric template ID (looked up directly)
|
|
8
|
+
# - an exact template name (resolved via search + exact match)
|
|
9
|
+
class TemplateResolver
|
|
10
|
+
class ResolutionError < StandardError; end
|
|
11
|
+
|
|
12
|
+
def initialize(client:)
|
|
13
|
+
@client = client
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def resolve(ref)
|
|
17
|
+
return nil if blank?(ref)
|
|
18
|
+
|
|
19
|
+
ref = ref.to_s.strip
|
|
20
|
+
template = ref.match?(/\A\d+\z/) ? find_by_id(ref) : find_by_name(ref)
|
|
21
|
+
raise ResolutionError, "no schwarm task template matches `#{ref}`." if template.nil?
|
|
22
|
+
|
|
23
|
+
template["id"]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def blank?(ref)
|
|
29
|
+
ref.nil? || ref.to_s.strip.empty?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def find_by_id(id)
|
|
33
|
+
@client.templates.find(id)["data"]
|
|
34
|
+
rescue Faraday::ResourceNotFound
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def find_by_name(name)
|
|
39
|
+
response = @client.templates.list(query: name)
|
|
40
|
+
Array(response["data"]).find { |t| t["name"] == name }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/schwarm_cli/version.rb
CHANGED
data/lib/schwarm_cli.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "schwarm_cli/config"
|
|
|
5
5
|
require_relative "schwarm_cli/client"
|
|
6
6
|
require_relative "schwarm_cli/git"
|
|
7
7
|
require_relative "schwarm_cli/repository_resolver"
|
|
8
|
+
require_relative "schwarm_cli/template_resolver"
|
|
8
9
|
require_relative "schwarm_cli/hands_off_task"
|
|
9
10
|
require_relative "schwarm_cli/formatters/json"
|
|
10
11
|
require_relative "schwarm_cli/formatters/table"
|
data/schwarm-skill.md
CHANGED
|
@@ -86,6 +86,42 @@ EOF
|
|
|
86
86
|
will print the conflicting task ID. Archive or reset it first.
|
|
87
87
|
- The repository is auto-detected from `git remote get-url origin`. Use
|
|
88
88
|
`--repo <id>` if you need to override it (e.g. in a fork).
|
|
89
|
+
- Pass `--template <id-or-name>` to apply a task template to the handoff. The
|
|
90
|
+
value can be a template ID or its exact name.
|
|
91
|
+
|
|
92
|
+
### 1c. Hand off a WIP branch with follow-up tasks
|
|
93
|
+
|
|
94
|
+
**When:** You're handing off a WIP branch and want other tasks to run after it
|
|
95
|
+
on the same repo (e.g. add docs, cut a release PR, update a dashboard). The
|
|
96
|
+
handoff task starts immediately; the follow-ups wait until it's archived.
|
|
97
|
+
|
|
98
|
+
**Steps:**
|
|
99
|
+
1. Hand off the current branch first and note the task ID it prints:
|
|
100
|
+
```bash
|
|
101
|
+
schwarm tasks handoff --prompt "continue per HANDOFF.md"
|
|
102
|
+
# => Task a1b2c3 created (status: ready).
|
|
103
|
+
```
|
|
104
|
+
2. Create follow-up tasks as drafts depending on the handoff task ID:
|
|
105
|
+
```bash
|
|
106
|
+
schwarm tasks create --repo <REPO_ID> --name "Update docs" \
|
|
107
|
+
--prompt "Update README and CHANGELOG once the branch lands" --depends-on a1b2c3
|
|
108
|
+
schwarm tasks create --repo <REPO_ID> --name "Announce in #releases" \
|
|
109
|
+
--prompt "Post the summary to Slack" --depends-on a1b2c3
|
|
110
|
+
```
|
|
111
|
+
3. Start each follow-up — it will move to `waiting` until the handoff archives:
|
|
112
|
+
```bash
|
|
113
|
+
schwarm tasks start <FOLLOWUP_ID>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Watch for:**
|
|
117
|
+
- The handoff task is created with `status: ready` and runs immediately. You
|
|
118
|
+
don't need to start it.
|
|
119
|
+
- Follow-ups run on fresh worktrees off the repo's base branch by default, not
|
|
120
|
+
off the handoff branch. `--depends-on` keeps them in `waiting` until the
|
|
121
|
+
handoff task is archived, so by the time they start the handoff's changes are
|
|
122
|
+
already on the base branch.
|
|
123
|
+
- Pass `--repo <id>` on `tasks create` (handoff auto-detects it from `origin`,
|
|
124
|
+
but `create` does not).
|
|
89
125
|
|
|
90
126
|
### 2. Dispatch a DAG of tasks
|
|
91
127
|
|
|
@@ -192,9 +228,10 @@ must `start` again.
|
|
|
192
228
|
schwarm templates list
|
|
193
229
|
schwarm templates show <TEMPLATE_ID>
|
|
194
230
|
```
|
|
195
|
-
3. Create a task from a template
|
|
231
|
+
3. Create a task from a template. `--template` accepts either the template ID or
|
|
232
|
+
its exact name:
|
|
196
233
|
```bash
|
|
197
|
-
schwarm tasks create --repo <REPO_ID> --name "Apply template" --template <
|
|
234
|
+
schwarm tasks create --repo <REPO_ID> --name "Apply template" --template <TEMPLATE_ID_OR_NAME> --status ready
|
|
198
235
|
```
|
|
199
236
|
|
|
200
237
|
### 7. Manage recurring tasks
|
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.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vincent Garrigues
|
|
@@ -123,6 +123,7 @@ files:
|
|
|
123
123
|
- lib/schwarm_cli/git.rb
|
|
124
124
|
- lib/schwarm_cli/hands_off_task.rb
|
|
125
125
|
- lib/schwarm_cli/repository_resolver.rb
|
|
126
|
+
- lib/schwarm_cli/template_resolver.rb
|
|
126
127
|
- lib/schwarm_cli/version.rb
|
|
127
128
|
- schwarm-skill.md
|
|
128
129
|
homepage: https://github.com/getdexter/schwarm
|