things-mcp 0.1.2 → 0.1.3

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: 66584b39e6d5286abeb4a75f08f029bab5c7c1b7b7f42129ee12986086fa7927
4
- data.tar.gz: bb7603686e15d0dc366f746e263dbb99cebb00ce35377e8ae49fb7f468658dc0
3
+ metadata.gz: c04d398fe0ae4d889f3b1283f170b7ea16d89d3aadb15c0729f02224051fb40c
4
+ data.tar.gz: 63edc670dfefbf746f0397a3a41e95e81ae977237421057e89564b4b77ce998b
5
5
  SHA512:
6
- metadata.gz: 5ff846805290fb8e7e628b8809855c2538852be5f0ea69766d176fa6065081ae93af010b2958c68bdc4ce5014f580375939701bf7964a1d915ea3d72d0d5f30e
7
- data.tar.gz: 8450f45b5c44b9cf8a37762e48cb7c4fe2b64a6e409ebcd2603036726c49535ad862b9ded6796dc13788694562c10e1406ec7547c7364c1bfa1d91b61312b8e9
6
+ metadata.gz: 749a56ffef198d8bfabf98212faa51e8d47f8b79280a09899910a49938ea20391ebe08e8a5387ef593ec3095bea9aa7d138ca7294058d3b995ed3f4385fc0ee0
7
+ data.tar.gz: 570aeb516c134b4de385a25b672677976879dcccab000be09ba697568de5ff03864ea328a695ba03212c49b28352f5be1e820402f2ed172483aba476df9a1eca
@@ -11,6 +11,10 @@ module ThingsMcp
11
11
  # This class provides read-only access to the Things 3 database, enabling retrieval of todos, projects, areas, and
12
12
  # tags. It handles dynamic database path resolution and provides formatted data structures.
13
13
  class Database
14
+ # Things date encoding constants
15
+ UNITS_PER_DAY = 128.0
16
+ EPOCH = Date.new(-814, 4, 1)
17
+
14
18
  class << self
15
19
  def database_path
16
20
  @database_path ||= find_database_path
@@ -62,7 +66,24 @@ module ThingsMcp
62
66
  end
63
67
 
64
68
  def get_today
65
- get_todos_by_start(1)
69
+ # Get todos scheduled for today or overdue (startDate <= today's value)
70
+ today_value = (Date.today - EPOCH) * UNITS_PER_DAY
71
+
72
+ with_database do |db|
73
+ query = <<~SQL
74
+ SELECT #{todo_columns}
75
+ FROM TMTask
76
+ WHERE type = 0
77
+ AND status = 0
78
+ AND trashed = 0
79
+ AND startDate IS NOT NULL
80
+ AND startDate <= #{today_value.to_i}
81
+ ORDER BY "index"
82
+ SQL
83
+
84
+ results = db.execute(query).map { |row| format_todo(row) }
85
+ add_checklist_items_and_tags(db, results)
86
+ end
66
87
  end
67
88
 
68
89
  def get_upcoming
@@ -78,21 +99,56 @@ module ThingsMcp
78
99
  ORDER BY COALESCE(startDate, deadline)
79
100
  SQL
80
101
 
81
- db.execute(query).map { |row| format_todo(row) }
102
+ results = db.execute(query).map { |row| format_todo(row) }
103
+ add_checklist_items_and_tags(db, results)
82
104
  end
83
105
  end
84
106
 
85
107
  def get_anytime
86
- get_todos_by_start(2).reject { |t| t[:start_date] || t[:deadline] }
108
+ with_database do |db|
109
+ query = <<~SQL
110
+ SELECT #{todo_columns}
111
+ FROM TMTask
112
+ WHERE type = 0
113
+ AND status = 0
114
+ AND trashed = 0
115
+ AND start = 1
116
+ ORDER BY "index"
117
+ SQL
118
+
119
+ results = db.execute(query).map { |row| format_todo(row) }
120
+ add_checklist_items_and_tags(db, results)
121
+ end
87
122
  end
88
123
 
89
124
  def get_someday
90
- get_todos_by_start(3)
125
+ # Get todos in someday: start=2 with no dates
126
+ # FIXME: Recurring items (like "Update mileage log") appear here when they should
127
+ # appear in Today or Scheduled based on their next occurrence. Things likely uses
128
+ # additional metadata to track recurring patterns that we haven't implemented.
129
+ with_database do |db|
130
+ query = <<~SQL
131
+ SELECT #{todo_columns}
132
+ FROM TMTask
133
+ WHERE type = 0
134
+ AND status = 0
135
+ AND trashed = 0
136
+ AND start = 2
137
+ AND startDate IS NULL
138
+ AND deadline IS NULL
139
+ ORDER BY "index"
140
+ SQL
141
+
142
+ results = db.execute(query).map { |row| format_todo(row) }
143
+ add_checklist_items_and_tags(db, results)
144
+ end
91
145
  end
92
146
 
93
147
  def get_logbook(period: "7d", limit: 50)
94
148
  days = parse_period(period)
95
149
  cutoff = Date.today - days
150
+ # stopDate is a REAL column with Unix timestamp
151
+ cutoff_timestamp = cutoff.to_time.to_i
96
152
 
97
153
  with_database do |db|
98
154
  query = <<~SQL
@@ -101,7 +157,7 @@ module ThingsMcp
101
157
  WHERE type = 0
