liferewind 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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/dist/api/client.d.ts +31 -0
  4. package/dist/api/client.d.ts.map +1 -0
  5. package/dist/api/client.js +115 -0
  6. package/dist/cli/commands/collect.d.ts +3 -0
  7. package/dist/cli/commands/collect.d.ts.map +1 -0
  8. package/dist/cli/commands/collect.js +62 -0
  9. package/dist/cli/commands/config.d.ts +3 -0
  10. package/dist/cli/commands/config.d.ts.map +1 -0
  11. package/dist/cli/commands/config.js +112 -0
  12. package/dist/cli/commands/doctor.d.ts +3 -0
  13. package/dist/cli/commands/doctor.d.ts.map +1 -0
  14. package/dist/cli/commands/doctor.js +150 -0
  15. package/dist/cli/commands/init.d.ts +3 -0
  16. package/dist/cli/commands/init.d.ts.map +1 -0
  17. package/dist/cli/commands/init.js +244 -0
  18. package/dist/cli/commands/start.d.ts +3 -0
  19. package/dist/cli/commands/start.d.ts.map +1 -0
  20. package/dist/cli/commands/start.js +59 -0
  21. package/dist/cli/commands/status.d.ts +3 -0
  22. package/dist/cli/commands/status.d.ts.map +1 -0
  23. package/dist/cli/commands/status.js +49 -0
  24. package/dist/cli/detect/browsers.d.ts +3 -0
  25. package/dist/cli/detect/browsers.d.ts.map +1 -0
  26. package/dist/cli/detect/browsers.js +19 -0
  27. package/dist/cli/detect/chatbot.d.ts +3 -0
  28. package/dist/cli/detect/chatbot.d.ts.map +1 -0
  29. package/dist/cli/detect/chatbot.js +15 -0
  30. package/dist/cli/detect/git.d.ts +2 -0
  31. package/dist/cli/detect/git.d.ts.map +1 -0
  32. package/dist/cli/detect/git.js +10 -0
  33. package/dist/cli/detect/index.d.ts +4 -0
  34. package/dist/cli/detect/index.d.ts.map +1 -0
  35. package/dist/cli/detect/index.js +3 -0
  36. package/dist/cli/index.d.ts +3 -0
  37. package/dist/cli/index.d.ts.map +1 -0
  38. package/dist/cli/index.js +30 -0
  39. package/dist/cli/utils/output.d.ts +8 -0
  40. package/dist/cli/utils/output.d.ts.map +1 -0
  41. package/dist/cli/utils/output.js +28 -0
  42. package/dist/cli/utils/path.d.ts +9 -0
  43. package/dist/cli/utils/path.d.ts.map +1 -0
  44. package/dist/cli/utils/path.js +23 -0
  45. package/dist/config/loader.d.ts +7 -0
  46. package/dist/config/loader.d.ts.map +1 -0
  47. package/dist/config/loader.js +64 -0
  48. package/dist/config/paths.d.ts +8 -0
  49. package/dist/config/paths.d.ts.map +1 -0
  50. package/dist/config/paths.js +21 -0
  51. package/dist/config/schema.d.ts +95 -0
  52. package/dist/config/schema.d.ts.map +1 -0
  53. package/dist/config/schema.js +110 -0
  54. package/dist/config/writer.d.ts +3 -0
  55. package/dist/config/writer.d.ts.map +1 -0
  56. package/dist/config/writer.js +19 -0
  57. package/dist/core/collector.d.ts +19 -0
  58. package/dist/core/collector.d.ts.map +1 -0
  59. package/dist/core/collector.js +83 -0
  60. package/dist/core/scheduler.d.ts +12 -0
  61. package/dist/core/scheduler.d.ts.map +1 -0
  62. package/dist/core/scheduler.js +48 -0
  63. package/dist/core/types.d.ts +29 -0
  64. package/dist/core/types.d.ts.map +1 -0
  65. package/dist/core/types.js +2 -0
  66. package/dist/index.d.ts +9 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.js +7 -0
  69. package/dist/sources/base.d.ts +27 -0
  70. package/dist/sources/base.d.ts.map +1 -0
  71. package/dist/sources/base.js +23 -0
  72. package/dist/sources/browser/index.d.ts +14 -0
  73. package/dist/sources/browser/index.d.ts.map +1 -0
  74. package/dist/sources/browser/index.js +116 -0
  75. package/dist/sources/browser/readers/base.d.ts +28 -0
  76. package/dist/sources/browser/readers/base.d.ts.map +1 -0
  77. package/dist/sources/browser/readers/base.js +110 -0
  78. package/dist/sources/browser/readers/chromium.d.ts +13 -0
  79. package/dist/sources/browser/readers/chromium.d.ts.map +1 -0
  80. package/dist/sources/browser/readers/chromium.js +64 -0
  81. package/dist/sources/browser/readers/safari.d.ts +9 -0
  82. package/dist/sources/browser/readers/safari.d.ts.map +1 -0
  83. package/dist/sources/browser/readers/safari.js +34 -0
  84. package/dist/sources/browser/types.d.ts +35 -0
  85. package/dist/sources/browser/types.d.ts.map +1 -0
  86. package/dist/sources/browser/types.js +1 -0
  87. package/dist/sources/chatbot/index.d.ts +12 -0
  88. package/dist/sources/chatbot/index.d.ts.map +1 -0
  89. package/dist/sources/chatbot/index.js +67 -0
  90. package/dist/sources/chatbot/readers/base.d.ts +25 -0
  91. package/dist/sources/chatbot/readers/base.d.ts.map +1 -0
  92. package/dist/sources/chatbot/readers/base.js +109 -0
  93. package/dist/sources/chatbot/readers/chatwise.d.ts +14 -0
  94. package/dist/sources/chatbot/readers/chatwise.d.ts.map +1 -0
  95. package/dist/sources/chatbot/readers/chatwise.js +117 -0
  96. package/dist/sources/chatbot/types.d.ts +33 -0
  97. package/dist/sources/chatbot/types.d.ts.map +1 -0
  98. package/dist/sources/chatbot/types.js +1 -0
  99. package/dist/sources/filesystem/index.d.ts +10 -0
  100. package/dist/sources/filesystem/index.d.ts.map +1 -0
  101. package/dist/sources/filesystem/index.js +58 -0
  102. package/dist/sources/filesystem/scanner.d.ts +24 -0
  103. package/dist/sources/filesystem/scanner.d.ts.map +1 -0
  104. package/dist/sources/filesystem/scanner.js +264 -0
  105. package/dist/sources/filesystem/types.d.ts +39 -0
  106. package/dist/sources/filesystem/types.d.ts.map +1 -0
  107. package/dist/sources/filesystem/types.js +1 -0
  108. package/dist/sources/git/index.d.ts +16 -0
  109. package/dist/sources/git/index.d.ts.map +1 -0
  110. package/dist/sources/git/index.js +169 -0
  111. package/dist/sources/git/types.d.ts +25 -0
  112. package/dist/sources/git/types.d.ts.map +1 -0
  113. package/dist/sources/git/types.js +1 -0
  114. package/dist/sources/index.d.ts +7 -0
  115. package/dist/sources/index.d.ts.map +1 -0
  116. package/dist/sources/index.js +16 -0
  117. package/dist/sources/registry.d.ts +13 -0
  118. package/dist/sources/registry.d.ts.map +1 -0
  119. package/dist/sources/registry.js +19 -0
  120. package/dist/utils/logger.d.ts +12 -0
  121. package/dist/utils/logger.d.ts.map +1 -0
  122. package/dist/utils/logger.js +34 -0
  123. package/dist/utils/path.d.ts +9 -0
  124. package/dist/utils/path.d.ts.map +1 -0
  125. package/dist/utils/path.js +22 -0
  126. package/dist/utils/retry.d.ts +8 -0
  127. package/dist/utils/retry.d.ts.map +1 -0
  128. package/dist/utils/retry.js +22 -0
  129. package/package.json +81 -0
