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,109 @@
1
+ import { copyFileSync, unlinkSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import Database from 'better-sqlite3';
5
+ export class ChatbotReader {
6
+ context;
7
+ constructor(context) {
8
+ this.context = context;
9
+ }
10
+ read() {
11
+ const dbPath = this.getDbPath();
12
+ if (!dbPath) {
13
+ this.context.logger.debug(`No ${this.clientType} database found`);
14
+ return [];
15
+ }
16
+ return this.readFromDb(dbPath);
17
+ }
18
+ hasValidDatabase() {
19
+ const dbPath = this.getDbPath();
20
+ return dbPath !== null && existsSync(dbPath);
21
+ }
22
+ readFromDb(dbPath) {
23
+ const tempPath = join(tmpdir(), `chatbot-history-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
24
+ let db = null;
25
+ try {
26
+ // Copy database to avoid locking issues
27
+ copyFileSync(dbPath, tempPath);
28
+ db = new Database(tempPath, { readonly: true });
29
+ const sinceTimestamp = Date.now() - this.context.sinceDays * 24 * 60 * 60 * 1000;
30
+ // Get chat sessions
31
+ const chatStmt = db.prepare(this.getChatQuery());
32
+ const chatRows = chatStmt.all(sinceTimestamp);
33
+ const results = [];
34
+ for (const chatRow of chatRows) {
35
+ const session = this.parseChatRow(chatRow);
36
+ if (!session)
37
+ continue;
38
+ // Check model exclusion
39
+ if (session.model && this.isModelExcluded(session.model)) {
40
+ continue;
41
+ }
42
+ // Get messages for this chat
43
+ const messages = this.getMessagesForChat(db, session.id);
44
+ // Update message count
45
+ session.messageCount = messages.length;
46
+ results.push({
47
+ client: this.clientType,
48
+ session,
49
+ messages,
50
+ });
51
+ }
52
+ this.context.logger.debug(`Read ${results.length} chats from ${this.clientType}`);
53
+ return results;
54
+ }
55
+ finally {
56
+ // Close database connection
57
+ if (db) {
58
+ try {
59
+ db.close();
60
+ }
61
+ catch (error) {
62
+ this.context.logger.debug('Failed to close database connection', error);
63
+ }
64
+ }
65
+ // Clean up temp file
66
+ if (existsSync(tempPath)) {
67
+ try {
68
+ unlinkSync(tempPath);
69
+ }
70
+ catch (error) {
71
+ this.context.logger.debug(`Failed to clean up temp file: ${tempPath}`, error);
72
+ }
73
+ }
74
+ }
75
+ }
76
+ getMessagesForChat(db, chatId) {
77
+ const stmt = db.prepare(this.getMessageQuery());
78
+ const rows = stmt.all(chatId);
79
+ let messages = rows
80
+ .map((row) => this.parseMessageRow(row))
81
+ .filter((msg) => msg !== null);
82
+ // Filter by model exclusion
83
+ messages = messages.filter((msg) => {
84
+ if (msg.model && this.isModelExcluded(msg.model)) {
85
+ return false;
86
+ }
87
+ return true;
88
+ });
89
+ // Strip content if not included
90
+ if (!this.context.includeContent) {
91
+ messages = messages.map((msg) => ({
92
+ ...msg,
93
+ content: '',
94
+ reasoningContent: undefined,
95
+ }));
96
+ }
97
+ // Limit message count (keep most recent)
98
+ if (this.context.maxMessagesPerChat && messages.length > this.context.maxMessagesPerChat) {
99
+ messages = messages.slice(-this.context.maxMessagesPerChat);
100
+ }
101
+ return messages;
102
+ }
103
+ isModelExcluded(model) {
104
+ if (!this.context.excludeModels || this.context.excludeModels.length === 0) {
105
+ return false;
106
+ }
107
+ return this.context.excludeModels.some((excluded) => model.toLowerCase().includes(excluded.toLowerCase()));
108
+ }
109
+ }
@@ -0,0 +1,14 @@
1
+ import { ChatbotReader } from './base.js';
2
+ import type { ChatbotType, ChatSession, ChatMessage } from '../types.js';
3
+ export declare class ChatWiseReader extends ChatbotReader {
4
+ readonly clientType: ChatbotType;
5
+ getDbPath(): string | null;
6
+ getChatQuery(): string;
7
+ getMessageQuery(): string;
8
+ parseChatRow(row: unknown): ChatSession | null;
9
+ parseMessageRow(row: unknown): ChatMessage | null;
10
+ private isValidChatRow;
11
+ private isValidMessageRow;
12
+ private normalizeRole;
13
+ }
14
+ //# sourceMappingURL=chatwise.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chatwise.d.ts","sourceRoot":"","sources":["../../../../src/sources/chatbot/readers/chatwise.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAwBzE,qBAAa,cAAe,SAAQ,aAAa;IAC/C,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAc;IAE9C,SAAS,IAAI,MAAM,GAAG,IAAI;IAO1B,YAAY,IAAI,MAAM;IAgBtB,eAAe,IAAI,MAAM;IAiBzB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI;IAgB9C,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI;IAkCjD,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,aAAa;CAOtB"}
@@ -0,0 +1,117 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { ChatbotReader } from './base.js';
5
+ const CHATWISE_DB_PATH = join(homedir(), 'Library', 'Application Support', 'app.chatwise', 'app.db');
6
+ export class ChatWiseReader extends ChatbotReader {
7
+ clientType = 'chatwise';
8
+ getDbPath() {
9
+ if (existsSync(CHATWISE_DB_PATH)) {
10
+ return CHATWISE_DB_PATH;
11
+ }
12
+ return null;
13
+ }
14
+ getChatQuery() {
15
+ // lastReplyAt is stored as Unix milliseconds
16
+ return `
17
+ SELECT
18
+ id,
19
+ title,
20
+ model,
21
+ createdAt,
22
+ lastReplyAt,
23
+ assistantId
24
+ FROM chat
25
+ WHERE lastReplyAt > ?
26
+ ORDER BY lastReplyAt DESC
27
+ `;
28
+ }
29
+ getMessageQuery() {
30
+ return `
31
+ SELECT
32
+ id,
33
+ chatId,
34
+ role,
35
+ content,
36
+ model,
37
+ createdAt,
38
+ files,
39
+ reasoningContent
40
+ FROM message
41
+ WHERE chatId = ?
42
+ ORDER BY createdAt ASC
43
+ `;
44
+ }
45
+ parseChatRow(row) {
46
+ if (!this.isValidChatRow(row)) {
47
+ return null;
48
+ }
49
+ return {
50
+ id: row.id,
51
+ title: row.title ?? 'Untitled',
52
+ model: row.model ?? undefined,
53
+ createdAt: new Date(row.createdAt).toISOString(),
54
+ lastReplyAt: new Date(row.lastReplyAt).toISOString(),
55
+ assistantId: row.assistantId ?? undefined,
56
+ messageCount: 0, // Will be updated after fetching messages
57
+ };
58
+ }
59
+ parseMessageRow(row) {
60
+ if (!this.isValidMessageRow(row)) {
61
+ return null;
62
+ }
63
+ let files;
64
+ if (row.files) {
65
+ try {
66
+ const parsed = JSON.parse(row.files);
67
+ if (Array.isArray(parsed) && parsed.every((f) => typeof f === 'string')) {
68
+ files = parsed;
69
+ }
70
+ }
71
+ catch {
72
+ // Invalid JSON, ignore
73
+ }
74
+ }
75
+ const role = this.normalizeRole(row.role);
76
+ if (!role) {
77
+ return null;
78
+ }
79
+ return {
80
+ id: row.id,
81
+ chatId: row.chatId,
82
+ role,
83
+ content: row.content ?? '',
84
+ model: row.model ?? undefined,
85
+ createdAt: new Date(row.createdAt).toISOString(),
86
+ files,
87
+ reasoningContent: row.reasoningContent ?? undefined,
88
+ };
89
+ }
90
+ isValidChatRow(row) {
91
+ if (typeof row !== 'object' || row === null)
92
+ return false;
93
+ const r = row;
94
+ return (typeof r['id'] === 'string' &&
95
+ typeof r['createdAt'] === 'number' &&
96
+ typeof r['lastReplyAt'] === 'number');
97
+ }
98
+ isValidMessageRow(row) {
99
+ if (typeof row !== 'object' || row === null)
100
+ return false;
101
+ const r = row;
102
+ return (typeof r['id'] === 'string' &&
103
+ typeof r['chatId'] === 'string' &&
104
+ typeof r['role'] === 'string' &&
105
+ typeof r['createdAt'] === 'number');
106
+ }
107
+ normalizeRole(role) {
108
+ const normalized = role.toLowerCase();
109
+ if (normalized === 'user')
110
+ return 'user';
111
+ if (normalized === 'assistant')
112
+ return 'assistant';
113
+ if (normalized === 'system')
114
+ return 'system';
115
+ return null;
116
+ }
117
+ }
@@ -0,0 +1,33 @@
1
+ export type ChatbotType = 'chatwise';
2
+ export interface ChatMessage {
3
+ id: string;
4
+ chatId: string;
5
+ role: 'user' | 'assistant' | 'system';
6
+ content: string;
7
+ model?: string;
8
+ createdAt: string;
9
+ files?: string[];
10
+ reasoningContent?: string;
11
+ }
12
+ export interface ChatSession {
13
+ id: string;
14
+ title: string;
15
+ model?: string;
16
+ createdAt: string;
17
+ lastReplyAt: string;
18
+ assistantId?: string;
19
+ messageCount: number;
20
+ }
21
+ export interface ChatHistoryItem {
22
+ client: ChatbotType;
23
+ session: ChatSession;
24
+ messages: ChatMessage[];
25
+ }
26
+ export interface ChatbotSourceOptions {
27
+ clients: ChatbotType[];
28
+ sinceDays: number;
29
+ includeContent: boolean;
30
+ maxMessagesPerChat?: number;
31
+ excludeModels?: string[];
32
+ }
33
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/sources/chatbot/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC;AAErC,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,WAAW,CAAC;IACrB,QAAQ,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,OAAO,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { DataSource } from '../base.js';
2
+ import type { CollectionResult } from '../../core/types.js';
3
+ import type { FilesystemSourceOptions } from './types.js';
4
+ export declare class FilesystemSource extends DataSource<FilesystemSourceOptions> {
5
+ readonly type: "filesystem";
6
+ readonly name = "Filesystem Changes";
7
+ validate(): Promise<boolean>;
8
+ collect(): Promise<CollectionResult>;
9
+ }
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sources/filesystem/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAI1D,qBAAa,gBAAiB,SAAQ,UAAU,CAAC,uBAAuB,CAAC;IACvE,QAAQ,CAAC,IAAI,EAAG,YAAY,CAAU;IACtC,QAAQ,CAAC,IAAI,wBAAwB;IAE/B,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IAqC5B,OAAO,IAAI,OAAO,CAAC,gBAAgB,CAAC;CAsB3C"}
@@ -0,0 +1,58 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import { DataSource } from '../base.js';
3
+ import { FilesystemScanner } from './scanner.js';
4
+ import { expandPath } from '../../utils/path.js';
5
+ export class FilesystemSource extends DataSource {
6
+ type = 'filesystem';
7
+ name = 'Filesystem Changes';
8
+ async validate() {
9
+ if (!this.options.watchPaths || this.options.watchPaths.length === 0) {
10
+ this.context.logger.error('No watch paths configured for filesystem source');
11
+ return false;
12
+ }
13
+ const validPaths = [];
14
+ for (const watchPath of this.options.watchPaths) {
15
+ const expanded = expandPath(watchPath);
16
+ if (!existsSync(expanded)) {
17
+ this.context.logger.warn(`Watch path does not exist: ${watchPath}`);
18
+ continue;
19
+ }
20
+ try {
21
+ const stat = statSync(expanded);
22
+ if (!stat.isDirectory()) {
23
+ this.context.logger.warn(`Watch path is not a directory: ${watchPath}`);
24
+ continue;
25
+ }
26
+ validPaths.push(expanded);
27
+ }
28
+ catch (error) {
29
+ this.context.logger.warn(`Cannot access watch path: ${watchPath}`, error);
30
+ }
31
+ }
32
+ if (validPaths.length === 0) {
33
+ this.context.logger.error('No valid watch paths found');
34
+ return false;
35
+ }
36
+ this.context.logger.info(`Validated ${validPaths.length} watch paths for filesystem source`);
37
+ return true;
38
+ }
39
+ async collect() {
40
+ const scanner = new FilesystemScanner({
41
+ logger: this.context.logger,
42
+ options: this.options,
43
+ });
44
+ const items = scanner.scan();
45
+ this.context.logger.info(`Found ${items.length} modified files`);
46
+ return {
47
+ sourceType: this.type,
48
+ success: true,
49
+ itemsCollected: items.length,
50
+ items: items.map((item) => ({
51
+ sourceType: this.type,
52
+ timestamp: new Date(item.modifiedAt),
53
+ data: item,
54
+ })),
55
+ collectedAt: new Date(),
56
+ };
57
+ }
58
+ }
@@ -0,0 +1,24 @@
1
+ import type { Logger } from '../../utils/logger.js';
2
+ import type { FileChangeItem, FilesystemSourceOptions } from './types.js';
3
+ export interface ScannerContext {
4
+ logger: Logger;
5
+ options: FilesystemSourceOptions;
6
+ }
7
+ export declare class FilesystemScanner {
8
+ private context;
9
+ private sinceTimestamp;
10
+ constructor(context: ScannerContext);
11
+ /** Check if file matches exclude patterns */
12
+ private isExcluded;
13
+ /** Check if file matches allowed file types */
14
+ private matchesFileTypes;
15
+ /** Get MIME type based on extension */
16
+ private getMimeType;
17
+ /** Get content preview for text files */
18
+ private getContentPreview;
19
+ /** Scan a single directory recursively */
20
+ private scanDirectory;
21
+ /** Scan all configured watch paths */
22
+ scan(): FileChangeItem[];
23
+ }
24
+ //# sourceMappingURL=scanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../../src/sources/filesystem/scanner.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AA8I1E,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,uBAAuB,CAAC;CAClC;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,cAAc,CAAS;gBAEnB,OAAO,EAAE,cAAc;IAMnC,6CAA6C;IAC7C,OAAO,CAAC,UAAU;IAMlB,+CAA+C;IAC/C,OAAO,CAAC,gBAAgB;IAQxB,uCAAuC;IACvC,OAAO,CAAC,WAAW;IAInB,yCAAyC;IACzC,OAAO,CAAC,iBAAiB;IAezB,0CAA0C;IAC1C,OAAO,CAAC,aAAa;IAuErB,sCAAsC;IACtC,IAAI,IAAI,cAAc,EAAE;CAiCzB"}
@@ -0,0 +1,264 @@
1
+ import { readdirSync, statSync, readFileSync } from 'node:fs';
2
+ import { resolve, extname, basename, dirname } from 'node:path';
3
+ import { minimatch } from 'minimatch';
4
+ import { expandPath, isGitRepository } from '../../utils/path.js';
5
+ /** Text file extensions that support content preview */
6
+ const TEXT_EXTENSIONS = new Set([
7
+ '.txt',
8
+ '.md',
9
+ '.markdown',
10
+ '.json',
11
+ '.yaml',
12
+ '.yml',
13
+ '.xml',
14
+ '.csv',
15
+ '.tsv',
16
+ '.log',
17
+ '.rtf',
18
+ '.sql',
19
+ '.rmd',
20
+ '.qmd',
21
+ ]);
22
+ /** Maximum characters to include in content preview */
23
+ const CONTENT_PREVIEW_LENGTH = 500;
24
+ /** MIME type mapping for common document formats */
25
+ const MIME_MAP = {
26
+ // Text & Markdown
27
+ '.md': 'text/markdown',
28
+ '.markdown': 'text/markdown',
29
+ '.txt': 'text/plain',
30
+ '.rtf': 'text/rtf',
31
+ '.log': 'text/plain',
32
+ // Data formats
33
+ '.json': 'application/json',
34
+ '.yaml': 'application/x-yaml',
35
+ '.yml': 'application/x-yaml',
36
+ '.xml': 'application/xml',
37
+ '.csv': 'text/csv',
38
+ '.tsv': 'text/tab-separated-values',
39
+ // Documents
40
+ '.pdf': 'application/pdf',
41
+ '.doc': 'application/msword',
42
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
43
+ '.ppt': 'application/vnd.ms-powerpoint',
44
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
45
+ '.xls': 'application/vnd.ms-excel',
46
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
47
+ '.pages': 'application/vnd.apple.pages',
48
+ '.numbers': 'application/vnd.apple.numbers',
49
+ '.key': 'application/vnd.apple.keynote',
50
+ // Images
51
+ '.jpg': 'image/jpeg',
52
+ '.jpeg': 'image/jpeg',
53
+ '.png': 'image/png',
54
+ '.gif': 'image/gif',
55
+ '.webp': 'image/webp',
56
+ '.svg': 'image/svg+xml',
57
+ '.ico': 'image/x-icon',
58
+ '.bmp': 'image/bmp',
59
+ '.tiff': 'image/tiff',
60
+ '.tif': 'image/tiff',
61
+ '.heic': 'image/heic',
62
+ '.heif': 'image/heif',
63
+ '.avif': 'image/avif',
64
+ '.psd': 'image/vnd.adobe.photoshop',
65
+ '.ai': 'application/postscript',
66
+ '.eps': 'application/postscript',
67
+ '.sketch': 'application/x-sketch',
68
+ '.fig': 'application/x-figma',
69
+ // Archives
70
+ '.zip': 'application/zip',
71
+ '.rar': 'application/vnd.rar',
72
+ '.7z': 'application/x-7z-compressed',
73
+ '.tar': 'application/x-tar',
74
+ '.gz': 'application/gzip',
75
+ '.bz2': 'application/x-bzip2',
76
+ '.xz': 'application/x-xz',
77
+ '.dmg': 'application/x-apple-diskimage',
78
+ // Applications & Installers
79
+ '.exe': 'application/x-msdownload',
80
+ '.msi': 'application/x-msi',
81
+ '.app': 'application/x-apple-application',
82
+ '.pkg': 'application/x-newton-compatible-pkg',
83
+ '.deb': 'application/x-deb',
84
+ '.rpm': 'application/x-rpm',
85
+ '.apk': 'application/vnd.android.package-archive',
86
+ '.ipa': 'application/x-ios-app',
87
+ // Data Science & Analytics
88
+ '.parquet': 'application/x-parquet',
89
+ '.feather': 'application/x-feather',
90
+ '.arrow': 'application/x-arrow',
91
+ '.pickle': 'application/x-pickle',
92
+ '.pkl': 'application/x-pickle',
93
+ '.h5': 'application/x-hdf5',
94
+ '.hdf5': 'application/x-hdf5',
95
+ '.npy': 'application/x-numpy',
96
+ '.npz': 'application/x-numpy',
97
+ '.sav': 'application/x-spss-sav',
98
+ '.dta': 'application/x-stata-dta',
99
+ '.rds': 'application/x-r-data',
100
+ '.rdata': 'application/x-r-data',
101
+ // Tableau
102
+ '.twb': 'application/x-tableau-workbook',
103
+ '.twbx': 'application/x-tableau-packaged-workbook',
104
+ '.tds': 'application/x-tableau-datasource',
105
+ '.tdsx': 'application/x-tableau-packaged-datasource',
106
+ '.hyper': 'application/x-tableau-hyper',
107
+ '.tde': 'application/x-tableau-extract',
108
+ // Database
109
+ '.sql': 'application/sql',
110
+ '.db': 'application/x-sqlite3',
111
+ '.sqlite': 'application/x-sqlite3',
112
+ '.sqlite3': 'application/x-sqlite3',
113
+ // Notebooks & Scripts
114
+ '.ipynb': 'application/x-ipynb+json',
115
+ '.rmd': 'text/x-r-markdown',
116
+ '.qmd': 'text/x-quarto-markdown',
117
+ // E-books
118
+ '.epub': 'application/epub+zip',
119
+ '.mobi': 'application/x-mobipocket-ebook',
120
+ '.azw3': 'application/vnd.amazon.ebook',
121
+ '.djvu': 'image/vnd.djvu',
122
+ '.cbz': 'application/vnd.comicbook+zip',
123
+ '.cbr': 'application/vnd.comicbook-rar',
124
+ // Fonts
125
+ '.ttf': 'font/ttf',
126
+ '.otf': 'font/otf',
127
+ '.woff': 'font/woff',
128
+ '.woff2': 'font/woff2',
129
+ };
130
+ export class FilesystemScanner {
131
+ context;
132
+ sinceTimestamp;
133
+ constructor(context) {
134
+ this.context = context;
135
+ const sinceDays = context.options.sinceDays ?? 7;
136
+ this.sinceTimestamp = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
137
+ }
138
+ /** Check if file matches exclude patterns */
139
+ isExcluded(filePath) {
140
+ return this.context.options.excludePatterns.some((pattern) => minimatch(filePath, pattern, { dot: true }));
141
+ }
142
+ /** Check if file matches allowed file types */
143
+ matchesFileTypes(filePath) {
144
+ const types = this.context.options.fileTypes;
145
+ if (!types || types.length === 0)
146
+ return true;
147
+ const ext = extname(filePath).toLowerCase();
148
+ return types.some((t) => t.toLowerCase() === ext);
149
+ }
150
+ /** Get MIME type based on extension */
151
+ getMimeType(ext) {
152
+ return MIME_MAP[ext.toLowerCase()];
153
+ }
154
+ /** Get content preview for text files */
155
+ getContentPreview(filePath, ext) {
156
+ if (!this.context.options.includeContent)
157
+ return undefined;
158
+ if (!TEXT_EXTENSIONS.has(ext.toLowerCase()))
159
+ return undefined;
160
+ try {
161
+ const content = readFileSync(filePath, 'utf-8');
162
+ if (content.length <= CONTENT_PREVIEW_LENGTH) {
163
+ return content;
164
+ }
165
+ return content.slice(0, CONTENT_PREVIEW_LENGTH) + '...';
166
+ }
167
+ catch {
168
+ return undefined;
169
+ }
170
+ }
171
+ /** Scan a single directory recursively */
172
+ scanDirectory(dirPath) {
173
+ const results = [];
174
+ let entries;
175
+ try {
176
+ entries = readdirSync(dirPath, { withFileTypes: true });
177
+ }
178
+ catch (error) {
179
+ this.context.logger.warn(`Cannot read directory: ${dirPath}`, error);
180
+ return results;
181
+ }
182
+ for (const entry of entries) {
183
+ const fullPath = resolve(dirPath, entry.name);
184
+ // Check exclusion patterns
185
+ if (this.isExcluded(fullPath)) {
186
+ continue;
187
+ }
188
+ if (entry.isDirectory()) {
189
+ // Skip git repositories entirely
190
+ if (isGitRepository(fullPath)) {
191
+ this.context.logger.debug(`Skipping git repository: ${fullPath}`);
192
+ continue;
193
+ }
194
+ // Recurse into subdirectory
195
+ results.push(...this.scanDirectory(fullPath));
196
+ }
197
+ else if (entry.isFile()) {
198
+ // Check file type filter
199
+ if (!this.matchesFileTypes(fullPath)) {
200
+ continue;
201
+ }
202
+ try {
203
+ const stat = statSync(fullPath);
204
+ const mtimeMs = stat.mtimeMs;
205
+ // Check if modified within sinceDays
206
+ if (mtimeMs < this.sinceTimestamp) {
207
+ continue;
208
+ }
209
+ // Check max file size
210
+ const maxSize = this.context.options.maxFileSize;
211
+ if (maxSize !== undefined && stat.size > maxSize) {
212
+ this.context.logger.debug(`Skipping large file: ${fullPath} (${stat.size} bytes)`);
213
+ continue;
214
+ }
215
+ const ext = extname(fullPath);
216
+ results.push({
217
+ filePath: fullPath,
218
+ fileName: basename(fullPath),
219
+ eventType: 'modify', // Stateless mode: all changes are 'modify'
220
+ modifiedAt: new Date(mtimeMs).toISOString(),
221
+ fileSize: stat.size,
222
+ extension: ext,
223
+ mimeType: this.getMimeType(ext),
224
+ contentPreview: this.getContentPreview(fullPath, ext),
225
+ parentDirectory: dirname(fullPath),
226
+ });
227
+ }
228
+ catch (error) {
229
+ this.context.logger.warn(`Cannot stat file: ${fullPath}`, error);
230
+ }
231
+ }
232
+ }
233
+ return results;
234
+ }
235
+ /** Scan all configured watch paths */
236
+ scan() {
237
+ const allItems = [];
238
+ for (const watchPath of this.context.options.watchPaths) {
239
+ const expandedPath = expandPath(watchPath);
240
+ try {
241
+ const stat = statSync(expandedPath);
242
+ if (!stat.isDirectory()) {
243
+ this.context.logger.warn(`Watch path is not a directory: ${watchPath}`);
244
+ continue;
245
+ }
246
+ // Skip if watchPath itself is a git repository
247
+ if (isGitRepository(expandedPath)) {
248
+ this.context.logger.info(`Skipping git repository: ${watchPath}`);
249
+ continue;
250
+ }
251
+ this.context.logger.debug(`Scanning: ${expandedPath}`);
252
+ const items = this.scanDirectory(expandedPath);
253
+ allItems.push(...items);
254
+ this.context.logger.debug(`Found ${items.length} files in ${watchPath}`);
255
+ }
256
+ catch (error) {
257
+ this.context.logger.error(`Cannot access watch path: ${watchPath}`, error);
258
+ }
259
+ }
260
+ // Sort by modification time descending
261
+ allItems.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime());
262
+ return allItems;
263
+ }
264
+ }