@chronicle.app/things-todo 0.2.0 → 0.4.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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @chronicle.app/things-todo
|
|
2
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.
|
|
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. The task owner is a source-local `Agent` tagged `@me`, named after the OS account's full name (`id -F` on macOS); set `agentName` to use another name. The name is a label only: the owner is keyed by `['@type', 'source']` and gets no invented identifiers. Elsewhere, or when the account has no full name, the owner has no name.
|
|
4
4
|
|
|
5
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
6
|
|
|
@@ -11,6 +11,13 @@ export declare class ThingsTodoExtractor extends SqliteExtractor<typeof ThingsTo
|
|
|
11
11
|
static default: boolean;
|
|
12
12
|
static schema: any;
|
|
13
13
|
static defaultTransformer: typeof ThingsTodoTransformer;
|
|
14
|
+
/**
|
|
15
|
+
* The default database lives in a folder macOS guards: say whether it's
|
|
16
|
+
* missing or refused before SQLite reports "unable to open database file".
|
|
17
|
+
*/
|
|
18
|
+
setup(): Promise<void>;
|
|
19
|
+
/** The owner's name from the local machine, used when `agentName` isn't set. */
|
|
20
|
+
protected resolveAgentName(): string | undefined;
|
|
14
21
|
private getStatusLabel;
|
|
15
22
|
extract(): AsyncGenerator<Record>;
|
|
16
23
|
private getEnrichedAreas;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { fileError } from '@chronicle.app/etl';
|
|
1
2
|
import { SqliteExtractor, timeRangeConditions } from '@chronicle.app/etl-sqlite';
|
|
2
3
|
import { existsSync, readdirSync } from 'node:fs';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
import { z } from 'zod';
|
|
6
|
+
import { localAccountName } from './ownerName.js';
|
|
5
7
|
import ThingsTodoTransformer from './ThingsTodoTransformer.js';
|
|
6
8
|
/**
|
|
7
9
|
* Resolve the Things 3 SQLite database path.
|
|
@@ -11,11 +13,20 @@ import ThingsTodoTransformer from './ThingsTodoTransformer.js';
|
|
|
11
13
|
* cannot be hardcoded. Glob the Group Containers directory for the
|
|
12
14
|
* `ThingsData-*` folder and return the database inside it.
|
|
13
15
|
*/
|
|
16
|
+
const containerDir = `${process.env.HOME}/Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac`;
|
|
14
17
|
function resolveThingsDbPath() {
|
|
15
|
-
const containerDir = `${process.env.HOME}/Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac`;
|
|
16
18
|
const dbSuffix = 'Things Database.thingsdatabase/main.sqlite';
|
|
19
|
+
// Listing the container needs Full Disk Access; without it, fall through to
|
|
20
|
+
// the conventional path and let setup() say what's wrong.
|
|
17
21
|
if (existsSync(containerDir)) {
|
|
18
|
-
|
|
22
|
+
let entries = [];
|
|
23
|
+
try {
|
|
24
|
+
entries = readdirSync(containerDir);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Reported by setup().
|
|
28
|
+
}
|
|
29
|
+
const thingsDataDirs = entries.filter(name => name.startsWith('ThingsData-'));
|
|
19
30
|
for (const dir of thingsDataDirs) {
|
|
20
31
|
const dbPath = join(containerDir, dir, dbSuffix);
|
|
21
32
|
if (existsSync(dbPath)) {
|
|
@@ -44,9 +55,32 @@ export class ThingsTodoExtractor extends SqliteExtractor {
|
|
|
44
55
|
.string()
|
|
45
56
|
.describe('The db file to read from')
|
|
46
57
|
.default(() => resolveThingsDbPath()),
|
|
47
|
-
agentName: z
|
|
58
|
+
agentName: z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("Name of the task owner. Defaults to the OS account's full name"),
|
|
48
62
|
});
|
|
49
63
|
static defaultTransformer = ThingsTodoTransformer;
|
|
64
|
+
/**
|
|
65
|
+
* The default database lives in a folder macOS guards: say whether it's
|
|
66
|
+
* missing or refused before SQLite reports "unable to open database file".
|
|
67
|
+
*/
|
|
68
|
+
async setup() {
|
|
69
|
+
const { input } = this.config;
|
|
70
|
+
if (input.startsWith(containerDir)) {
|
|
71
|
+
try {
|
|
72
|
+
readdirSync(containerDir);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
throw fileError(error, containerDir, 'Things library');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
await super.setup();
|
|
79
|
+
}
|
|
80
|
+
/** The owner's name from the local machine, used when `agentName` isn't set. */
|
|
81
|
+
resolveAgentName() {
|
|
82
|
+
return localAccountName();
|
|
83
|
+
}
|
|
50
84
|
getStatusLabel(status) {
|
|
51
85
|
switch (status) {
|
|
52
86
|
case 0:
|
|
@@ -81,6 +115,7 @@ export class ThingsTodoExtractor extends SqliteExtractor {
|
|
|
81
115
|
values.push(limit);
|
|
82
116
|
}
|
|
83
117
|
const rows = this.db.prepare(sql).all(...values);
|
|
118
|
+
const agentName = this.config.agentName ?? this.resolveAgentName();
|
|
84
119
|
// Get enriched areas and projects with nested data
|
|
85
120
|
const areaIds = [...new Set(rows.map(row => row.area))].filter(Boolean);
|
|
86
121
|
const enrichedAreas = await this.getEnrichedAreas(areaIds);
|
|
@@ -127,8 +162,6 @@ export class ThingsTodoExtractor extends SqliteExtractor {
|
|
|
127
162
|
lastUsedAt: hierarchy?.lastUsedAt || null,
|
|
128
163
|
};
|
|
129
164
|
});
|
|
130
|
-
// Get user's real name for Person object deduplication
|
|
131
|
-
const realName = this.config.agentName;
|
|
132
165
|
// Determine the project relationship
|
|
133
166
|
let projectRelation = null;
|
|
134
167
|
let headingRelation = null;
|
|
@@ -158,9 +191,7 @@ export class ThingsTodoExtractor extends SqliteExtractor {
|
|
|
158
191
|
heading: headingRelation,
|
|
159
192
|
area: row.area ? enrichedAreas.get(row.area) || null : null,
|
|
160
193
|
tags: enrichedTags,
|
|
161
|
-
agent: {
|
|
162
|
-
name: realName, // Just the real name for Person [@type, title, source] deduplication
|
|
163
|
-
},
|
|
194
|
+
agent: { name: agentName },
|
|
164
195
|
});
|
|
165
196
|
}
|
|
166
197
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface OwnerNameOptions {
|
|
2
|
+
platform?: string;
|
|
3
|
+
run?: (command: string, args: string[]) => string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* The full name of the OS account running the extraction, as the owner of the
|
|
7
|
+
* local Things database. On macOS this is the account's full name from
|
|
8
|
+
* `id -F`. Returns undefined elsewhere or when the name is unset or unreadable.
|
|
9
|
+
*/
|
|
10
|
+
export declare function localAccountName(options?: OwnerNameOptions): string | undefined;
|
|
11
|
+
//# sourceMappingURL=ownerName.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* The full name of the OS account running the extraction, as the owner of the
|
|
4
|
+
* local Things database. On macOS this is the account's full name from
|
|
5
|
+
* `id -F`. Returns undefined elsewhere or when the name is unset or unreadable.
|
|
6
|
+
*/
|
|
7
|
+
export function localAccountName(options = {}) {
|
|
8
|
+
if ((options.platform ?? process.platform) !== 'darwin')
|
|
9
|
+
return undefined;
|
|
10
|
+
const run = options.run ??
|
|
11
|
+
((command, args) => execFileSync(command, args, {
|
|
12
|
+
encoding: 'utf8',
|
|
13
|
+
timeout: 5000,
|
|
14
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
15
|
+
}));
|
|
16
|
+
try {
|
|
17
|
+
return run('/usr/bin/id', ['-F']).trim() || undefined;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chronicle.app/things-todo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"files": [
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"type": "module",
|
|
20
20
|
"description": "Chronicle things-todo extraction plugin",
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@chronicle.app/etl": "0.
|
|
23
|
-
"@chronicle.app/schema": "0.
|
|
24
|
-
"@chronicle.app/tsconfig": "0.
|
|
22
|
+
"@chronicle.app/etl": "0.4.0",
|
|
23
|
+
"@chronicle.app/schema": "0.4.0",
|
|
24
|
+
"@chronicle.app/tsconfig": "0.4.0",
|
|
25
25
|
"@types/node": "^22.10.1",
|
|
26
26
|
"typescript": "^5.7.2"
|
|
27
27
|
},
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"registry": "https://registry.npmjs.org/"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@chronicle.app/etl-sqlite": "0.
|
|
39
|
+
"@chronicle.app/etl-sqlite": "0.4.0",
|
|
40
40
|
"zod": "^3.24.1"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
@@ -53,7 +53,23 @@
|
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
"chronicle": {
|
|
56
|
-
"plugin": true
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
"plugin": true,
|
|
57
|
+
"sources": {
|
|
58
|
+
"things-todo": {
|
|
59
|
+
"strategies": {
|
|
60
|
+
"app-db": {
|
|
61
|
+
"delivery": "local",
|
|
62
|
+
"recordTypes": [
|
|
63
|
+
"tasks",
|
|
64
|
+
"projects"
|
|
65
|
+
],
|
|
66
|
+
"default": true
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"platforms": [
|
|
70
|
+
"darwin"
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
59
75
|
}
|