cline-rb 1.0.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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +139 -0
  3. data/README.md +1216 -0
  4. data/TODO.md +2 -0
  5. data/lib/cline/cli.rb +373 -0
  6. data/lib/cline/config.rb +100 -0
  7. data/lib/cline/configuration.rb +23 -0
  8. data/lib/cline/data.rb +119 -0
  9. data/lib/cline/file_content.rb +33 -0
  10. data/lib/cline/global_settings.rb +17 -0
  11. data/lib/cline/global_state/api_providers.rb +48 -0
  12. data/lib/cline/global_state/auto_approval.rb +73 -0
  13. data/lib/cline/global_state/browser.rb +52 -0
  14. data/lib/cline/global_state/features.rb +56 -0
  15. data/lib/cline/global_state/general.rb +77 -0
  16. data/lib/cline/global_state/models.rb +127 -0
  17. data/lib/cline/global_state/toggles.rb +33 -0
  18. data/lib/cline/global_state/workspace.rb +41 -0
  19. data/lib/cline/global_state.rb +16 -0
  20. data/lib/cline/log.rb +288 -0
  21. data/lib/cline/logs.rb +136 -0
  22. data/lib/cline/mcp_settings.rb +30 -0
  23. data/lib/cline/model.rb +47 -0
  24. data/lib/cline/models.rb +11 -0
  25. data/lib/cline/overlay_hash.rb +125 -0
  26. data/lib/cline/providers.rb +59 -0
  27. data/lib/cline/schema.rb +144 -0
  28. data/lib/cline/secret_string.rb +83 -0
  29. data/lib/cline/secrets.rb +119 -0
  30. data/lib/cline/serializable/cline_data.rb +131 -0
  31. data/lib/cline/serializable/dir.rb +81 -0
  32. data/lib/cline/serializable/file.rb +106 -0
  33. data/lib/cline/session.rb +87 -0
  34. data/lib/cline/session_data.rb +154 -0
  35. data/lib/cline/session_message.rb +178 -0
  36. data/lib/cline/session_messages.rb +61 -0
  37. data/lib/cline/sessions.rb +30 -0
  38. data/lib/cline/skill.rb +148 -0
  39. data/lib/cline/skills.rb +8 -0
  40. data/lib/cline/task.rb +75 -0
  41. data/lib/cline/task_message.rb +247 -0
  42. data/lib/cline/task_messages.rb +11 -0
  43. data/lib/cline/tasks.rb +30 -0
  44. data/lib/cline/usage.rb +37 -0
  45. data/lib/cline/utils/enumerable_dir_objects.rb +103 -0
  46. data/lib/cline/utils/file.rb +71 -0
  47. data/lib/cline/utils/file_monitor.rb +56 -0
  48. data/lib/cline/utils/logger.rb +37 -0
  49. data/lib/cline/utils/os/linux.rb +43 -0
  50. data/lib/cline/utils/os/mingw32.rb +46 -0
  51. data/lib/cline/utils/os.rb +31 -0
  52. data/lib/cline/utils/schema.rb +290 -0
  53. data/lib/cline/version.rb +6 -0
  54. data/lib/cline/workspace.rb +25 -0
  55. data/lib/cline/workspace_settings.rb +29 -0
  56. data/lib/cline/workspaces.rb +8 -0
  57. data/lib/cline.rb +22 -0
  58. metadata +249 -0
