glad-web 1.0.45 → 2.0.1

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 (68) hide show
  1. package/README.md +4 -192
  2. package/THIRD_PARTY_NOTICES.md +27 -0
  3. package/bin/glad.cjs +56 -0
  4. package/package.json +19 -58
  5. package/README.zh-CN.md +0 -198
  6. package/assets/logo.svg +0 -43
  7. package/bin/cli.js +0 -65
  8. package/lib/ai-tools/demo/enhanced-demo.js +0 -625
  9. package/lib/ai-tools/demo/index.js +0 -24
  10. package/lib/ai-tools/demo/responses.js +0 -88
  11. package/lib/ai-tools/detector.js +0 -76
  12. package/lib/ai-tools/registry.js +0 -300
  13. package/lib/claude/cli-usage.js +0 -95
  14. package/lib/claude/config.js +0 -82
  15. package/lib/claude/structured-session.js +0 -884
  16. package/lib/claude/transcript-repository.js +0 -216
  17. package/lib/codex/image-store.js +0 -174
  18. package/lib/codex/structured-session.js +0 -1578
  19. package/lib/commands/config.js +0 -78
  20. package/lib/commands/tools.js +0 -128
  21. package/lib/commands/web.js +0 -586
  22. package/lib/config/constants.js +0 -17
  23. package/lib/config/manager.js +0 -89
  24. package/lib/git/service.js +0 -83
  25. package/lib/notifications/message-formatter.js +0 -94
  26. package/lib/notifications/notification-service.js +0 -143
  27. package/lib/notifications/serverchan-client.js +0 -58
  28. package/lib/notifications/serverchan-settings-store.js +0 -115
  29. package/lib/schedule/job-runner.js +0 -162
  30. package/lib/schedule/job-store.js +0 -167
  31. package/lib/schedule/key-sequences.js +0 -49
  32. package/lib/schedule/scheduler-service.js +0 -39
  33. package/lib/server/routes/notifications.js +0 -52
  34. package/lib/server/routes/providers.js +0 -114
  35. package/lib/server/routes/schedules.js +0 -54
  36. package/lib/server/routes/usage.js +0 -23
  37. package/lib/server/routes/workspace.js +0 -77
  38. package/lib/session/buffer.js +0 -102
  39. package/lib/session/file-attachment-store.js +0 -168
  40. package/lib/session/pty-manager.js +0 -255
  41. package/lib/session/rendered-history.js +0 -225
  42. package/lib/session/session-manager.js +0 -1001
  43. package/lib/session/text-history.js +0 -274
  44. package/lib/usage/ccusage-runner.js +0 -128
  45. package/lib/usage/source-catalog.js +0 -26
  46. package/lib/usage/usage-service.js +0 -226
  47. package/lib/utils/logger.js +0 -74
  48. package/lib/utils/pid.js +0 -67
  49. package/lib/utils/validation.js +0 -53
  50. package/lib/web/claude.js +0 -1129
  51. package/lib/web/codex.js +0 -1042
  52. package/lib/web/composer.js +0 -463
  53. package/lib/web/core.js +0 -373
  54. package/lib/web/git.js +0 -535
  55. package/lib/web/gitgraph.js +0 -293
  56. package/lib/web/index.html +0 -516
  57. package/lib/web/layout.js +0 -72
  58. package/lib/web/notifications.js +0 -163
  59. package/lib/web/schedules.js +0 -245
  60. package/lib/web/session.js +0 -360
  61. package/lib/web/shell.js +0 -59
  62. package/lib/web/styles.css +0 -905
  63. package/lib/web/terminal-scroll.js +0 -81
  64. package/lib/web/theme.js +0 -60
  65. package/lib/web/timed-inputs.js +0 -216
  66. package/lib/web/usage.js +0 -323
  67. package/lib/workspace/service.js +0 -77
  68. package/scripts/check-syntax.js +0 -26
