@jsayubi/ccgram 1.0.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 (247) hide show
  1. package/.env.example +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +338 -0
  4. package/ccgram.service +24 -0
  5. package/config/channels.json +58 -0
  6. package/config/default.json +27 -0
  7. package/config/defaults/config.json +16 -0
  8. package/config/defaults/i18n.json +32 -0
  9. package/config/email-template.json +31 -0
  10. package/config/test-with-subagent.json +16 -0
  11. package/config/user.json +27 -0
  12. package/dist/claude-hook-notify.d.ts +7 -0
  13. package/dist/claude-hook-notify.d.ts.map +1 -0
  14. package/dist/claude-hook-notify.js +154 -0
  15. package/dist/claude-hook-notify.js.map +1 -0
  16. package/dist/claude-remote.d.ts +50 -0
  17. package/dist/claude-remote.d.ts.map +1 -0
  18. package/dist/claude-remote.js +927 -0
  19. package/dist/claude-remote.js.map +1 -0
  20. package/dist/cli.d.ts +3 -0
  21. package/dist/cli.d.ts.map +1 -0
  22. package/dist/cli.js +110 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/enhanced-hook-notify.d.ts +16 -0
  25. package/dist/enhanced-hook-notify.d.ts.map +1 -0
  26. package/dist/enhanced-hook-notify.js +288 -0
  27. package/dist/enhanced-hook-notify.js.map +1 -0
  28. package/dist/permission-hook.d.ts +15 -0
  29. package/dist/permission-hook.d.ts.map +1 -0
  30. package/dist/permission-hook.js +357 -0
  31. package/dist/permission-hook.js.map +1 -0
  32. package/dist/prompt-bridge.d.ts +50 -0
  33. package/dist/prompt-bridge.d.ts.map +1 -0
  34. package/dist/prompt-bridge.js +173 -0
  35. package/dist/prompt-bridge.js.map +1 -0
  36. package/dist/question-notify.d.ts +16 -0
  37. package/dist/question-notify.d.ts.map +1 -0
  38. package/dist/question-notify.js +272 -0
  39. package/dist/question-notify.js.map +1 -0
  40. package/dist/setup.d.ts +10 -0
  41. package/dist/setup.d.ts.map +1 -0
  42. package/dist/setup.js +649 -0
  43. package/dist/setup.js.map +1 -0
  44. package/dist/smart-monitor.d.ts +7 -0
  45. package/dist/smart-monitor.d.ts.map +1 -0
  46. package/dist/smart-monitor.js +256 -0
  47. package/dist/smart-monitor.js.map +1 -0
  48. package/dist/src/automation/claude-automation.d.ts +45 -0
  49. package/dist/src/automation/claude-automation.d.ts.map +1 -0
  50. package/dist/src/automation/claude-automation.js +367 -0
  51. package/dist/src/automation/claude-automation.js.map +1 -0
  52. package/dist/src/automation/clipboard-automation.d.ts +35 -0
  53. package/dist/src/automation/clipboard-automation.d.ts.map +1 -0
  54. package/dist/src/automation/clipboard-automation.js +242 -0
  55. package/dist/src/automation/clipboard-automation.js.map +1 -0
  56. package/dist/src/automation/simple-automation.d.ts +56 -0
  57. package/dist/src/automation/simple-automation.d.ts.map +1 -0
  58. package/dist/src/automation/simple-automation.js +283 -0
  59. package/dist/src/automation/simple-automation.js.map +1 -0
  60. package/dist/src/channels/base/channel.d.ts +60 -0
  61. package/dist/src/channels/base/channel.d.ts.map +1 -0
  62. package/dist/src/channels/base/channel.js +96 -0
  63. package/dist/src/channels/base/channel.js.map +1 -0
  64. package/dist/src/channels/email/smtp.d.ts +74 -0
  65. package/dist/src/channels/email/smtp.d.ts.map +1 -0
  66. package/dist/src/channels/email/smtp.js +605 -0
  67. package/dist/src/channels/email/smtp.js.map +1 -0
  68. package/dist/src/channels/line/line.d.ts +36 -0
  69. package/dist/src/channels/line/line.d.ts.map +1 -0
  70. package/dist/src/channels/line/line.js +180 -0
  71. package/dist/src/channels/line/line.js.map +1 -0
  72. package/dist/src/channels/line/webhook.d.ts +55 -0
  73. package/dist/src/channels/line/webhook.d.ts.map +1 -0
  74. package/dist/src/channels/line/webhook.js +191 -0
  75. package/dist/src/channels/line/webhook.js.map +1 -0
  76. package/dist/src/channels/local/desktop.d.ts +30 -0
  77. package/dist/src/channels/local/desktop.d.ts.map +1 -0
  78. package/dist/src/channels/local/desktop.js +161 -0
  79. package/dist/src/channels/local/desktop.js.map +1 -0
  80. package/dist/src/channels/telegram/telegram.d.ts +43 -0
  81. package/dist/src/channels/telegram/telegram.d.ts.map +1 -0
  82. package/dist/src/channels/telegram/telegram.js +223 -0
  83. package/dist/src/channels/telegram/telegram.js.map +1 -0
  84. package/dist/src/channels/telegram/webhook.d.ts +75 -0
  85. package/dist/src/channels/telegram/webhook.d.ts.map +1 -0
  86. package/dist/src/channels/telegram/webhook.js +278 -0
  87. package/dist/src/channels/telegram/webhook.js.map +1 -0
  88. package/dist/src/config-manager.d.ts +16 -0
  89. package/dist/src/config-manager.d.ts.map +1 -0
  90. package/dist/src/config-manager.js +152 -0
  91. package/dist/src/config-manager.js.map +1 -0
  92. package/dist/src/core/config.d.ts +28 -0
  93. package/dist/src/core/config.d.ts.map +1 -0
  94. package/dist/src/core/config.js +248 -0
  95. package/dist/src/core/config.js.map +1 -0
  96. package/dist/src/core/logger.d.ts +19 -0
  97. package/dist/src/core/logger.d.ts.map +1 -0
  98. package/dist/src/core/logger.js +47 -0
  99. package/dist/src/core/logger.js.map +1 -0
  100. package/dist/src/core/notifier.d.ts +45 -0
  101. package/dist/src/core/notifier.d.ts.map +1 -0
  102. package/dist/src/core/notifier.js +189 -0
  103. package/dist/src/core/notifier.js.map +1 -0
  104. package/dist/src/daemon/taskping-daemon.d.ts +38 -0
  105. package/dist/src/daemon/taskping-daemon.d.ts.map +1 -0
  106. package/dist/src/daemon/taskping-daemon.js +306 -0
  107. package/dist/src/daemon/taskping-daemon.js.map +1 -0
  108. package/dist/src/relay/claude-command-bridge.d.ts +57 -0
  109. package/dist/src/relay/claude-command-bridge.d.ts.map +1 -0
  110. package/dist/src/relay/claude-command-bridge.js +188 -0
  111. package/dist/src/relay/claude-command-bridge.js.map +1 -0
  112. package/dist/src/relay/command-relay.d.ts +94 -0
  113. package/dist/src/relay/command-relay.d.ts.map +1 -0
  114. package/dist/src/relay/command-relay.js +463 -0
  115. package/dist/src/relay/command-relay.js.map +1 -0
  116. package/dist/src/relay/email-listener.d.ts +65 -0
  117. package/dist/src/relay/email-listener.d.ts.map +1 -0
  118. package/dist/src/relay/email-listener.js +460 -0
  119. package/dist/src/relay/email-listener.js.map +1 -0
  120. package/dist/src/relay/relay-pty.d.ts +21 -0
  121. package/dist/src/relay/relay-pty.d.ts.map +1 -0
  122. package/dist/src/relay/relay-pty.js +696 -0
  123. package/dist/src/relay/relay-pty.js.map +1 -0
  124. package/dist/src/relay/smart-injector.d.ts +30 -0
  125. package/dist/src/relay/smart-injector.d.ts.map +1 -0
  126. package/dist/src/relay/smart-injector.js +233 -0
  127. package/dist/src/relay/smart-injector.js.map +1 -0
  128. package/dist/src/relay/tmux-injector.d.ts +46 -0
  129. package/dist/src/relay/tmux-injector.d.ts.map +1 -0
  130. package/dist/src/relay/tmux-injector.js +413 -0
  131. package/dist/src/relay/tmux-injector.js.map +1 -0
  132. package/dist/src/tools/config-manager.d.ts +33 -0
  133. package/dist/src/tools/config-manager.d.ts.map +1 -0
  134. package/dist/src/tools/config-manager.js +448 -0
  135. package/dist/src/tools/config-manager.js.map +1 -0
  136. package/dist/src/tools/installer.d.ts +38 -0
  137. package/dist/src/tools/installer.d.ts.map +1 -0
  138. package/dist/src/tools/installer.js +222 -0
  139. package/dist/src/tools/installer.js.map +1 -0
  140. package/dist/src/types/callbacks.d.ts +29 -0
  141. package/dist/src/types/callbacks.d.ts.map +1 -0
  142. package/dist/src/types/callbacks.js +7 -0
  143. package/dist/src/types/callbacks.js.map +1 -0
  144. package/dist/src/types/config.d.ts +56 -0
  145. package/dist/src/types/config.d.ts.map +1 -0
  146. package/dist/src/types/config.js +6 -0
  147. package/dist/src/types/config.js.map +1 -0
  148. package/dist/src/types/hooks.d.ts +47 -0
  149. package/dist/src/types/hooks.d.ts.map +1 -0
  150. package/dist/src/types/hooks.js +6 -0
  151. package/dist/src/types/hooks.js.map +1 -0
  152. package/dist/src/types/index.d.ts +7 -0
  153. package/dist/src/types/index.d.ts.map +1 -0
  154. package/dist/src/types/index.js +23 -0
  155. package/dist/src/types/index.js.map +1 -0
  156. package/dist/src/types/ipc.d.ts +43 -0
  157. package/dist/src/types/ipc.d.ts.map +1 -0
  158. package/dist/src/types/ipc.js +7 -0
  159. package/dist/src/types/ipc.js.map +1 -0
  160. package/dist/src/types/session.d.ts +70 -0
  161. package/dist/src/types/session.d.ts.map +1 -0
  162. package/dist/src/types/session.js +9 -0
  163. package/dist/src/types/session.js.map +1 -0
  164. package/dist/src/types/telegram.d.ts +58 -0
  165. package/dist/src/types/telegram.d.ts.map +1 -0
  166. package/dist/src/types/telegram.js +6 -0
  167. package/dist/src/types/telegram.js.map +1 -0
  168. package/dist/src/utils/active-check.d.ts +19 -0
  169. package/dist/src/utils/active-check.d.ts.map +1 -0
  170. package/dist/src/utils/active-check.js +41 -0
  171. package/dist/src/utils/active-check.js.map +1 -0
  172. package/dist/src/utils/callback-parser.d.ts +21 -0
  173. package/dist/src/utils/callback-parser.d.ts.map +1 -0
  174. package/dist/src/utils/callback-parser.js +58 -0
  175. package/dist/src/utils/callback-parser.js.map +1 -0
  176. package/dist/src/utils/controller-injector.d.ts +21 -0
  177. package/dist/src/utils/controller-injector.d.ts.map +1 -0
  178. package/dist/src/utils/controller-injector.js +108 -0
  179. package/dist/src/utils/controller-injector.js.map +1 -0
  180. package/dist/src/utils/conversation-tracker.d.ts +32 -0
  181. package/dist/src/utils/conversation-tracker.d.ts.map +1 -0
  182. package/dist/src/utils/conversation-tracker.js +119 -0
  183. package/dist/src/utils/conversation-tracker.js.map +1 -0
  184. package/dist/src/utils/http-request.d.ts +25 -0
  185. package/dist/src/utils/http-request.d.ts.map +1 -0
  186. package/dist/src/utils/http-request.js +66 -0
  187. package/dist/src/utils/http-request.js.map +1 -0
  188. package/dist/src/utils/optional-require.d.ts +13 -0
  189. package/dist/src/utils/optional-require.d.ts.map +1 -0
  190. package/dist/src/utils/optional-require.js +37 -0
  191. package/dist/src/utils/optional-require.js.map +1 -0
  192. package/dist/src/utils/paths.d.ts +11 -0
  193. package/dist/src/utils/paths.d.ts.map +1 -0
  194. package/dist/src/utils/paths.js +28 -0
  195. package/dist/src/utils/paths.js.map +1 -0
  196. package/dist/src/utils/pty-session-manager.d.ts +42 -0
  197. package/dist/src/utils/pty-session-manager.d.ts.map +1 -0
  198. package/dist/src/utils/pty-session-manager.js +182 -0
  199. package/dist/src/utils/pty-session-manager.js.map +1 -0
  200. package/dist/src/utils/subagent-tracker.d.ts +64 -0
  201. package/dist/src/utils/subagent-tracker.d.ts.map +1 -0
  202. package/dist/src/utils/subagent-tracker.js +191 -0
  203. package/dist/src/utils/subagent-tracker.js.map +1 -0
  204. package/dist/src/utils/tmux-monitor.d.ts +102 -0
  205. package/dist/src/utils/tmux-monitor.d.ts.map +1 -0
  206. package/dist/src/utils/tmux-monitor.js +642 -0
  207. package/dist/src/utils/tmux-monitor.js.map +1 -0
  208. package/dist/src/utils/trace-capture.d.ts +42 -0
  209. package/dist/src/utils/trace-capture.d.ts.map +1 -0
  210. package/dist/src/utils/trace-capture.js +102 -0
  211. package/dist/src/utils/trace-capture.js.map +1 -0
  212. package/dist/start-all-webhooks.d.ts +7 -0
  213. package/dist/start-all-webhooks.d.ts.map +1 -0
  214. package/dist/start-all-webhooks.js +98 -0
  215. package/dist/start-all-webhooks.js.map +1 -0
  216. package/dist/start-line-webhook.d.ts +7 -0
  217. package/dist/start-line-webhook.d.ts.map +1 -0
  218. package/dist/start-line-webhook.js +59 -0
  219. package/dist/start-line-webhook.js.map +1 -0
  220. package/dist/start-relay-pty.d.ts +7 -0
  221. package/dist/start-relay-pty.d.ts.map +1 -0
  222. package/dist/start-relay-pty.js +173 -0
  223. package/dist/start-relay-pty.js.map +1 -0
  224. package/dist/start-telegram-webhook.d.ts +7 -0
  225. package/dist/start-telegram-webhook.d.ts.map +1 -0
  226. package/dist/start-telegram-webhook.js +80 -0
  227. package/dist/start-telegram-webhook.js.map +1 -0
  228. package/dist/user-prompt-hook.d.ts +13 -0
  229. package/dist/user-prompt-hook.d.ts.map +1 -0
  230. package/dist/user-prompt-hook.js +45 -0
  231. package/dist/user-prompt-hook.js.map +1 -0
  232. package/dist/workspace-router.d.ts +78 -0
  233. package/dist/workspace-router.d.ts.map +1 -0
  234. package/dist/workspace-router.js +408 -0
  235. package/dist/workspace-router.js.map +1 -0
  236. package/dist/workspace-telegram-bot.d.ts +3 -0
  237. package/dist/workspace-telegram-bot.d.ts.map +1 -0
  238. package/dist/workspace-telegram-bot.js +1172 -0
  239. package/dist/workspace-telegram-bot.js.map +1 -0
  240. package/package.json +80 -0
  241. package/src/types/callbacks.ts +39 -0
  242. package/src/types/config.ts +63 -0
  243. package/src/types/hooks.ts +50 -0
  244. package/src/types/index.ts +6 -0
  245. package/src/types/ipc.ts +55 -0
  246. package/src/types/session.ts +72 -0
  247. package/src/types/telegram.ts +66 -0