102
158
  AND status IN (3, 2)
103
159
  AND trashed = 0
104
- AND stopDate >= julianday('#{cutoff}')
160
+ AND stopDate >= #{cutoff_timestamp}
105
161
  ORDER BY stopDate DESC
106
162
  LIMIT #{limit}
107
163
  SQL
@@ -247,11 +303,17 @@ module ThingsMcp
247
303
 
248
304
  # Date filters
249
305
  if filters[:start_date]
250
- conditions << "startDate >= julianday('#{filters[:start_date]}')"
306
+ # startDate is INTEGER column with NSDate seconds
307
+ nsdate_epoch = Time.new(2001, 1, 1, 0, 0, 0, "+00:00")
308
+ start_timestamp = (Date.parse(filters[:start_date]).to_time - nsdate_epoch).to_i
309
+ conditions << "startDate >= #{start_timestamp}"
251
310
  end
252
311
 
253
312
  if filters[:deadline]
254
- conditions << "deadline <= julianday('#{filters[:deadline]}')"
313
+ # deadline is INTEGER column with NSDate seconds
314
+ nsdate_epoch = Time.new(2001, 1, 1, 0, 0, 0, "+00:00")
315
+ deadline_timestamp = (Date.parse(filters[:deadline]).to_time - nsdate_epoch).to_i
316
+ conditions << "deadline <= #{deadline_timestamp}"
255
317
  end
256
318
 
257
319
  # Tag filter
@@ -285,6 +347,8 @@ module ThingsMcp
285
347
  def get_recent(period)
286
348
  days = parse_period(period)
287
349
  cutoff = Date.today - days
350
+ # creationDate is a REAL column with Unix timestamp
351
+ cutoff_timestamp = cutoff.to_time.to_i
288
352
 
289
353
  with_database do |db|
290
354
  query = <<~SQL
@@ -292,7 +356,7 @@ module ThingsMcp
292
356
  FROM TMTask
293
357
  WHERE type = 0
294
358
  AND trashed = 0
295
- AND creationDate >= julianday('#{cutoff}')
359
+ AND creationDate >= #{cutoff_timestamp}
296
360
  ORDER BY creationDate DESC
297
361
  SQL
298
362
 
@@ -413,10 +477,10 @@ module ThingsMcp
413
477
  area: row["area"],
414
478
  tags: [], # Tags will be populated separately
415
479
  when: format_when(row["start"]),
416
- start_date: julian_to_date(row["startDate"]),
417
- deadline: julian_to_date(row["deadline"]),
418
- created: julian_to_date(row["creationDate"]),
419
- modified: julian_to_date(row["userModificationDate"]),
480
+ start_date: things_date_to_date(row["startDate"]),
481
+ deadline: things_date_to_date(row["deadline"]),
482
+ created: unix_timestamp_to_date(row["creationDate"]),
483
+ modified: unix_timestamp_to_date(row["userModificationDate"]),
420
484
  }
421
485
  end
422
486
 
@@ -429,10 +493,10 @@ module ThingsMcp
429
493
  area: row["area"],
430
494
  tags: [], # Tags will be populated separately for projects too
431
495
  when: format_when(row["start"]),
432
- start_date: julian_to_date(row["startDate"]),
433
- deadline: julian_to_date(row["deadline"]),
434
- created: julian_to_date(row["creationDate"]),
435
- modified: julian_to_date(row["userModificationDate"]),
496
+ start_date: things_date_to_date(row["startDate"]),
497
+ deadline: things_date_to_date(row["deadline"]),
498
+ created: unix_timestamp_to_date(row["creationDate"]),
499
+ modified: unix_timestamp_to_date(row["userModificationDate"]),
436
500
  }
437
501
  end
438
502
 
@@ -470,11 +534,20 @@ module ThingsMcp
470
534
  end
471
535
  end
472
536
 
473
- def julian_to_date(julian_days)
474
- return unless julian_days
537
+ def things_date_to_date(value)
538
+ return unless value
539
+
540
+ # Things uses custom date encoding: 128 units = 1 day (11.25 minutes per unit)
541
+ # Epoch calculated from reverse engineering known date values
542
+ days = value / UNITS_PER_DAY
543
+ (EPOCH + days).to_s
544
+ end
545
+
546
+ def unix_timestamp_to_date(value)
547
+ return unless value
475
548
 
476
- # SQLite Julian day starts from noon on November 24, 4714 BC
477
- Date.jd(julian_days.to_i + 2400001).to_s
549
+ # REAL columns (creationDate, userModificationDate): Unix timestamps (seconds since 1970-01-01)
550
+ Time.at(value.to_f).to_date.to_s
478
551
  end
479
552
 
480
553
  def parse_period(period)
@@ -501,6 +574,15 @@ module ThingsMcp
501
574
  # If decoding fails, return original title
502
575
  title
503
576
  end
577
+
578
+ # Helper method to consistently add checklist items and tags to todos
579
+ def add_checklist_items_and_tags(db, todos)
580
+ todos.each do |todo|
581
+ todo[:checklist_items] = get_checklist_items(db, todo[:uuid])
582
+ todo[:tags] = get_tags_for_task(db, todo[:uuid])
583
+ end
584
+ todos
585
+ end
504
586
  end
505
587
  end
506
588
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ThingsMcp
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: things-mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hakan Ensari