@@ -1,83 +0,0 @@
1
- const { execFile } = require('child_process');
2
-
3
- function execFilePromise(file, args, cwd) {
4
- return new Promise((resolve) => {
5
- execFile(file, args, { cwd, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
6
- resolve({ success: !error, error: error?.message, stdout, stderr });
7
- });
8
- });
9
- }
10
-
11
- function parseGitStatusZ(stdout) {
12
- if (!stdout) return [];
13
-
14
- const entries = [];
15
- const records = stdout.split('\0').filter(Boolean);
16
-
17
- for (let i = 0; i < records.length; i++) {
18
- const record = records[i];
19
- if (record.length < 3) continue;
20
-
21
- const status = record.substring(0, 2);
22
- const path = record.substring(3);
23
- const entry = { path, status };
24
-
25
- if ((status.includes('R') || status.includes('C')) && i + 1 < records.length) {
26
- entry.originalPath = records[++i];
27
- }
28
-
29
- entries.push(entry);
30
- }
31
-
32
- return entries;
33
- }
34
-
35
- class GitService {
36
- async show(cwd, hash) {
37
- return execFilePromise('git', ['show', '--format=fuller', '--stat', '-p', hash], cwd);
38
- }
39
-
40
- async log(cwd, maxCount = 100) {
41
- const count = Number.parseInt(maxCount, 10) || 100;
42
- const result = await execFilePromise(
43
- 'git',
44
- ['log', '--all', '--date-order', `--max-count=${count}`, '--pretty=format:%h|%p|%d|%s|%an|%ar'],
45
- cwd
46
- );
47
- if (!result.success) return result;
48
-
49
- const commits = result.stdout.split('\n').filter(Boolean).map(line => {
50
- const [hash, parents, refs, subject, author, time] = line.split('|');
51
- return { hash, parents: parents ? parents.split(' ') : [], refs: refs ? refs.trim() : '', subject, author, time };
52
- });
53
- return { ...result, commits };
54
- }
55
-
56
- async status(cwd) {
57
- const result = await execFilePromise('git', ['status', '--porcelain=v1', '-z', '--untracked-files=all'], cwd);
58
- if (!result.success) return result;
59
- return { ...result, files: parseGitStatusZ(result.stdout) };
60
- }
61
-
62
- diffNumstat(cwd, isStaged = false) {
63
- const args = isStaged ? ['diff', '--cached', '--numstat'] : ['diff', '--numstat'];
64
- return execFilePromise('git', args, cwd);
65
- }
66
-
67
- diffFile(cwd, filePath, isStaged = false) {
68
- const args = isStaged
69
- ? ['diff', '--cached', '--no-ext-diff', '--', filePath]
70
- : ['diff', '--no-ext-diff', '--', filePath];
71
- return execFilePromise('git', args, cwd);
72
- }
73
-
74
- async nameRev(cwd, hash) {
75
- return execFilePromise('git', ['name-rev', '--name-only', '--exclude=tags/*', hash], cwd);
76
- }
77
- }
78
-
79
- module.exports = {
80
- GitService,
81
- execFilePromise,
82
- parseGitStatusZ
83
- };
@@ -1,94 +0,0 @@
1
- const path = require('path');
2
-
3
- const TITLE_LABELS = {
4
- approval: '待审批',
5
- completed: '已完成',
6
- failed: '执行失败',
7
- disconnected: '连接中断',
8
- test: '通知测试'
9
- };
10
-
11
- function compactText(value, maxLength = 20) {
12
- const text = String(value || '').replace(/[\r\n]+/g, ' ').trim();
13
- if (text.length <= maxLength) return text;
14
- return `${text.slice(0, Math.max(1, maxLength - 1))}…`;
15
- }
16
-
17
- function formatLocalDateTime(value) {
18
- const date = new Date(Number(value) || Date.now());
19
- const pad = number => String(number).padStart(2, '0');
20
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} `
21
- + `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
22
- }
23
-
24
- function formatDuration(durationMs) {
25
- const totalSeconds = Math.max(0, Math.round(Number(durationMs) / 1000));
26
- if (!Number.isFinite(totalSeconds) || totalSeconds <= 0) return '';
27
- if (totalSeconds < 60) return `${totalSeconds}秒`;
28
- const minutes = Math.floor(totalSeconds / 60);
29
- const seconds = totalSeconds % 60;
30
- return seconds ? `${minutes}分${seconds}秒` : `${minutes}分`;
31
- }
32
-
33
- function escapeMarkdown(value) {
34
- return String(value || '').replace(/([\\`*_{}\[\]()#+\-.!|>])/g, '\\$1');
35
- }
36
-
37
- function providerName(provider, session) {
38
- if (provider === 'claude') return 'Claude';
39
- if (provider === 'codex') return 'Codex';
40
- return session?.tool?.displayName || 'AI';
41
- }
42
-
43
- function sessionDirectory(session) {
44
- return session?.workingDir || session?.workingDirectory || session?.ptyManager?.workingDir || process.cwd();
45
- }
46
-
47
- function formatNotification({ kind, provider, session, durationMs = null }, clientType = 'wechat') {
48
- const name = String(session?.name || providerName(provider, session));
49
- const directory = sessionDirectory(session);
50
- const type = providerName(provider, session);
51
- const title = `${TITLE_LABELS[kind] || '通知'}|${compactText(name)}`;
52
- const details = [
53
- ['类型', type],
54
- ['会话', name],
55
- ['创建', formatLocalDateTime(session?.startTime)],
56
- ['目录', directory]
57
- ];
58
- const duration = formatDuration(durationMs);
59
- if (duration) details.push(['本轮耗时', duration]);
60
-
61
- if (clientType === 'pushdeer') {
62
- return {
63
- title,
64
- description: details
65
- .map(([label, value]) => label === '目录'
66
- ? `**${label}:** \`${escapeMarkdown(value)}\``
67
- : `**${label}:** ${escapeMarkdown(value)}`)
68
- .join('\n\n')
69
- };
70
- }
71
-
72
- return {
73
- title,
74
- description: details.map(([label, value]) => `${label}:${value}`).join('\n\n')
75
- };
76
- }
77
-
78
- function createTestSession(session = null) {
79
- if (session) return session;
80
- return {
81
- name: 'Glad 测试会话',
82
- startTime: Date.now(),
83
- workingDir: path.resolve(process.cwd()),
84
- tool: { displayName: 'Glad' }
85
- };
86
- }
87
-
88
- module.exports = {
89
- formatNotification,
90
- createTestSession,
91
- compactText,
92
- formatLocalDateTime,
93
- formatDuration
94
- };
@@ -1,143 +0,0 @@
1
- const { formatNotification, createTestSession } = require('./message-formatter');
2
-
3
- class NotificationService {
4
- constructor({ sessionManager, settingsStore, channel, logger = console }) {
5
- this.sessionManager = sessionManager;
6
- this.settingsStore = settingsStore;
7
- this.channel = channel;
8
- this.logger = logger;
9
- this.seenEvents = new Map();
10
- this.handlers = {
11
- claude: payload => this.handleProviderEvent('claude', payload),
12
- codex: payload => this.handleProviderEvent('codex', payload),
13
- exit: payload => this.clearSession(payload.sessionId)
14
- };
15
- sessionManager.on('claude-event', this.handlers.claude);
16
- sessionManager.on('codex-event', this.handlers.codex);
17
- sessionManager.on('exit', this.handlers.exit);
18
- }
19
-
20
- stop() {
21
- this.sessionManager.off('claude-event', this.handlers.claude);
22
- this.sessionManager.off('codex-event', this.handlers.codex);
23
- this.sessionManager.off('exit', this.handlers.exit);
24
- this.seenEvents.clear();
25
- }
26
-
27
- getSessionState(sessionId) {
28
- const session = this.sessionManager.get(sessionId);
29
- if (!session) return null;
30
- return {
31
- enabled: Boolean(session.serverChanNotificationEnabled),
32
- configured: this.settingsStore.getPublic().configured
33
- };
34
- }
35
-
36
- async setSessionEnabled(sessionId, enabled) {
37
- const session = this.sessionManager.get(sessionId);
38
- if (!session) {
39
- const error = new Error('Session not found');
40
- error.statusCode = 404;
41
- throw error;
42
- }
43
- if (enabled && !this.settingsStore.getPublic().configured) {
44
- const error = new Error('请先配置 Server酱');
45
- error.statusCode = 409;
46
- error.code = 'SERVERCHAN_NOT_CONFIGURED';
47
- throw error;
48
- }
49
- session.serverChanNotificationEnabled = Boolean(enabled);
50
- if (enabled && session.pendingPermissions?.size > 0) {
51
- await this.sendEvent({
52
- kind: 'approval',
53
- provider: session.kind === 'claude-structured' ? 'claude' : 'codex',
54
- session
55
- });
56
- }
57
- return this.getSessionState(sessionId);
58
- }
59
-
60
- disableAllSessions() {
61
- for (const session of this.sessionManager.sessions.values()) {
62
- session.serverChanNotificationEnabled = false;
63
- }
64
- }
65
-
66
- async sendTest(input = {}) {
67
- const settings = this.settingsStore.resolve(input);
68
- const session = createTestSession(
69
- input.sessionId ? this.sessionManager.get(input.sessionId) : null
70
- );
71
- const provider = session?.kind === 'claude-structured'
72
- ? 'claude'
73
- : session?.kind === 'codex-structured' ? 'codex' : null;
74
- const message = formatNotification({ kind: 'test', provider, session }, settings.clientType);
75
- await this.channel.send({ ...settings, ...message });
76
- return { success: true };
77
- }
78
-
79
- handleProviderEvent(provider, payload = {}) {
80
- const { session, event, sessionId } = payload;
81
- if (!session || !event || !session.serverChanNotificationEnabled) return;
82
-
83
- if (event.type === 'permission-request') {
84
- const requestId = event.request?.id || event.request?.toolUseId || 'pending';
85
- if (!this.markSeen(sessionId, `approval:${requestId}`)) return;
86
- this.sendEvent({ kind: 'approval', provider, session });
87
- return;
88
- }
89
-
90
- if (event.type === 'runtime-disconnected') {
91
- if (!event.activeTurn || !this.markSeen(sessionId, `disconnected:${event.turnId || 'active'}`)) return;
92
- this.sendEvent({ kind: 'disconnected', provider, session });
93
- return;
94
- }
95
-
96
- if (event.type === 'turn-failed') {
97
- if (!this.markSeen(sessionId, `failed:${event.turnId || event.createdAt || 'start'}`)) return;
98
- this.sendEvent({ kind: 'failed', provider, session, durationMs: event.durationMs });
99
- return;
100
- }
101
-
102
- const message = event.type === 'message' ? event.message : null;
103
- if (!message || message.kind !== 'turn-end') return;
104
- if (provider === 'codex' && message.threadId && session.threadId && message.threadId !== session.threadId) return;
105
-
106
- const status = String(message.turnStatus || message.status || 'completed');
107
- if (status === 'cancelled' || status === 'interrupted') return;
108
- const turnId = message.turnId || message.id;
109
- if (!this.markSeen(sessionId, `turn:${turnId}`)) return;
110
- this.sendEvent({
111
- kind: status === 'failed' ? 'failed' : 'completed',
112
- provider,
113
- session,
114
- durationMs: message.durationMs
115
- });
116
- }
117
-
118
- markSeen(sessionId, key) {
119
- const seen = this.seenEvents.get(sessionId) || new Set();
120
- if (seen.has(key)) return false;
121
- seen.add(key);
122
- if (seen.size > 100) seen.delete(seen.values().next().value);
123
- this.seenEvents.set(sessionId, seen);
124
- return true;
125
- }
126
-
127
- clearSession(sessionId) {
128
- this.seenEvents.delete(sessionId);
129
- }
130
-
131
- async sendEvent(event) {
132
- try {
133
- const settings = this.settingsStore.get();
134
- if (!settings.sendKey) return;
135
- const message = formatNotification(event, settings.clientType);
136
- await this.channel.send({ ...settings, ...message });
137
- } catch (error) {
138
- this.logger.error?.(`[serverchan] notification failed: ${error.message}`);
139
- }
140
- }
141
- }
142
-
143
- module.exports = NotificationService;
@@ -1,58 +0,0 @@
1
- const API_BASE = 'https://sctapi.ftqq.com';
2
-
3
- function sanitizeServerChanError(payload, fallback) {
4
- const message = payload && typeof payload === 'object'
5
- ? payload.message || payload.error || payload.data?.error
6
- : '';
7
- const text = String(message || fallback || 'Server酱发送失败').trim();
8
- return text.slice(0, 300);
9
- }
10
-
11
- class ServerChanClient {
12
- constructor({ fetchImpl = globalThis.fetch, timeoutMs = 10_000 } = {}) {
13
- this.fetchImpl = fetchImpl;
14
- this.timeoutMs = timeoutMs;
15
- }
16
-
17
- async send({ sendKey, title, description = '' }) {
18
- const controller = new AbortController();
19
- const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
20
- try {
21
- const response = await this.fetchImpl(`${API_BASE}/${encodeURIComponent(sendKey)}.send`, {
22
- method: 'POST',
23
- headers: {
24
- 'content-type': 'application/x-www-form-urlencoded'
25
- },
26
- body: new URLSearchParams({
27
- title: String(title || '').replace(/[\r\n]+/g, ' ').trim().slice(0, 64),
28
- desp: String(description || '')
29
- }),
30
- signal: controller.signal
31
- });
32
- const text = await response.text();
33
- let payload = null;
34
- try {
35
- payload = text ? JSON.parse(text) : null;
36
- } catch (_) {
37
- payload = null;
38
- }
39
- if (!response.ok || (payload && Number(payload.code) !== 0)) {
40
- const error = new Error(sanitizeServerChanError(payload, `HTTP ${response.status}`));
41
- error.statusCode = 502;
42
- throw error;
43
- }
44
- return payload || { code: 0 };
45
- } catch (error) {
46
- if (error && error.name === 'AbortError') {
47
- const timeoutError = new Error('Server酱请求超时');
48
- timeoutError.statusCode = 504;
49
- throw timeoutError;
50
- }
51
- throw error;
52
- } finally {
53
- clearTimeout(timeout);
54
- }
55
- }
56
- }
57
-
58
- module.exports = ServerChanClient;
@@ -1,115 +0,0 @@
1
- const fs = require('fs');
2
- const {
3
- getConfig,
4
- setConfig,
5
- getConfigPath
6
- } = require('../config/manager');
7
-
8
- const CLIENT_TYPES = new Set(['wechat', 'pushdeer']);
9
-
10
- function normalizeClientType(value) {
11
- const clientType = String(value || 'wechat').trim().toLowerCase();
12
- if (!CLIENT_TYPES.has(clientType)) {
13
- const error = new Error('接收客户端必须是微信或 PushDeer');
14
- error.statusCode = 400;
15
- throw error;
16
- }
17
- return clientType;
18
- }
19
-
20
- function normalizeSendKey(value) {
21
- const sendKey = String(value || '').trim();
22
- if (!sendKey || sendKey.length < 8 || sendKey.length > 512 || /\s/.test(sendKey)) {
23
- const error = new Error('请输入有效的 Server酱 SendKey');
24
- error.statusCode = 400;
25
- throw error;
26
- }
27
- return sendKey;
28
- }
29
-
30
- function maskSendKey(sendKey) {
31
- if (!sendKey) return '';
32
- return `${sendKey.slice(0, Math.min(3, sendKey.length))}${'•'.repeat(10)}`;
33
- }
34
-
35
- class ServerChanSettingsStore {
36
- constructor({
37
- readConfig = getConfig,
38
- writeConfig = setConfig,
39
- configPath = getConfigPath,
40
- chmod = fs.chmodSync
41
- } = {}) {
42
- this.readConfig = readConfig;
43
- this.writeConfig = writeConfig;
44
- this.configPath = configPath;
45
- this.chmod = chmod;
46
- }
47
-
48
- get() {
49
- const stored = this.readConfig('serverChan') || {};
50
- return {
51
- sendKey: String(stored.sendKey || '').trim(),
52
- clientType: CLIENT_TYPES.has(stored.clientType) ? stored.clientType : 'wechat'
53
- };
54
- }
55
-
56
- getPublic() {
57
- const settings = this.get();
58
- return {
59
- configured: Boolean(settings.sendKey),
60
- maskedKey: maskSendKey(settings.sendKey),
61
- clientType: settings.clientType
62
- };
63
- }
64
-
65
- save(input = {}) {
66
- const existing = this.get();
67
- const sendKey = input.sendKey == null || String(input.sendKey).trim() === ''
68
- ? existing.sendKey
69
- : normalizeSendKey(input.sendKey);
70
- if (!sendKey) {
71
- const error = new Error('请先填写 Server酱 SendKey');
72
- error.statusCode = 400;
73
- throw error;
74
- }
75
- const settings = {
76
- sendKey,
77
- clientType: normalizeClientType(input.clientType ?? existing.clientType)
78
- };
79
- this.writeConfig('serverChan', settings);
80
- this.restrictConfigFile();
81
- return this.getPublic();
82
- }
83
-
84
- resolve(input = {}) {
85
- const existing = this.get();
86
- return {
87
- sendKey: input.sendKey == null || String(input.sendKey).trim() === ''
88
- ? normalizeSendKey(existing.sendKey)
89
- : normalizeSendKey(input.sendKey),
90
- clientType: normalizeClientType(input.clientType ?? existing.clientType)
91
- };
92
- }
93
-
94
- clear() {
95
- this.writeConfig('serverChan', { sendKey: '', clientType: 'wechat' });
96
- this.restrictConfigFile();
97
- return this.getPublic();
98
- }
99
-
100
- restrictConfigFile() {
101
- try {
102
- const configPath = typeof this.configPath === 'function' ? this.configPath() : this.configPath;
103
- if (configPath && fs.existsSync(configPath)) this.chmod(configPath, 0o600);
104
- } catch (_) {
105
- // Best effort: configuration remains usable on filesystems without chmod support.
106
- }
107
- }
108
- }
109
-
110
- module.exports = {
111
- ServerChanSettingsStore,
112
- normalizeClientType,
113
- normalizeSendKey,
114
- maskSendKey
115
- };
@@ -1,162 +0,0 @@
1
- const { sequenceForKey } = require('./key-sequences');
2
-
3
- function sleep(ms) {
4
- return new Promise(resolve => setTimeout(resolve, ms));
5
- }
6
-
7
- class SessionEndedError extends Error {
8
- constructor() {
9
- super('Session ended');
10
- this.code = 'SESSION_ENDED';
11
- }
12
- }
13
-
14
- class JobRunner {
15
- constructor({ createSession, getJob, updateJob, logger }) {
16
- this.createSession = createSession;
17
- this.getJob = getJob;
18
- this.updateJob = updateJob;
19
- this.logger = logger;
20
- this.running = new Set();
21
- }
22
-
23
- async run(jobId, options = {}) {
24
- const job = this.getJob(jobId);
25
- if (!job) throw new Error('Scheduled task not found');
26
- if (this.running.has(job.id)) {
27
- this.updateJob(job.id, {
28
- lastRunAt: Date.now(),
29
- lastRunStatus: 'skipped',
30
- lastRunMessage: 'Previous run is still active'
31
- });
32
- return { skipped: true, reason: 'Previous run is still active' };
33
- }
34
-
35
- this.running.add(job.id);
36
- this.updateJob(job.id, {
37
- running: true,
38
- lastRunAt: Date.now(),
39
- lastRunStatus: options.manual ? 'manual-running' : 'running',
40
- lastRunMessage: ''
41
- });
42
-
43
- let session = null;
44
- try {
45
- session = this.createSession({
46
- toolKey: job.target.toolKey,
47
- workingDirectory: job.target.workingDirectory,
48
- name: `${job.name}${options.manual ? ' (Test)' : ''}`
49
- });
50
-
51
- this.updateJob(job.id, {
52
- lastSessionId: session.id,
53
- lastRunMessage: `Started session ${session.id}`
54
- });
55
-
56
- const execute = async () => {
57
- try {
58
- await sleep(1000);
59
- await this.executeSteps(job, session);
60
- this.updateJob(job.id, {
61
- running: false,
62
- lastRunStatus: options.manual ? 'manual-success' : 'success',
63
- lastRunMessage: `Started session ${session.id}`,
64
- lastSessionId: session.id
65
- });
66
- } catch (error) {
67
- const ended = error && error.code === 'SESSION_ENDED';
68
- if (!ended) this.logger?.error?.(`Scheduled task failed: ${error.message}`);
69
- this.updateJob(job.id, {
70
- running: false,
71
- lastRunStatus: ended ? 'cancelled' : 'failed',
72
- lastRunMessage: ended ? 'Session ended' : error.message,
73
- lastSessionId: session?.id || null
74
- });
75
- if (!options.background) throw error;
76
- } finally {
77
- this.running.delete(job.id);
78
- this.updateJob(job.id, { running: false });
79
- }
80
- };
81
-
82
- if (options.background) {
83
- execute();
84
- return { success: true, sessionId: session.id };
85
- }
86
-
87
- await execute();
88
- return { success: true, sessionId: session.id };
89
- } catch (error) {
90
- if (!session) {
91
- this.running.delete(job.id);
92
- this.updateJob(job.id, { running: false });
93
- }
94
- this.logger?.error?.(`Scheduled task failed: ${error.message}`);
95
- this.updateJob(job.id, {
96
- running: false,
97
- lastRunStatus: 'failed',
98
- lastRunMessage: error.message,
99
- lastSessionId: session?.id || null
100
- });
101
- throw error;
102
- }
103
- }
104
-
105
- async executeSteps(job, session) {
106
- const modifiers = { ctrl: false, alt: false };
107
-
108
- for (const step of job.steps || []) {
109
- this.assertSessionRunning(session);
110
- if (step.type === 'sleep') {
111
- await this.sleepWhileRunning(session, Math.max(0, Number(step.seconds) || 0) * 1000);
112
- continue;
113
- }
114
-
115
- if (step.type === 'sendText') {
116
- if (!session.write(String(step.text || ''))) throw new SessionEndedError();
117
- continue;
118
- }
119
-
120
- if (step.type === 'sendKey') {
121
- if (!session.write(sequenceForKey(step.key, modifiers))) throw new SessionEndedError();
122
- continue;
123
- }
124
-
125
- if (step.type === 'keyDown') {
126
- const key = String(step.key || '').toLowerCase();
127
- if (key === 'ctrl' || key === 'alt') modifiers[key] = true;
128
- continue;
129
- }
130
-
131
- if (step.type === 'keyUp') {
132
- const key = String(step.key || '').toLowerCase();
133
- if (key === 'ctrl' || key === 'alt') modifiers[key] = false;
134
- continue;
135
- }
136
-
137
- if (step.type === 'stop') break;
138
-
139
- if (step.type === 'closeSession') {
140
- session.kill?.();
141
- break;
142
- }
143
- }
144
- }
145
-
146
- assertSessionRunning(session) {
147
- if (session && typeof session.isRunning === 'function' && !session.isRunning()) {
148
- throw new SessionEndedError();
149
- }
150
- }
151
-
152
- async sleepWhileRunning(session, ms) {
153
- const end = Date.now() + ms;
154
- while (Date.now() < end) {
155
- this.assertSessionRunning(session);
156
- await sleep(Math.min(500, end - Date.now()));
157
- }
158
- this.assertSessionRunning(session);
159
- }
160
- }
161
-
162
- module.exports = JobRunner;