@@ -0,0 +1,696 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * relay-pty.ts - Fixed version
5
+ * Uses node-imap instead of ImapFlow to resolve Feishu email compatibility issues
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.startImap = startImap;
12
+ exports.handleMailMessage = handleMailMessage;
13
+ exports.extractTokenFromSubject = extractTokenFromSubject;
14
+ exports.cleanEmailText = cleanEmailText;
15
+ const path_1 = __importDefault(require("path"));
16
+ const fs_1 = require("fs");
17
+ const dotenv_1 = __importDefault(require("dotenv"));
18
+ const paths_1 = require("../utils/paths");
19
+ const optional_require_1 = require("../utils/optional-require");
20
+ const envPath = path_1.default.join(paths_1.PROJECT_ROOT, '.env');
21
+ dotenv_1.default.config({ path: envPath });
22
+ const Imap = (0, optional_require_1.optionalRequire)('node-imap', 'IMAP email relay');
23
+ const mailparserModule = (0, optional_require_1.optionalRequire)('mailparser', 'email parsing');
24
+ const simpleParser = mailparserModule ? mailparserModule.simpleParser : null;
25
+ const nodePty = (0, optional_require_1.optionalRequire)('node-pty', 'PTY terminal emulation');
26
+ const _spawn = nodePty ? nodePty.spawn : null;
27
+ const pinoModule = (0, optional_require_1.optionalRequire)('pino', 'structured logging');
28
+ // Configure logging with fallback to console
29
+ let log;
30
+ if (pinoModule) {
31
+ const pinoPretty = (0, optional_require_1.optionalRequire)('pino-pretty', 'log formatting');
32
+ if (pinoPretty) {
33
+ log = pinoModule({
34
+ level: process.env.LOG_LEVEL || 'info',
35
+ transport: {
36
+ target: 'pino-pretty',
37
+ options: { colorize: true, translateTime: 'HH:MM:ss' }
38
+ }
39
+ });
40
+ }
41
+ else {
42
+ log = pinoModule({ level: process.env.LOG_LEVEL || 'info' });
43
+ }
44
+ }
45
+ else {
46
+ const logLevel = process.env.LOG_LEVEL || 'info';
47
+ const levels = { debug: 0, info: 1, warn: 2, error: 3 };
48
+ const minLevel = levels[logLevel] ?? 1;
49
+ log = {
50
+ debug: (...args) => { if (minLevel <= 0)
51
+ console.log('[DEBUG]', ...args); },
52
+ info: (...args) => { if (minLevel <= 1)
53
+ console.log('[INFO]', ...args); },
54
+ warn: (...args) => { if (minLevel <= 2)
55
+ console.warn('[WARN]', ...args); },
56
+ error: (...args) => { if (minLevel <= 3)
57
+ console.error('[ERROR]', ...args); },
58
+ };
59
+ }
60
+ // Global configuration
61
+ const SESS_PATH = process.env.SESSION_MAP_PATH || path_1.default.join(paths_1.PROJECT_ROOT, 'src/data/session-map.json');
62
+ const PROCESSED_PATH = path_1.default.join(paths_1.PROJECT_ROOT, 'src/data/processed-messages.json');
63
+ const SENT_MESSAGES_PATH = path_1.default.join(paths_1.PROJECT_ROOT, 'src/data/sent-messages.json');
64
+ const ALLOWED_SENDERS = (process.env.ALLOWED_SENDERS || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
65
+ const PTY_POOL = new Map();
66
+ let PROCESSED_MESSAGES = new Set();
67
+ // Load processed messages
68
+ function loadProcessedMessages() {
69
+ if ((0, fs_1.existsSync)(PROCESSED_PATH)) {
70
+ try {
71
+ const data = JSON.parse((0, fs_1.readFileSync)(PROCESSED_PATH, 'utf8'));
72
+ const now = Date.now();
73
+ // Keep only records from the last 7 days
74
+ const validMessages = data.filter(item => (now - item.timestamp) < 7 * 24 * 60 * 60 * 1000);
75
+ PROCESSED_MESSAGES = new Set(validMessages.map(item => item.id));
76
+ // Update file, remove expired records
77
+ saveProcessedMessages();
78
+ }
79
+ catch (error) {
80
+ log.error({ error }, 'Failed to load processed messages');
81
+ PROCESSED_MESSAGES = new Set();
82
+ }
83
+ }
84
+ }
85
+ // Save processed messages
86
+ function saveProcessedMessages() {
87
+ try {
88
+ const now = Date.now();
89
+ const data = Array.from(PROCESSED_MESSAGES).map(id => ({
90
+ id,
91
+ timestamp: now
92
+ }));
93
+ // Ensure directory exists
94
+ const dir = path_1.default.dirname(PROCESSED_PATH);
95
+ if (!(0, fs_1.existsSync)(dir)) {
96
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
97
+ }
98
+ (0, fs_1.writeFileSync)(PROCESSED_PATH, JSON.stringify(data, null, 2));
99
+ }
100
+ catch (error) {
101
+ log.error({ error }, 'Failed to save processed messages');
102
+ }
103
+ }
104
+ // Load session mapping
105
+ function loadSessions() {
106
+ if (!(0, fs_1.existsSync)(SESS_PATH))
107
+ return {};
108
+ try {
109
+ return JSON.parse((0, fs_1.readFileSync)(SESS_PATH, 'utf8'));
110
+ }
111
+ catch (error) {
112
+ log.error({ error }, 'Failed to load session map');
113
+ return {};
114
+ }
115
+ }
116
+ // Check if sender is in whitelist
117
+ function isAllowed(fromAddress) {
118
+ if (!fromAddress)
119
+ return false;
120
+ const addr = fromAddress.toLowerCase();
121
+ return ALLOWED_SENDERS.some(allowed => addr.includes(allowed));
122
+ }
123
+ // Extract CCGram token from subject
124
+ function extractTokenFromSubject(subject = '') {
125
+ const patterns = [
126
+ /\[CCGram\s+#([A-Z0-9]+)\]/,
127
+ /Re:\s*\[CCGram\s+#([A-Z0-9]+)\]/
128
+ ];
129
+ for (const pattern of patterns) {
130
+ const match = subject.match(pattern);
131
+ if (match)
132
+ return match[1];
133
+ }
134
+ return null;
135
+ }
136
+ // Clean email text
137
+ function cleanEmailText(text = '') {
138
+ const lines = text.split(/\r?\n/);
139
+ const cleanLines = [];
140
+ for (const line of lines) {
141
+ // Detect quoted content (more comprehensive detection)
142
+ if (line.includes('-----Original Message-----') ||
143
+ line.includes('--- Original Message ---') ||
144
+ line.includes('at') && line.includes('wrote:') ||
145
+ line.includes('On') && line.includes('wrote:') ||
146
+ line.includes('Session ID:') ||
147
+ line.includes(`<${process.env.SMTP_USER}>`) ||
148
+ line.includes('CCGram Notification System') ||
149
+ line.includes('on 2025') && line.includes('wrote:') ||
150
+ line.match(/^>.*/) || // Quote lines start with >
151
+ line.includes('From:') && line.includes('@') ||
152
+ line.includes('To:') && line.includes('@') ||
153
+ line.includes('Subject:') ||
154
+ line.includes('Sent:') ||
155
+ line.includes('Date:')) {
156
+ break;
157
+ }
158
+ // Detect email signature
159
+ if (line.match(/^--\s*$/) ||
160
+ line.includes('Sent from') ||
161
+ line.includes('Sent from my') ||
162
+ line.includes('Best regards') ||
163
+ line.includes('Sincerely')) {
164
+ break;
165
+ }
166
+ cleanLines.push(line);
167
+ }
168
+ // Get valid content
169
+ const cleanText = cleanLines.join('\n').trim();
170
+ // Find actual command content (skip greetings, etc.)
171
+ const contentLines = cleanText.split(/\r?\n/).filter(l => l.trim().length > 0);
172
+ // Collect all valid command lines (support multi-line commands)
173
+ const validCommandLines = [];
174
+ for (const line of contentLines) {
175
+ const trimmedLine = line.trim();
176
+ // Skip common greetings (but only if they're standalone)
177
+ if (trimmedLine.match(/^(hi|hello|thank you|thanks|ok|yes)$/i)) {
178
+ continue;
179
+ }
180
+ // Skip remaining email quotes
181
+ if (trimmedLine.includes('CCGram Notification System') ||
182
+ trimmedLine.includes(`<${process.env.SMTP_USER}>`) ||
183
+ trimmedLine.includes('on 2025')) {
184
+ continue;
185
+ }
186
+ // Collect valid command lines
187
+ if (trimmedLine.length > 0) {
188
+ validCommandLines.push(trimmedLine);
189
+ }
190
+ }
191
+ // Join all valid lines to form the complete command
192
+ if (validCommandLines.length > 0) {
193
+ const fullCommand = validCommandLines.join('\n').slice(0, 8192);
194
+ const deduplicatedCommand = deduplicateCommand(fullCommand);
195
+ return deduplicatedCommand;
196
+ }
197
+ // If no obvious command is found, return first non-empty line (and deduplicate)
198
+ const firstLine = contentLines[0] || '';
199
+ const command = firstLine.slice(0, 8192).trim();
200
+ return deduplicateCommand(command);
201
+ }
202
+ // Deduplicate command text (handle cases like: "drink cola okay drink cola okay" -> "drink cola okay")
203
+ function deduplicateCommand(command) {
204
+ if (!command || command.length === 0) {
205
+ return command;
206
+ }
207
+ // Check if command is self-repeating
208
+ const length = command.length;
209
+ for (let i = 1; i <= Math.floor(length / 2); i++) {
210
+ const firstPart = command.substring(0, i);
211
+ const remaining = command.substring(i);
212
+ // Check if remaining part completely repeats the first part
213
+ if (remaining === firstPart.repeat(Math.floor(remaining.length / firstPart.length))) {
214
+ // Found repetition pattern, return first part
215
+ log.debug({
216
+ originalCommand: command,
217
+ deduplicatedCommand: firstPart,
218
+ pattern: firstPart
219
+ }, 'Detected and removed command duplication');
220
+ return firstPart;
221
+ }
222
+ }
223
+ // No repetition detected, return original command
224
+ return command;
225
+ }
226
+ // Unattended remote command injection - tmux priority, smart fallback
227
+ async function injectCommandRemote(token, command) {
228
+ const sessions = loadSessions();
229
+ const session = sessions[token];
230
+ if (!session) {
231
+ log.warn({ token }, 'Session not found');
232
+ return false;
233
+ }
234
+ // Check if session has expired
235
+ const now = Math.floor(Date.now() / 1000);
236
+ if (session.expiresAt && session.expiresAt < now) {
237
+ log.warn({ token }, 'Session expired');
238
+ return false;
239
+ }
240
+ try {
241
+ log.info({ token, command }, 'Starting remote command injection');
242
+ // Method 1: Prefer tmux unattended injection
243
+ const TmuxInjector = require('./tmux-injector');
244
+ const tmuxSessionName = session.tmuxSession || 'claude-taskping';
245
+ const tmuxInjector = new TmuxInjector(log, tmuxSessionName);
246
+ const tmuxResult = await tmuxInjector.injectCommandFull(token, command);
247
+ if (tmuxResult.success) {
248
+ log.info({ token, session: tmuxResult.session }, 'Tmux remote injection successful');
249
+ return true;
250
+ }
251
+ else {
252
+ log.warn({ token, error: tmuxResult.error }, 'Tmux injection failed, trying smart fallback');
253
+ // Method 2: Fall back to smart injector
254
+ const SmartInjector = require('./smart-injector');
255
+ const smartInjector = new SmartInjector(log);
256
+ const smartResult = await smartInjector.injectCommand(token, command);
257
+ if (smartResult) {
258
+ log.info({ token }, 'Smart injection fallback successful');
259
+ return true;
260
+ }
261
+ else {
262
+ log.error({ token }, 'All remote injection methods failed');
263
+ return false;
264
+ }
265
+ }
266
+ }
267
+ catch (error) {
268
+ log.error({ error, token }, 'Failed to inject command remotely');
269
+ return false;
270
+ }
271
+ }
272
+ // Try automatic paste to active window
273
+ async function tryAutoPaste(command) {
274
+ return new Promise((resolve) => {
275
+ // First copy command to clipboard
276
+ const { spawn } = require('child_process');
277
+ const pbcopy = spawn('pbcopy');
278
+ pbcopy.stdin.write(command);
279
+ pbcopy.stdin.end();
280
+ pbcopy.on('close', (code) => {
281
+ if (code !== 0) {
282
+ resolve({ success: false, error: 'clipboard_copy_failed' });
283
+ return;
284
+ }
285
+ // Execute AppleScript auto-paste
286
+ const autoScript = `
287
+ tell application "System Events"
288
+ set claudeApps to {"Claude", "Claude Code", "Terminal", "iTerm2", "iTerm"}
289
+ set targetApp to null
290
+ set targetName to ""
291
+
292
+ repeat with appName in claudeApps
293
+ try
294
+ if application process appName exists then
295
+ set targetApp to application process appName
296
+ set targetName to appName
297
+ exit repeat
298
+ end if
299
+ end try
300
+ end repeat
301
+
302
+ if targetApp is not null then
303
+ set frontmost of targetApp to true
304
+ delay 0.8
305
+
306
+ repeat 10 times
307
+ if frontmost of targetApp then exit repeat
308
+ delay 0.1
309
+ end repeat
310
+
311
+ if targetName is in {"Terminal", "iTerm2", "iTerm"} then
312
+ keystroke "${command.replace(/"/g, '\\"')}"
313
+ delay 0.3
314
+ keystroke return
315
+ return "terminal_typed"
316
+ else
317
+ keystroke "a" using command down
318
+ delay 0.2
319
+ keystroke "v" using command down
320
+ delay 0.5
321
+ keystroke return
322
+ return "claude_pasted"
323
+ end if
324
+ else
325
+ return "no_target_found"
326
+ end if
327
+ end tell
328
+ `;
329
+ const { exec } = require('child_process');
330
+ exec(`osascript -e '${autoScript}'`, (error, stdout, _stderr) => {
331
+ if (error) {
332
+ resolve({ success: false, error: error.message });
333
+ return;
334
+ }
335
+ const result = stdout.trim();
336
+ switch (result) {
337
+ case 'terminal_typed':
338
+ resolve({ success: true, method: 'Terminal direct input' });
339
+ break;
340
+ case 'claude_pasted':
341
+ resolve({ success: true, method: 'Claude app paste' });
342
+ break;
343
+ case 'no_target_found':
344
+ resolve({ success: false, error: 'no_target_application' });
345
+ break;
346
+ default:
347
+ resolve({ success: false, error: `unknown_result: ${result}` });
348
+ }
349
+ });
350
+ });
351
+ });
352
+ }
353
+ // Fallback to clipboard + strong reminder
354
+ async function fallbackToClipboard(command) {
355
+ return new Promise((resolve) => {
356
+ // Copy to clipboard
357
+ const { spawn } = require('child_process');
358
+ const pbcopy = spawn('pbcopy');
359
+ pbcopy.stdin.write(command);
360
+ pbcopy.stdin.end();
361
+ pbcopy.on('close', (code) => {
362
+ if (code !== 0) {
363
+ resolve(false);
364
+ return;
365
+ }
366
+ // Send strong reminder notification
367
+ const shortCommand = command.length > 30 ? command.substring(0, 30) + '...' : command;
368
+ const notificationScript = `
369
+ display notification "\u{1f6a8} Email command auto-copied! Please paste and execute in Claude Code immediately (Cmd+V)" with title "TaskPing Auto-Injection" subtitle "${shortCommand.replace(/"/g, '\\"')}" sound name "Basso"
370
+ `;
371
+ const { exec } = require('child_process');
372
+ exec(`osascript -e '${notificationScript}'`, (error) => {
373
+ if (error) {
374
+ log.warn({ error: error.message }, 'Failed to send notification');
375
+ }
376
+ else {
377
+ log.info('Strong reminder notification sent');
378
+ }
379
+ resolve(true);
380
+ });
381
+ });
382
+ });
383
+ }
384
+ // Handle email message
385
+ async function handleMailMessage(parsed) {
386
+ try {
387
+ log.debug({ uid: parsed.uid, messageId: parsed.messageId }, 'handleMailMessage called');
388
+ // Check if this is a system-sent email
389
+ const messageId = parsed.messageId;
390
+ if (await isSystemSentEmail(messageId)) {
391
+ log.info({ messageId }, 'Skipping system-sent email');
392
+ await removeFromSentMessages(messageId);
393
+ return;
394
+ }
395
+ // Simplified duplicate detection (UID already checked earlier)
396
+ const uid = parsed.uid;
397
+ // Only perform additional checks for emails without UID
398
+ if (!uid) {
399
+ const identifier = messageId;
400
+ if (identifier && PROCESSED_MESSAGES.has(identifier)) {
401
+ log.debug({ messageId, identifier }, 'Message already processed by messageId, skipping');
402
+ return;
403
+ }
404
+ // Content hash deduplication (as last resort)
405
+ const emailSubject = parsed.subject || '';
406
+ const emailDate = parsed.date || new Date();
407
+ const contentHash = `${emailSubject}_${emailDate.getTime()}`;
408
+ if (PROCESSED_MESSAGES.has(contentHash)) {
409
+ log.debug({ subject: emailSubject, date: emailDate, contentHash }, 'Message already processed by content hash, skipping');
410
+ return;
411
+ }
412
+ }
413
+ // Verify sender
414
+ if (!isAllowed(parsed.from?.text || '')) {
415
+ log.warn({ from: parsed.from?.text }, 'Sender not allowed');
416
+ return;
417
+ }
418
+ // Extract token
419
+ const subject = parsed.subject || '';
420
+ const token = extractTokenFromSubject(subject);
421
+ if (!token) {
422
+ log.warn({ subject }, 'No token found in email');
423
+ return;
424
+ }
425
+ // Extract command - add detailed debugging
426
+ log.debug({
427
+ token,
428
+ rawEmailText: parsed.text?.substring(0, 500),
429
+ emailSubject: parsed.subject
430
+ }, 'Raw email content before cleaning');
431
+ const command = cleanEmailText(parsed.text);
432
+ log.debug({
433
+ token,
434
+ cleanedCommand: command,
435
+ commandLength: command?.length
436
+ }, 'Email content after cleaning');
437
+ if (!command) {
438
+ log.warn({ token }, 'No command found in email');
439
+ return;
440
+ }
441
+ log.info({ token, command }, 'Processing email command');
442
+ // Unattended remote command injection (tmux priority, smart fallback)
443
+ const success = await injectCommandRemote(token, command);
444
+ if (!success) {
445
+ log.warn({ token }, 'Could not inject command');
446
+ return;
447
+ }
448
+ // Mark as processed (only mark after successful processing)
449
+ if (uid) {
450
+ // Mark UID as processed
451
+ PROCESSED_MESSAGES.add(uid);
452
+ log.debug({ uid }, 'Marked message UID as processed');
453
+ }
454
+ else {
455
+ // For emails without UID, use messageId and content hash
456
+ if (messageId) {
457
+ PROCESSED_MESSAGES.add(messageId);
458
+ log.debug({ messageId }, 'Marked message as processed by messageId');
459
+ }
460
+ // Content hash marking
461
+ const emailSubject = parsed.subject || '';
462
+ const emailDate = parsed.date || new Date();
463
+ const contentHash = `${emailSubject}_${emailDate.getTime()}`;
464
+ PROCESSED_MESSAGES.add(contentHash);
465
+ log.debug({ contentHash }, 'Marked message as processed by content hash');
466
+ }
467
+ // Persist processed messages
468
+ saveProcessedMessages();
469
+ log.info({ token }, 'Command injected successfully via remote method');
470
+ }
471
+ catch (error) {
472
+ log.error({ error }, 'Failed to handle email message');
473
+ }
474
+ }
475
+ // Start IMAP listening
476
+ function startImap() {
477
+ if (!Imap) {
478
+ console.error('Error: node-imap is required for the email relay. Install with: npm install node-imap');
479
+ process.exit(1);
480
+ }
481
+ if (!simpleParser) {
482
+ console.error('Error: mailparser is required for the email relay. Install with: npm install mailparser');
483
+ process.exit(1);
484
+ }
485
+ // First load processed messages
486
+ loadProcessedMessages();
487
+ log.info('Starting relay-pty service', {
488
+ mode: 'pty',
489
+ imapHost: process.env.IMAP_HOST,
490
+ imapUser: process.env.IMAP_USER,
491
+ allowedSenders: ALLOWED_SENDERS,
492
+ sessionMapPath: SESS_PATH,
493
+ processedCount: PROCESSED_MESSAGES.size
494
+ });
495
+ const imap = new Imap({
496
+ user: process.env.IMAP_USER,
497
+ password: process.env.IMAP_PASS,
498
+ host: process.env.IMAP_HOST,
499
+ port: parseInt(process.env.IMAP_PORT || '993') || 993,
500
+ tls: process.env.IMAP_SECURE === 'true',
501
+ connTimeout: 60000,
502
+ authTimeout: 30000,
503
+ keepalive: true
504
+ });
505
+ imap.once('ready', function () {
506
+ log.info('Connected to IMAP server');
507
+ imap.openBox('INBOX', false, function (err, box) {
508
+ if (err) {
509
+ log.error({ error: err.message }, 'Failed to open INBOX');
510
+ return;
511
+ }
512
+ log.info(`Mailbox opened: ${box.messages.total} total messages, ${box.messages.new} new`);
513
+ // Only process existing unread emails at startup
514
+ processExistingEmails(imap);
515
+ // Listen for new emails (main mechanism)
516
+ imap.on('mail', function (numNewMsgs) {
517
+ log.info({ newMessages: numNewMsgs }, 'New mail arrived');
518
+ // Add delay to avoid conflicts with existing email processing
519
+ setTimeout(() => {
520
+ processNewEmails(imap);
521
+ }, 1000);
522
+ });
523
+ // Periodic check for new emails (backup only, extended interval)
524
+ setInterval(() => {
525
+ log.debug('Periodic email check...');
526
+ processNewEmails(imap);
527
+ }, 120000); // Check every 2 minutes, reduced frequency
528
+ });
529
+ });
530
+ imap.once('error', function (err) {
531
+ log.error({ error: err.message }, 'IMAP error');
532
+ // Reconnection mechanism
533
+ setTimeout(() => {
534
+ log.info('Attempting to reconnect...');
535
+ startImap();
536
+ }, 10000);
537
+ });
538
+ imap.once('end', function () {
539
+ log.info('IMAP connection ended');
540
+ });
541
+ imap.connect();
542
+ // Graceful shutdown
543
+ process.on('SIGINT', () => {
544
+ log.info('Shutting down gracefully...');
545
+ imap.end();
546
+ process.exit(0);
547
+ });
548
+ }
549
+ // Process existing emails
550
+ function processExistingEmails(imap) {
551
+ // Search unread emails
552
+ imap.search(['UNSEEN'], function (err, results) {
553
+ if (err) {
554
+ log.error({ error: err.message }, 'Failed to search emails');
555
+ return;
556
+ }
557
+ if (results.length > 0) {
558
+ log.info(`Found ${results.length} unread messages`);
559
+ log.debug({ uids: results }, 'Unread message UIDs');
560
+ fetchAndProcessEmails(imap, results);
561
+ }
562
+ else {
563
+ log.debug('No unread messages found');
564
+ }
565
+ });
566
+ }
567
+ // Process new emails
568
+ function processNewEmails(imap) {
569
+ // Search emails from the last 5 minutes
570
+ const since = new Date();
571
+ since.setMinutes(since.getMinutes() - 5);
572
+ const sinceStr = since.toISOString().split('T')[0]; // YYYY-MM-DD
573
+ imap.search([['SINCE', sinceStr], 'UNSEEN'], function (err, results) {
574
+ if (err) {
575
+ log.error({ error: err.message }, 'Failed to search new emails');
576
+ return;
577
+ }
578
+ if (results.length > 0) {
579
+ log.info(`Found ${results.length} new messages`);
580
+ fetchAndProcessEmails(imap, results);
581
+ }
582
+ });
583
+ }
584
+ // Fetch and process emails
585
+ function fetchAndProcessEmails(imap, uids) {
586
+ log.debug({ uids }, 'Starting to fetch emails');
587
+ const fetch = imap.fetch(uids, {
588
+ bodies: '', // Get complete email
589
+ markSeen: true // Mark as read
590
+ });
591
+ fetch.on('message', function (msg, seqno) {
592
+ let buffer = '';
593
+ let messageUid = null;
594
+ let skipProcessing = false;
595
+ let bodyProcessed = false;
596
+ let attributesReceived = false;
597
+ // Get UID to prevent duplicate processing
598
+ msg.once('attributes', function (attrs) {
599
+ messageUid = attrs.uid;
600
+ attributesReceived = true;
601
+ log.debug({ uid: messageUid, seqno }, 'Received attributes');
602
+ // Only check if already processed, don't mark immediately
603
+ if (messageUid && PROCESSED_MESSAGES.has(messageUid)) {
604
+ log.debug({ uid: messageUid, seqno }, 'Message UID already processed, skipping entire message');
605
+ skipProcessing = true;
606
+ return; // Return directly, do not continue processing
607
+ }
608
+ log.debug({ uid: messageUid, seqno }, 'Message UID ready for processing');
609
+ // If body is processed, can now parse email
610
+ if (bodyProcessed && !skipProcessing) {
611
+ processEmailBuffer(buffer, messageUid, seqno);
612
+ }
613
+ });
614
+ msg.on('body', function (stream, _info) {
615
+ stream.on('data', function (chunk) {
616
+ buffer += chunk.toString('utf8');
617
+ });
618
+ stream.once('end', function () {
619
+ bodyProcessed = true;
620
+ log.debug({ uid: messageUid, seqno, bufferLength: buffer.length, attributesReceived }, 'Body stream ended');
621
+ // If attributes received and not marked to skip, can now parse email
622
+ if (attributesReceived && !skipProcessing) {
623
+ processEmailBuffer(buffer, messageUid, seqno);
624
+ }
625
+ });
626
+ });
627
+ // Separated email processing function
628
+ function processEmailBuffer(buffer, uid, seqno) {
629
+ if (buffer.length > 0 && uid) {
630
+ log.debug({ uid, seqno }, 'Starting email parsing');
631
+ simpleParser(buffer, function (err, parsed) {
632
+ if (err) {
633
+ log.error({ error: err.message, seqno, uid }, 'Failed to parse email');
634
+ PROCESSED_MESSAGES.delete(uid);
635
+ }
636
+ else {
637
+ log.debug({ uid, seqno }, 'Email parsed successfully, calling handleMailMessage');
638
+ parsed.uid = uid;
639
+ handleMailMessage(parsed);
640
+ }
641
+ });
642
+ }
643
+ else {
644
+ log.debug({ uid, seqno, bufferLength: buffer.length }, 'Skipping email - no buffer or uid');
645
+ }
646
+ }
647
+ msg.once('error', function (err) {
648
+ log.error({ error: err.message, seqno, uid: messageUid }, 'Error fetching message');
649
+ });
650
+ });
651
+ fetch.once('error', function (err) {
652
+ log.error({ error: err.message }, 'Error fetching emails');
653
+ });
654
+ fetch.once('end', function () {
655
+ log.debug('Email fetch completed');
656
+ });
657
+ }
658
+ // Check if email is system-sent
659
+ async function isSystemSentEmail(messageId) {
660
+ if (!messageId || !(0, fs_1.existsSync)(SENT_MESSAGES_PATH)) {
661
+ return false;
662
+ }
663
+ try {
664
+ const sentMessages = JSON.parse((0, fs_1.readFileSync)(SENT_MESSAGES_PATH, 'utf8'));
665
+ return sentMessages.messages.some(msg => msg.messageId === messageId);
666
+ }
667
+ catch (error) {
668
+ log.error({ error }, 'Error reading sent messages');
669
+ return false;
670
+ }
671
+ }
672
+ // Remove email from sent messages tracking
673
+ async function removeFromSentMessages(messageId) {
674
+ if (!(0, fs_1.existsSync)(SENT_MESSAGES_PATH)) {
675
+ return;
676
+ }
677
+ try {
678
+ const sentMessages = JSON.parse((0, fs_1.readFileSync)(SENT_MESSAGES_PATH, 'utf8'));
679
+ sentMessages.messages = sentMessages.messages.filter(msg => msg.messageId !== messageId);
680
+ // Also clean up old messages (older than 24 hours)
681
+ const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
682
+ sentMessages.messages = sentMessages.messages.filter(msg => {
683
+ return new Date(msg.sentAt) > oneDayAgo;
684
+ });
685
+ (0, fs_1.writeFileSync)(SENT_MESSAGES_PATH, JSON.stringify(sentMessages, null, 2));
686
+ log.debug({ messageId }, 'Removed message from sent tracking');
687
+ }
688
+ catch (error) {
689
+ log.error({ error }, 'Error removing from sent messages');
690
+ }
691
+ }
692
+ // Start service
693
+ if (require.main === module) {
694
+ startImap();
695
+ }
696
+ //# sourceMappingURL=relay-pty.js.map