glad-web 1.0.46 → 2.0.2

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 (74) 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 -61
  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 -1590
  19. package/lib/commands/config.js +0 -78
  20. package/lib/commands/tools.js +0 -128
  21. package/lib/commands/web.js +0 -605
  22. package/lib/config/constants.js +0 -17
  23. package/lib/config/manager.js +0 -108
  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/skillhub.js +0 -104
  37. package/lib/server/routes/usage.js +0 -23
  38. package/lib/server/routes/workspace.js +0 -77
  39. package/lib/session/buffer.js +0 -102
  40. package/lib/session/file-attachment-store.js +0 -168
  41. package/lib/session/pty-manager.js +0 -255
  42. package/lib/session/rendered-history.js +0 -225
  43. package/lib/session/session-manager.js +0 -1032
  44. package/lib/session/text-history.js +0 -274
  45. package/lib/skillhub/client.js +0 -121
  46. package/lib/skillhub/settings-store.js +0 -168
  47. package/lib/skillhub/skill-installer.js +0 -320
  48. package/lib/usage/ccusage-runner.js +0 -128
  49. package/lib/usage/source-catalog.js +0 -26
  50. package/lib/usage/usage-service.js +0 -226
  51. package/lib/utils/logger.js +0 -74
  52. package/lib/utils/pid.js +0 -67
  53. package/lib/utils/validation.js +0 -53
  54. package/lib/web/bootstrap.js +0 -34
  55. package/lib/web/claude.js +0 -1150
  56. package/lib/web/codex.js +0 -1045
  57. package/lib/web/composer.js +0 -493
  58. package/lib/web/core.js +0 -385
  59. package/lib/web/git.js +0 -535
  60. package/lib/web/gitgraph.js +0 -293
  61. package/lib/web/index.html +0 -547
  62. package/lib/web/layout.js +0 -69
  63. package/lib/web/notifications.js +0 -164
  64. package/lib/web/schedules.js +0 -245
  65. package/lib/web/session.js +0 -361
  66. package/lib/web/shell.js +0 -74
  67. package/lib/web/skillhub.js +0 -197
  68. package/lib/web/styles.css +0 -932
  69. package/lib/web/terminal-scroll.js +0 -81
  70. package/lib/web/theme.js +0 -60
  71. package/lib/web/timed-inputs.js +0 -216
  72. package/lib/web/usage.js +0 -323
  73. package/lib/workspace/service.js +0 -77
  74. package/scripts/check-syntax.js +0 -26
@@ -1,108 +0,0 @@
1
- const Conf = require('conf');
2
- const path = require('path');
3
- const os = require('os');
4
-
5
- // Config schema - only user preferences, no environment config
6
- const schema = {
7
- defaultAI: {
8
- type: 'string',
9
- default: ''
10
- },
11
- serverChan: {
12
- type: 'object',
13
- properties: {
14
- sendKey: {
15
- type: 'string',
16
- default: ''
17
- },
18
- clientType: {
19
- type: 'string',
20
- enum: ['wechat', 'pushdeer'],
21
- default: 'wechat'
22
- }
23
- },
24
- default: {
25
- sendKey: '',
26
- clientType: 'wechat'
27
- }
28
- },
29
- skillHub: {
30
- type: 'object',
31
- properties: {
32
- baseUrl: { type: 'string', default: '' },
33
- token: {
34
- type: 'object',
35
- properties: {
36
- ciphertext: { type: 'string', default: '' },
37
- iv: { type: 'string', default: '' },
38
- authTag: { type: 'string', default: '' }
39
- },
40
- default: { ciphertext: '', iv: '', authTag: '' }
41
- }
42
- },
43
- default: {
44
- baseUrl: '',
45
- token: { ciphertext: '', iv: '', authTag: '' }
46
- }
47
- },
48
- version: {
49
- type: 'string',
50
- default: '1.0.0'
51
- },
52
- lastUpdated: {
53
- type: 'string',
54
- default: ''
55
- }
56
- };
57
-
58
- // Create config instance
59
- const config = new Conf({
60
- projectName: 'glad',
61
- cwd: path.join(os.homedir(), '.glad'),
62
- configName: 'config',
63
- schema
64
- });
65
-
66
- // Get config value
67
- function getConfig(key) {
68
- if (key) {
69
- return config.get(key);
70
- }
71
- return config.store;
72
- }
73
-
74
- // Set config value
75
- function setConfig(key, value) {
76
- config.set(key, value);
77
- config.set('lastUpdated', new Date().toISOString());
78
- }
79
-
80
- // Get default AI tool
81
- function getDefaultAI() {
82
- const value = config.get('defaultAI');
83
- return value || null;
84
- }
85
-
86
- // Set default AI tool
87
- function setDefaultAI(tool) {
88
- setConfig('defaultAI', tool);
89
- }
90
-
91
- // Get config file path
92
- function getConfigPath() {
93
- return config.path;
94
- }
95
-
96
- // Reset config to defaults
97
- function resetConfig() {
98
- config.clear();
99
- }
100
-
101
- module.exports = {
102
- getConfig,
103
- setConfig,
104
- getDefaultAI,
105
- setDefaultAI,
106
- getConfigPath,
107
- resetConfig
108
- };
@@ -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
- };