@chronicle.app/timing-app 0.3.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 +21 -0
- package/README.md +86 -0
- package/dist/connectors/AppUsageExtractor.d.ts +20 -0
- package/dist/connectors/AppUsageExtractor.js +86 -0
- package/dist/connectors/TimeEntriesExtractor.d.ts +21 -0
- package/dist/connectors/TimeEntriesExtractor.js +73 -0
- package/dist/connectors/TimingAppTransformer.d.ts +48 -0
- package/dist/connectors/TimingAppTransformer.js +331 -0
- package/dist/connectors/TimingCallExtractor.d.ts +25 -0
- package/dist/connectors/TimingCallExtractor.js +78 -0
- package/dist/connectors/TimingCallTransformer.d.ts +22 -0
- package/dist/connectors/TimingCallTransformer.js +158 -0
- package/dist/connectors/TimingDbExtractor.d.ts +40 -0
- package/dist/connectors/TimingDbExtractor.js +121 -0
- package/dist/connectors/TimingDefaultExtractor.d.ts +25 -0
- package/dist/connectors/TimingDefaultExtractor.js +37 -0
- package/dist/connectors/TimingDefaultTransformer.d.ts +17 -0
- package/dist/connectors/TimingDefaultTransformer.js +16 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +10 -0
- package/package.json +82 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { SqliteExtractor } from '@chronicle.app/etl-sqlite';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export const DEFAULT_DB = `${process.env.HOME}/Library/Application Support/info.eurocomp.Timing2/SQLite.db`;
|
|
4
|
+
export const DEFAULT_WINDOW_DAYS = 90;
|
|
5
|
+
/**
|
|
6
|
+
* Shared base for the extractors that read Timing.app's local SQLite: opens the
|
|
7
|
+
* DB read-only, pre-loads the small `Device`/`Project` lookups, and resolves the
|
|
8
|
+
* time window. `AppActivity` (6M+ rows) is streamed with a cursor by subclasses.
|
|
9
|
+
*/
|
|
10
|
+
export class TimingDbExtractor extends SqliteExtractor {
|
|
11
|
+
static delivery = 'local';
|
|
12
|
+
static strategy = 'app-db';
|
|
13
|
+
static temporality = 'event';
|
|
14
|
+
static schema = SqliteExtractor.schema.omit({ input: true }).extend({
|
|
15
|
+
input: z.string().describe("Path to Timing.app's SQLite.db").default(DEFAULT_DB),
|
|
16
|
+
all: z
|
|
17
|
+
.boolean()
|
|
18
|
+
.default(false)
|
|
19
|
+
.describe(`Import the full history instead of the last ${DEFAULT_WINDOW_DAYS} days`),
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* Lower bound (epoch seconds): explicit `since`, else a default window, else
|
|
23
|
+
* none. An unbounded `--limit 0` also drops the window — asking for "no cap"
|
|
24
|
+
* is a request for a full backfill, not the newest 90 days without a count cap.
|
|
25
|
+
*/
|
|
26
|
+
sinceSeconds() {
|
|
27
|
+
if (this.config.since)
|
|
28
|
+
return this.config.since.getTime() / 1000;
|
|
29
|
+
if (this.config.all || this.getEffectiveLimit() === null)
|
|
30
|
+
return null;
|
|
31
|
+
return (Date.now() - DEFAULT_WINDOW_DAYS * 86_400_000) / 1000;
|
|
32
|
+
}
|
|
33
|
+
untilSeconds() {
|
|
34
|
+
return this.config.until ? this.config.until.getTime() / 1000 : null;
|
|
35
|
+
}
|
|
36
|
+
loadDevices() {
|
|
37
|
+
const rows = this.db.prepare(`SELECT CAST(localID AS TEXT) AS localId, CAST(globalID AS TEXT) AS globalId,
|
|
38
|
+
macAddress, displayName, property_bag AS propertyBag
|
|
39
|
+
FROM Device`).all();
|
|
40
|
+
const devices = new Map();
|
|
41
|
+
for (const row of rows) {
|
|
42
|
+
const bag = parseBag(row.propertyBag);
|
|
43
|
+
const relayed = bag._imported_via_screen_time === true;
|
|
44
|
+
const mac = !relayed && row.macAddress instanceof Uint8Array && row.macAddress.length === 6
|
|
45
|
+
? formatMac(row.macAddress)
|
|
46
|
+
: null;
|
|
47
|
+
devices.set(row.localId, {
|
|
48
|
+
globalId: row.globalId,
|
|
49
|
+
name: bag._display_name_override || row.displayName || row.globalId,
|
|
50
|
+
mac,
|
|
51
|
+
model: bag._device_model ?? null,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return devices;
|
|
55
|
+
}
|
|
56
|
+
loadProjects() {
|
|
57
|
+
const rows = this.db.prepare(`SELECT CAST(id AS TEXT) AS id, title, CAST(parentID AS TEXT) AS parentId,
|
|
58
|
+
color, productivityScore, isArchived, property_bag AS propertyBag
|
|
59
|
+
FROM Project WHERE isSample = 0`).all();
|
|
60
|
+
const projects = new Map();
|
|
61
|
+
for (const row of rows) {
|
|
62
|
+
const bag = parseBag(row.propertyBag);
|
|
63
|
+
projects.set(row.id, {
|
|
64
|
+
id: row.id,
|
|
65
|
+
title: row.title,
|
|
66
|
+
parentId: row.parentId ?? null,
|
|
67
|
+
color: row.color ?? null,
|
|
68
|
+
isArchived: Boolean(row.isArchived),
|
|
69
|
+
billing: bag._default_billing_status ?? null,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return projects;
|
|
73
|
+
}
|
|
74
|
+
/** A resolver that nests a project's full ancestry (leaf-first) for a row. */
|
|
75
|
+
projectChainResolver() {
|
|
76
|
+
const projects = this.loadProjects();
|
|
77
|
+
return (id) => resolveChain(projects, id);
|
|
78
|
+
}
|
|
79
|
+
countWindow(table, col, extra = '') {
|
|
80
|
+
const since = this.sinceSeconds();
|
|
81
|
+
const until = this.untilSeconds();
|
|
82
|
+
const conditions = ['isDeleted = 0'];
|
|
83
|
+
const params = [];
|
|
84
|
+
if (since !== null) {
|
|
85
|
+
conditions.push(`${col} > ?`);
|
|
86
|
+
params.push(since);
|
|
87
|
+
}
|
|
88
|
+
if (until !== null) {
|
|
89
|
+
conditions.push(`${col} < ?`);
|
|
90
|
+
params.push(until);
|
|
91
|
+
}
|
|
92
|
+
const where = conditions.join(' AND ') + extra;
|
|
93
|
+
return this.db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE ${where}`).get(...params).n;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** A project's ancestry, leaf-first: [self, parent, grandparent, ...]. */
|
|
97
|
+
export function resolveChain(projects, id) {
|
|
98
|
+
const chain = [];
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
let cur = id;
|
|
101
|
+
while (cur && projects.has(cur) && !seen.has(cur)) {
|
|
102
|
+
seen.add(cur);
|
|
103
|
+
const p = projects.get(cur);
|
|
104
|
+
chain.push({ id: p.id, title: p.title });
|
|
105
|
+
cur = p.parentId;
|
|
106
|
+
}
|
|
107
|
+
return chain;
|
|
108
|
+
}
|
|
109
|
+
function formatMac(buf) {
|
|
110
|
+
return [...buf].map(b => b.toString(16).padStart(2, '0')).join(':');
|
|
111
|
+
}
|
|
112
|
+
function parseBag(value) {
|
|
113
|
+
if (typeof value !== 'string' || !value)
|
|
114
|
+
return {};
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(value);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { MergingExtractor, Record } from '@chronicle.app/etl';
|
|
2
|
+
import { AppUsageExtractor } from './AppUsageExtractor.js';
|
|
3
|
+
import { TimeEntriesExtractor } from './TimeEntriesExtractor.js';
|
|
4
|
+
import { TimingCallExtractor } from './TimingCallExtractor.js';
|
|
5
|
+
import TimingDefaultTransformer from './TimingDefaultTransformer.js';
|
|
6
|
+
/**
|
|
7
|
+
* The default `timing-app` extractor: a reverse-chronological merge of the
|
|
8
|
+
* curated core streams — app usage, time entries, and calls. `--limit` applies
|
|
9
|
+
* to the merged output, so `-l 1` yields the single most-recent record across
|
|
10
|
+
* all three. Focused extractors remain available individually via `-t`.
|
|
11
|
+
*/
|
|
12
|
+
export declare class TimingDefaultExtractor extends MergingExtractor<typeof TimingDefaultExtractor> {
|
|
13
|
+
static source: string;
|
|
14
|
+
static description: string;
|
|
15
|
+
static delivery: "local";
|
|
16
|
+
static strategy: string;
|
|
17
|
+
static recordTypes: string[];
|
|
18
|
+
static default: boolean;
|
|
19
|
+
static defaultTransformer: typeof TimingDefaultTransformer;
|
|
20
|
+
static children: (typeof AppUsageExtractor | typeof TimeEntriesExtractor | typeof TimingCallExtractor)[];
|
|
21
|
+
static schema: any;
|
|
22
|
+
/** Calls key their occurrence on `startSec`; activity/entries on `startDate`. */
|
|
23
|
+
protected sortKey(record: Record): number;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=TimingDefaultExtractor.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { MergingExtractor } from '@chronicle.app/etl';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { AppUsageExtractor } from './AppUsageExtractor.js';
|
|
4
|
+
import { TimeEntriesExtractor } from './TimeEntriesExtractor.js';
|
|
5
|
+
import { TimingCallExtractor } from './TimingCallExtractor.js';
|
|
6
|
+
import TimingDefaultTransformer from './TimingDefaultTransformer.js';
|
|
7
|
+
import { DEFAULT_DB, DEFAULT_WINDOW_DAYS } from './TimingDbExtractor.js';
|
|
8
|
+
/**
|
|
9
|
+
* The default `timing-app` extractor: a reverse-chronological merge of the
|
|
10
|
+
* curated core streams — app usage, time entries, and calls. `--limit` applies
|
|
11
|
+
* to the merged output, so `-l 1` yields the single most-recent record across
|
|
12
|
+
* all three. Focused extractors remain available individually via `-t`.
|
|
13
|
+
*/
|
|
14
|
+
export class TimingDefaultExtractor extends MergingExtractor {
|
|
15
|
+
static source = 'timing-app';
|
|
16
|
+
static description = 'Everything: app usage, time entries, and calls';
|
|
17
|
+
static delivery = 'local';
|
|
18
|
+
static strategy = 'app-db';
|
|
19
|
+
static recordTypes = ['app-activities', 'time-entries', 'calls'];
|
|
20
|
+
static default = true;
|
|
21
|
+
static defaultTransformer = TimingDefaultTransformer;
|
|
22
|
+
static children = [AppUsageExtractor, TimeEntriesExtractor, TimingCallExtractor];
|
|
23
|
+
// Superset of the children's flags, so RunnerBuilder passes them through to
|
|
24
|
+
// the merge, which forwards the raw config to each child.
|
|
25
|
+
static schema = MergingExtractor.schema.extend({
|
|
26
|
+
input: z.string().describe("Path to Timing.app's SQLite.db").default(DEFAULT_DB),
|
|
27
|
+
all: z
|
|
28
|
+
.boolean()
|
|
29
|
+
.default(false)
|
|
30
|
+
.describe(`Import the full history instead of the last ${DEFAULT_WINDOW_DAYS} days`),
|
|
31
|
+
});
|
|
32
|
+
/** Calls key their occurrence on `startSec`; activity/entries on `startDate`. */
|
|
33
|
+
sortKey(record) {
|
|
34
|
+
const ctx = record.context;
|
|
35
|
+
return record.extraction.recordType === 'calls' ? (ctx.startSec ?? 0) : (ctx.startDate ?? 0);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DispatchingTransformer } from '@chronicle.app/etl';
|
|
2
|
+
import TimingAppTransformer from './TimingAppTransformer.js';
|
|
3
|
+
import TimingCallTransformer from './TimingCallTransformer.js';
|
|
4
|
+
/**
|
|
5
|
+
* Routes the merged default stream to the focused transformers by record type:
|
|
6
|
+
* app usage and time entries → `TimingAppTransformer` (source `timing-app`),
|
|
7
|
+
* calls → `TimingCallTransformer` (re-sourced to `apple-call-history` so they fold with
|
|
8
|
+
* the Apple Call History plugin).
|
|
9
|
+
*/
|
|
10
|
+
export default class TimingDefaultTransformer extends DispatchingTransformer {
|
|
11
|
+
static routes: {
|
|
12
|
+
'app-activities': typeof TimingAppTransformer;
|
|
13
|
+
'time-entries': typeof TimingAppTransformer;
|
|
14
|
+
calls: typeof TimingCallTransformer;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=TimingDefaultTransformer.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DispatchingTransformer } from '@chronicle.app/etl';
|
|
2
|
+
import TimingAppTransformer from './TimingAppTransformer.js';
|
|
3
|
+
import TimingCallTransformer from './TimingCallTransformer.js';
|
|
4
|
+
/**
|
|
5
|
+
* Routes the merged default stream to the focused transformers by record type:
|
|
6
|
+
* app usage and time entries → `TimingAppTransformer` (source `timing-app`),
|
|
7
|
+
* calls → `TimingCallTransformer` (re-sourced to `apple-call-history` so they fold with
|
|
8
|
+
* the Apple Call History plugin).
|
|
9
|
+
*/
|
|
10
|
+
export default class TimingDefaultTransformer extends DispatchingTransformer {
|
|
11
|
+
static routes = {
|
|
12
|
+
'app-activities': TimingAppTransformer,
|
|
13
|
+
'time-entries': TimingAppTransformer,
|
|
14
|
+
calls: TimingCallTransformer,
|
|
15
|
+
};
|
|
16
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { TimingDefaultExtractor } from './connectors/TimingDefaultExtractor.js';
|
|
2
|
+
export { default as TimingDefaultTransformer } from './connectors/TimingDefaultTransformer.js';
|
|
3
|
+
export { AppUsageExtractor } from './connectors/AppUsageExtractor.js';
|
|
4
|
+
export { TimeEntriesExtractor } from './connectors/TimeEntriesExtractor.js';
|
|
5
|
+
export { default as TimingAppTransformer } from './connectors/TimingAppTransformer.js';
|
|
6
|
+
export { TimingCallExtractor } from './connectors/TimingCallExtractor.js';
|
|
7
|
+
export { default as TimingCallTransformer } from './connectors/TimingCallTransformer.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Export extractors + transformers for plugin scanning.
|
|
2
|
+
// `default` (the reverse-chronological merge) is the default type; the focused
|
|
3
|
+
// extractors are opt-in via `-t app-usage|time-entries|calls`.
|
|
4
|
+
export { TimingDefaultExtractor } from './connectors/TimingDefaultExtractor.js';
|
|
5
|
+
export { default as TimingDefaultTransformer } from './connectors/TimingDefaultTransformer.js';
|
|
6
|
+
export { AppUsageExtractor } from './connectors/AppUsageExtractor.js';
|
|
7
|
+
export { TimeEntriesExtractor } from './connectors/TimeEntriesExtractor.js';
|
|
8
|
+
export { default as TimingAppTransformer } from './connectors/TimingAppTransformer.js';
|
|
9
|
+
export { TimingCallExtractor } from './connectors/TimingCallExtractor.js';
|
|
10
|
+
export { default as TimingCallTransformer } from './connectors/TimingCallTransformer.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chronicle.app/timing-app",
|
|
3
|
+
"version": "0.3.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
|
+
"chronicle",
|
|
18
|
+
"timing",
|
|
19
|
+
"app-usage",
|
|
20
|
+
"time-tracking"
|
|
21
|
+
],
|
|
22
|
+
"author": "Andrew Louis",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"description": "Chronicle plugin for extracting app usage, projects, and time blocks from Timing.app",
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@chronicle.app/etl": "0.3.0",
|
|
28
|
+
"@chronicle.app/schema": "0.3.0",
|
|
29
|
+
"@chronicle.app/tsconfig": "0.3.0",
|
|
30
|
+
"@types/node": "^22.10.1",
|
|
31
|
+
"typescript": "^5.7.2"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://chronicle.app",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/chronicle-app/chronicle.git",
|
|
37
|
+
"directory": "plugins/timing-app"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public",
|
|
41
|
+
"registry": "https://registry.npmjs.org/"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@chronicle.app/etl-sqlite": "0.3.0",
|
|
45
|
+
"@chronicle.app/icloud": "0.3.0",
|
|
46
|
+
"zod": "^3.24.1"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@chronicle.app/etl": ">=0.1.0 <1.0.0",
|
|
50
|
+
"@chronicle.app/schema": ">=0.1.0 <1.0.0"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=22.13.0"
|
|
54
|
+
},
|
|
55
|
+
"exports": {
|
|
56
|
+
".": {
|
|
57
|
+
"types": "./dist/index.d.ts",
|
|
58
|
+
"import": "./dist/index.js"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"chronicle": {
|
|
62
|
+
"plugin": true,
|
|
63
|
+
"sources": {
|
|
64
|
+
"timing-app": {
|
|
65
|
+
"strategies": {
|
|
66
|
+
"app-db": {
|
|
67
|
+
"delivery": "local",
|
|
68
|
+
"recordTypes": [
|
|
69
|
+
"app-activities",
|
|
70
|
+
"time-entries",
|
|
71
|
+
"calls"
|
|
72
|
+
],
|
|
73
|
+
"default": true
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"platforms": [
|
|
77
|
+
"darwin"
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|