@exreve/exk 1.0.45 → 1.0.46
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/dist/agentSession.js +108 -8
- package/dist/app-child.js +144 -1673
- package/dist/appHandlers.js +142 -0
- package/dist/appManager.js +5 -5
- package/dist/appRunner.js +2 -2
- package/dist/cloudflaredHandlers.js +279 -0
- package/dist/containerHandlers.js +193 -0
- package/dist/fsHandlers.js +86 -0
- package/dist/githubHandlers.js +521 -0
- package/dist/index.js +142 -1741
- package/dist/projectManager.js +1 -1
- package/dist/runnerGenerator.js +2 -3
- package/dist/sessionHandlers.js +271 -0
- package/dist/ttc-cli.tar.gz +0 -0
- package/dist/updateHandlers.js +82 -0
- package/dist/updater.js +2 -5
- package/package.json +1 -1
package/dist/projectManager.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
const toAbsolutePath = (p) => path.isAbsolute(p) ? p : path.resolve(p);
|
|
4
4
|
export async function createProject(request) {
|
|
5
5
|
try {
|
|
6
|
-
const {
|
|
6
|
+
const { path: projectPath, sourcePath } = request;
|
|
7
7
|
const absolutePath = toAbsolutePath(projectPath);
|
|
8
8
|
// Check if source path exists (if linking)
|
|
9
9
|
if (sourcePath) {
|
package/dist/runnerGenerator.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Generate TypeScript runner code for an app
|
|
3
3
|
*/
|
|
4
4
|
export function generateRunnerCode(app, projectPath) {
|
|
5
|
-
const appName = app.name.replace(/[^a-zA-Z0-9_-]/g, '_'); // Sanitize app name for filename
|
|
6
5
|
const appType = app.appType || (app.framework?.toLowerCase().includes('react') ||
|
|
7
6
|
app.framework?.toLowerCase().includes('vue') ||
|
|
8
7
|
app.framework?.toLowerCase().includes('angular') ||
|
|
@@ -14,7 +13,7 @@ export function generateRunnerCode(app, projectPath) {
|
|
|
14
13
|
return generateBackendRunner(app, projectPath);
|
|
15
14
|
}
|
|
16
15
|
}
|
|
17
|
-
function generateStaticFrontendRunner(app,
|
|
16
|
+
function generateStaticFrontendRunner(app, _projectPath) {
|
|
18
17
|
const appName = app.name.replace(/[^a-zA-Z0-9_-]/g, '_'); // Sanitize app name for filename
|
|
19
18
|
const port = app.port || 3000;
|
|
20
19
|
const buildDir = app.buildDir || 'dist';
|
|
@@ -99,7 +98,7 @@ process.on('SIGINT', async () => {
|
|
|
99
98
|
start()
|
|
100
99
|
`;
|
|
101
100
|
}
|
|
102
|
-
function generateBackendRunner(app,
|
|
101
|
+
function generateBackendRunner(app, _projectPath) {
|
|
103
102
|
const appName = app.name.replace(/[^a-zA-Z0-9_-]/g, '_'); // Sanitize app name for filename
|
|
104
103
|
const appDir = app.directory || '';
|
|
105
104
|
const startCommand = app.startCommand;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Handlers Module
|
|
3
|
+
*
|
|
4
|
+
* Handles session:create, session:delete, session:prompt,
|
|
5
|
+
* prompt:cancel, emergency:stop, project:config:analyze.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import fsSync from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { agentSessionManager } from './agentSession.js';
|
|
11
|
+
import { analyzeProjectWithClaude, saveProjectConfig } from './projectAnalyzer.js';
|
|
12
|
+
import { generateRunnerCode } from './runnerGenerator.js';
|
|
13
|
+
/**
|
|
14
|
+
* Register session handlers.
|
|
15
|
+
* `getSocket` is a function that returns the current live socket — this ensures
|
|
16
|
+
* that callbacks registered on a previous socket instance still emit on the
|
|
17
|
+
* active socket after a reconnect (fixes in-flight output loss on disconnect).
|
|
18
|
+
*/
|
|
19
|
+
export function registerSessionHandlers(socket, foreground, activeSessions, getSocket) {
|
|
20
|
+
socket.on('session:create', async (data) => {
|
|
21
|
+
try {
|
|
22
|
+
let { sessionId, projectPath } = data;
|
|
23
|
+
// Remap /home/abc to /tmp/abc if /home/abc doesn't exist (container workaround)
|
|
24
|
+
if (projectPath === '/home/abc' && !fsSync.existsSync('/home/abc')) {
|
|
25
|
+
const fallbackPath = '/tmp/abc';
|
|
26
|
+
fsSync.mkdirSync(fallbackPath, { recursive: true });
|
|
27
|
+
projectPath = fallbackPath;
|
|
28
|
+
console.log(`[CLI] Remapped /home/abc -> ${fallbackPath}`);
|
|
29
|
+
}
|
|
30
|
+
activeSessions.set(sessionId, { projectPath });
|
|
31
|
+
if (foreground) {
|
|
32
|
+
console.log(`💬 Session created: ${sessionId}`);
|
|
33
|
+
console.log(` Project: ${projectPath}`);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
console.log(`Session created: ${sessionId} in project ${projectPath}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (foreground) {
|
|
41
|
+
console.error(`✗ Failed to create session: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
console.error('Failed to create session:', error.message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
socket.on('session:delete', async (data) => {
|
|
49
|
+
try {
|
|
50
|
+
const { sessionId } = data;
|
|
51
|
+
if (foreground) {
|
|
52
|
+
console.log(`🗑️ Deleting session: ${sessionId}`);
|
|
53
|
+
}
|
|
54
|
+
await agentSessionManager.deleteSession(sessionId);
|
|
55
|
+
activeSessions.delete(sessionId);
|
|
56
|
+
if (foreground) {
|
|
57
|
+
console.log(`✓ Session deleted: ${sessionId}`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.log(`Session deleted: ${sessionId}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (foreground) {
|
|
65
|
+
console.error(`✗ Failed to delete session: ${error.message}`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.error('Failed to delete session:', error.message);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
socket.on('project:config:analyze', async (data) => {
|
|
73
|
+
try {
|
|
74
|
+
const { projectId, projectPath, projectName, analysisId } = data;
|
|
75
|
+
if (foreground) {
|
|
76
|
+
console.log(`🔍 Analyzing project: ${projectName} at ${projectPath}`);
|
|
77
|
+
}
|
|
78
|
+
const config = await analyzeProjectWithClaude(projectPath, projectName);
|
|
79
|
+
await saveProjectConfig(projectPath, config);
|
|
80
|
+
for (const app of config.apps) {
|
|
81
|
+
const runnerCode = generateRunnerCode(app, projectPath);
|
|
82
|
+
const runnerFileName = `${app.name}_runner.ts`;
|
|
83
|
+
const runnerPath = path.join(projectPath, app.directory || '', runnerFileName);
|
|
84
|
+
const runnerDir = path.dirname(runnerPath);
|
|
85
|
+
await fs.mkdir(runnerDir, { recursive: true });
|
|
86
|
+
await fs.writeFile(runnerPath, runnerCode, 'utf-8');
|
|
87
|
+
if (foreground) {
|
|
88
|
+
console.log(`✓ Generated runner: ${runnerFileName}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (foreground) {
|
|
92
|
+
console.log(`✓ Analysis complete: Found ${config.apps.length} apps`);
|
|
93
|
+
console.log(`✓ Generated ${config.apps.length} runner files`);
|
|
94
|
+
}
|
|
95
|
+
socket.emit('project:config:analyzed', {
|
|
96
|
+
projectId,
|
|
97
|
+
config,
|
|
98
|
+
analysisId
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (foreground) {
|
|
103
|
+
console.error(`✗ Analysis error: ${error.message}`);
|
|
104
|
+
}
|
|
105
|
+
socket.emit('project:config:analyze:error', {
|
|
106
|
+
projectId: data.projectId,
|
|
107
|
+
error: error.message,
|
|
108
|
+
analysisId: data.analysisId
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
socket.on('session:prompt', async (data) => {
|
|
113
|
+
try {
|
|
114
|
+
const { sessionId, prompt, projectPath: providedProjectPath, promptId, enhancers, model } = data;
|
|
115
|
+
if (!promptId) {
|
|
116
|
+
if (foreground) {
|
|
117
|
+
console.error(`✗ Missing required promptId for session: ${sessionId}`);
|
|
118
|
+
}
|
|
119
|
+
socket.emit('session:error', { sessionId, error: 'Missing required promptId' });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
let projectPath = providedProjectPath || activeSessions.get(sessionId)?.projectPath;
|
|
123
|
+
if (!projectPath) {
|
|
124
|
+
if (foreground) {
|
|
125
|
+
console.error(`✗ Session not found: ${sessionId} (missing projectPath)`);
|
|
126
|
+
}
|
|
127
|
+
socket.emit('session:error', { sessionId, error: 'Session not found or projectPath missing' });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
activeSessions.set(sessionId, { projectPath, currentPromptId: promptId, model });
|
|
131
|
+
const capturedPromptId = promptId;
|
|
132
|
+
if (foreground) {
|
|
133
|
+
console.log(`\n[CLI] 📤 Received prompt for session: ${sessionId}, promptId: ${capturedPromptId}`);
|
|
134
|
+
console.log(`[CLI] Prompt: ${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}`);
|
|
135
|
+
}
|
|
136
|
+
await agentSessionManager.createSession({
|
|
137
|
+
sessionId,
|
|
138
|
+
projectPath,
|
|
139
|
+
});
|
|
140
|
+
await agentSessionManager.sendPrompt(sessionId, prompt, enhancers || [], {
|
|
141
|
+
sessionId,
|
|
142
|
+
projectPath,
|
|
143
|
+
promptId: capturedPromptId,
|
|
144
|
+
model: model,
|
|
145
|
+
attachments: data.attachments,
|
|
146
|
+
onStatusUpdate: (status) => {
|
|
147
|
+
if (!capturedPromptId) {
|
|
148
|
+
console.error(`[CLI] Missing promptId for status update, cannot emit prompt:updated`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
console.log(`[CLI] 📊 Status update: promptId=${capturedPromptId}, status=${status}, sessionId=${sessionId}`);
|
|
152
|
+
getSocket().emit('prompt:updated', {
|
|
153
|
+
promptId: capturedPromptId,
|
|
154
|
+
sessionId,
|
|
155
|
+
text: prompt,
|
|
156
|
+
status,
|
|
157
|
+
createdAt: new Date().toISOString(),
|
|
158
|
+
...(status === 'running' ? { startedAt: new Date().toISOString() } : {}),
|
|
159
|
+
...(status === 'completed' || status === 'error' || status === 'cancelled' ? { completedAt: new Date().toISOString() } : {}),
|
|
160
|
+
messages: []
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
onOutput: (output) => {
|
|
164
|
+
const dataString = typeof output.data === 'string' ? output.data : JSON.stringify(output.data);
|
|
165
|
+
if (!capturedPromptId) {
|
|
166
|
+
console.error(`[CLI] Missing promptId for session ${sessionId}, cannot emit prompt:output`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (foreground && output.type === 'stdout') {
|
|
170
|
+
process.stdout.write(dataString);
|
|
171
|
+
}
|
|
172
|
+
getSocket().emit('prompt:output', {
|
|
173
|
+
sessionId,
|
|
174
|
+
promptId: capturedPromptId,
|
|
175
|
+
type: output.type,
|
|
176
|
+
data: dataString,
|
|
177
|
+
timestamp: output.timestamp,
|
|
178
|
+
metadata: output.metadata
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
onError: (error) => {
|
|
182
|
+
if (foreground) {
|
|
183
|
+
console.error(`\n[CLI] ✗ Session error: ${error}`);
|
|
184
|
+
}
|
|
185
|
+
getSocket().emit('session:error', { sessionId, error });
|
|
186
|
+
},
|
|
187
|
+
onComplete: (exitCode) => {
|
|
188
|
+
if (foreground) {
|
|
189
|
+
console.log(`\n[CLI] ✓ Session completed with exit code: ${exitCode ?? 'null'}`);
|
|
190
|
+
}
|
|
191
|
+
if (!capturedPromptId) {
|
|
192
|
+
console.error(`[CLI] Missing promptId for session ${sessionId}, cannot emit session:result`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
getSocket().emit('session:result', {
|
|
196
|
+
sessionId,
|
|
197
|
+
promptId: capturedPromptId,
|
|
198
|
+
exitCode
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (foreground) {
|
|
205
|
+
console.error(`✗ Error processing prompt: ${error.message}`);
|
|
206
|
+
}
|
|
207
|
+
socket.emit('session:error', { sessionId: data.sessionId, error: error.message });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
socket.on('prompt:cancel', async (data, callback) => {
|
|
211
|
+
try {
|
|
212
|
+
const { promptId, sessionId } = data;
|
|
213
|
+
if (!promptId || !sessionId) {
|
|
214
|
+
callback?.({ success: false, error: 'Missing promptId or sessionId' });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (foreground) {
|
|
218
|
+
console.log(`[CLI] 🛑 Cancelling prompt: ${promptId}`);
|
|
219
|
+
}
|
|
220
|
+
const cancelled = await agentSessionManager.cancelPrompt(promptId, sessionId, (_status) => {
|
|
221
|
+
getSocket().emit('prompt:updated', {
|
|
222
|
+
promptId,
|
|
223
|
+
sessionId,
|
|
224
|
+
text: '',
|
|
225
|
+
status: 'cancelled',
|
|
226
|
+
createdAt: new Date().toISOString(),
|
|
227
|
+
completedAt: new Date().toISOString(),
|
|
228
|
+
messages: []
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
if (cancelled) {
|
|
232
|
+
callback?.({ success: true });
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
callback?.({ success: false, error: 'Prompt not found or already completed' });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
if (foreground) {
|
|
240
|
+
console.error(`✗ Error cancelling prompt: ${error.message}`);
|
|
241
|
+
}
|
|
242
|
+
callback?.({ success: false, error: error.message });
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
socket.on('emergency:stop', async (data, callback) => {
|
|
246
|
+
try {
|
|
247
|
+
const { sessionId } = data;
|
|
248
|
+
if (!sessionId) {
|
|
249
|
+
callback?.({ success: false, message: 'Missing sessionId' });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (foreground) {
|
|
253
|
+
console.log(`[CLI] ☠️ EMERGENCY STOP for session: ${sessionId}`);
|
|
254
|
+
}
|
|
255
|
+
const result = await agentSessionManager.emergencyStop(sessionId);
|
|
256
|
+
getSocket().emit('emergency:stopped', {
|
|
257
|
+
sessionId,
|
|
258
|
+
success: result.success,
|
|
259
|
+
message: result.message,
|
|
260
|
+
timestamp: new Date().toISOString()
|
|
261
|
+
});
|
|
262
|
+
callback?.(result);
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
if (foreground) {
|
|
266
|
+
console.error(`✗ Error during emergency stop: ${error.message}`);
|
|
267
|
+
}
|
|
268
|
+
callback?.({ success: false, message: error.message });
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
package/dist/ttc-cli.tar.gz
CHANGED
|
Binary file
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update & Version Handlers Module
|
|
3
|
+
*
|
|
4
|
+
* Handles update:check, update:start, version:info operations.
|
|
5
|
+
*/
|
|
6
|
+
import fsSync from 'fs';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { createHash } from 'crypto';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
export function registerUpdateHandlers(socket, _foreground, readConfig, requestUpdateFromParent) {
|
|
12
|
+
socket.on('update:check', async (_data, callback) => {
|
|
13
|
+
try {
|
|
14
|
+
const config = await readConfig();
|
|
15
|
+
const currentHash = createHash('sha256').update(fsSync.readFileSync(fileURLToPath(import.meta.url))).digest('hex');
|
|
16
|
+
const response = await fetch(`${config.apiUrl}/update/check`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
hash: currentHash,
|
|
21
|
+
platform: os.platform(),
|
|
22
|
+
arch: os.arch()
|
|
23
|
+
})
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
callback?.({ success: false, error: `HTTP ${response.status}` });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const info = await response.json();
|
|
30
|
+
callback?.({
|
|
31
|
+
success: true,
|
|
32
|
+
updateAvailable: info.updateAvailable,
|
|
33
|
+
version: info.version,
|
|
34
|
+
changelog: info.changelog,
|
|
35
|
+
size: info.size
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
callback?.({ success: false, error: error.message });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
socket.on('update:start', async (_data, callback) => {
|
|
43
|
+
try {
|
|
44
|
+
requestUpdateFromParent();
|
|
45
|
+
callback?.({
|
|
46
|
+
success: true,
|
|
47
|
+
message: 'Update initiated. The CLI will restart automatically when complete.'
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
callback?.({ success: false, error: error.message });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
socket.on('version:info', async (_data, callback) => {
|
|
55
|
+
try {
|
|
56
|
+
const currentHash = createHash('sha256').update(fsSync.readFileSync(fileURLToPath(import.meta.url))).digest('hex');
|
|
57
|
+
let version = 'unknown';
|
|
58
|
+
let date = undefined;
|
|
59
|
+
try {
|
|
60
|
+
const hashesPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'binary-hashes.json');
|
|
61
|
+
const hashes = JSON.parse(fsSync.readFileSync(hashesPath, 'utf-8'));
|
|
62
|
+
if (hashes['js-bundle']) {
|
|
63
|
+
version = hashes['js-bundle'].version || version;
|
|
64
|
+
date = hashes['js-bundle'].date;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
callback?.({
|
|
69
|
+
success: true,
|
|
70
|
+
version,
|
|
71
|
+
hash: currentHash.substring(0, 16) + '...',
|
|
72
|
+
date,
|
|
73
|
+
nodeVersion: process.version,
|
|
74
|
+
platform: os.platform(),
|
|
75
|
+
arch: os.arch()
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
callback?.({ success: false, error: error.message });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
package/dist/updater.js
CHANGED
|
@@ -28,14 +28,11 @@ import { fileURLToPath } from 'url';
|
|
|
28
28
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
29
|
const CONFIG_DIR = path.join(os.homedir(), '.talk-to-code');
|
|
30
30
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
31
|
-
const DEVICE_ID_FILE = path.join(CONFIG_DIR, 'device-id.json');
|
|
32
31
|
// File paths
|
|
33
|
-
const UPDATER_FILE = fileURLToPath(import.meta.url);
|
|
34
32
|
const APP_BUNDLE = path.join(__dirname, 'dist', 'app-child.js');
|
|
35
33
|
const APP_BUNDLE_BACKUP = APP_BUNDLE + '.backup';
|
|
36
34
|
const APP_BUNDLE_OLD = APP_BUNDLE + '.old';
|
|
37
35
|
const BUNDLE_HASHES_FILE = path.join(__dirname, 'binary-hashes.json');
|
|
38
|
-
const UPDATE_LOCK_FILE = path.join(CONFIG_DIR, '.update-lock');
|
|
39
36
|
// State
|
|
40
37
|
let childProcess = null;
|
|
41
38
|
let isUpdating = false;
|
|
@@ -207,7 +204,7 @@ function spawnChild() {
|
|
|
207
204
|
console.log('✓ Recovered, restarting...');
|
|
208
205
|
childProcess = spawnChild();
|
|
209
206
|
})
|
|
210
|
-
.catch((
|
|
207
|
+
.catch((_err) => {
|
|
211
208
|
console.error('❌ Recovery failed, giving up');
|
|
212
209
|
process.exit(1);
|
|
213
210
|
});
|
|
@@ -251,7 +248,7 @@ function setupSignalHandlers() {
|
|
|
251
248
|
}
|
|
252
249
|
process.exit(1);
|
|
253
250
|
});
|
|
254
|
-
process.on('unhandledRejection', (reason,
|
|
251
|
+
process.on('unhandledRejection', (reason, _promise) => {
|
|
255
252
|
console.error('❌ Unhandled rejection in updater:', reason);
|
|
256
253
|
});
|
|
257
254
|
}
|