@@ -0,0 +1,27 @@
1
+ import type { CollectionResult, SourceConfig, SourceType } from '../core/types.js';
2
+ import type { Logger } from '../utils/logger.js';
3
+ export interface DataSourceContext {
4
+ logger: Logger;
5
+ lastCollectionTime?: Date;
6
+ }
7
+ export declare abstract class DataSource<TOptions = unknown> {
8
+ abstract readonly type: SourceType;
9
+ abstract readonly name: string;
10
+ protected config: SourceConfig;
11
+ protected context: DataSourceContext;
12
+ constructor(config: SourceConfig, context: DataSourceContext);
13
+ /**
14
+ * Source-specific options, cast to the concrete type.
15
+ * Type assertion is safe because options are validated by Zod schema in config loader.
16
+ */
17
+ protected get options(): TOptions;
18
+ /** Validate that this source can run on the current system */
19
+ abstract validate(): Promise<boolean>;
20
+ /** Collect data from this source */
21
+ abstract collect(): Promise<CollectionResult>;
22
+ /** Get the schedule for this source */
23
+ getSchedule(): import("../core/types.js").ScheduleFrequency;
24
+ /** Check if this source is enabled */
25
+ isEnabled(): boolean;
26
+ }
27
+ //# sourceMappingURL=base.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/sources/base.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,IAAI,CAAC;CAC3B;AAED,8BAAsB,UAAU,CAAC,QAAQ,GAAG,OAAO;IACjD,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IACnC,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAE/B,SAAS,CAAC,MAAM,EAAE,YAAY,CAAC;IAC/B,SAAS,CAAC,OAAO,EAAE,iBAAiB,CAAC;gBAEzB,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,iBAAiB;IAK5D;;;OAGG;IACH,SAAS,KAAK,OAAO,IAAI,QAAQ,CAEhC;IAED,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IAErC,oCAAoC;IACpC,QAAQ,CAAC,OAAO,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAE7C,uCAAuC;IACvC,WAAW;IAIX,sCAAsC;IACtC,SAAS;CAGV"}
@@ -0,0 +1,23 @@
1
+ export class DataSource {
2
+ config;
3
+ context;
4
+ constructor(config, context) {
5
+ this.config = config;
6
+ this.context = context;
7
+ }
8
+ /**
9
+ * Source-specific options, cast to the concrete type.
10
+ * Type assertion is safe because options are validated by Zod schema in config loader.
11
+ */
12
+ get options() {
13
+ return this.config.options;
14
+ }
15
+ /** Get the schedule for this source */
16
+ getSchedule() {
17
+ return this.config.schedule;
18
+ }
19
+ /** Check if this source is enabled */
20
+ isEnabled() {
21
+ return this.config.enabled;
22
+ }
23
+ }
@@ -0,0 +1,14 @@
1
+ import { DataSource } from '../base.js';
2
+ import type { CollectionResult } from '../../core/types.js';
3
+ import type { BrowserSourceOptions } from './types.js';
4
+ export declare class BrowserSource extends DataSource<BrowserSourceOptions> {
5
+ readonly type: "browser";
6
+ readonly name = "Browser History";
7
+ private readers;
8
+ validate(): Promise<boolean>;
9
+ collect(): Promise<CollectionResult>;
10
+ /** Aggregate raw browser history items by URL + browser + date */
11
+ private aggregateByDay;
12
+ private createReader;
13
+ }
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sources/browser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAEV,oBAAoB,EAGrB,MAAM,YAAY,CAAC;AAKpB,qBAAa,aAAc,SAAQ,UAAU,CAAC,oBAAoB,CAAC;IACjE,QAAQ,CAAC,IAAI,EAAG,SAAS,CAAU;IACnC,QAAQ,CAAC,IAAI,qBAAqB;IAElC,OAAO,CAAC,OAAO,CAAuB;IAEhC,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IA0B5B,OAAO,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAqC1C,kEAAkE;IAClE,OAAO,CAAC,cAAc;IAmDtB,OAAO,CAAC,YAAY;CAcrB"}
@@ -0,0 +1,116 @@
1
+ import { DataSource } from '../base.js';
2
+ import { ChromiumReader } from './readers/chromium.js';
3
+ import { SafariReader } from './readers/safari.js';
4
+ export class BrowserSource extends DataSource {
5
+ type = 'browser';
6
+ name = 'Browser History';
7
+ readers = [];
8
+ async validate() {
9
+ const readerContext = {
10
+ logger: this.context.logger,
11
+ sinceDays: this.options.sinceDays,
12
+ excludeDomains: this.options.excludeDomains,
13
+ };
14
+ // Create readers for enabled browsers
15
+ for (const browser of this.options.browsers) {
16
+ const reader = this.createReader(browser, readerContext);
17
+ if (reader && reader.hasValidDatabases()) {
18
+ this.readers.push(reader);
19
+ this.context.logger.info(`Browser ${browser} validated and ready`);
20
+ }
21
+ else {
22
+ this.context.logger.warn(`Browser ${browser} has no accessible history database`);
23
+ }
24
+ }
25
+ if (this.readers.length === 0) {
26
+ this.context.logger.error('No valid browser history databases found');
27
+ return false;
28
+ }
29
+ return true;
30
+ }
31
+ async collect() {
32
+ const allItems = [];
33
+ for (const reader of this.readers) {
34
+ try {
35
+ const items = reader.read();
36
+ allItems.push(...items);
37
+ this.context.logger.debug(`Collected ${items.length} items from ${reader.browserType}`);
38
+ }
39
+ catch (error) {
40
+ this.context.logger.error(`Failed to collect from ${reader.browserType}`, error);
41
+ }
42
+ }
43
+ // Aggregate by URL + browser + date
44
+ const aggregated = this.aggregateByDay(allItems);
45
+ this.context.logger.info(`Aggregated ${allItems.length} raw visits into ${aggregated.length} daily records`);
46
+ // Sort by last visit time descending
47
+ aggregated.sort((a, b) => new Date(b.lastVisitTime).getTime() - new Date(a.lastVisitTime).getTime());
48
+ return {
49
+ sourceType: this.type,
50
+ success: true,
51
+ itemsCollected: aggregated.length,
52
+ items: aggregated.map((item) => ({
53
+ sourceType: this.type,
54
+ timestamp: new Date(item.lastVisitTime),
55
+ data: item,
56
+ })),
57
+ collectedAt: new Date(),
58
+ };
59
+ }
60
+ /** Aggregate raw browser history items by URL + browser + date */
61
+ aggregateByDay(items) {
62
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
63
+ const grouped = new Map();
64
+ for (const item of items) {
65
+ // Get local date string in YYYY-MM-DD format
66
+ const date = new Date(item.visitTime).toLocaleDateString('en-CA', { timeZone: timezone });
67
+ const key = `${item.url}|${item.browser}|${date}`;
68
+ if (!grouped.has(key)) {
69
+ grouped.set(key, []);
70
+ }
71
+ grouped.get(key).push(item);
72
+ }
73
+ return Array.from(grouped.entries()).map(([key, visits]) => {
74
+ const parts = key.split('|');
75
+ // Key format is guaranteed: `${url}|${browser}|${date}`
76
+ if (parts.length < 3) {
77
+ throw new Error(`Invalid aggregation key format: ${key}`);
78
+ }
79
+ const url = parts[0];
80
+ const browser = parts[1];
81
+ const date = parts[2];
82
+ // Sort visits by time ascending to get first and last
83
+ const sorted = visits.sort((a, b) => new Date(a.visitTime).getTime() - new Date(b.visitTime).getTime());
84
+ // Collect unique profiles
85
+ const profiles = [...new Set(visits.map((v) => v.profile).filter((p) => !!p))];
86
+ // sorted is guaranteed non-empty since visits came from grouping
87
+ const firstVisit = sorted[0];
88
+ const lastVisit = sorted[sorted.length - 1];
89
+ return {
90
+ url,
91
+ title: lastVisit.title, // Use title from last visit
92
+ browser,
93
+ profiles,
94
+ date,
95
+ timezone,
96
+ dailyVisitCount: visits.length,
97
+ firstVisitTime: firstVisit.visitTime,
98
+ lastVisitTime: lastVisit.visitTime,
99
+ };
100
+ });
101
+ }
102
+ createReader(browser, context) {
103
+ switch (browser) {
104
+ case 'chrome':
105
+ case 'arc':
106
+ case 'dia':
107
+ case 'comet':
108
+ return new ChromiumReader(browser, context);
109
+ case 'safari':
110
+ return new SafariReader(context);
111
+ default:
112
+ this.context.logger.warn(`Unknown browser type: ${browser}`);
113
+ return null;
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,28 @@
1
+ import type { BrowserHistoryItem, BrowserType } from '../types.js';
2
+ import type { Logger } from '../../../utils/logger.js';
3
+ export interface ReaderContext {
4
+ logger: Logger;
5
+ sinceDays: number;
6
+ excludeDomains: string[];
7
+ }
8
+ export declare abstract class BrowserReader {
9
+ abstract readonly browserType: BrowserType;
10
+ protected context: ReaderContext;
11
+ constructor(context: ReaderContext);
12
+ /** Get all database paths for this browser */
13
+ abstract getDbPaths(): string[];
14
+ /** Get the SQL query for this browser's history format */
15
+ abstract getQuery(): string;
16
+ /** Get profile name from database path (if applicable) */
17
+ abstract getProfileName(dbPath: string): string | undefined;
18
+ /** Read history from all available databases */
19
+ read(): BrowserHistoryItem[];
20
+ /** Check if any history databases exist */
21
+ hasValidDatabases(): boolean;
22
+ private readFromDb;
23
+ /** Type guard to validate raw SQL row structure */
24
+ private isValidRow;
25
+ private isValidUrl;
26
+ private isExcluded;
27
+ }
28
+ //# sourceMappingURL=base.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/sources/browser/readers/base.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAiB,MAAM,aAAa,CAAC;AAClF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAEvD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,8BAAsB,aAAa;IACjC,QAAQ,CAAC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAE3C,SAAS,CAAC,OAAO,EAAE,aAAa,CAAC;gBAErB,OAAO,EAAE,aAAa;IAIlC,8CAA8C;IAC9C,QAAQ,CAAC,UAAU,IAAI,MAAM,EAAE;IAE/B,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,IAAI,MAAM;IAE3B,0DAA0D;IAC1D,QAAQ,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAE3D,gDAAgD;IAChD,IAAI,IAAI,kBAAkB,EAAE;IAwB5B,2CAA2C;IAC3C,iBAAiB,IAAI,OAAO;IAI5B,OAAO,CAAC,UAAU;IAgDlB,mDAAmD;IACnD,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,UAAU;CAYnB"}
@@ -0,0 +1,110 @@
1
+ import { copyFileSync, unlinkSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { randomUUID } from 'node:crypto';
5
+ import Database from 'better-sqlite3';
6
+ export class BrowserReader {
7
+ context;
8
+ constructor(context) {
9
+ this.context = context;
10
+ }
11
+ /** Read history from all available databases */
12
+ read() {
13
+ const items = [];
14
+ const dbPaths = this.getDbPaths();
15
+ if (dbPaths.length === 0) {
16
+ this.context.logger.debug(`No ${this.browserType} history databases found`);
17
+ return items;
18
+ }
19
+ for (const dbPath of dbPaths) {
20
+ try {
21
+ const profileItems = this.readFromDb(dbPath);
22
+ items.push(...profileItems);
23
+ this.context.logger.debug(`Read ${profileItems.length} items from ${this.browserType} (${this.getProfileName(dbPath) ?? 'default'})`);
24
+ }
25
+ catch (error) {
26
+ this.context.logger.warn(`Failed to read from ${dbPath}`, error);
27
+ }
28
+ }
29
+ return items;
30
+ }
31
+ /** Check if any history databases exist */
32
+ hasValidDatabases() {
33
+ return this.getDbPaths().length > 0;
34
+ }
35
+ readFromDb(dbPath) {
36
+ const tempPath = join(tmpdir(), `browser-history-${randomUUID()}.db`);
37
+ let db = null;
38
+ try {
39
+ // Copy database to avoid locking issues
40
+ copyFileSync(dbPath, tempPath);
41
+ db = new Database(tempPath, { readonly: true });
42
+ const sinceTimestamp = Math.floor(Date.now() / 1000) - this.context.sinceDays * 24 * 60 * 60;
43
+ const stmt = db.prepare(this.getQuery());
44
+ const rows = stmt.all(sinceTimestamp);
45
+ const profile = this.getProfileName(dbPath);
46
+ return rows
47
+ .filter((row) => this.isValidRow(row))
48
+ .filter((row) => this.isValidUrl(row.url))
49
+ .filter((row) => !this.isExcluded(row.url))
50
+ .map((row) => ({
51
+ url: row.url,
52
+ title: row.title ?? '',
53
+ visitTime: new Date(row.visit_unix * 1000).toISOString(),
54
+ visitCount: row.visit_count,
55
+ browser: this.browserType,
56
+ ...(profile && { profile }),
57
+ }));
58
+ }
59
+ finally {
60
+ // Close database connection
61
+ if (db) {
62
+ try {
63
+ db.close();
64
+ }
65
+ catch (error) {
66
+ this.context.logger.debug('Failed to close database connection', error);
67
+ }
68
+ }
69
+ // Clean up temp file
70
+ if (existsSync(tempPath)) {
71
+ try {
72
+ unlinkSync(tempPath);
73
+ }
74
+ catch (error) {
75
+ this.context.logger.debug(`Failed to clean up temp file: ${tempPath}`, error);
76
+ }
77
+ }
78
+ }
79
+ }
80
+ /** Type guard to validate raw SQL row structure */
81
+ isValidRow(row) {
82
+ if (typeof row !== 'object' || row === null)
83
+ return false;
84
+ const record = row;
85
+ return (typeof record['url'] === 'string' &&
86
+ (typeof record['title'] === 'string' || record['title'] === null) &&
87
+ typeof record['visit_count'] === 'number' &&
88
+ typeof record['visit_unix'] === 'number');
89
+ }
90
+ isValidUrl(url) {
91
+ try {
92
+ const parsed = new URL(url);
93
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
99
+ isExcluded(url) {
100
+ if (this.context.excludeDomains.length === 0)
101
+ return false;
102
+ try {
103
+ const hostname = new URL(url).hostname;
104
+ return this.context.excludeDomains.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,13 @@
1
+ import { BrowserReader, type ReaderContext } from './base.js';
2
+ import type { BrowserType } from '../types.js';
3
+ export type ChromiumBrowserType = 'chrome' | 'arc' | 'dia' | 'comet';
4
+ export declare class ChromiumReader extends BrowserReader {
5
+ readonly browserType: BrowserType;
6
+ private readonly browserPath;
7
+ constructor(browserType: ChromiumBrowserType, context: ReaderContext);
8
+ getDbPaths(): string[];
9
+ getQuery(): string;
10
+ getProfileName(dbPath: string): string | undefined;
11
+ private discoverProfiles;
12
+ }
13
+ //# sourceMappingURL=chromium.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chromium.d.ts","sourceRoot":"","sources":["../../../../src/sources/browser/readers/chromium.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAM/C,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,KAAK,GAAG,KAAK,GAAG,OAAO,CAAC;AASrE,qBAAa,cAAe,SAAQ,aAAa;IAC/C,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAEzB,WAAW,EAAE,mBAAmB,EAAE,OAAO,EAAE,aAAa;IAMpE,UAAU,IAAI,MAAM,EAAE;IAUtB,QAAQ,IAAI,MAAM;IAclB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAUlD,OAAO,CAAC,gBAAgB;CAWzB"}
@@ -0,0 +1,64 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import { join, sep } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { BrowserReader } from './base.js';
5
+ // Chromium timestamp: microseconds since 1601-01-01
6
+ // Conversion: timestamp / 1000000 - 11644473600 = unix timestamp
7
+ const CHROMIUM_EPOCH_OFFSET = 11644473600;
8
+ const BROWSER_PATHS = {
9
+ chrome: join(homedir(), 'Library', 'Application Support', 'Google', 'Chrome'),
10
+ arc: join(homedir(), 'Library', 'Application Support', 'Arc', 'User Data'),
11
+ dia: join(homedir(), 'Library', 'Application Support', 'Dia', 'User Data'),
12
+ comet: join(homedir(), 'Library', 'Application Support', 'Comet'),
13
+ };
14
+ export class ChromiumReader extends BrowserReader {
15
+ browserType;
16
+ browserPath;
17
+ constructor(browserType, context) {
18
+ super(context);
19
+ this.browserType = browserType;
20
+ this.browserPath = BROWSER_PATHS[browserType];
21
+ }
22
+ getDbPaths() {
23
+ if (!existsSync(this.browserPath)) {
24
+ return [];
25
+ }
26
+ return this.discoverProfiles()
27
+ .map((profile) => join(this.browserPath, profile, 'History'))
28
+ .filter((path) => existsSync(path));
29
+ }
30
+ getQuery() {
31
+ return `
32
+ SELECT
33
+ urls.url,
34
+ urls.title,
35
+ urls.visit_count,
36
+ CAST((visits.visit_time / 1000000 - ${CHROMIUM_EPOCH_OFFSET}) AS INTEGER) as visit_unix
37
+ FROM urls
38
+ JOIN visits ON urls.id = visits.url
39
+ WHERE visit_unix > ?
40
+ ORDER BY visits.visit_time DESC
41
+ `;
42
+ }
43
+ getProfileName(dbPath) {
44
+ const parts = dbPath.split(sep);
45
+ const historyIndex = parts.findIndex((p) => p === 'History');
46
+ if (historyIndex > 0) {
47
+ const profile = parts[historyIndex - 1];
48
+ return profile === 'Default' ? undefined : profile;
49
+ }
50
+ return undefined;
51
+ }
52
+ discoverProfiles() {
53
+ try {
54
+ const entries = readdirSync(this.browserPath, { withFileTypes: true });
55
+ return entries
56
+ .filter((e) => e.isDirectory())
57
+ .filter((e) => e.name === 'Default' || e.name.startsWith('Profile '))
58
+ .map((e) => e.name);
59
+ }
60
+ catch {
61
+ return [];
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,9 @@
1
+ import { BrowserReader } from './base.js';
2
+ import type { BrowserType } from '../types.js';
3
+ export declare class SafariReader extends BrowserReader {
4
+ readonly browserType: BrowserType;
5
+ getDbPaths(): string[];
6
+ getQuery(): string;
7
+ getProfileName(_dbPath: string): string | undefined;
8
+ }
9
+ //# sourceMappingURL=safari.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safari.d.ts","sourceRoot":"","sources":["../../../../src/sources/browser/readers/safari.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAQ/C,qBAAa,YAAa,SAAQ,aAAa;IAC7C,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAY;IAE7C,UAAU,IAAI,MAAM,EAAE;IAOtB,QAAQ,IAAI,MAAM;IAclB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAIpD"}
@@ -0,0 +1,34 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { BrowserReader } from './base.js';
5
+ // Safari timestamp: seconds since 2001-01-01 (Apple Cocoa epoch)
6
+ // Conversion: timestamp + 978307200 = unix timestamp
7
+ const SAFARI_EPOCH_OFFSET = 978307200;
8
+ const SAFARI_HISTORY_PATH = join(homedir(), 'Library', 'Safari', 'History.db');
9
+ export class SafariReader extends BrowserReader {
10
+ browserType = 'safari';
11
+ getDbPaths() {
12
+ if (existsSync(SAFARI_HISTORY_PATH)) {
13
+ return [SAFARI_HISTORY_PATH];
14
+ }
15
+ return [];
16
+ }
17
+ getQuery() {
18
+ return `
19
+ SELECT
20
+ i.url,
21
+ v.title,
22
+ i.visit_count,
23
+ CAST((v.visit_time + ${SAFARI_EPOCH_OFFSET}) AS INTEGER) as visit_unix
24
+ FROM history_items i
25
+ JOIN history_visits v ON i.id = v.history_item
26
+ WHERE visit_unix > ?
27
+ ORDER BY v.visit_time DESC
28
+ `;
29
+ }
30
+ getProfileName(_dbPath) {
31
+ // Safari doesn't have profiles
32
+ return undefined;
33
+ }
34
+ }
@@ -0,0 +1,35 @@
1
+ export type BrowserType = 'chrome' | 'safari' | 'arc' | 'dia' | 'comet';
2
+ /** Raw browser history item from SQLite database */
3
+ export interface BrowserHistoryItem {
4
+ url: string;
5
+ title: string;
6
+ visitTime: string;
7
+ visitCount: number;
8
+ browser: BrowserType;
9
+ profile?: string;
10
+ }
11
+ /** Aggregated browser history item (grouped by URL + browser + date) */
12
+ export interface AggregatedBrowserHistoryItem {
13
+ url: string;
14
+ title: string;
15
+ browser: BrowserType;
16
+ profiles: string[];
17
+ date: string;
18
+ timezone: string;
19
+ dailyVisitCount: number;
20
+ firstVisitTime: string;
21
+ lastVisitTime: string;
22
+ }
23
+ export interface BrowserSourceOptions {
24
+ browsers: BrowserType[];
25
+ excludeDomains: string[];
26
+ sinceDays: number;
27
+ }
28
+ /** Internal representation of a raw history row from SQLite */
29
+ export interface RawHistoryRow {
30
+ url: string;
31
+ title: string | null;
32
+ visit_count: number;
33
+ visit_unix: number;
34
+ }
35
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/sources/browser/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,GAAG,KAAK,GAAG,OAAO,CAAC;AAExE,oDAAoD;AACpD,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,WAAW,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wEAAwE;AACxE,MAAM,WAAW,4BAA4B;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,+DAA+D;AAC/D,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { DataSource } from '../base.js';
2
+ import type { CollectionResult } from '../../core/types.js';
3
+ import type { ChatbotSourceOptions } from './types.js';
4
+ export declare class ChatbotSource extends DataSource<ChatbotSourceOptions> {
5
+ readonly type: "chatbot";
6
+ readonly name = "Chatbot History";
7
+ private readers;
8
+ validate(): Promise<boolean>;
9
+ collect(): Promise<CollectionResult>;
10
+ private createReader;
11
+ }
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sources/chatbot/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAAgC,MAAM,YAAY,CAAC;AAIrF,qBAAa,aAAc,SAAQ,UAAU,CAAC,oBAAoB,CAAC;IACjE,QAAQ,CAAC,IAAI,EAAG,SAAS,CAAU;IACnC,QAAQ,CAAC,IAAI,qBAAqB;IAElC,OAAO,CAAC,OAAO,CAAuB;IAEhC,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IA4B5B,OAAO,IAAI,OAAO,CAAC,gBAAgB,CAAC;IA+B1C,OAAO,CAAC,YAAY;CASrB"}
@@ -0,0 +1,67 @@
1
+ import { DataSource } from '../base.js';
2
+ import { ChatWiseReader } from './readers/chatwise.js';
3
+ export class ChatbotSource extends DataSource {
4
+ type = 'chatbot';
5
+ name = 'Chatbot History';
6
+ readers = [];
7
+ async validate() {
8
+ const readerContext = {
9
+ logger: this.context.logger,
10
+ sinceDays: this.options.sinceDays,
11
+ includeContent: this.options.includeContent,
12
+ maxMessagesPerChat: this.options.maxMessagesPerChat,
13
+ excludeModels: this.options.excludeModels,
14
+ };
15
+ // Create readers for enabled clients
16
+ for (const client of this.options.clients) {
17
+ const reader = this.createReader(client, readerContext);
18
+ if (reader && reader.hasValidDatabase()) {
19
+ this.readers.push(reader);
20
+ this.context.logger.info(`Chatbot client ${client} validated and ready`);
21
+ }
22
+ else {
23
+ this.context.logger.warn(`Chatbot client ${client} has no accessible database`);
24
+ }
25
+ }
26
+ if (this.readers.length === 0) {
27
+ this.context.logger.error('No valid chatbot databases found');
28
+ return false;
29
+ }
30
+ return true;
31
+ }
32
+ async collect() {
33
+ const allItems = [];
34
+ for (const reader of this.readers) {
35
+ try {
36
+ const items = reader.read();
37
+ allItems.push(...items);
38
+ this.context.logger.debug(`Collected ${items.length} chats from ${reader.clientType}`);
39
+ }
40
+ catch (error) {
41
+ this.context.logger.error(`Failed to collect from ${reader.clientType}`, error);
42
+ }
43
+ }
44
+ // Sort by last reply time descending
45
+ allItems.sort((a, b) => new Date(b.session.lastReplyAt).getTime() - new Date(a.session.lastReplyAt).getTime());
46
+ return {
47
+ sourceType: this.type,
48
+ success: true,
49
+ itemsCollected: allItems.length,
50
+ items: allItems.map((item) => ({
51
+ sourceType: this.type,
52
+ timestamp: new Date(item.session.lastReplyAt),
53
+ data: item,
54
+ })),
55
+ collectedAt: new Date(),
56
+ };
57
+ }
58
+ createReader(client, context) {
59
+ switch (client) {
60
+ case 'chatwise':
61
+ return new ChatWiseReader(context);
62
+ default:
63
+ this.context.logger.warn(`Unknown chatbot client: ${client}`);
64
+ return null;
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,25 @@
1
+ import type { ChatbotType, ChatSession, ChatMessage, ChatHistoryItem } from '../types.js';
2
+ import type { Logger } from '../../../utils/logger.js';
3
+ export interface ReaderContext {
4
+ logger: Logger;
5
+ sinceDays: number;
6
+ includeContent: boolean;
7
+ maxMessagesPerChat?: number;
8
+ excludeModels?: string[];
9
+ }
10
+ export declare abstract class ChatbotReader {
11
+ abstract readonly clientType: ChatbotType;
12
+ protected context: ReaderContext;
13
+ constructor(context: ReaderContext);
14
+ abstract getDbPath(): string | null;
15
+ abstract getChatQuery(): string;
16
+ abstract getMessageQuery(): string;
17
+ abstract parseChatRow(row: unknown): ChatSession | null;
18
+ abstract parseMessageRow(row: unknown): ChatMessage | null;
19
+ read(): ChatHistoryItem[];
20
+ hasValidDatabase(): boolean;
21
+ private readFromDb;
22
+ private getMessagesForChat;
23
+ private isModelExcluded;
24
+ }
25
+ //# sourceMappingURL=base.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/sources/chatbot/readers/base.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC1F,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAEvD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,OAAO,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,8BAAsB,aAAa;IACjC,QAAQ,CAAC,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC;IAE1C,SAAS,CAAC,OAAO,EAAE,aAAa,CAAC;gBAErB,OAAO,EAAE,aAAa;IAIlC,QAAQ,CAAC,SAAS,IAAI,MAAM,GAAG,IAAI;IACnC,QAAQ,CAAC,YAAY,IAAI,MAAM;IAC/B,QAAQ,CAAC,eAAe,IAAI,MAAM;IAClC,QAAQ,CAAC,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI;IACvD,QAAQ,CAAC,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI;IAE1D,IAAI,IAAI,eAAe,EAAE;IAUzB,gBAAgB,IAAI,OAAO;IAK3B,OAAO,CAAC,UAAU;IAiElB,OAAO,CAAC,kBAAkB;IAiC1B,OAAO,CAAC,eAAe;CAQxB"}