@chronicle.app/chrome 0.4.0 → 0.5.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.
@@ -1,4 +1,4 @@
1
- import { Record } from '@chronicle.app/etl';
1
+ import { Record, type Survey, type SurveyOptions } from '@chronicle.app/etl';
2
2
  import { SqliteExtractor } from '@chronicle.app/etl-sqlite';
3
3
  import ChromeTransformer from './ChromeTransformer.js';
4
4
  /** A link followed or a form submitted, from one page to the visit's. */
@@ -59,5 +59,7 @@ export declare class ChromeExtractor extends SqliteExtractor<typeof ChromeExtrac
59
59
  private navigation;
60
60
  private visit;
61
61
  determineCount(): Promise<number | null>;
62
+ /** The visits a run reads and when, with how many pages they were to. */
63
+ survey(options?: SurveyOptions): Promise<Survey | null>;
62
64
  }
63
65
  //# sourceMappingURL=ChromeExtractor.d.ts.map
@@ -1,8 +1,8 @@
1
1
  import { constants as fsConstants, copyFileSync, mkdtempSync, rmSync, statSync } from 'node:fs';
2
2
  import { homedir, tmpdir } from 'node:os';
3
3
  import { dirname, join } from 'node:path';
4
- import { EXIT_CODES, ExtractorError, delay } from '@chronicle.app/etl';
5
- import { SqliteExtractor, allRows, chromeToUnixMsSql, getRow, iterateRows, timeRangeConditions, unixMsToChromeTimestamp, } from '@chronicle.app/etl-sqlite';
4
+ import { EXIT_CODES, ExtractorError, delay, } from '@chronicle.app/etl';
5
+ import { SqliteExtractor, allRows, chromeToUnixMsSql, countsByMonth, getRow, iterateRows, monthSql, timeRangeConditions, unixMsToChromeTimestamp, } from '@chronicle.app/etl-sqlite';
6
6
  import { z } from 'zod';
7
7
  import ChromeTransformer from './ChromeTransformer.js';
8
8
  import { readChromeAccount } from './profile.js';
@@ -251,6 +251,30 @@ export class ChromeExtractor extends SqliteExtractor {
251
251
  return null;
252
252
  }
253
253
  }
254
+ /** The visits a run reads and when, with how many pages they were to. */
255
+ async survey(options = {}) {
256
+ if (!this.db)
257
+ return null;
258
+ const { where, values } = this.visitFilter();
259
+ const row = this.db
260
+ .prepare(`SELECT COUNT(*) AS n, MIN(${visitTime}) AS first, MAX(${visitTime}) AS last,
261
+ COUNT(DISTINCT urls.id) AS pages ${ChromeExtractor.FROM} WHERE ${where}`)
262
+ .get(...values);
263
+ const byMonth = options.byMonth &&
264
+ countsByMonth(this.db
265
+ .prepare(`SELECT ${monthSql(`${visitTime} / 1000`)} AS month, COUNT(*) AS n
266
+ ${ChromeExtractor.FROM} WHERE ${where} GROUP BY month`)
267
+ .all(...values));
268
+ return {
269
+ views: {
270
+ count: row.n,
271
+ ...(row.first !== null && { earliest: new Date(row.first) }),
272
+ ...(row.last !== null && { latest: new Date(row.last) }),
273
+ ...(byMonth && { byMonth }),
274
+ measures: [{ key: 'pages', label: 'pages', value: row.pages }],
275
+ },
276
+ };
277
+ }
254
278
  }
