git-fit 0.22.0 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f9fda747ee0bdc1b1d6588bce636576cb8e371ea0ae36b2bcfedd377a516891
4
- data.tar.gz: e19094889528196ead4715706a46fe035516d219ba2c69264cf32e4fc0820da6
3
+ metadata.gz: 6d74822bf3f31986abcfc5c12266b5563a2f52b62b2f3495c4fc4a13c351540c
4
+ data.tar.gz: 423954310f4f01049fa29b24d40ca8e6b9bce856429446d22a84b91817bd0024
5
5
  SHA512:
6
- metadata.gz: a3fec0c7cfe0f51d7087698900585e480b9ce52d73aa6a8238cfa2493d5500e9d20e93087dda785da91844b3099ea1c4d36b4e642dbd867187d94843ae63b466
7
- data.tar.gz: b98ec2e23dc34c3c5694d4627c6ae12a153a2b185d76f0f9567903c2e39a4ff6bb7995ff928e4b17ef4978cdd0a03485e38ae41ef17d9f95bf63f16870d2324b
6
+ metadata.gz: 1c01b5a2fe18991ad86bddb9c2d52f529b69d4c047e168e63fa38cd184baedbbd72539b6be210cf2b21bc11bf0d4c4e27c996d7eb0437e2319a60bda60660999
7
+ data.tar.gz: ebcbb62b7e5ff968c6e814affb9fc733c441e850205ba99ec063df96c36e72b2418892229490d8a6d528e3f4d44f49284cd5f0be0659b21e930415069e336cfb
data/lib/git-fit.rb CHANGED
@@ -90,7 +90,9 @@ require_relative 'git_fit/install/actions'
90
90
  require_relative 'git_fit/cli/install_cli'
91
91
  require_relative 'git_fit/cli/purge_cli'
92
92
  require_relative 'git_fit/cli/gh_cli'
93
+ require_relative 'git_fit/mcp'
93
94
  require_relative 'git_fit/cli/export'
95
+ require_relative 'git_fit/cli/mcp'
94
96
  require_relative 'git_fit/cli/import_cli'
95
97
  require_relative 'git_fit/cli/geo_cli'
96
98
  require_relative 'git_fit/strava_web/file_check'
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mcp'
4
+
5
+ module GitFit
6
+ class McpCLI < Thor
7
+ desc 'serve', 'Start the MCP stdio server (read-only workouts data for AI clients)'
8
+
9
+ no_commands do
10
+ def git_fit_config
11
+ @git_fit_config ||= GitFit::Config.new(options[:config])
12
+ end
13
+ end
14
+
15
+ def serve
16
+ config = git_fit_config
17
+ server = GitFit::MCP.build_server(config)
18
+ ::MCP::Server::Transports::StdioTransport.new(server).open
19
+ rescue StandardError => e
20
+ say_status :error, "MCP server failed: #{e.message}", :red
21
+ exit 1
22
+ end
23
+ end
24
+ end
data/lib/git_fit/cli.rb CHANGED
@@ -23,6 +23,9 @@ module GitFit
23
23
  desc 'export SUBCOMMAND', 'Export activities to various formats'
24
24
  subcommand 'export', ExportCLI
25
25
 
26
+ desc 'mcp serve', 'Start the MCP stdio server (read-only workouts data for AI clients)'
27
+ subcommand 'mcp', McpCLI
28
+
26
29
  desc 'version', 'Show version'
27
30
  def version
28
31
  puts "git-fit v#{GitFit::VERSION}"
@@ -90,10 +90,34 @@ module GitFit
90
90
  end
91
91
  end
92
92
 
93
+ # 报告周期取「最近完整期」:当最新数据落在进行中的当期,报告主体回退到
94
+ # 上一完整期(当期在上一期无数据时保留当期,如新用户/空库场景)。
95
+ # 当月的部分数据仍会出现在活动摘要中,模板要求模型仅作『最新动态』提及。
93
96
  def period_label
94
97
  latest = activity_rows.find { |r| r[:start_date_local] }
