@chronicle.app/things-todo 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrew Louis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @chronicle.app/things-todo
2
+
3
+ Read Things 3 tasks/projects with `ThingsTodoExtractor`, then map task lifecycle events with `ThingsTodoTransformer`. The default path locates the installed Things database; pass `input` to use a consistent backup or fixture. Optional `agentName` labels the source-local task owner.
4
+
5
+ Uses read-only `node:sqlite`. `since`/`until` are inclusive modification-date bounds; `limit: 0` is unlimited. Records are newest-modified first. Raw project/heading records enrich task relationships; the transformer emits task actions (plan, update, complete, cancel, delete), not standalone project actions. Source timestamps remain event timestamps; no store annotations are generated.
6
+
7
+ Use the ETL Runner or call setup/extract/teardown directly, always tearing down in a finally block. This package has no CLI dependency. The tests build synthetic Things tables without accessing an installed application.
@@ -0,0 +1,20 @@
1
+ import { Record } from '@chronicle.app/etl';
2
+ import { SqliteExtractor } from '@chronicle.app/etl-sqlite';
3
+ import ThingsTodoTransformer from './ThingsTodoTransformer.js';
4
+ export declare class ThingsTodoExtractor extends SqliteExtractor<typeof ThingsTodoExtractor> {
5
+ static source: string;
6
+ static delivery: "local";
7
+ static strategy: string;
8
+ static temporality: "snapshot";
9
+ static description: string;
10
+ static recordTypes: string[];
11
+ static default: boolean;
12
+ static schema: any;
13
+ static defaultTransformer: typeof ThingsTodoTransformer;
14
+ private getStatusLabel;
15
+ extract(): AsyncGenerator<Record>;
16
+ private getEnrichedAreas;
17
+ private getEnrichedProjects;
18
+ private getTagHierarchies;
19
+ }
20
+ //# sourceMappingURL=ThingsTodoExtractor.d.ts.map
@@ -0,0 +1,304 @@
1
+ import { SqliteExtractor, timeRangeConditions } from '@chronicle.app/etl-sqlite';
2
+ import { existsSync, readdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { z } from 'zod';
5
+ import ThingsTodoTransformer from './ThingsTodoTransformer.js';
6
+ /**
7
+ * Resolve the Things 3 SQLite database path.
8
+ *
9
+ * Things stores its data under a per-install directory whose name is
10
+ * `ThingsData-XXXXX` (the suffix is unique to each installation), so the path
11
+ * cannot be hardcoded. Glob the Group Containers directory for the
12
+ * `ThingsData-*` folder and return the database inside it.
13
+ */
14
+ function resolveThingsDbPath() {
15
+ const containerDir = `${process.env.HOME}/Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac`;
16
+ const dbSuffix = 'Things Database.thingsdatabase/main.sqlite';
17
+ if (existsSync(containerDir)) {
18
+ const thingsDataDirs = readdirSync(containerDir).filter(name => name.startsWith('ThingsData-'));
19
+ for (const dir of thingsDataDirs) {
20
+ const dbPath = join(containerDir, dir, dbSuffix);
21
+ if (existsSync(dbPath)) {
22
+ return dbPath;
23
+ }
24
+ }
25
+ }
26
+ // Fall back to the conventional location so the error message is meaningful
27
+ // when Things is not installed.
28
+ return join(containerDir, 'ThingsData', dbSuffix);
29
+ }
30
+ export class ThingsTodoExtractor extends SqliteExtractor {
31
+ static source = 'things-todo';
32
+ static delivery = 'local';
33
+ static strategy = 'app-db';
34
+ // Re-reads current task state, so attributes (name, status) are sighted at
35
+ // read time — not back-dated to the creation/modification timestamps the
36
+ // actions carry. Lets a rename supersede cleanly instead of colliding with the
37
+ // old name at the creation instant. See Extractor.temporality.
38
+ static temporality = 'snapshot';
39
+ static description = 'Tasks and projects';
40
+ static recordTypes = ['tasks', 'projects'];
41
+ static default = true;
42
+ static schema = SqliteExtractor.schema.omit({ input: true }).extend({
43
+ input: z
44
+ .string()
45
+ .describe('The db file to read from')
46
+ .default(() => resolveThingsDbPath()),
47
+ agentName: z.string().optional().describe('Name of the task owner, when known'),
48
+ });
49
+ static defaultTransformer = ThingsTodoTransformer;
50
+ getStatusLabel(status) {
51
+ switch (status) {
52
+ case 0:
53
+ return 'open';
54
+ case 2:
55
+ return 'cancelled';
56
+ case 3:
57
+ return 'completed';
58
+ default:
59
+ return 'unknown';
60
+ }
61
+ }
62
+ async *extract() {
63
+ if (!this.db) {
64
+ throw new Error('Database not initialized');
65
+ }
66
+ const limit = this.getEffectiveLimit();
67
+ const { conditions, values } = timeRangeConditions('task.userModificationDate', this.config, {
68
+ sinceOp: '>=',
69
+ untilOp: '<=',
70
+ });
71
+ let sql = `SELECT task.*, group_concat(DISTINCT tag.uuid) AS tag_ids,
72
+ group_concat(DISTINCT tag.title) AS tag_titles FROM TMTask task
73
+ LEFT JOIN TMTaskTag tagging ON task.uuid = tagging.tasks
74
+ LEFT JOIN TMTag tag ON tagging.tags = tag.uuid
75
+ WHERE task.type IN (0, 1, 2)`;
76
+ if (conditions.length > 0)
77
+ sql += ' AND ' + conditions.join(' AND ');
78
+ sql += ' GROUP BY task.uuid ORDER BY task.userModificationDate DESC, task.uuid';
79
+ if (limit !== null) {
80
+ sql += ' LIMIT ?';
81
+ values.push(limit);
82
+ }
83
+ const rows = this.db.prepare(sql).all(...values);
84
+ // Get enriched areas and projects with nested data
85
+ const areaIds = [...new Set(rows.map(row => row.area))].filter(Boolean);
86
+ const enrichedAreas = await this.getEnrichedAreas(areaIds);
87
+ // Get both direct projects and headings (which can also be projects)
88
+ const directProjectIds = [...new Set(rows.map(row => row.project))].filter(Boolean);
89
+ const headingIds = [...new Set(rows.map(row => row.heading))].filter(Boolean);
90
+ const allProjectIds = [...new Set([...directProjectIds, ...headingIds])];
91
+ const enrichedProjects = this.getEnrichedProjects(allProjectIds);
92
+ // Get tag hierarchies for all tags used
93
+ const allTagIds = rows
94
+ .map(row => row.tag_ids)
95
+ .filter(Boolean)
96
+ .join(',')
97
+ .split(',')
98
+ .filter(Boolean);
99
+ const tagHierarchies = this.getTagHierarchies([...new Set(allTagIds)]);
100
+ for (const row of rows) {
101
+ const recordType = row.type === 1 || row.type === 2 ? 'projects' : 'tasks';
102
+ // Convert timestamps to dates
103
+ const timeline = {
104
+ createdAt: row.creationDate ? new Date(row.creationDate * 1000) : null,
105
+ startedAt: row.startDate ? new Date(row.startDate * 1000) : null,
106
+ completedAt: row.stopDate ? new Date(row.stopDate * 1000) : null,
107
+ status: this.getStatusLabel(row.status),
108
+ lastModifiedAt: row.userModificationDate ? new Date(row.userModificationDate * 1000) : null,
109
+ };
110
+ // Build enriched tags array with hierarchies
111
+ const taskTagIds = row.tag_ids ? row.tag_ids.split(',') : [];
112
+ const taskTagTitles = row.tag_titles ? row.tag_titles.split(',') : [];
113
+ const enrichedTags = taskTagIds.map((tagId, index) => {
114
+ const hierarchy = tagHierarchies.get(tagId);
115
+ return {
116
+ uuid: tagId,
117
+ title: taskTagTitles[index] || null,
118
+ shortcut: hierarchy?.shortcut || null,
119
+ hierarchy: hierarchy
120
+ ? {
121
+ parent: hierarchy.parent,
122
+ children: hierarchy.children,
123
+ path: hierarchy.path,
124
+ level: hierarchy.level,
125
+ }
126
+ : null,
127
+ lastUsedAt: hierarchy?.lastUsedAt || null,
128
+ };
129
+ });
130
+ // Get user's real name for Person object deduplication
131
+ const realName = this.config.agentName;
132
+ // Determine the project relationship
133
+ let projectRelation = null;
134
+ let headingRelation = null;
135
+ // If task has a direct project, use that
136
+ if (row.project) {
137
+ projectRelation = enrichedProjects.get(row.project);
138
+ }
139
+ // If task has a heading, the heading might belong to a project
140
+ else if (row.heading) {
141
+ const heading = enrichedProjects.get(row.heading);
142
+ if (heading) {
143
+ headingRelation = {
144
+ uuid: heading.uuid,
145
+ title: heading.title,
146
+ type: heading.type,
147
+ };
148
+ // If the heading belongs to a project, use that project
149
+ if (heading.project) {
150
+ projectRelation = enrichedProjects.get(heading.project);
151
+ }
152
+ }
153
+ }
154
+ yield this.createRecord(row, {
155
+ recordType,
156
+ timeline,
157
+ project: projectRelation,
158
+ heading: headingRelation,
159
+ area: row.area ? enrichedAreas.get(row.area) || null : null,
160
+ tags: enrichedTags,
161
+ agent: {
162
+ name: realName, // Just the real name for Person [@type, title, source] deduplication
163
+ },
164
+ });
165
+ }
166
+ }
167
+ async getEnrichedAreas(areaIds) {
168
+ const enrichedAreas = new Map();
169
+ if (areaIds.length === 0)
170
+ return enrichedAreas;
171
+ const areas = this.db.prepare(`SELECT area.*,
172
+ group_concat(DISTINCT area_tag.tags) AS area_tag_ids,
173
+ group_concat(DISTINCT tag.title) AS area_tag_titles,
174
+ COUNT(DISTINCT task.uuid) AS task_count,
175
+ COUNT(DISTINCT project.uuid) AS project_count
176
+ FROM TMArea area
177
+ LEFT JOIN TMAreaTag area_tag ON area.uuid = area_tag.areas
178
+ LEFT JOIN TMTag tag ON area_tag.tags = tag.uuid
179
+ LEFT JOIN TMTask task ON area.uuid = task.area AND task.type IN (0, 1)
180
+ LEFT JOIN TMTask project ON area.uuid = project.area AND project.type = 2
181
+ WHERE area.uuid IN (${areaIds.map(() => '?').join(',')}) GROUP BY area.uuid
182
+ `).all(...areaIds);
183
+ for (const area of areas) {
184
+ const areaData = area;
185
+ const areaTagIds = areaData.area_tag_ids ? areaData.area_tag_ids.split(',') : [];
186
+ const areaTagTitles = areaData.area_tag_titles ? areaData.area_tag_titles.split(',') : [];
187
+ const tags = areaTagIds.map((id, index) => ({
188
+ uuid: id,
189
+ title: areaTagTitles[index] || null,
190
+ }));
191
+ enrichedAreas.set(areaData.uuid, {
192
+ ...areaData,
193
+ tags,
194
+ metrics: {
195
+ taskCount: Number.parseInt(areaData.task_count) || 0,
196
+ projectCount: Number.parseInt(areaData.project_count) || 0,
197
+ },
198
+ });
199
+ }
200
+ return enrichedAreas;
201
+ }
202
+ getEnrichedProjects(projectIds) {
203
+ const enrichedProjects = new Map();
204
+ if (projectIds.length === 0)
205
+ return enrichedProjects;
206
+ const query = `
207
+ SELECT
208
+ project.*,
209
+ area.title as area_title,
210
+ area.uuid as area_uuid,
211
+ COUNT(DISTINCT task.uuid) as task_count,
212
+ COUNT(DISTINCT completed_task.uuid) as completed_task_count
213
+ FROM TMTask project
214
+ LEFT JOIN TMArea area ON project.area = area.uuid
215
+ LEFT JOIN TMTask task ON (project.uuid = task.project OR project.uuid = task.heading) AND task.type IN (0, 1)
216
+ LEFT JOIN TMTask completed_task ON (project.uuid = completed_task.project OR project.uuid = completed_task.heading) AND completed_task.type IN (0, 1) AND completed_task.status = 3
217
+ WHERE project.uuid IN (${projectIds.map(() => '?').join(',')}) AND project.type IN (1, 2)
218
+ GROUP BY project.uuid
219
+ `;
220
+ const projects = this.db.prepare(query).all(...projectIds);
221
+ for (const project of projects) {
222
+ const projectData = project;
223
+ enrichedProjects.set(projectData.uuid, {
224
+ ...projectData,
225
+ area: projectData.area_uuid
226
+ ? {
227
+ uuid: projectData.area_uuid,
228
+ title: projectData.area_title,
229
+ }
230
+ : null,
231
+ metrics: {
232
+ totalTasks: Number.parseInt(projectData.task_count) || 0,
233
+ completedTasks: Number.parseInt(projectData.completed_task_count) || 0,
234
+ progress: projectData.task_count > 0
235
+ ? projectData.completed_task_count / projectData.task_count
236
+ : 0,
237
+ },
238
+ });
239
+ }
240
+ return enrichedProjects;
241
+ }
242
+ getTagHierarchies(tagIds) {
243
+ const tagHierarchies = new Map();
244
+ if (tagIds.length === 0)
245
+ return tagHierarchies;
246
+ // Get all tags with their parent relationships
247
+ const query = `
248
+ SELECT
249
+ tag.*,
250
+ parent.title as parent_title,
251
+ group_concat(DISTINCT child.uuid) as child_ids,
252
+ group_concat(DISTINCT child.title) as child_titles
253
+ FROM TMTag tag
254
+ LEFT JOIN TMTag parent ON tag.parent = parent.uuid
255
+ LEFT JOIN TMTag child ON tag.uuid = child.parent
256
+ WHERE tag.uuid IN (${tagIds.map(() => '?').join(',')})
257
+ GROUP BY tag.uuid
258
+ ORDER BY tag.title
259
+ `;
260
+ const tags = this.db.prepare(query).all(...tagIds);
261
+ // Build hierarchical paths
262
+ const buildPath = (tagId, visited = new Set()) => {
263
+ if (visited.has(tagId))
264
+ return ''; // Prevent cycles
265
+ visited.add(tagId);
266
+ const tag = tags.find((t) => t.uuid === tagId);
267
+ if (!tag)
268
+ return '';
269
+ const tagData = tag;
270
+ if (tagData.parent) {
271
+ const parentPath = buildPath(tagData.parent, visited);
272
+ return parentPath ? `${parentPath} > ${tagData.title}` : tagData.title;
273
+ }
274
+ return tagData.title;
275
+ };
276
+ for (const tag of tags) {
277
+ const tagData = tag;
278
+ const childIds = tagData.child_ids ? tagData.child_ids.split(',') : [];
279
+ const childTitles = tagData.child_titles ? tagData.child_titles.split(',') : [];
280
+ const children = childIds.map((id, index) => ({
281
+ uuid: id,
282
+ title: childTitles[index] || null,
283
+ }));
284
+ const path = buildPath(tagData.uuid);
285
+ const level = path.split(' > ').length - 1;
286
+ tagHierarchies.set(tagData.uuid, {
287
+ uuid: tagData.uuid,
288
+ title: tagData.title,
289
+ shortcut: tagData.shortcut,
290
+ lastUsedAt: tagData.usedDate ? new Date(tagData.usedDate * 1000) : null,
291
+ parent: tagData.parent
292
+ ? {
293
+ uuid: tagData.parent,
294
+ title: tagData.parent_title,
295
+ }
296
+ : null,
297
+ children,
298
+ path,
299
+ level,
300
+ });
301
+ }
302
+ return tagHierarchies;
303
+ }
304
+ }
@@ -0,0 +1,13 @@
1
+ import { ChronicleTransformer, Record } from '@chronicle.app/etl';
2
+ import { ActionAndChildren } from '@chronicle.app/schema';
3
+ export default class ThingsTodoTransformer extends ChronicleTransformer {
4
+ transform(record: Record): Promise<ActionAndChildren[]>;
5
+ private buildTaskActions;
6
+ private buildAction;
7
+ private buildUser;
8
+ private buildTask;
9
+ private buildProject;
10
+ private buildHeading;
11
+ private buildArea;
12
+ }
13
+ //# sourceMappingURL=ThingsTodoTransformer.d.ts.map
@@ -0,0 +1,166 @@
1
+ import { ChronicleTransformer } from '@chronicle.app/etl';
2
+ export default class ThingsTodoTransformer extends ChronicleTransformer {
3
+ async transform(record) {
4
+ const actions = [];
5
+ if (record.extraction.recordType === 'tasks') {
6
+ actions.push(...this.buildTaskActions(record));
7
+ }
8
+ return actions;
9
+ }
10
+ buildTaskActions(record) {
11
+ const actions = [];
12
+ const task = this.buildTask({
13
+ task: record.data,
14
+ project: record.context.project,
15
+ area: record.context.area,
16
+ heading: record.context.heading,
17
+ tags: record.context.tags,
18
+ });
19
+ const user = this.buildUser(record.context.agent.name);
20
+ // Always create the initial PlanAction with creation timestamp
21
+ actions.push(this.buildAction({
22
+ data: record.data,
23
+ timestamp: record.data.creationDate,
24
+ type: 'PlanAction',
25
+ agentObj: user,
26
+ taskObj: task,
27
+ }));
28
+ // If task was edited (modification date differs significantly from creation), create UpdateAction
29
+ const timeDifferenceSeconds = Math.abs(record.data.userModificationDate - record.data.creationDate);
30
+ const wasEdited = timeDifferenceSeconds > 60; // More than 60 seconds difference indicates an edit
31
+ // Completing/cancelling/trashing a task bumps userModificationDate, which would
32
+ // otherwise emit a redundant UpdateAction alongside the terminal action. Skip the
33
+ // update when the modification just reflects that terminal event.
34
+ const terminalTimestamp = record.data.status == 3 || record.data.status == 2
35
+ ? record.data.stopDate
36
+ : record.data.trashed == 1
37
+ ? record.data.userModificationDate
38
+ : undefined;
39
+ const modificationIsTerminal = terminalTimestamp !== undefined &&
40
+ Math.abs(record.data.userModificationDate - terminalTimestamp) <= 60;
41
+ if (wasEdited && !modificationIsTerminal) {
42
+ const updateAction = {
43
+ '@type': 'UpdateAction',
44
+ timestamp: new Date(record.data.userModificationDate * 1000),
45
+ '@key': ['@type', 'source', 'sourceId'],
46
+ source: 'things-todo',
47
+ sourceId: record.data.uuid,
48
+ agent: user,
49
+ object: task,
50
+ };
51
+ actions.push(updateAction);
52
+ }
53
+ if (record.data.status == 3) {
54
+ actions.push(this.buildAction({
55
+ data: record.data,
56
+ timestamp: record.data.stopDate,
57
+ type: 'CompleteAction',
58
+ agentObj: user,
59
+ taskObj: task,
60
+ }));
61
+ }
62
+ else if (record.data.status == 2) {
63
+ actions.push(this.buildAction({
64
+ data: record.data,
65
+ timestamp: record.data.stopDate,
66
+ type: 'CancelAction',
67
+ agentObj: user,
68
+ taskObj: task,
69
+ }));
70
+ }
71
+ if (record.data.trashed == 1) {
72
+ actions.push(this.buildAction({
73
+ data: record.data,
74
+ timestamp: record.data.userModificationDate,
75
+ type: 'DeleteAction',
76
+ agentObj: user,
77
+ taskObj: task,
78
+ }));
79
+ }
80
+ return actions;
81
+ }
82
+ buildAction({ data, timestamp, type, agentObj, taskObj, }) {
83
+ return {
84
+ '@type': type,
85
+ timestamp: new Date(timestamp * 1000),
86
+ '@key': ['@type', 'source', 'sourceId'],
87
+ source: 'things-todo',
88
+ sourceId: data.uuid,
89
+ agent: agentObj,
90
+ object: taskObj,
91
+ };
92
+ }
93
+ buildUser(realName) {
94
+ return {
95
+ '@type': 'Agent',
96
+ '@key': ['@type', 'source'],
97
+ source: 'things-todo',
98
+ name: realName,
99
+ sameAs: ['@me'],
100
+ };
101
+ }
102
+ buildTask({ task, project, area, heading, tags, }) {
103
+ const obj = {
104
+ '@type': 'Task',
105
+ '@key': ['@type', 'source', 'sourceId'],
106
+ source: 'things-todo',
107
+ sourceId: task.uuid,
108
+ name: task.title,
109
+ };
110
+ if (task.notes) {
111
+ obj.description = task.notes;
112
+ }
113
+ const isPartOf = [];
114
+ if (project) {
115
+ isPartOf.push(this.buildProject(project));
116
+ }
117
+ if (heading) {
118
+ isPartOf.push(this.buildHeading(heading)); // Headings are organizational sections
119
+ }
120
+ if (area) {
121
+ isPartOf.push(this.buildArea(area));
122
+ }
123
+ if (isPartOf.length > 0) {
124
+ obj.isPartOf = isPartOf;
125
+ }
126
+ // Add tags as 'about' entities (representing what the task is about)
127
+ if (tags && tags.length > 0) {
128
+ obj.about = tags.map(tag => ({
129
+ '@type': 'Tag',
130
+ '@key': ['@type', 'source', 'sourceId'],
131
+ source: 'things-todo',
132
+ sourceId: tag.uuid,
133
+ handle: tag.title,
134
+ name: tag.title,
135
+ }));
136
+ }
137
+ return obj;
138
+ }
139
+ buildProject(project) {
140
+ return {
141
+ '@type': 'Project',
142
+ '@key': ['@type', 'source', 'sourceId'],
143
+ source: 'things-todo',
144
+ sourceId: project.uuid,
145
+ name: project.title,
146
+ };
147
+ }
148
+ buildHeading(heading) {
149
+ return {
150
+ '@type': 'Collection',
151
+ '@key': ['@type', 'source', 'sourceId'],
152
+ source: 'things-todo',
153
+ sourceId: heading.uuid,
154
+ name: heading.title,
155
+ };
156
+ }
157
+ buildArea(area) {
158
+ return {
159
+ '@type': 'Collection',
160
+ '@key': ['@type', 'source', 'sourceId'],
161
+ source: 'things-todo',
162
+ sourceId: area.uuid,
163
+ name: area.title,
164
+ };
165
+ }
166
+ }
@@ -0,0 +1,3 @@
1
+ export { ThingsTodoExtractor } from './connectors/ThingsTodoExtractor.js';
2
+ export { default as ThingsTodoTransformer } from './connectors/ThingsTodoTransformer.js';
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ // Export extractors for plugin scanning
2
+ export { ThingsTodoExtractor } from './connectors/ThingsTodoExtractor.js';
3
+ export { default as ThingsTodoTransformer } from './connectors/ThingsTodoTransformer.js';
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@chronicle.app/things-todo",
3
+ "version": "0.1.0",
4
+ "main": "./dist/index.js",
5
+ "types": "./dist/index.d.ts",
6
+ "files": [
7
+ "dist/**/*.js",
8
+ "dist/**/*.d.ts"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc -b tsconfig.lib.json",
12
+ "typecheck": "tsc -p tsconfig.json",
13
+ "lint": "eslint src --ext .ts,.js",
14
+ "test": "node --test src/*.test.js"
15
+ },
16
+ "keywords": [],
17
+ "author": "Andrew Louis",
18
+ "license": "MIT",
19
+ "type": "module",
20
+ "description": "Chronicle things-todo extraction plugin",
21
+ "devDependencies": {
22
+ "@chronicle.app/tsconfig": "0.1.0",
23
+ "@types/node": "^22.10.1",
24
+ "typescript": "^5.7.2"
25
+ },
26
+ "homepage": "https://chronicle.app",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/chronicle-app/chronicle.git",
30
+ "directory": "plugins/things-todo"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "registry": "https://registry.npmjs.org/"
35
+ },
36
+ "dependencies": {
37
+ "@chronicle.app/etl": "0.1.0",
38
+ "@chronicle.app/schema": "0.1.0",
39
+ "zod": "^3.24.1",
40
+ "@chronicle.app/etl-sqlite": "0.1.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=22.13.0"
44
+ },
45
+ "exports": {
46
+ ".": {
47
+ "types": "./dist/index.d.ts",
48
+ "import": "./dist/index.js"
49
+ }
50
+ },
51
+ "chronicle": {
52
+ "plugin": true
53
+ },
54
+ "oclif": {}
55
+ }