255
279
  /**
256
280
  * History's size and modification time while it holds only saved changes,
@@ -0,0 +1,13 @@
1
+ import { Locator, type Candidate, type LocationMatch } from '@chronicle.app/etl';
2
+ /**
3
+ * Chrome's profiles, named as its profile menu names them, with the account
4
+ * each is signed in to. The profile list in `Local State` decides which
5
+ * folders are profiles, since Chrome keeps others beside them, like
6
+ * `System Profile`, that no one browses in.
7
+ */
8
+ export declare class ChromeLocator extends Locator {
9
+ static source: string;
10
+ static strategy: string;
11
+ locate(matches: LocationMatch[]): AsyncGenerator<Candidate>;
12
+ }
13
+ //# sourceMappingURL=ChromeLocator.d.ts.map
@@ -0,0 +1,40 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { Locator } from '@chronicle.app/etl';
4
+ import { readChromeAccount } from './profile.js';
5
+ /** Chrome's profiles by folder, with the name each shows in Chrome's profile menu. */
6
+ function readProfiles(localStatePath) {
7
+ try {
8
+ const profiles = JSON.parse(readFileSync(localStatePath, 'utf8'))?.profile?.info_cache;
9
+ return profiles && typeof profiles === 'object' ? profiles : null;
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ /**
16
+ * Chrome's profiles, named as its profile menu names them, with the account
17
+ * each is signed in to. The profile list in `Local State` decides which
18
+ * folders are profiles, since Chrome keeps others beside them, like
19
+ * `System Profile`, that no one browses in.
20
+ */
21
+ export class ChromeLocator extends Locator {
22
+ static source = 'chrome';
23
+ static strategy = 'app-db';
24
+ async *locate(matches) {
25
+ for (const match of matches) {
26
+ const profile = String(match.options.profile);
27
+ const profileDir = dirname(match.path);
28
+ const profiles = readProfiles(join(dirname(profileDir), 'Local State'));
29
+ if (profiles && !profiles[profile])
30
+ continue;
31
+ const name = profiles?.[profile]?.name || profile;
32
+ const account = readChromeAccount(join(profileDir, 'Preferences'));
33
+ yield {
34
+ label: account ? `${name} (${account.email})` : name,
35
+ config: { profile },
36
+ path: match.path,
37
+ };
38
+ }
39
+ }
40
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { ChromeExtractor } from './connectors/ChromeExtractor.js';
2
+ export { ChromeLocator } from './connectors/ChromeLocator.js';
2
3
  export { default as ChromeTransformer } from './connectors/ChromeTransformer.js';
3
4
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  // Export extractors for plugin scanning
2
2
  export { ChromeExtractor } from './connectors/ChromeExtractor.js';
3
+ export { ChromeLocator } from './connectors/ChromeLocator.js';
3
4
  export { default as ChromeTransformer } from './connectors/ChromeTransformer.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chronicle.app/chrome",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [
@@ -20,9 +20,9 @@
20
20
  "type": "module",
21
21
  "description": "Chronicle Chrome browsing history plugin",
22
22
  "devDependencies": {
23
- "@chronicle.app/etl": "0.4.0",
24
- "@chronicle.app/schema": "0.4.0",
25
- "@chronicle.app/tsconfig": "0.4.0",
23
+ "@chronicle.app/etl": "0.5.0",
24
+ "@chronicle.app/schema": "0.5.0",
25
+ "@chronicle.app/tsconfig": "0.5.0",
26
26
  "@types/node": "^22.10.1",
27
27
  "typescript": "^5.7.2"
28
28
  },
@@ -37,8 +37,8 @@
37
37
  "registry": "https://registry.npmjs.org/"
38
38
  },
39
39
  "dependencies": {
40
- "@chronicle.app/etl-sqlite": "0.4.0",
41
- "@chronicle.app/google": "0.4.0",
40
+ "@chronicle.app/etl-sqlite": "0.5.0",
41
+ "@chronicle.app/google": "0.5.0",
42
42
  "zod": "^3.24.1"
43
43
  },
44
44
  "peerDependencies": {
@@ -64,13 +64,30 @@
64
64
  "recordTypes": [
65
65
  "views"
66
66
  ],
67
- "default": true
67
+ "default": true,
68
+ "locations": [
69
+ {
70
+ "path": "~/Library/Application Support/Google/Chrome/{profile}/History",
71
+ "platforms": [
72
+ "darwin"
73
+ ]
74
+ },
75
+ {
76
+ "path": "~/.config/google-chrome/{profile}/History",
77
+ "platforms": [
78
+ "linux"
79
+ ]
80
+ }
81
+ ]
68
82
  }
69
83
  },
70
84
  "platforms": [
71
85
  "darwin",
72
86
  "linux",
73
87
  "win32"
88
+ ],
89
+ "requires": [
90
+ "full-disk-access"
74
91
  ]
75
92
  }
76
93
  }