95
- base = latest ? latest[:start_date_local].to_s : Time.now.utc.iso8601
96
- @period == 'yearly' ? base[0, 4] : base[0, 7]
98
+ base = latest ? latest[:start_date_local].to_s[0, period_width] : Time.now.utc.iso8601[0, period_width]
99
+ current = Time.now.strftime(period_format)
100
+ return base if base < current
101
+
102
+ previous = previous_period(current)
103
+ activity_rows.any? { |r| r[:start_date_local].to_s.start_with?(previous) } ? previous : base
104
+ end
105
+
106
+ def period_width
107
+ @period == 'yearly' ? 4 : 7
108
+ end
109
+
110
+ def period_format
111
+ @period == 'yearly' ? '%Y' : '%Y-%m'
112
+ end
113
+
114
+ def previous_period(period)
115
+ if @period == 'yearly'
116
+ (period.to_i - 1).to_s
117
+ else
118
+ require 'date'
119
+ (Date.parse("#{period}-01") << 1).strftime('%Y-%m')
120
+ end
97
121
  end
98
122
 
99
123
  # Digest covers everything that changes the output: data, prompt mode,
@@ -104,6 +128,7 @@ module GitFit
104
128
  'model' => @client.model,
105
129
  'language' => @language,
106
130
  'period_mode' => @period,
131
+ 'period' => period_label,
107
132
  'stats' => stats,
108
133
  'activities' => lines,
109
134
  )
@@ -185,6 +210,7 @@ module GitFit
185
210
  def slots(stats, _rows)
