harnex 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.
- checksums.yaml +7 -0
- data/GUIDE.md +242 -0
- data/LICENSE +21 -0
- data/README.md +119 -0
- data/TECHNICAL.md +595 -0
- data/bin/harnex +18 -0
- data/lib/harnex/adapters/base.rb +134 -0
- data/lib/harnex/adapters/claude.rb +105 -0
- data/lib/harnex/adapters/codex.rb +112 -0
- data/lib/harnex/adapters/generic.rb +14 -0
- data/lib/harnex/adapters.rb +32 -0
- data/lib/harnex/cli.rb +115 -0
- data/lib/harnex/commands/guide.rb +23 -0
- data/lib/harnex/commands/logs.rb +184 -0
- data/lib/harnex/commands/pane.rb +251 -0
- data/lib/harnex/commands/recipes.rb +104 -0
- data/lib/harnex/commands/run.rb +384 -0
- data/lib/harnex/commands/send.rb +415 -0
- data/lib/harnex/commands/skills.rb +163 -0
- data/lib/harnex/commands/status.rb +171 -0
- data/lib/harnex/commands/stop.rb +127 -0
- data/lib/harnex/commands/wait.rb +165 -0
- data/lib/harnex/core.rb +286 -0
- data/lib/harnex/runtime/api_server.rb +187 -0
- data/lib/harnex/runtime/file_change_hook.rb +111 -0
- data/lib/harnex/runtime/inbox.rb +207 -0
- data/lib/harnex/runtime/message.rb +23 -0
- data/lib/harnex/runtime/session.rb +380 -0
- data/lib/harnex/runtime/session_state.rb +55 -0
- data/lib/harnex/version.rb +3 -0
- data/lib/harnex/watcher/inotify.rb +43 -0
- data/lib/harnex/watcher/polling.rb +92 -0
- data/lib/harnex/watcher.rb +24 -0
- data/lib/harnex.rb +25 -0
- data/recipes/01_fire_and_watch.md +82 -0
- data/recipes/02_chain_implement.md +115 -0
- data/skills/chain-implement/SKILL.md +234 -0
- data/skills/close/SKILL.md +47 -0
- data/skills/dispatch/SKILL.md +171 -0
- data/skills/harnex/SKILL.md +304 -0
- data/skills/open/SKILL.md +32 -0
- metadata +88 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module Harnex
|
|
2
|
+
module Adapters
|
|
3
|
+
class Base
|
|
4
|
+
PROMPT_PREFIXES = [">", "\u203A", "\u276F"].freeze
|
|
5
|
+
|
|
6
|
+
# Adapter contract — subclasses MUST implement:
|
|
7
|
+
# base_command -> Array[String] CLI args to spawn
|
|
8
|
+
#
|
|
9
|
+
# Subclasses MAY override:
|
|
10
|
+
# input_state(text) -> Hash Parse screen for state
|
|
11
|
+
# build_send_payload -> Hash Build injection payload
|
|
12
|
+
# inject_exit(writer) -> void Send a stop/exit sequence
|
|
13
|
+
# infer_repo_path(argv) -> String Extract repo path from CLI args
|
|
14
|
+
# wait_for_sendable -> String Wait for a send-ready snapshot
|
|
15
|
+
|
|
16
|
+
attr_reader :key
|
|
17
|
+
|
|
18
|
+
def initialize(key, extra_args = [])
|
|
19
|
+
@key = key
|
|
20
|
+
@extra_args = extra_args.dup
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def build_command
|
|
24
|
+
base_command + @extra_args
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def base_command
|
|
28
|
+
raise NotImplementedError, "#{self.class} must define #base_command"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def infer_repo_path(_argv)
|
|
32
|
+
Dir.pwd
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def input_state(screen_text)
|
|
36
|
+
{
|
|
37
|
+
state: "unknown",
|
|
38
|
+
input_ready: nil
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def send_wait_seconds(submit:, enter_only:)
|
|
43
|
+
0.0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def wait_for_sendable_state?(_state, submit:, enter_only:)
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def wait_for_sendable(screen_snapshot_fn, submit:, enter_only:, force:)
|
|
51
|
+
snapshot = screen_snapshot_fn.call
|
|
52
|
+
return snapshot if force
|
|
53
|
+
|
|
54
|
+
wait_secs = send_wait_seconds(submit: submit, enter_only: enter_only).to_f
|
|
55
|
+
return snapshot unless wait_secs.positive?
|
|
56
|
+
|
|
57
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + wait_secs
|
|
58
|
+
state = input_state(snapshot)
|
|
59
|
+
|
|
60
|
+
while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline &&
|
|
61
|
+
wait_for_sendable_state?(state, submit: submit, enter_only: enter_only)
|
|
62
|
+
sleep 0.05
|
|
63
|
+
snapshot = screen_snapshot_fn.call
|
|
64
|
+
state = input_state(snapshot)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
snapshot
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_send_payload(text:, submit:, enter_only:, screen_text:, force: false)
|
|
71
|
+
state = input_state(screen_text)
|
|
72
|
+
if !force && blocked_state?(state, enter_only: enter_only)
|
|
73
|
+
raise ArgumentError, blocked_message(state, enter_only: enter_only)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
payload = enter_only ? "" : text.to_s
|
|
77
|
+
payload << submit_bytes if submit || enter_only
|
|
78
|
+
|
|
79
|
+
{
|
|
80
|
+
text: payload,
|
|
81
|
+
newline: false,
|
|
82
|
+
input_state: state,
|
|
83
|
+
force: force
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def inject_exit(writer, delay_ms: 0)
|
|
88
|
+
writer.write("/exit")
|
|
89
|
+
writer.flush
|
|
90
|
+
sleep(delay_ms / 1000.0) if delay_ms.positive?
|
|
91
|
+
writer.write(submit_bytes)
|
|
92
|
+
writer.flush
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
protected
|
|
96
|
+
|
|
97
|
+
def submit_bytes
|
|
98
|
+
"\r"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def blocked_state?(state, enter_only:)
|
|
102
|
+
state[:input_ready] == false && !allow_control_action?(state, enter_only: enter_only)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def allow_control_action?(_state, enter_only:)
|
|
106
|
+
enter_only ? false : false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def blocked_message(state, enter_only:)
|
|
110
|
+
suffix = enter_only ? " for Enter-only input" : ""
|
|
111
|
+
"session is not ready for #{key} prompt input#{suffix} (state: #{state[:state]})"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def prompt_line?(line)
|
|
115
|
+
stripped = line.to_s.strip
|
|
116
|
+
return false if stripped.empty?
|
|
117
|
+
return false if stripped.match?(/\A(?:[>\u203A\u276F]\s*)?\d+\./)
|
|
118
|
+
|
|
119
|
+
PROMPT_PREFIXES.any? { |prefix| stripped.start_with?(prefix) }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def recent_lines(screen_text, limit: 40)
|
|
123
|
+
normalized_screen_text(screen_text).lines.last(limit)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def normalized_screen_text(screen_text)
|
|
127
|
+
text = screen_text.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "")
|
|
128
|
+
text = text.gsub(/\e\][^\a]*?(?:\a|\e\\)/, "")
|
|
129
|
+
text = text.gsub(/\e(?:[@-Z\\-_]|\[[0-?]*[ -\/]*[@-~])/, "")
|
|
130
|
+
text.gsub(/\r\n?/, "\n")
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module Harnex
|
|
2
|
+
module Adapters
|
|
3
|
+
class Claude < Base
|
|
4
|
+
SUBMIT_DELAY_MS = 75
|
|
5
|
+
SUBMIT_DELAY_PER_KB_MS = 50
|
|
6
|
+
|
|
7
|
+
def initialize(extra_args = [])
|
|
8
|
+
super("claude", extra_args)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def base_command
|
|
12
|
+
[
|
|
13
|
+
"claude",
|
|
14
|
+
"--dangerously-skip-permissions"
|
|
15
|
+
]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def input_state(screen_text)
|
|
19
|
+
lines = recent_lines(screen_text, limit: 20)
|
|
20
|
+
text = lines.join
|
|
21
|
+
compact = text.gsub(/\s+/, "")
|
|
22
|
+
|
|
23
|
+
if compact.include?("Quicksafetycheck:") && compact.include?("Yes,Itrustthisfolder")
|
|
24
|
+
{
|
|
25
|
+
state: "workspace-trust-prompt",
|
|
26
|
+
input_ready: false,
|
|
27
|
+
action: "press-enter-to-confirm"
|
|
28
|
+
}
|
|
29
|
+
elsif compact.include?("Entertoconfirm") && compact.include?("Esctocancel")
|
|
30
|
+
{
|
|
31
|
+
state: "confirmation",
|
|
32
|
+
input_ready: false
|
|
33
|
+
}
|
|
34
|
+
elsif compact.include?("--INSERT--") || compact.include?("bypasspermissionson")
|
|
35
|
+
{
|
|
36
|
+
state: "prompt",
|
|
37
|
+
input_ready: true
|
|
38
|
+
}
|
|
39
|
+
elsif compact.include?("NORMAL") || compact.include?("--NORMAL--")
|
|
40
|
+
{
|
|
41
|
+
state: "vim-normal",
|
|
42
|
+
input_ready: true
|
|
43
|
+
}
|
|
44
|
+
elsif lines.any? { |line| prompt_line?(line) }
|
|
45
|
+
{
|
|
46
|
+
state: "prompt",
|
|
47
|
+
input_ready: true
|
|
48
|
+
}
|
|
49
|
+
else
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def build_send_payload(text:, submit:, enter_only:, screen_text:, force: false)
|
|
55
|
+
state = input_state(screen_text)
|
|
56
|
+
if !force && blocked_state?(state, enter_only: enter_only)
|
|
57
|
+
raise ArgumentError, blocked_message(state, enter_only: enter_only)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
steps = []
|
|
61
|
+
unless enter_only
|
|
62
|
+
body = text.to_s
|
|
63
|
+
steps << { text: body, newline: false } unless body.empty?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if submit || enter_only
|
|
67
|
+
step = { text: submit_bytes, newline: false }
|
|
68
|
+
step[:delay_ms] = submit_delay_ms(text) if steps.any?
|
|
69
|
+
steps << step
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
{
|
|
73
|
+
steps: steps,
|
|
74
|
+
input_state: state,
|
|
75
|
+
force: force
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def inject_exit(writer)
|
|
80
|
+
super(writer, delay_ms: SUBMIT_DELAY_MS)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
protected
|
|
84
|
+
|
|
85
|
+
def submit_delay_ms(text)
|
|
86
|
+
extra = (text.to_s.bytesize / 1024.0 * SUBMIT_DELAY_PER_KB_MS).ceil
|
|
87
|
+
SUBMIT_DELAY_MS + extra
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def allow_control_action?(state, enter_only:)
|
|
91
|
+
enter_only && state[:state] == "workspace-trust-prompt"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def blocked_message(state, enter_only:)
|
|
95
|
+
return super unless state[:state] == "workspace-trust-prompt"
|
|
96
|
+
|
|
97
|
+
if enter_only
|
|
98
|
+
"Claude is waiting on the workspace trust prompt"
|
|
99
|
+
else
|
|
100
|
+
"Claude is waiting on the workspace trust prompt; use `harnex send --submit-only` first or `--force` to bypass"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
module Harnex
|
|
2
|
+
module Adapters
|
|
3
|
+
class Codex < Base
|
|
4
|
+
SUBMIT_DELAY_MS = 75
|
|
5
|
+
SUBMIT_DELAY_PER_KB_MS = 50
|
|
6
|
+
SEND_WAIT_SECONDS = 2.0
|
|
7
|
+
|
|
8
|
+
def initialize(extra_args = [])
|
|
9
|
+
super("codex", extra_args)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def base_command
|
|
13
|
+
[
|
|
14
|
+
"codex",
|
|
15
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
16
|
+
"--no-alt-screen"
|
|
17
|
+
]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def infer_repo_path(argv)
|
|
21
|
+
index = 0
|
|
22
|
+
while index < argv.length
|
|
23
|
+
arg = argv[index]
|
|
24
|
+
case arg
|
|
25
|
+
when "-C", "--cd"
|
|
26
|
+
next_value = argv[index + 1]
|
|
27
|
+
return next_value if next_value
|
|
28
|
+
break
|
|
29
|
+
when /\A-C(.+)\z/
|
|
30
|
+
return Regexp.last_match(1)
|
|
31
|
+
end
|
|
32
|
+
index += 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Dir.pwd
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def input_state(screen_text)
|
|
39
|
+
lines = recent_lines(screen_text)
|
|
40
|
+
return super unless lines.any? { |line| line.include?("OpenAI Codex") || line.include?("gpt-") }
|
|
41
|
+
|
|
42
|
+
if lines.any? { |line| prompt_line?(line) }
|
|
43
|
+
{
|
|
44
|
+
state: "prompt",
|
|
45
|
+
input_ready: true
|
|
46
|
+
}
|
|
47
|
+
else
|
|
48
|
+
{
|
|
49
|
+
state: "session",
|
|
50
|
+
input_ready: nil
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def send_wait_seconds(submit:, enter_only:)
|
|
56
|
+
return 0.0 unless submit
|
|
57
|
+
return 0.0 if enter_only
|
|
58
|
+
|
|
59
|
+
SEND_WAIT_SECONDS
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def wait_for_sendable_state?(state, submit:, enter_only:)
|
|
63
|
+
return false unless submit
|
|
64
|
+
return false if enter_only
|
|
65
|
+
|
|
66
|
+
state[:input_ready] != true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_send_payload(text:, submit:, enter_only:, screen_text:, force: false)
|
|
70
|
+
state = input_state(screen_text)
|
|
71
|
+
if !force && submit && !enter_only && state[:input_ready] != true
|
|
72
|
+
raise ArgumentError, blocked_message(state, enter_only: enter_only)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
steps = []
|
|
76
|
+
unless enter_only
|
|
77
|
+
body = text.to_s
|
|
78
|
+
steps << { text: body, newline: false } unless body.empty?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if submit || enter_only
|
|
82
|
+
step = { text: submit_bytes, newline: false }
|
|
83
|
+
step[:delay_ms] = submit_delay_ms(text) if steps.any?
|
|
84
|
+
steps << step
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
steps: steps,
|
|
89
|
+
input_state: state,
|
|
90
|
+
force: force
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def inject_exit(writer)
|
|
95
|
+
super(writer, delay_ms: SUBMIT_DELAY_MS)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
protected
|
|
99
|
+
|
|
100
|
+
def submit_delay_ms(text)
|
|
101
|
+
extra = (text.to_s.bytesize / 1024.0 * SUBMIT_DELAY_PER_KB_MS).ceil
|
|
102
|
+
SUBMIT_DELAY_MS + extra
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def blocked_message(state, enter_only:)
|
|
106
|
+
return super if enter_only
|
|
107
|
+
|
|
108
|
+
"Codex is not at a prompt; wait and retry or use `harnex send --force` (state: #{state[:state]})"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require_relative "adapters/base"
|
|
2
|
+
require_relative "adapters/generic"
|
|
3
|
+
require_relative "adapters/codex"
|
|
4
|
+
require_relative "adapters/claude"
|
|
5
|
+
|
|
6
|
+
module Harnex
|
|
7
|
+
module Adapters
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def known
|
|
11
|
+
registry.keys.sort
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def supported?(key)
|
|
15
|
+
!key.to_s.strip.empty?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def build(key, extra_args = [])
|
|
19
|
+
adapter_class = registry[key.to_s]
|
|
20
|
+
return adapter_class.new(extra_args) if adapter_class
|
|
21
|
+
|
|
22
|
+
Generic.new(key.to_s, extra_args)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def registry
|
|
26
|
+
@registry ||= {
|
|
27
|
+
"claude" => Claude,
|
|
28
|
+
"codex" => Codex
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/harnex/cli.rb
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
module Harnex
|
|
2
|
+
class CLI
|
|
3
|
+
def initialize(argv)
|
|
4
|
+
@argv = argv.dup
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def run
|
|
8
|
+
case @argv.first
|
|
9
|
+
when nil
|
|
10
|
+
puts usage
|
|
11
|
+
0
|
|
12
|
+
when "run"
|
|
13
|
+
Runner.new(@argv.drop(1)).run
|
|
14
|
+
when "send"
|
|
15
|
+
Sender.new(@argv.drop(1)).run
|
|
16
|
+
when "wait"
|
|
17
|
+
Waiter.new(@argv.drop(1)).run
|
|
18
|
+
when "stop"
|
|
19
|
+
Stopper.new(@argv.drop(1)).run
|
|
20
|
+
when "status"
|
|
21
|
+
Status.new(@argv.drop(1)).run
|
|
22
|
+
when "logs"
|
|
23
|
+
Logs.new(@argv.drop(1)).run
|
|
24
|
+
when "pane"
|
|
25
|
+
Pane.new(@argv.drop(1)).run
|
|
26
|
+
when "recipes"
|
|
27
|
+
Recipes.new(@argv.drop(1)).run
|
|
28
|
+
when "guide"
|
|
29
|
+
Guide.new.run
|
|
30
|
+
when "skills"
|
|
31
|
+
Skills.new(@argv.drop(1)).run
|
|
32
|
+
when "help"
|
|
33
|
+
puts help(@argv[1])
|
|
34
|
+
0
|
|
35
|
+
when "-h", "--help"
|
|
36
|
+
puts usage
|
|
37
|
+
0
|
|
38
|
+
else
|
|
39
|
+
raise OptionParser::ParseError, "unknown command #{@argv.first.inspect}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def help(topic)
|
|
46
|
+
case topic
|
|
47
|
+
when "run"
|
|
48
|
+
Runner.usage
|
|
49
|
+
when "send"
|
|
50
|
+
Sender.usage
|
|
51
|
+
when "wait"
|
|
52
|
+
Waiter.usage
|
|
53
|
+
when "stop"
|
|
54
|
+
Stopper.usage
|
|
55
|
+
when "status"
|
|
56
|
+
Status.usage
|
|
57
|
+
when "logs"
|
|
58
|
+
Logs.usage
|
|
59
|
+
when "pane"
|
|
60
|
+
Pane.usage
|
|
61
|
+
when "recipes"
|
|
62
|
+
Recipes.usage
|
|
63
|
+
when "guide"
|
|
64
|
+
Guide.usage
|
|
65
|
+
when "skills"
|
|
66
|
+
Skills.usage
|
|
67
|
+
else
|
|
68
|
+
usage
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def usage
|
|
73
|
+
<<~TEXT
|
|
74
|
+
Usage:
|
|
75
|
+
harnex run <cli> [options] [--] [cli-args...]
|
|
76
|
+
harnex send --id ID [options] [text...]
|
|
77
|
+
harnex wait --id ID [options]
|
|
78
|
+
harnex stop --id ID [options]
|
|
79
|
+
harnex status [options]
|
|
80
|
+
harnex logs --id ID [options]
|
|
81
|
+
harnex pane --id ID [options]
|
|
82
|
+
harnex help [command]
|
|
83
|
+
|
|
84
|
+
Commands:
|
|
85
|
+
run Start a wrapped interactive session and local API
|
|
86
|
+
send Send text to an active session
|
|
87
|
+
wait Block until a session exits or reaches a state
|
|
88
|
+
stop Send the adapter stop sequence to a session
|
|
89
|
+
status List live sessions
|
|
90
|
+
logs Read session output transcripts
|
|
91
|
+
pane Capture the current tmux pane for a live session
|
|
92
|
+
recipes List and read workflow recipes
|
|
93
|
+
guide Show the getting started guide
|
|
94
|
+
skills Install bundled skills into a repo or globally
|
|
95
|
+
help Show command help
|
|
96
|
+
|
|
97
|
+
New to harnex? Start with: harnex guide
|
|
98
|
+
|
|
99
|
+
Notes:
|
|
100
|
+
CLIs with smart prompt detection: #{Adapters.known.join(', ')}
|
|
101
|
+
Any other CLI name is launched with generic wrapping.
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
harnex run codex
|
|
105
|
+
harnex run aider --id blue-cat
|
|
106
|
+
harnex run codex -- --cd /path/to/repo
|
|
107
|
+
harnex status
|
|
108
|
+
harnex logs --id main --follow
|
|
109
|
+
harnex pane --id main --lines 40
|
|
110
|
+
harnex send --id main --message "Summarize current progress."
|
|
111
|
+
harnex skills install close
|
|
112
|
+
TEXT
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Harnex
|
|
2
|
+
class Guide
|
|
3
|
+
GUIDE_PATH = File.expand_path("../../../../GUIDE.md", __FILE__)
|
|
4
|
+
|
|
5
|
+
def self.usage
|
|
6
|
+
<<~TEXT
|
|
7
|
+
Usage: harnex guide
|
|
8
|
+
|
|
9
|
+
Print the getting started guide.
|
|
10
|
+
TEXT
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run
|
|
14
|
+
unless File.exist?(GUIDE_PATH)
|
|
15
|
+
warn("harnex guide: GUIDE.md not found at #{GUIDE_PATH}")
|
|
16
|
+
return 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
puts File.read(GUIDE_PATH)
|
|
20
|
+
0
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|