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.
- package/README.md +4 -192
- package/THIRD_PARTY_NOTICES.md +27 -0
- package/bin/glad.cjs +56 -0
- package/package.json +19 -61
- package/README.zh-CN.md +0 -198
- package/assets/logo.svg +0 -43
- package/bin/cli.js +0 -65
- package/lib/ai-tools/demo/enhanced-demo.js +0 -625
- package/lib/ai-tools/demo/index.js +0 -24
- package/lib/ai-tools/demo/responses.js +0 -88
- package/lib/ai-tools/detector.js +0 -76
- package/lib/ai-tools/registry.js +0 -300
- package/lib/claude/cli-usage.js +0 -95
- package/lib/claude/config.js +0 -82
- package/lib/claude/structured-session.js +0 -884
- package/lib/claude/transcript-repository.js +0 -216
- package/lib/codex/image-store.js +0 -174
- package/lib/codex/structured-session.js +0 -1590
- package/lib/commands/config.js +0 -78
- package/lib/commands/tools.js +0 -128
- package/lib/commands/web.js +0 -605
- package/lib/config/constants.js +0 -17
- package/lib/config/manager.js +0 -108
- package/lib/git/service.js +0 -83
- package/lib/notifications/message-formatter.js +0 -94
- package/lib/notifications/notification-service.js +0 -143
- package/lib/notifications/serverchan-client.js +0 -58
- package/lib/notifications/serverchan-settings-store.js +0 -115
- package/lib/schedule/job-runner.js +0 -162
- package/lib/schedule/job-store.js +0 -167
- package/lib/schedule/key-sequences.js +0 -49
- package/lib/schedule/scheduler-service.js +0 -39
- package/lib/server/routes/notifications.js +0 -52
- package/lib/server/routes/providers.js +0 -114
- package/lib/server/routes/schedules.js +0 -54
- package/lib/server/routes/skillhub.js +0 -104
- package/lib/server/routes/usage.js +0 -23
- package/lib/server/routes/workspace.js +0 -77
- package/lib/session/buffer.js +0 -102
- package/lib/session/file-attachment-store.js +0 -168
- package/lib/session/pty-manager.js +0 -255
- package/lib/session/rendered-history.js +0 -225
- package/lib/session/session-manager.js +0 -1032
- package/lib/session/text-history.js +0 -274
- package/lib/skillhub/client.js +0 -121
- package/lib/skillhub/settings-store.js +0 -168
- package/lib/skillhub/skill-installer.js +0 -320
- package/lib/usage/ccusage-runner.js +0 -128
- package/lib/usage/source-catalog.js +0 -26
- package/lib/usage/usage-service.js +0 -226
- package/lib/utils/logger.js +0 -74
- package/lib/utils/pid.js +0 -67
- package/lib/utils/validation.js +0 -53
- package/lib/web/bootstrap.js +0 -34
- package/lib/web/claude.js +0 -1150
- package/lib/web/codex.js +0 -1045
- package/lib/web/composer.js +0 -493
- package/lib/web/core.js +0 -385
- package/lib/web/git.js +0 -535
- package/lib/web/gitgraph.js +0 -293
- package/lib/web/index.html +0 -547
- package/lib/web/layout.js +0 -69
- package/lib/web/notifications.js +0 -164
- package/lib/web/schedules.js +0 -245
- package/lib/web/session.js +0 -361
- package/lib/web/shell.js +0 -74
- package/lib/web/skillhub.js +0 -197
- package/lib/web/styles.css +0 -932
- package/lib/web/terminal-scroll.js +0 -81
- package/lib/web/theme.js +0 -60
- package/lib/web/timed-inputs.js +0 -216
- package/lib/web/usage.js +0 -323
- package/lib/workspace/service.js +0 -77
- package/scripts/check-syntax.js +0 -26
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { v4: uuidv4 } = require('uuid');
|
|
5
|
-
|
|
6
|
-
function previewText(text, maxChars) {
|
|
7
|
-
const normalized = String(text || '').replace(/\s+/g, ' ').trim();
|
|
8
|
-
return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}...` : normalized;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function isLocalCommandTranscript(text) {
|
|
12
|
-
const value = String(text || '').trim();
|
|
13
|
-
return value.startsWith('<command-name>/') || value.startsWith('<local-command-caveat>');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
class ClaudeTranscriptRepository {
|
|
17
|
-
constructor({ baseDir, logger } = {}) {
|
|
18
|
-
this.baseDir = baseDir || process.cwd();
|
|
19
|
-
this.logger = logger || console;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
list(workingDirectory) {
|
|
23
|
-
const projectDir = this.getProjectDirectory(workingDirectory);
|
|
24
|
-
if (!fs.existsSync(projectDir)) return [];
|
|
25
|
-
const files = fs.readdirSync(projectDir)
|
|
26
|
-
.filter(file => /^[0-9a-f-]{36}\.jsonl$/i.test(file))
|
|
27
|
-
.map(file => {
|
|
28
|
-
const fullPath = path.join(projectDir, file);
|
|
29
|
-
const stat = fs.statSync(fullPath);
|
|
30
|
-
return {
|
|
31
|
-
id: file.replace(/\.jsonl$/i, ''),
|
|
32
|
-
path: fullPath,
|
|
33
|
-
mtimeMs: stat.mtimeMs,
|
|
34
|
-
size: stat.size
|
|
35
|
-
};
|
|
36
|
-
})
|
|
37
|
-
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
38
|
-
.slice(0, 40);
|
|
39
|
-
|
|
40
|
-
return files.map(file => this.describe(file));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
readMessages(workingDirectory, resumeSessionId) {
|
|
44
|
-
const id = String(resumeSessionId || '').trim();
|
|
45
|
-
if (!/^[0-9a-f-]{36}$/i.test(id)) return [];
|
|
46
|
-
const filePath = path.join(this.getProjectDirectory(workingDirectory), `${id}.jsonl`);
|
|
47
|
-
if (!fs.existsSync(filePath)) return [];
|
|
48
|
-
|
|
49
|
-
const messages = [];
|
|
50
|
-
try {
|
|
51
|
-
const lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
|
|
52
|
-
for (const line of lines) {
|
|
53
|
-
const record = this.parseLine(line);
|
|
54
|
-
if (!record || record.isSidechain) continue;
|
|
55
|
-
messages.push(...this.mapRecord(record));
|
|
56
|
-
}
|
|
57
|
-
} catch (error) {
|
|
58
|
-
this.logger.debugInfo?.(`[claude-resume] Failed to backfill ${filePath}: ${error.message}`);
|
|
59
|
-
return [];
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const maxMessages = 1000;
|
|
63
|
-
const visible = messages.filter(Boolean).slice(-maxMessages);
|
|
64
|
-
if (messages.length > maxMessages) {
|
|
65
|
-
visible.unshift({
|
|
66
|
-
id: uuidv4(),
|
|
67
|
-
kind: 'event',
|
|
68
|
-
level: 'info',
|
|
69
|
-
text: `Showing the latest ${maxMessages} resumed transcript items.`,
|
|
70
|
-
createdAt: Date.now()
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
return visible;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
getProjectDirectory(workingDirectory) {
|
|
77
|
-
const cwd = path.resolve(workingDirectory || this.baseDir);
|
|
78
|
-
const encoded = cwd.replace(/[^a-zA-Z0-9]/g, '-');
|
|
79
|
-
return path.join(os.homedir(), '.claude', 'projects', encoded);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
describe(file) {
|
|
83
|
-
let cwd = '';
|
|
84
|
-
let firstText = '';
|
|
85
|
-
let lastText = '';
|
|
86
|
-
const userQuestions = [];
|
|
87
|
-
try {
|
|
88
|
-
const lines = fs.readFileSync(file.path, 'utf8').split('\n').filter(Boolean);
|
|
89
|
-
for (const line of lines) {
|
|
90
|
-
const parsed = this.parseLine(line);
|
|
91
|
-
if (!parsed) continue;
|
|
92
|
-
if (!cwd && typeof parsed.cwd === 'string') cwd = parsed.cwd;
|
|
93
|
-
const userText = this.extractUserText(parsed);
|
|
94
|
-
if (userText && userQuestions.at(-1) !== userText) userQuestions.push(userText);
|
|
95
|
-
const text = this.extractText(parsed);
|
|
96
|
-
if (!text) continue;
|
|
97
|
-
if (!firstText) firstText = text;
|
|
98
|
-
lastText = text;
|
|
99
|
-
}
|
|
100
|
-
} catch (error) {
|
|
101
|
-
this.logger.debugInfo?.(`[claude-resume] Failed to read ${file.path}: ${error.message}`);
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
id: file.id,
|
|
105
|
-
cwd,
|
|
106
|
-
updatedAt: file.mtimeMs,
|
|
107
|
-
size: file.size,
|
|
108
|
-
questions: userQuestions.slice(-2).reverse().map(text => previewText(text, 120)),
|
|
109
|
-
firstText: firstText ? previewText(firstText, 120) : '',
|
|
110
|
-
lastText: lastText ? previewText(lastText, 160) : ''
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
mapRecord(record) {
|
|
115
|
-
const createdAt = Number.isFinite(Date.parse(record.timestamp)) ? Date.parse(record.timestamp) : Date.now();
|
|
116
|
-
const message = record.message || {};
|
|
117
|
-
const content = message.content;
|
|
118
|
-
if (record.type === 'user') {
|
|
119
|
-
if (typeof content === 'string') {
|
|
120
|
-
const text = content.trim();
|
|
121
|
-
return text && !isLocalCommandTranscript(text) ? [{ id: uuidv4(), kind: 'user', text, createdAt }] : [];
|
|
122
|
-
}
|
|
123
|
-
if (Array.isArray(content)) {
|
|
124
|
-
return content.flatMap(item => {
|
|
125
|
-
if (!item || typeof item !== 'object') return [];
|
|
126
|
-
if (item.type === 'tool_result') {
|
|
127
|
-
const text = this.textFromContent(item.content).trim();
|
|
128
|
-
return text ? [{
|
|
129
|
-
id: uuidv4(), kind: 'tool-result', toolUseId: item.tool_use_id,
|
|
130
|
-
text, isError: Boolean(item.is_error), createdAt
|
|
131
|
-
}] : [];
|
|
132
|
-
}
|
|
133
|
-
if (item.type === 'text' && typeof item.text === 'string' && item.text.trim()) {
|
|
134
|
-
const text = item.text.trim();
|
|
135
|
-
return isLocalCommandTranscript(text) ? [] : [{ id: uuidv4(), kind: 'user', text, createdAt }];
|
|
136
|
-
}
|
|
137
|
-
return [];
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (record.type === 'assistant' && Array.isArray(content)) {
|
|
143
|
-
const mapped = [];
|
|
144
|
-
const text = this.textFromContent(content).trim();
|
|
145
|
-
if (text) mapped.push({ id: uuidv4(), kind: 'assistant', text, createdAt });
|
|
146
|
-
for (const item of content) {
|
|
147
|
-
if (!item || item.type !== 'tool_use') continue;
|
|
148
|
-
mapped.push({
|
|
149
|
-
id: uuidv4(), kind: 'tool', name: item.name || 'tool',
|
|
150
|
-
summary: this.summarizeToolInput(item.input), input: item.input,
|
|
151
|
-
toolUseId: item.id, createdAt
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
return mapped;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (record.type === 'summary' && typeof record.summary === 'string' && record.summary.trim()) {
|
|
158
|
-
return [{ id: uuidv4(), kind: 'event', level: 'info', text: `Summary: ${record.summary.trim()}`, createdAt }];
|
|
159
|
-
}
|
|
160
|
-
return [];
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
textFromContent(content) {
|
|
164
|
-
if (typeof content === 'string') return content;
|
|
165
|
-
if (!Array.isArray(content)) return '';
|
|
166
|
-
return content.map(item => {
|
|
167
|
-
if (!item || typeof item !== 'object') return '';
|
|
168
|
-
if (item.type === 'text' && typeof item.text === 'string') return item.text;
|
|
169
|
-
if (item.type === 'tool_result') return this.textFromContent(item.content);
|
|
170
|
-
return '';
|
|
171
|
-
}).filter(Boolean).join('\n');
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
summarizeToolInput(input) {
|
|
175
|
-
if (!input || typeof input !== 'object') return '';
|
|
176
|
-
if (typeof input.command === 'string') return input.command;
|
|
177
|
-
if (typeof input.file_path === 'string') return input.file_path;
|
|
178
|
-
if (typeof input.path === 'string') return input.path;
|
|
179
|
-
const serialized = JSON.stringify(input);
|
|
180
|
-
return serialized.length > 240 ? `${serialized.slice(0, 240)}...` : serialized;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
parseLine(line) {
|
|
184
|
-
try {
|
|
185
|
-
return JSON.parse(line);
|
|
186
|
-
} catch (_) {
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
extractText(record) {
|
|
192
|
-
const content = record?.message?.content;
|
|
193
|
-
if (typeof content === 'string') return content.trim();
|
|
194
|
-
if (!Array.isArray(content)) return '';
|
|
195
|
-
return content.map(item => {
|
|
196
|
-
if (!item || typeof item !== 'object') return '';
|
|
197
|
-
if (item.type === 'text' && typeof item.text === 'string') return item.text;
|
|
198
|
-
if (item.type === 'tool_use') return `[tool] ${item.name || 'tool'}`;
|
|
199
|
-
return '';
|
|
200
|
-
}).filter(Boolean).join('\n').trim();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
extractUserText(record) {
|
|
204
|
-
if (record?.type !== 'user') return '';
|
|
205
|
-
const content = record?.message?.content;
|
|
206
|
-
if (typeof content === 'string') {
|
|
207
|
-
const text = content.trim();
|
|
208
|
-
return isLocalCommandTranscript(text) ? '' : text;
|
|
209
|
-
}
|
|
210
|
-
if (!Array.isArray(content)) return '';
|
|
211
|
-
return content.map(item => item?.type === 'text' && typeof item.text === 'string' && !isLocalCommandTranscript(item.text) ? item.text : '')
|
|
212
|
-
.filter(Boolean).join('\n').trim();
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
module.exports = ClaudeTranscriptRepository;
|
package/lib/codex/image-store.js
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { v4: uuidv4 } = require('uuid');
|
|
5
|
-
|
|
6
|
-
const MAX_BYTES = 50 * 1024 * 1024;
|
|
7
|
-
const MAX_PER_SESSION = 5;
|
|
8
|
-
const CLEANUP_DELAY_MS = 5 * 60 * 1000;
|
|
9
|
-
const MAX_CHUNKS = 128;
|
|
10
|
-
|
|
11
|
-
function imageExtension(buffer) {
|
|
12
|
-
if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) return 'png';
|
|
13
|
-
if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) return 'jpg';
|
|
14
|
-
if (buffer.length >= 6 && ['GIF87a', 'GIF89a'].includes(buffer.subarray(0, 6).toString('ascii'))) return 'gif';
|
|
15
|
-
if (buffer.length >= 12 && buffer.subarray(0, 4).toString('ascii') === 'RIFF' && buffer.subarray(8, 12).toString('ascii') === 'WEBP') return 'webp';
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function inputError(message, statusCode = 400) {
|
|
20
|
-
const error = new Error(message);
|
|
21
|
-
error.statusCode = statusCode;
|
|
22
|
-
return error;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function safeUploadId(value) {
|
|
26
|
-
return typeof value === 'string' && /^[a-zA-Z0-9-]{8,100}$/.test(value);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
class CodexImageStore {
|
|
30
|
-
constructor({ logger, root, uploadRoot } = {}) {
|
|
31
|
-
this.logger = logger || console;
|
|
32
|
-
this.root = root || path.join(os.tmpdir(), 'glad', 'codex-images');
|
|
33
|
-
this.uploadRoot = uploadRoot || path.join(os.tmpdir(), 'glad', 'codex-image-uploads');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
assertSession(session) {
|
|
37
|
-
if (!session) throw inputError('Session not found', 404);
|
|
38
|
-
const structured = ['claude-structured', 'codex-structured'].includes(session.kind);
|
|
39
|
-
if (!structured) {
|
|
40
|
-
throw inputError('Image attachments are available only in structured chat mode');
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async store(session, bytes) {
|
|
45
|
-
this.assertSession(session);
|
|
46
|
-
if (!Buffer.isBuffer(bytes) || bytes.length === 0) throw inputError('Image data is required');
|
|
47
|
-
if (bytes.length > MAX_BYTES) throw inputError('Image must be 50 MB or smaller');
|
|
48
|
-
if (session.imageAttachments.size >= MAX_PER_SESSION) throw inputError(`You can attach at most ${MAX_PER_SESSION} images at a time`);
|
|
49
|
-
|
|
50
|
-
const extension = imageExtension(bytes);
|
|
51
|
-
if (!extension) throw inputError('Only PNG, JPEG, GIF, and WebP images are supported');
|
|
52
|
-
const directory = path.join(this.root, session.id);
|
|
53
|
-
await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
54
|
-
const attachment = {
|
|
55
|
-
id: uuidv4(),
|
|
56
|
-
name: `image.${extension}`,
|
|
57
|
-
path: path.join(directory, `${uuidv4()}.${extension}`),
|
|
58
|
-
size: bytes.length,
|
|
59
|
-
createdAt: Date.now(),
|
|
60
|
-
cleanupTimer: null
|
|
61
|
-
};
|
|
62
|
-
await fs.promises.writeFile(attachment.path, bytes, { mode: 0o600, flag: 'wx' });
|
|
63
|
-
session.imageAttachments.set(attachment.id, attachment);
|
|
64
|
-
return { id: attachment.id, name: attachment.name, size: attachment.size };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async appendChunk(session, input = {}, bytes) {
|
|
68
|
-
this.assertSession(session);
|
|
69
|
-
if (!Buffer.isBuffer(bytes) || bytes.length === 0) throw inputError('Image chunk is required');
|
|
70
|
-
const uploadId = String(input.uploadId || '');
|
|
71
|
-
const chunkIndex = Number(input.chunkIndex);
|
|
72
|
-
const chunkTotal = Number(input.chunkTotal);
|
|
73
|
-
if (!safeUploadId(uploadId)) throw inputError('Invalid image upload id');
|
|
74
|
-
if (!Number.isInteger(chunkIndex) || !Number.isInteger(chunkTotal) || chunkIndex < 0 || chunkTotal < 1 || chunkTotal > MAX_CHUNKS || chunkIndex >= chunkTotal) {
|
|
75
|
-
throw inputError('Invalid image chunk metadata');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
let upload = session.imageUploads.get(uploadId);
|
|
79
|
-
if (!upload) {
|
|
80
|
-
if (chunkIndex !== 0) throw inputError('Image upload must start with the first chunk');
|
|
81
|
-
const directory = path.join(this.uploadRoot, session.id);
|
|
82
|
-
await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
83
|
-
upload = { id: uploadId, path: path.join(directory, `${uploadId}.part`), chunkTotal, nextChunkIndex: 0, bytes: 0 };
|
|
84
|
-
session.imageUploads.set(uploadId, upload);
|
|
85
|
-
}
|
|
86
|
-
if (upload.chunkTotal !== chunkTotal || upload.nextChunkIndex !== chunkIndex) throw inputError('Image chunks arrived out of order');
|
|
87
|
-
if (upload.bytes + bytes.length > MAX_BYTES) {
|
|
88
|
-
await this.discardUpload(session, uploadId);
|
|
89
|
-
throw inputError('Image must be 50 MB or smaller');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (chunkIndex === 0) await fs.promises.writeFile(upload.path, bytes, { mode: 0o600, flag: 'wx' });
|
|
93
|
-
else await fs.promises.appendFile(upload.path, bytes, { mode: 0o600 });
|
|
94
|
-
upload.bytes += bytes.length;
|
|
95
|
-
upload.nextChunkIndex += 1;
|
|
96
|
-
if (upload.nextChunkIndex < upload.chunkTotal) {
|
|
97
|
-
return { complete: false, receivedChunks: upload.nextChunkIndex, size: upload.bytes };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
session.imageUploads.delete(uploadId);
|
|
101
|
-
try {
|
|
102
|
-
const attachment = await this.store(session, await fs.promises.readFile(upload.path));
|
|
103
|
-
return { complete: true, attachment };
|
|
104
|
-
} finally {
|
|
105
|
-
await fs.promises.rm(upload.path, { force: true });
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async discardUpload(session, uploadId) {
|
|
110
|
-
if (!session?.imageUploads || !safeUploadId(uploadId)) return false;
|
|
111
|
-
const upload = session.imageUploads.get(uploadId);
|
|
112
|
-
if (!upload) return false;
|
|
113
|
-
session.imageUploads.delete(uploadId);
|
|
114
|
-
await fs.promises.rm(upload.path, { force: true });
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async discardAttachment(session, attachmentId) {
|
|
119
|
-
if (!session || !['claude-structured', 'codex-structured'].includes(session.kind)) return false;
|
|
120
|
-
const attachment = session.imageAttachments.get(attachmentId);
|
|
121
|
-
if (!attachment) return false;
|
|
122
|
-
clearTimeout(attachment.cleanupTimer);
|
|
123
|
-
session.imageAttachments.delete(attachmentId);
|
|
124
|
-
await fs.promises.rm(attachment.path, { force: true });
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
resolve(session, attachmentIds = []) {
|
|
129
|
-
if (!session || !['claude-structured', 'codex-structured'].includes(session.kind)) throw inputError('Structured session not found', 404);
|
|
130
|
-
const ids = Array.isArray(attachmentIds) ? attachmentIds : [];
|
|
131
|
-
if (ids.length > MAX_PER_SESSION) throw inputError(`You can attach at most ${MAX_PER_SESSION} images at a time`);
|
|
132
|
-
const uniqueIds = [...new Set(ids.map(String))];
|
|
133
|
-
if (uniqueIds.length !== ids.length) throw inputError('Duplicate image attachment');
|
|
134
|
-
return uniqueIds.map(attachmentId => {
|
|
135
|
-
const attachment = session.imageAttachments.get(attachmentId);
|
|
136
|
-
if (!attachment) throw inputError('Image attachment is no longer available');
|
|
137
|
-
return attachment;
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
scheduleCleanup(session, attachmentIds) {
|
|
142
|
-
if (!session || !['claude-structured', 'codex-structured'].includes(session.kind)) return;
|
|
143
|
-
for (const attachmentId of attachmentIds) {
|
|
144
|
-
const attachment = session.imageAttachments.get(attachmentId);
|
|
145
|
-
if (!attachment) continue;
|
|
146
|
-
clearTimeout(attachment.cleanupTimer);
|
|
147
|
-
attachment.cleanupTimer = setTimeout(() => {
|
|
148
|
-
this.discardAttachment(session, attachmentId).catch(error => {
|
|
149
|
-
this.logger.debugInfo?.(`[structured-image] cleanup failed: ${error.message}`);
|
|
150
|
-
});
|
|
151
|
-
}, CLEANUP_DELAY_MS);
|
|
152
|
-
attachment.cleanupTimer.unref?.();
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
clearAttachments(session) {
|
|
157
|
-
if (!session?.imageAttachments) return;
|
|
158
|
-
for (const attachment of session.imageAttachments.values()) clearTimeout(attachment.cleanupTimer);
|
|
159
|
-
session.imageAttachments.clear();
|
|
160
|
-
fs.promises.rm(path.join(this.root, session.id), { recursive: true, force: true }).catch(error => {
|
|
161
|
-
this.logger.debugInfo?.(`[structured-image] session cleanup failed: ${error.message}`);
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
clearUploads(session) {
|
|
166
|
-
if (!session?.imageUploads) return;
|
|
167
|
-
session.imageUploads.clear();
|
|
168
|
-
fs.promises.rm(path.join(this.uploadRoot, session.id), { recursive: true, force: true }).catch(error => {
|
|
169
|
-
this.logger.debugInfo?.(`[structured-image] upload cleanup failed: ${error.message}`);
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
module.exports = CodexImageStore;
|