@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. Optional `agentName` labels the source-local task owner.
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
- const thingsDataDirs = readdirSync(containerDir).filter(name => name.startsWith('ThingsData-'));
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.string().optional().describe('Name of the task owner, when known'),
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.2.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.2.0",
23
- "@chronicle.app/schema": "0.2.0",
24
- "@chronicle.app/tsconfig": "0.2.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.2.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
- "oclif": {}
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
  }