186
211
  {
187
212
  'period' => period_label,
213
+ 'today' => Time.now.strftime('%Y-%m-%d'),
188
214
  'stats' => ::JSON.generate(stats),
189
215
  'activities_summary' => activity_lines.join("\n"),
190
216
  'language' => @language,
@@ -25,6 +25,7 @@ module GitFit
25
25
 
26
26
  ## 写作要求
27
27
  - 汇总统计中的 summary 是全历史累计,不是 {{period}} 当期数据;当期数据以 monthly/yearly 中 {{period}} 对应行为准,活动摘要可用作补充
28
+ - 今天是 {{today}}。若活动摘要中出现 {{period}} 之后的当月数据,那是不完整的当月数据:仅在结尾以『最新动态』一句带过并明确标注,不纳入总结主体,禁止据此得出训练量骤降/中断等跨周期结论
28
29
  - 用 {{language}} 撰写,Markdown 格式,正文 800 字以内
29
30
  - 结构:先总后分 —— 总体负荷印象 → 亮点 → 隐忧
30
31
  - 所有数字必须来自输入数据,禁止编造或外推
@@ -43,6 +44,7 @@ module GitFit
43
44
 
44
45
  ## 分析要求
45
46
  - 汇总统计中的 summary 是全历史累计,不是 {{period}} 当期数据;对比当期与历史时以 monthly/yearly 分组行为准
47
+ - 今天是 {{today}}。当月不完整的数据(如月内仅数天)只可作为『最新动态』级别观察并明确标注,禁止据此输出骤降/中断等趋势性 anomaly
46
48
  - 关注维度:趋势变化、个人纪录(PB)、疲劳信号、季节性规律、运动项目占比变化
47
49
  - 输出 3-6 条,按重要性排序
48
50
  - 数据不足以支撑的结论不要输出;无异常时给少量 info 条目即可,禁止强行制造发现
@@ -62,6 +64,7 @@ module GitFit
62
64
 
63
65
  ## 建议要求
64
66
  - 汇总统计中的 summary 是全历史累计,不是 {{period}} 当期数据;当期负荷以 monthly/yearly 中 {{period}} 对应行为准
67
+ - 今天是 {{today}}。不完整的当月数据不足以支撑训练量调整类建议;如有,仅作观察性提示并标注
65
68
  - 输出 2-4 条可执行建议,每条说明数据依据
66
69
  - 除非数据明确支持(如连续多周稳定负荷且无疲劳信号),不建议提高训练量
67
70
  - 关注一致性、恢复与渐进,而非单次表现
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'sequel'
5
+ require 'mcp'
6
+
7
+ module GitFit
8
+ module MCP
9
+ # 只读 MCP tools —— expose 本地 workouts DB 给 AI 客户端(Claude/Code/opencode 等)。
10
+ # 数据已公开(Pages 站点),无隐私增量;SQLite readonly 打开。
11
+ #
12
+ # 输出格式约定:query_activities 使用与 insights 摘要一致的紧凑行
13
+ # (run_id | 日期 类型 距离 时长 HR 爬升),供 get_activity 下钻。
14
+ module Tools
15
+ ACTIVITY_COLUMNS = %i[
16
+ run_id name distance moving_time elapsed_time sport_category sport_type
17
+ start_date start_date_local location_country summary_polyline
18
+ average_heartrate max_heartrate average_cadence max_cadence
19
+ average_power max_power average_temperature calories average_speed
20
+ elevation_gain elevation_loss elevation_min elevation_max steps source
21
+ ].freeze
22
+
23
+ module_function
24
+
25
+ def db(config)
26
+ Sequel.sqlite(config.db_path, readonly: true)
27
+ end
28
+
29
+ def activity_line(row)
30
+ parts = [row[:run_id].to_s, row[:start_date_local].to_s[0, 10], row[:sport_category].to_s]
31
+ parts << format('%.1fkm', row[:distance] / 1000.0) if row[:distance]
32
+ parts << "#{(row[:moving_time] / 60).round}min" if row[:moving_time]
33
+ parts << "HR#{row[:average_heartrate].round}" if row[:average_heartrate]
34
+ parts << "asc#{row[:elevation_gain].round}m" if row[:elevation_gain]
35
+ parts.join(' | ')
36
+ end
37
+ end
38
+
39
+ class QueryActivitiesTool < ::MCP::Tool
40
+ tool_name 'query_activities'
41
+ description '查询运动活动列表(按日期范围/运动类型过滤,返回最近的紧凑摘要行)。' \
42
+ '行格式:run_id | 日期 | 类型 | 距离 | 时长 | HR | 爬升。'
43
+
44
+ input_schema(
45
+ properties: {
46
+ from_date: { type: 'string', description: '起始日期 YYYY-MM-DD(含)', pattern: '^\\d{4}-\\d{2}-\\d{2}$' },
47
+ to_date: { type: 'string', description: '结束日期 YYYY-MM-DD(含)', pattern: '^\\d{4}-\\d{2}-\\d{2}$' },
48
+ sport: { type: 'string', description: '运动类型(ride/run/hike/walk/swim/workout/other)' },
49
+ limit: { type: 'integer', description: '返回条数上限(1-500)', default: 50 },
50
+ },
51
+ )
52
+
53
+ class << self
54
+ def call(server_context:, from_date: nil, to_date: nil, sport: nil, limit: 50)
55
+ db = GitFit::MCP::Tools.db(server_context[:config])
56
+ rows = db[:activities].order(Sequel.desc(:start_date_local)).limit(2000).all
57
+ rows = rows.select { |r| r[:start_date_local].to_s[0, 10] >= from_date } if from_date
58
+ rows = rows.select { |r| r[:start_date_local].to_s[0, 10] <= to_date } if to_date
59
+ rows = rows.select { |r| r[:sport_category] == sport } if sport && sport != 'all'
60
+ rows = rows.first(limit.to_i.clamp(1, 500))
61
+ lines = rows.map { |r| GitFit::MCP::Tools.activity_line(r) }
62
+ body = lines.empty? ? '无匹配活动' : "#{rows.size} 条匹配:\n#{lines.join("\n")}"
63
+ ::MCP::Tool::Response.new([{ type: 'text', text: body }])
64
+ ensure
65
+ db&.disconnect
66
+ end
67
+ end
68
+ end
69
+
70
+ class GetActivityTool < ::MCP::Tool
71
+ tool_name 'get_activity'
72
+ description '按 run_id 获取单个活动的全字段详情。run_id 来自 query_activities。'
73
+
74
+ input_schema(
75
+ properties: {
76
+ run_id: { type: 'string', description: '活动 ID(如 garmin_cn_12345678)' },
77
+ },
78
+ required: ['run_id'],
79
+ )
80
+
81
+ class << self
82
+ def call(run_id:, server_context:)
83
+ db = GitFit::MCP::Tools.db(server_context[:config])
84
+ row = db[:activities].first(run_id: run_id)
85
+ unless row
86
+ return ::MCP::Tool::Response.new(
87
+ [{ type: 'text', text: "活动不存在:#{run_id}\n Fix: 先用 query_activities 查询可用 run_id" }],
88
+ error: true,
89
+ )
90
+ end
91
+ ::MCP::Tool::Response.new([{ type: 'text', text: JSON.pretty_generate(
92
+ GitFit::MCP::Tools::ACTIVITY_COLUMNS.to_h { |k| [k, row[k]] },
93
+ ) }])
94
+ ensure
95
+ db&.disconnect
96
+ end
97
+ end
98
+ end
99
+
100
+ class GetStatsTool < ::MCP::Tool
101
+ tool_name 'get_stats'
102
+ description '获取全量统计摘要:summary(全历史累计)/ monthly(按月)/ yearly(按年)/ streaks(连续天数)/ activity_types。' \
103
+ '注意 summary 是全历史累计,当期对比请看 monthly/yearly 分组行。'
104
+
105
+ input_schema(properties: {})
106
+
107
+ class << self
108
+ def call(server_context:)
109
+ db = GitFit::MCP::Tools.db(server_context[:config])
110
+ stats = GitFit::Export::Calculator.new(db: db).call
111
+ ::MCP::Tool::Response.new([{ type: 'text', text: JSON.pretty_generate(stats) }])
112
+ ensure
113
+ db&.disconnect
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mcp/tools'
4
+
5
+ module GitFit
6
+ module MCP
7
+ module_function
8
+
9
+ # 构建 MCP server(stdio transport 由 CLI 层启动)。
10
+ def build_server(config)
11
+ ::MCP::Server.new(
12
+ name: 'git-fit-workouts',
13
+ version: GitFit::VERSION,
14
+ tools: [QueryActivitiesTool, GetActivityTool, GetStatsTool],
15
+ server_context: { config: config },
16
+ )
17
+ end
18
+ end
19
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GitFit
4
- VERSION = '0.22.0'
4
+ VERSION = '0.23.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: git-fit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.22.0
4
+ version: 0.23.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lax
@@ -247,6 +247,20 @@ dependencies:
247
247
  - - "~>"
248
248
  - !ruby/object:Gem::Version
249
249
  version: '1.0'
250
+ - !ruby/object:Gem::Dependency
251
+ name: mcp
252
+ requirement: !ruby/object:Gem::Requirement
253
+ requirements:
254
+ - - "~>"
255
+ - !ruby/object:Gem::Version
256
+ version: '1.4'
257
+ type: :runtime
258
+ prerelease: false
259
+ version_requirements: !ruby/object:Gem::Requirement
260
+ requirements:
261
+ - - "~>"
262
+ - !ruby/object:Gem::Version
263
+ version: '1.4'
250
264
  description: A git extension CLI for aggregating, managing, and exporting fitness
251
265
  activity data from multiple sources (Garmin, Strava, Keep, etc.)
252
266
  email:
@@ -275,6 +289,7 @@ files:
275
289
  - lib/git_fit/cli/gh_cli.rb
276
290
  - lib/git_fit/cli/import_cli.rb
277
291
  - lib/git_fit/cli/install_cli.rb
292
+ - lib/git_fit/cli/mcp.rb
278
293
  - lib/git_fit/cli/purge_cli.rb
279
294
  - lib/git_fit/cli/strava.rb
280
295
  - lib/git_fit/cli/sync.rb
@@ -332,6 +347,8 @@ files:
332
347
  - lib/git_fit/llm/errors.rb
333
348
  - lib/git_fit/llm/prompts.rb
334
349
  - lib/git_fit/llm/template.rb
350
+ - lib/git_fit/mcp.rb
351
+ - lib/git_fit/mcp/tools.rb
335
352
  - lib/git_fit/parser.rb
336
353
  - lib/git_fit/parser/base.rb
337
354
  - lib/git_fit/parser/fit.rb