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 +4 -4
- data/lib/things_mcp/database.rb +102 -20
- data/lib/things_mcp/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c04d398fe0ae4d889f3b1283f170b7ea16d89d3aadb15c0729f02224051fb40c
|
4
|
+
data.tar.gz: 63edc670dfefbf746f0397a3a41e95e81ae977237421057e89564b4b77ce998b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 749a56ffef198d8bfabf98212faa51e8d47f8b79280a09899910a49938ea20391ebe08e8a5387ef593ec3095bea9aa7d138ca7294058d3b995ed3f4385fc0ee0
|
7
|
+
data.tar.gz: 570aeb516c134b4de385a25b672677976879dcccab000be09ba697568de5ff03864ea328a695ba03212c49b28352f5be1e820402f2ed172483aba476df9a1eca
|
data/lib/things_mcp/database.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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 >=
|
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
|
-
|
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
|
-
|
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 >=
|
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:
|
417
|
-
deadline:
|
418
|
-
created:
|
419
|
-
modified:
|
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:
|
433
|
-
deadline:
|
434
|
-
created:
|
435
|
-
modified:
|
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
|
474
|
-
return unless
|
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
|
-
#
|
477
|
-
|
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
|
data/lib/things_mcp/version.rb
CHANGED