@@ -0,0 +1,87 @@
1
+ require 'forwardable'
2
+
3
+ module Cline
4
+ # A session defined in a directory
5
+ class Session
6
+ extend Forwardable
7
+
8
+ # @!group Public API
9
+
10
+ include Serializable::Dir
11
+
12
+ # Delegates all data attributes to the data object
13
+ def_delegators :data, *SessionData.attributes.keys
14
+
15
+ # Get the session's data
16
+ #
17
+ # @param create [Boolean] Should the data be created if it does not exist?
18
+ # @return [SessionData, nil] The session's data, or nil if none
19
+ def data(create: self.create)
20
+ @data ||= SessionData.from_cline_data(dir, create:)
21
+ end
22
+
23
+ # Get the session's messages
24
+ #
25
+ # @param create [Boolean] Should the messages be created if they don't exist?
26
+ # @return [SessionMessages, nil] The session's messages, or nil if none
27
+ def messages(create: self.create)
28
+ @messages ||= SessionMessages.from_cline_data(dir, cline_models: @cline_models, create:)
29
+ end
30
+
31
+ # Equality check
32
+ #
33
+ # @param other [Object] The other to check equality with
34
+ # @return [Boolean] True if objects are equal
35
+ def ==(other)
36
+ other.is_a?(Session) &&
37
+ other.data == data &&
38
+ other.messages == messages
39
+ end
40
+
41
+ # Monitor messages with a callback called when new or updated messages arrive
42
+ #
43
+ # @param on_message [#call] Block called each time there is a new or updated message.
44
+ # * Param message [SessionMessage] Message that has happened
45
+ # * Param last [Boolean] Is this the last message fetched from the list of messages?
46
+ # * Param previous_version [SessionMessage or nil] Previous version of this message if it got updated, or nil if it is a new one
47
+ # @param monitoring_interval_secs [Float] The monitoring interval in seconds
48
+ # @yield Optional code called while monitoring is in place.
49
+ # If used then monitoring is stopped at the end of the block's execution.
50
+ # @return [FileMonitor, nil] If no block has been given, return the monitor that needs to be
51
+ # stopped by the caller when monitoring should end.
52
+ def monitor_messages(on_message:, monitoring_interval_secs: 1, &)
53
+ # Keep messages per timestamp to detect updates
54
+ messages = {}
55
+ SessionMessages.monitor_cline_data_changes(
56
+ dir,
57
+ cline_models: @cline_models,
58
+ on_change: proc do |new_messages|
59
+ # Update the messages we have
60
+ @messages = new_messages
61
+ if new_messages&.messages && !new_messages.messages.empty?
62
+ # Check for updates in all messages
63
+ last_idx = new_messages.messages.size - 1
64
+ new_messages.messages.each.with_index do |message, idx|
65
+ ts = message.ts
66
+ if message != messages[ts]
67
+ on_message.call(message, idx == last_idx, messages[ts])
68
+ messages[ts] = message
69
+ end
70
+ end
71
+ end
72
+ end,
73
+ monitoring_interval_secs:,
74
+ &
75
+ )
76
+ end
77
+
78
+ # @!group Internal
79
+
80
+ # Constructor
81
+ #
82
+ # @param cline_models [Models] The Cline models used to interpret the tasks' messages
83
+ def initialize(cline_models:)
84
+ @cline_models = cline_models
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,154 @@
1
+ module Cline
2
+ # Session data
3
+ class SessionData < Schema
4
+ # @!group Public API
5
+
6
+ Serializable::ClineData.include_for(self, proc { |base_dir| "#{File.basename(base_dir)}.json" })
7
+
8
+ # Session metadata
9
+ class Metadata < Schema
10
+ # @!group Public API
11
+
12
+ # Checkpoint metadata
13
+ class Checkpoint < Schema
14
+ # @!group Public API
15
+
16
+ # Checkpoint entry (ref, timestamps, etc.)
17
+ class CheckpointEntry < Schema
18
+ # @!group Public API
19
+
20
+ # @return [String] Git ref of the checkpoint
21
+ attribute :ref, :string
22
+
23
+ # @return [Integer] Timestamp when the checkpoint was created
24
+ attribute :created_at, :integer
25
+
26
+ # @return [Integer] Number of runs for this checkpoint
27
+ attribute :run_count, :integer
28
+
29
+ # @return [String] Kind of checkpoint (e.g. "stash")
30
+ attribute :kind, :string
31
+ end
32
+
33
+ # @return [CheckpointEntry] Latest checkpoint entry
34
+ attribute :latest, CheckpointEntry
35
+
36
+ # @return [Array<CheckpointEntry>] History of checkpoint entries
37
+ attribute :history, Utils::Schema.collection(CheckpointEntry)
38
+ end
39
+
40
+ # Usage statistics
41
+ class Usage < Schema
42
+ # @!group Public API
43
+
44
+ # @return [Integer] Input tokens count
45
+ attribute :input_tokens, :integer
46
+
47
+ # @return [Integer] Output tokens count
48
+ attribute :output_tokens, :integer
49
+
50
+ # @return [Integer] Cache read tokens count
51
+ attribute :cache_read_tokens, :integer
52
+
53
+ # @return [Integer] Cache write tokens count
54
+ attribute :cache_write_tokens, :integer
55
+
56
+ # @return [Float] Total cost of the API call
57
+ attribute :total_cost, :float
58
+ end
59
+
60
+ # @return [Checkpoint] Checkpoint information
61
+ attribute :checkpoint, Checkpoint
62
+
63
+ # @return [String] Title of the session
64
+ attribute :title, :string
65
+
66
+ # @return [Float] Total cost of the session
67
+ attribute :total_cost, :float
68
+
69
+ # @return [Float] Aggregated agents cost
70
+ attribute :aggregated_agents_cost, :float
71
+
72
+ # @return [Usage] Usage statistics
73
+ attribute :usage, Usage
74
+
75
+ # @return [Usage] Aggregate usage statistics
76
+ attribute :aggregate_usage, Usage
77
+ end
78
+
79
+ # @return [Integer] Version number
80
+ attribute :version, :integer
81
+
82
+ # @return [String] Unique session identifier
83
+ attribute :session_id, :string
84
+
85
+ # @return [String] Source of the session (e.g. "cli")
86
+ attribute :source, :string
87
+
88
+ # @return [Integer] Process ID
89
+ attribute :pid, :integer
90
+
91
+ # @return [String] Start time of the session (ISO 8601)
92
+ attribute :started_at, :string
93
+
94
+ # @return [String] End time of the session (ISO 8601)
95
+ attribute :ended_at, :string
96
+
97
+ # @return [Integer] Exit code of the process
98
+ attribute :exit_code, :integer
99
+
100
+ # @return [String] Session status (e.g. "completed")
101
+ attribute :status, :string
102
+
103
+ # @return [Boolean] Whether the session is interactive
104
+ attribute :interactive, :boolean
105
+
106
+ # @return [String] Provider name (e.g. "cline")
107
+ attribute :provider, :string
108
+
109
+ # @return [String] Model name (e.g. "deepseek/deepseek-v4-flash")
110
+ attribute :model, :string
111
+
112
+ # @return [String] Current working directory
113
+ attribute :cwd, :string
114
+
115
+ # @return [String] Workspace root path
116
+ attribute :workspace_root, :string
117
+
118
+ # @return [String] Team name
119
+ attribute :team_name, :string
120
+
121
+ # @return [Boolean] Whether tools are enabled
122
+ attribute :enable_tools, :boolean
123
+
124
+ # @return [Boolean] Whether spawn is enabled
125
+ attribute :enable_spawn, :boolean
126
+
127
+ # @return [Boolean] Whether teams are enabled
128
+ attribute :enable_teams, :boolean
129
+
130
+ # @return [String] Prompt for the session
131
+ attribute :prompt, :string
132
+
133
+ # @return [Metadata] Session metadata
134
+ attribute :metadata, Metadata
135
+
136
+ # @return [String] Path to the messages file
137
+ attribute :messages_path, :string
138
+
139
+ cline_snake_attributes(
140
+ *%i[
141
+ session_id
142
+ started_at
143
+ ended_at
144
+ exit_code
145
+ workspace_root
146
+ team_name
147
+ enable_tools
148
+ enable_spawn
149
+ enable_teams
150
+ messages_path
151
+ ]
152
+ )
153
+ end
154
+ end
@@ -0,0 +1,178 @@
1
+ require 'ellipsized'
2
+ require 'time'
3
+
4
+ module Cline
5
+ # Session's message
6
+ class SessionMessage < Schema
7
+ # @!group Public API
8
+
9
+ # Model info in session messages
10
+ class ModelInfo < Schema
11
+ # @!group Public API
12
+
13
+ # @return [String] Model ID
14
+ attribute :id, :string
15
+
16
+ # @return [String] Provider
17
+ attribute :provider, :string
18
+
19
+ # @return [String] Model family
20
+ attribute :family, :string
21
+ end
22
+
23
+ # Metrics in session messages
24
+ class Metrics < Schema
25
+ # @!group Public API
26
+
27
+ # @return [Integer] Input tokens count
28
+ attribute :input_tokens, :integer
29
+
30
+ # @return [Integer] Output tokens count
31
+ attribute :output_tokens, :integer
32
+
33
+ # @return [Integer] Cache read tokens count
34
+ attribute :cache_read_tokens, :integer
35
+
36
+ # @return [Integer] Cache write tokens count
37
+ attribute :cache_write_tokens, :integer
38
+
39
+ # @return [Float] Cost of the API call
40
+ attribute :cost, :float
41
+ end
42
+
43
+ # A content block in a session message.
44
+ # All attributes are optional, some will be nil depending on the type of content.
45
+ class MessageContent < Schema
46
+ # @!group Public API
47
+
48
+ # An input used for tool use content.
49
+ class ToolUseInput < Schema
50
+ # @!group Public API
51
+
52
+ # @return [String] Question
53
+ attribute :question, :string
54
+
55
+ # @return [Array<String>] List of options for the given question
56
+ attribute :options, Utils::Schema.collection(:string)
57
+ end
58
+
59
+ # @return [String] Content block type (text, tool_use, tool_result)
60
+ attribute :type, :string
61
+
62
+ # @return [String, nil] Text content (for type "text")
63
+ attribute :text, :string
64
+
65
+ # @return [String, nil] Tool use identifier (for type "tool_use")
66
+ attribute :id, :string
67
+
68
+ # @return [String, nil] Tool name (for type "tool_use")
69
+ attribute :name, :string
70
+
71
+ # @return [ToolUseInput, nil] Tool input parameters (for type "tool_use")
72
+ attribute :input, ToolUseInput
73
+
74
+ # @return [String, nil] Tool use identifier this result corresponds to (for type "tool_result")
75
+ attribute :tool_use_id, :string
76
+
77
+ # @return [String, nil] Content of the tool result (for type "tool_result")
78
+ attribute :content, :string
79
+
80
+ cline_snake_attributes :tool_use_id
81
+ end
82
+
83
+ # @return [String] Message identifier
84
+ attribute :id, :string
85
+
86
+ # @return [String] Role (user or assistant)
87
+ attribute :role, :string
88
+
89
+ # @return [Array<MessageContent>] Content blocks
90
+ attribute :content, Utils::Schema.collection(MessageContent)
91
+
92
+ # @return [Integer] Message timestamp in milliseconds
93
+ attribute :ts, :integer
94
+
95
+ # @return [ModelInfo, nil] Model metadata
96
+ attribute :model_info, ModelInfo
97
+
98
+ # @return [Metrics, nil] Usage metrics
99
+ attribute :metrics, Metrics
100
+
101
+ # Get the message timestamp as a Ruby time
102
+ #
103
+ # @return [Time] The message timestamp
104
+ def timestamp
105
+ @timestamp ||= Time.at(ts / 1000.0)
106
+ end
107
+
108
+ # Get the usage statistics of this message, if any
109
+ #
110
+ # @return [Usage, nil] The usage statistics, or nil if none
111
+ def usage
112
+ return unless metrics
113
+
114
+ @usage ||= Usage.new(
115
+ **{
116
+ cost: metrics.cost,
117
+ input_tokens: metrics.input_tokens,
118
+ output_tokens: metrics.output_tokens,
119
+ cache_read_tokens: metrics.cache_read_tokens,
120
+ cache_write_tokens: metrics.cache_write_tokens,
121
+ cline_model: cline_models && cline_models[model_info.id]
122
+ }.compact
123
+ )
124
+ end
125
+
126
+ # Return a human-friendly version of a message.
127
+ # Useful for stdout or logging.
128
+ #
129
+ # @param limit [Integer] Number of characters the message should be limited to
130
+ # @return [String] The human translation
131
+ def to_human(limit: 128)
132
+ (
133
+ case role
134
+ when 'user'
135
+ "User: #{first_text}"
136
+ when 'assistant'
137
+ tool_use = content.find { |c| c.type == 'tool_use' }
138
+ if tool_use
139
+ "Assistant uses #{tool_use.name}"
140
+ else
141
+ "Assistant: #{first_text}"
142
+ end
143
+ when 'tool'
144
+ "Tool result: #{content.find { |c| c.type == 'tool_result' }&.content}"
145
+ else
146
+ to_s
147
+ end
148
+ ).ellipsized(limit)
149
+ end
150
+
151
+ # @!group Internal
152
+
153
+ # Parse a Hash object and instantiate the proper instance from it.
154
+ #
155
+ # @param hash [Hash] Data
156
+ # @param args [Array] Remaining arguments to be transferred to Shale
157
+ # @param cline_models [Models, nil] The Clines models used to interpret the message, or nil if none
158
+ # @param kwargs [Hash] Remaining kwargs to be transferred to Shale
159
+ # @return [Schema] Corresponding instance
160
+ def self.of_hash(hash, *args, cline_models: nil, **kwargs)
161
+ instance = super(hash, *args, **kwargs)
162
+ instance.cline_models = cline_models if cline_models
163
+ instance
164
+ end
165
+
166
+ # @return [Models] The Clines models used to interpret the message
167
+ attr_accessor :cline_models
168
+
169
+ private
170
+
171
+ # Get the text from the first text content block.
172
+ #
173
+ # @return [String] The first text content, or an empty string if none
174
+ def first_text
175
+ content.find { |c| c.type == 'text' }&.text.to_s.gsub("\n", ' ')
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,61 @@
1
+ require 'forwardable'
2
+
3
+ module Cline
4
+ # Session messages
5
+ class SessionMessages < Schema
6
+ extend Forwardable
7
+
8
+ # @!group Public API
9
+
10
+ Serializable::ClineData.include_for(self, proc { |base_dir| "#{File.basename(base_dir)}.messages.json" })
11
+
12
+ # @return [Integer] Schema version
13
+ attribute :version, :integer
14
+
15
+ # @return [String] Last update timestamp of this messages file
16
+ attribute :updated_at, :string
17
+
18
+ # @return [String] Agent identifier (lead or agent)
19
+ attribute :agent, :string
20
+
21
+ # @return [String] Session ID
22
+ attribute :session_id, :string
23
+
24
+ # @return [Array<SessionMessage>] Messages of this session
25
+ attribute :messages, Utils::Schema.collection(SessionMessage)
26
+
27
+ # @return [String] System prompt used for this session
28
+ attribute :system_prompt, :string
29
+
30
+ cline_snake_attributes(*%i[updated_at system_prompt])
31
+
32
+ # Delegates enumerating methods to the internal messages
33
+ def_delegators :wrapped_messages, *%i[[] << each empty? last size]
34
+
35
+ include Enumerable
36
+
37
+ # @!group Internal
38
+
39
+ # Parse a Hash object and instantiate the proper instance from it.
40
+ #
41
+ # @param hash [Hash] Data
42
+ # @param args [Array] Remaining arguments to be transferred to Shale
43
+ # @param cline_models [Models] The Cline models used to interpret the tasks' messages
44
+ # @param kwargs [Hash] Remaining kwargs to be transferred to Shale
45
+ # @return [Schema] Corresponding instance
46
+ def self.of_hash(hash, *args, cline_models:, **kwargs)
47
+ instance = super(hash, *args, **kwargs)
48
+ # Shale doesn't pass extra kwargs to nested collection items,
49
+ # so we set cline_models on each message after deserialization
50
+ instance.messages&.each { |message| message.cline_models = cline_models }
51
+ instance
52
+ end
53
+
54
+ # Return the messages as an array, even if there are no message.
55
+ #
56
+ # @return [Array] The messages
57
+ def wrapped_messages
58
+ messages || []
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,30 @@
1
+ module Cline
2
+ # Provide a set of sessions from a directory
3
+ class Sessions
4
+ # @!group Public API
5
+
6
+ include Utils::EnumerableDirObjects
7
+
8
+ # @!group Internal
9
+
10
+ # Constructor
11
+ #
12
+ # @param cline_models [Models] The Cline models used to interpret the sessions' messages
13
+ def initialize(cline_models:)
14
+ @cline_models = cline_models
15
+ end
16
+
17
+ private
18
+
19
+ # Get an object and its name from a sub-directory
20
+ #
21
+ # @param dir [String] The directory containing the object
22
+ # @param create [Boolean] Should the instance be created if it does not exist?
23
+ # @return [Array(String, Object)] Return 2 values:
24
+ # 0. [String] The object name
25
+ # 1. [Object] The object itself
26
+ def object_from(dir, create:)
27
+ [File.basename(dir), Session.open(dir, cline_models: @cline_models, create:)]
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,148 @@
1
+ require 'front_matter_parser'
2
+ require 'json'
3
+ require 'yaml'
4
+
5
+ module Cline
6
+ # A skill defined in a directory
7
+ class Skill
8
+ # @!group Public API
9
+
10
+ include Serializable::Dir
11
+
12
+ # Get the skill's name
13
+ #
14
+ # @return [String] Skill name
15
+ def name
16
+ File.basename(dir)
17
+ end
18
+
19
+ # Get the skill's YAML front matter
20
+ #
21
+ # @return [Hash{Symbol => Object}, nil] The skill's front matter, or nil if none
22
+ def yaml_front_matter
23
+ content = skill_file_content('SKILL.md')&.content
24
+ JSON.parse(JSON[FrontMatterParser::Parser.new(:md).call(content).front_matter], symbolize_names: true) if content
25
+ end
26
+
27
+ # Equality check
28
+ #
29
+ # @param other [Object] The other to check equality with
30
+ # @return [Boolean] True if objects are equal
31
+ def ==(other)
32
+ other.is_a?(Skill) &&
33
+ other.name == name &&
34
+ other.files.compact == files.compact
35
+ end
36
+
37
+ # Get the files of this skill
38
+ #
39
+ # @return [Hash{String => FileContent, nil}] The files
40
+ def files
41
+ discover_files
42
+ @files
43
+ end
44
+
45
+ # Save the skill files
46
+ def save
47
+ raise 'This instance has not been initialized from a Skill directory' unless dir
48
+
49
+ # First create/update all known files
50
+ files.each do |file_path, file_content|
51
+ next unless file_content
52
+
53
+ file_full_path = File.join(dir, file_path)
54
+ FileUtils.mkdir_p(File.dirname(file_full_path))
55
+ File.write(file_full_path, file_content.content)
56
+ end
57
+ # Then delete any file that is not known
58
+ each_file do |file_path|
59
+ File.unlink(File.join(dir, file_path)) unless files[file_path]
60
+ end
61
+ end
62
+
63
+ # @return [Boolean] Is the skill enabled?
64
+ def enabled?
65
+ !yaml_front_matter || yaml_front_matter[:disabled] != true
66
+ end
67
+
68
+ # Enable the skill
69
+ def enable
70
+ return if enabled?
71
+
72
+ modify_skill_front_matter do |front_matter|
73
+ front_matter.except('disabled')
74
+ end
75
+ end
76
+
77
+ # Disable the skill
78
+ def disable
79
+ return unless enabled?
80
+
81
+ modify_skill_front_matter do |front_matter|
82
+ front_matter.merge('disabled' => true)
83
+ end
84
+ end
85
+
86
+ # @!group Internal
87
+
88
+ # Constructor
89
+ def initialize
90
+ @files = {}
91
+ @files_discovered = false
92
+ end
93
+
94
+ private
95
+
96
+ # Loop over all existing relative file paths inside our directory
97
+ #
98
+ # @yield The code to execute for each file found
99
+ # @yieldparam file_path [String] The relative file path
100
+ def each_file
101
+ dir_regexp = %r{^#{Regexp.escape(dir)}/}
102
+ Dir.glob("#{dir}/**/*", File::FNM_DOTMATCH).each do |path|
103
+ yield(path.gsub(dir_regexp, '')) unless File.directory?(path)
104
+ end
105
+ end
106
+
107
+ # Discover all files of this skill.
108
+ # Remember when files were discovered to cache it.
109
+ def discover_files
110
+ return if @files_discovered
111
+
112
+ each_file do |file_path|
113
+ skill_file_content(file_path)
114
+ end
115
+ @files_discovered = true
116
+ end
117
+
118
+ # Get the file content from a file path in this skill
119
+ #
120
+ # @param file_path [String] The file path to retrieve content from
121
+ # @return [FileContent, nil] The file content, or nil if none
122
+ def skill_file_content(file_path)
123
+ @files[file_path] = FileContent.open(subpath(file_path)) unless @files.key?(file_path)
124
+ @files[file_path]
125
+ end
126
+
127
+ # Modify the content of a skill file's front matter
128
+ #
129
+ # @yield [front_matter] The block to execute to modify the front matter
130
+ # @yieldparam front_matter [Hash] The parsed front matter hash
131
+ # @yieldreturn [Hash] The modified front matter hash
132
+ def modify_skill_front_matter
133
+ content = skill_file_content('SKILL.md')&.content
134
+ unless content
135
+ # Initialize it
136
+ files['SKILL.md'] = FileContent.new("---\n\n\n---\n")
137
+ content = skill_file_content('SKILL.md').content
138
+ end
139
+ new_front_matter = yield(YAML.safe_load(content.match(/^---\n(.+?)\n---/m)[1]) || {})
140
+ content.replace(
141
+ content.gsub(
142
+ /^---\n(.+?)\n---/m,
143
+ "---\n#{YAML.dump(new_front_matter).gsub(/\A---\n/, '') unless new_front_matter.empty?}---"
144
+ )
145
+ )
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,8 @@
1
+ module Cline
2
+ # Provide a set of skills from a directory
3
+ class Skills
4
+ # @!group Public API
5
+
6
+ Utils::EnumerableDirObjects.include_for(self, Skill)
7
+ end
8
+ end