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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +139 -0
- data/README.md +1216 -0
- data/TODO.md +2 -0
- data/lib/cline/cli.rb +373 -0
- data/lib/cline/config.rb +100 -0
- data/lib/cline/configuration.rb +23 -0
- data/lib/cline/data.rb +119 -0
- data/lib/cline/file_content.rb +33 -0
- data/lib/cline/global_settings.rb +17 -0
- data/lib/cline/global_state/api_providers.rb +48 -0
- data/lib/cline/global_state/auto_approval.rb +73 -0
- data/lib/cline/global_state/browser.rb +52 -0
- data/lib/cline/global_state/features.rb +56 -0
- data/lib/cline/global_state/general.rb +77 -0
- data/lib/cline/global_state/models.rb +127 -0
- data/lib/cline/global_state/toggles.rb +33 -0
- data/lib/cline/global_state/workspace.rb +41 -0
- data/lib/cline/global_state.rb +16 -0
- data/lib/cline/log.rb +288 -0
- data/lib/cline/logs.rb +136 -0
- data/lib/cline/mcp_settings.rb +30 -0
- data/lib/cline/model.rb +47 -0
- data/lib/cline/models.rb +11 -0
- data/lib/cline/overlay_hash.rb +125 -0
- data/lib/cline/providers.rb +59 -0
- data/lib/cline/schema.rb +144 -0
- data/lib/cline/secret_string.rb +83 -0
- data/lib/cline/secrets.rb +119 -0
- data/lib/cline/serializable/cline_data.rb +131 -0
- data/lib/cline/serializable/dir.rb +81 -0
- data/lib/cline/serializable/file.rb +106 -0
- data/lib/cline/session.rb +87 -0
- data/lib/cline/session_data.rb +154 -0
- data/lib/cline/session_message.rb +178 -0
- data/lib/cline/session_messages.rb +61 -0
- data/lib/cline/sessions.rb +30 -0
- data/lib/cline/skill.rb +148 -0
- data/lib/cline/skills.rb +8 -0
- data/lib/cline/task.rb +75 -0
- data/lib/cline/task_message.rb +247 -0
- data/lib/cline/task_messages.rb +11 -0
- data/lib/cline/tasks.rb +30 -0
- data/lib/cline/usage.rb +37 -0
- data/lib/cline/utils/enumerable_dir_objects.rb +103 -0
- data/lib/cline/utils/file.rb +71 -0
- data/lib/cline/utils/file_monitor.rb +56 -0
- data/lib/cline/utils/logger.rb +37 -0
- data/lib/cline/utils/os/linux.rb +43 -0
- data/lib/cline/utils/os/mingw32.rb +46 -0
- data/lib/cline/utils/os.rb +31 -0
- data/lib/cline/utils/schema.rb +290 -0
- data/lib/cline/version.rb +6 -0
- data/lib/cline/workspace.rb +25 -0
- data/lib/cline/workspace_settings.rb +29 -0
- data/lib/cline/workspaces.rb +8 -0
- data/lib/cline.rb +22 -0
- 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
|
data/lib/cline/skill.rb
ADDED
|
@@ -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
|