glad-web 1.0.18 → 1.0.20

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.
@@ -1,12 +1,14 @@
1
1
  const { EventEmitter } = require('events');
2
2
  const path = require('path');
3
3
  const fs = require('fs');
4
+ const os = require('os');
4
5
  const { v4: uuidv4 } = require('uuid');
5
6
  const PTYManager = require('./pty-manager');
6
7
  const TextHistory = require('./text-history');
7
8
  const RenderedHistory = require('./rendered-history');
8
9
  const CircularBuffer = require('./buffer');
9
10
  const { getToolByKey } = require('../ai-tools/registry');
11
+ const ClaudeStructuredSession = require('../claude/structured-session');
10
12
 
11
13
  function previewText(text, maxChars = 320) {
12
14
  if (!text) return '';
@@ -36,8 +38,12 @@ class SessionManager extends EventEmitter {
36
38
  tool: session.tool.displayName,
37
39
  startTime: session.startTime,
38
40
  toolKey: session.tool.key,
39
- workingDirectory: session.ptyManager.workingDir,
40
- hasUnreadCompletion: Boolean(session.hasUnreadCompletion)
41
+ workingDirectory: this.getSessionWorkingDirectory(session),
42
+ mode: session.kind === 'claude-structured' ? 'structured' : 'terminal',
43
+ hasUnreadCompletion: Boolean(session.hasUnreadCompletion),
44
+ timedInputCount: session.timedInputs
45
+ ? Array.from(session.timedInputs.values()).filter(item => item.sendAt > Date.now()).length
46
+ : 0
41
47
  }));
42
48
  }
43
49
 
@@ -49,7 +55,7 @@ class SessionManager extends EventEmitter {
49
55
  return this.sessions.has(id);
50
56
  }
51
57
 
52
- create({ toolKey, workingDirectory, name }) {
58
+ create({ toolKey, workingDirectory, name, claudeOptions }) {
53
59
  this.logger.info(`Creating session: toolKey=${toolKey || ''}, workingDirectory=${workingDirectory || '(default)'}`);
54
60
  const tool = getToolByKey(toolKey);
55
61
  if (!tool) {
@@ -59,6 +65,10 @@ class SessionManager extends EventEmitter {
59
65
  throw err;
60
66
  }
61
67
 
68
+ if (tool.key === 'claude-code') {
69
+ return this.createClaudeStructuredSession({ tool, workingDirectory, name, claudeOptions });
70
+ }
71
+
62
72
  const id = uuidv4();
63
73
  const buffer = new CircularBuffer(500000);
64
74
  const textHistory = new TextHistory({ maxBytes: 20 * 1024 * 1024, debugLabel: id });
@@ -99,6 +109,7 @@ class SessionManager extends EventEmitter {
99
109
  resizeOwner: null,
100
110
  hasConnectedWebClient: false,
101
111
  hasUnreadCompletion: false,
112
+ timedInputs: new Map(),
102
113
  write: data => this.write(id, data),
103
114
  isRunning: () => this.has(id) && ptyManager.isRunning(),
104
115
  kill: () => this.kill(id)
@@ -121,16 +132,86 @@ class SessionManager extends EventEmitter {
121
132
  return session;
122
133
  }
123
134
 
135
+ createClaudeStructuredSession({ tool, workingDirectory, name, claudeOptions = {} }) {
136
+ const sessionDir = workingDirectory && String(workingDirectory).trim()
137
+ ? path.resolve(this.baseDir, String(workingDirectory).trim())
138
+ : this.baseDir;
139
+
140
+ if (!fs.existsSync(sessionDir)) {
141
+ const err = new Error(`Directory does not exist: ${sessionDir}`);
142
+ err.statusCode = 400;
143
+ this.logger.error(`Create Claude session failed: missing directory ${sessionDir}`);
144
+ throw err;
145
+ }
146
+
147
+ const id = uuidv4();
148
+ const session = new ClaudeStructuredSession({
149
+ id,
150
+ tool,
151
+ workingDir: sessionDir,
152
+ name: name || tool.displayName,
153
+ logger: this.logger,
154
+ options: claudeOptions
155
+ });
156
+
157
+ this.sessions.set(id, session);
158
+ session.on('event', event => this.emit('claude-event', { sessionId: id, event, session }));
159
+ session.on('exit', () => this.handleExit(session));
160
+ this.logSessionDiagnostics('claude-session-created', session, {}, { compact: true });
161
+
162
+ return session;
163
+ }
164
+
124
165
  write(id, data) {
125
166
  const session = this.get(id);
126
167
  if (!session) return false;
127
168
  this.markSessionInput(session, data);
169
+ if (session.kind === 'claude-structured') return session.write(data);
128
170
  return session.ptyManager.write(data);
129
171
  }
130
172
 
173
+ sendClaudeInput(id, text) {
174
+ const session = this.get(id);
175
+ if (!session || session.kind !== 'claude-structured') return false;
176
+ this.markSessionInput(session, text);
177
+ return session.sendUserMessage(text);
178
+ }
179
+
180
+ respondClaudePermission(id, permissionId, approved) {
181
+ const session = this.get(id);
182
+ if (!session || session.kind !== 'claude-structured') return false;
183
+ return session.respondPermission(permissionId, approved);
184
+ }
185
+
186
+ updateClaudeSettings(id, settings) {
187
+ const session = this.get(id);
188
+ if (!session || session.kind !== 'claude-structured') return null;
189
+ return session.updateSettings(settings || {});
190
+ }
191
+
192
+ abortClaude(id) {
193
+ const session = this.get(id);
194
+ if (!session || session.kind !== 'claude-structured') return false;
195
+ return session.abort('Aborted by user');
196
+ }
197
+
198
+ resumeClaude(id, resumeSessionId) {
199
+ const session = this.get(id);
200
+ if (!session || session.kind !== 'claude-structured') return false;
201
+ const historyMessages = this.readClaudeTranscriptMessages(this.getSessionWorkingDirectory(session), resumeSessionId);
202
+ return session.selectResumeSession(resumeSessionId, historyMessages);
203
+ }
204
+
205
+ listClaudeResumeSessions(id) {
206
+ const session = this.get(id);
207
+ if (!session || session.kind !== 'claude-structured') return null;
208
+ return this.scanClaudeProjectSessions(this.getSessionWorkingDirectory(session));
209
+ }
210
+
131
211
  resize(id, cols, rows) {
132
212
  const session = this.get(id);
133
213
  if (!session) return false;
214
+ if (session.kind === 'claude-structured') return true;
134
215
  if (session.renderedHistory) {
135
216
  session.renderedHistory.resize(cols, rows);
136
217
  }
@@ -141,6 +222,7 @@ class SessionManager extends EventEmitter {
141
222
  redraw(id, cols, rows) {
142
223
  const session = this.get(id);
143
224
  if (!session) return false;
225
+ if (session.kind === 'claude-structured') return true;
144
226
  if (session.renderedHistory) {
145
227
  session.renderedHistory.resize(cols, rows);
146
228
  }
@@ -158,6 +240,11 @@ class SessionManager extends EventEmitter {
158
240
  markCompletionRead(id) {
159
241
  const session = this.get(id);
160
242
  if (!session) return null;
243
+ if (session.kind === 'claude-structured') {
244
+ session.markCompletionRead();
245
+ this.logSessionDiagnostics('completion-read', session, {}, { compact: true });
246
+ return session;
247
+ }
161
248
  session.hasUnreadCompletion = false;
162
249
  session.awaitingCompletion = false;
163
250
  session.isThinking = false;
@@ -167,12 +254,127 @@ class SessionManager extends EventEmitter {
167
254
  return session;
168
255
  }
169
256
 
257
+ listTimedInputs(id) {
258
+ const session = this.get(id);
259
+ if (!session) return null;
260
+ return Array.from(session.timedInputs.values()).map(item => ({
261
+ id: item.id,
262
+ text: item.text,
263
+ sendAt: item.sendAt,
264
+ createdAt: item.createdAt
265
+ })).sort((a, b) => a.sendAt - b.sendAt);
266
+ }
267
+
268
+ scheduleTimedInput(id, input = {}) {
269
+ const session = this.get(id);
270
+ if (!session) return null;
271
+
272
+ const { text, sendAt, delay } = this.validateTimedInput(input);
273
+
274
+ const item = {
275
+ id: uuidv4(),
276
+ text,
277
+ sendAt,
278
+ createdAt: Date.now(),
279
+ timer: null
280
+ };
281
+
282
+ item.timer = setTimeout(() => {
283
+ this.executeTimedInput(session.id, item.id);
284
+ }, delay);
285
+ session.timedInputs.set(item.id, item);
286
+ return {
287
+ id: item.id,
288
+ text: item.text,
289
+ sendAt: item.sendAt,
290
+ createdAt: item.createdAt
291
+ };
292
+ }
293
+
294
+ updateTimedInput(id, inputId, input = {}) {
295
+ const session = this.get(id);
296
+ if (!session) return null;
297
+ const item = session.timedInputs.get(inputId);
298
+ if (!item) return false;
299
+
300
+ const { text, sendAt, delay } = this.validateTimedInput(input);
301
+ clearTimeout(item.timer);
302
+ item.text = text;
303
+ item.sendAt = sendAt;
304
+ item.updatedAt = Date.now();
305
+ item.timer = setTimeout(() => {
306
+ this.executeTimedInput(session.id, item.id);
307
+ }, delay);
308
+
309
+ return {
310
+ id: item.id,
311
+ text: item.text,
312
+ sendAt: item.sendAt,
313
+ createdAt: item.createdAt,
314
+ updatedAt: item.updatedAt
315
+ };
316
+ }
317
+
318
+ validateTimedInput(input = {}) {
319
+ const text = String(input.text || '');
320
+ const sendAt = Number(input.sendAt);
321
+ if (!text.trim()) {
322
+ const err = new Error('Text is required');
323
+ err.statusCode = 400;
324
+ throw err;
325
+ }
326
+ if (!Number.isFinite(sendAt) || sendAt <= Date.now()) {
327
+ const err = new Error('Send time must be in the future');
328
+ err.statusCode = 400;
329
+ throw err;
330
+ }
331
+
332
+ const maxDelay = 30 * 24 * 60 * 60 * 1000;
333
+ const delay = sendAt - Date.now();
334
+ if (delay > maxDelay) {
335
+ const err = new Error('Send time must be within 30 days');
336
+ err.statusCode = 400;
337
+ throw err;
338
+ }
339
+
340
+ return { text, sendAt, delay };
341
+ }
342
+
343
+ cancelTimedInput(id, inputId) {
344
+ const session = this.get(id);
345
+ if (!session) return null;
346
+ const item = session.timedInputs.get(inputId);
347
+ if (!item) return false;
348
+ clearTimeout(item.timer);
349
+ session.timedInputs.delete(inputId);
350
+ return true;
351
+ }
352
+
353
+ executeTimedInput(id, inputId) {
354
+ const session = this.get(id);
355
+ if (!session) return false;
356
+ const item = session.timedInputs.get(inputId);
357
+ if (!item) return false;
358
+ session.timedInputs.delete(inputId);
359
+ const formatted = item.text.replace(/\n/g, '\r');
360
+ this.write(id, formatted);
361
+ setTimeout(() => {
362
+ if (this.has(id)) this.write(id, '\r');
363
+ }, 1000);
364
+ return true;
365
+ }
366
+
170
367
  kill(id) {
171
368
  const session = this.get(id);
172
369
  if (!session) return false;
173
370
  clearTimeout(session.completionTimer);
371
+ this.clearTimedInputs(session);
174
372
  this.logSessionDiagnostics('session-deleted', session, {}, { compact: true });
175
373
  this.disposeSessionHistory(session);
374
+ if (session.kind === 'claude-structured') {
375
+ session.ptyManager.kill();
376
+ return true;
377
+ }
176
378
  session.ptyManager.kill();
177
379
  this.sessions.delete(id);
178
380
  this.emit('exit', { sessionId: id, session });
@@ -182,6 +384,7 @@ class SessionManager extends EventEmitter {
182
384
  killAll() {
183
385
  for (const session of this.sessions.values()) {
184
386
  clearTimeout(session.completionTimer);
387
+ this.clearTimedInputs(session);
185
388
  this.disposeSessionHistory(session);
186
389
  session.ptyManager.kill();
187
390
  }
@@ -191,6 +394,7 @@ class SessionManager extends EventEmitter {
191
394
  getHistory(id) {
192
395
  const session = this.get(id);
193
396
  if (!session) return null;
397
+ if (session.kind === 'claude-structured') return session.getHistory();
194
398
  const historySource = session.renderedHistory || session.textHistory;
195
399
  return {
196
400
  success: true,
@@ -205,6 +409,7 @@ class SessionManager extends EventEmitter {
205
409
  getCatchupOutput(id) {
206
410
  const session = this.get(id);
207
411
  if (!session) return null;
412
+ if (session.kind === 'claude-structured') return session.getCatchupOutput();
208
413
 
209
414
  const bufferHistory = session.buffer.getAfter(0);
210
415
  if (bufferHistory.length > 0) {
@@ -234,6 +439,201 @@ class SessionManager extends EventEmitter {
234
439
  return session ? this.getSessionDiagnostics(session) : null;
235
440
  }
236
441
 
442
+ getClaudeSnapshot(id) {
443
+ const session = this.get(id);
444
+ if (!session || session.kind !== 'claude-structured') return null;
445
+ return session.snapshot();
446
+ }
447
+
448
+ scanClaudeProjectSessions(workingDirectory) {
449
+ const projectDir = this.getClaudeProjectDir(workingDirectory);
450
+ if (!projectDir || !fs.existsSync(projectDir)) return [];
451
+ const files = fs.readdirSync(projectDir)
452
+ .filter(file => /^[0-9a-f-]{36}\.jsonl$/i.test(file))
453
+ .map(file => {
454
+ const fullPath = path.join(projectDir, file);
455
+ const stat = fs.statSync(fullPath);
456
+ return {
457
+ id: file.replace(/\.jsonl$/i, ''),
458
+ path: fullPath,
459
+ mtimeMs: stat.mtimeMs,
460
+ size: stat.size
461
+ };
462
+ })
463
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
464
+ .slice(0, 40);
465
+
466
+ return files.map(file => this.describeClaudeSessionFile(file));
467
+ }
468
+
469
+ getClaudeProjectDir(workingDirectory) {
470
+ const cwd = path.resolve(workingDirectory || this.baseDir);
471
+ const encoded = cwd.replace(/[^a-zA-Z0-9]/g, '-');
472
+ return path.join(os.homedir(), '.claude', 'projects', encoded);
473
+ }
474
+
475
+ describeClaudeSessionFile(file) {
476
+ let cwd = '';
477
+ let firstText = '';
478
+ let lastText = '';
479
+ try {
480
+ const content = fs.readFileSync(file.path, 'utf8');
481
+ const lines = content.split('\n').filter(Boolean);
482
+ for (const line of lines) {
483
+ const parsed = this.parseClaudeJsonLine(line);
484
+ if (!parsed) continue;
485
+ if (!cwd && typeof parsed.cwd === 'string') cwd = parsed.cwd;
486
+ const text = this.extractClaudeTranscriptText(parsed);
487
+ if (!text) continue;
488
+ if (!firstText) firstText = text;
489
+ lastText = text;
490
+ }
491
+ } catch (error) {
492
+ this.logger.debugInfo?.(`[claude-resume] Failed to read ${file.path}: ${error.message}`);
493
+ }
494
+ return {
495
+ id: file.id,
496
+ cwd,
497
+ updatedAt: file.mtimeMs,
498
+ size: file.size,
499
+ firstText: firstText ? previewText(firstText, 120) : '',
500
+ lastText: lastText ? previewText(lastText, 160) : ''
501
+ };
502
+ }
503
+
504
+ readClaudeTranscriptMessages(workingDirectory, resumeSessionId) {
505
+ const id = String(resumeSessionId || '').trim();
506
+ if (!/^[0-9a-f-]{36}$/i.test(id)) return [];
507
+ const filePath = path.join(this.getClaudeProjectDir(workingDirectory), `${id}.jsonl`);
508
+ if (!fs.existsSync(filePath)) return [];
509
+
510
+ const messages = [];
511
+ try {
512
+ const lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
513
+ for (const line of lines) {
514
+ const record = this.parseClaudeJsonLine(line);
515
+ if (!record || record.isSidechain) continue;
516
+ messages.push(...this.mapClaudeTranscriptRecord(record));
517
+ }
518
+ } catch (error) {
519
+ this.logger.debugInfo?.(`[claude-resume] Failed to backfill ${filePath}: ${error.message}`);
520
+ return [];
521
+ }
522
+
523
+ const maxMessages = 1000;
524
+ const visible = messages.filter(Boolean).slice(-maxMessages);
525
+ if (messages.length > maxMessages) {
526
+ visible.unshift({
527
+ id: uuidv4(),
528
+ kind: 'event',
529
+ level: 'info',
530
+ text: `Showing the latest ${maxMessages} resumed transcript items.`,
531
+ createdAt: Date.now()
532
+ });
533
+ }
534
+ return visible;
535
+ }
536
+
537
+ mapClaudeTranscriptRecord(record) {
538
+ const createdAt = Number.isFinite(Date.parse(record.timestamp)) ? Date.parse(record.timestamp) : Date.now();
539
+ const message = record.message || {};
540
+ const content = message.content;
541
+ if (record.type === 'user') {
542
+ if (typeof content === 'string') {
543
+ const text = content.trim();
544
+ return text ? [{ id: uuidv4(), kind: 'user', text, createdAt }] : [];
545
+ }
546
+ if (Array.isArray(content)) {
547
+ return content.flatMap(item => {
548
+ if (!item || typeof item !== 'object') return [];
549
+ if (item.type === 'tool_result') {
550
+ const text = this.textFromClaudeContent(item.content).trim();
551
+ return text ? [{
552
+ id: uuidv4(),
553
+ kind: 'tool-result',
554
+ toolUseId: item.tool_use_id,
555
+ text,
556
+ isError: Boolean(item.is_error),
557
+ createdAt
558
+ }] : [];
559
+ }
560
+ if (item.type === 'text' && typeof item.text === 'string' && item.text.trim()) {
561
+ return [{ id: uuidv4(), kind: 'user', text: item.text.trim(), createdAt }];
562
+ }
563
+ return [];
564
+ });
565
+ }
566
+ }
567
+
568
+ if (record.type === 'assistant' && Array.isArray(content)) {
569
+ const mapped = [];
570
+ const text = this.textFromClaudeContent(content).trim();
571
+ if (text) mapped.push({ id: uuidv4(), kind: 'assistant', text, createdAt });
572
+ for (const item of content) {
573
+ if (!item || item.type !== 'tool_use') continue;
574
+ mapped.push({
575
+ id: uuidv4(),
576
+ kind: 'tool',
577
+ name: item.name || 'tool',
578
+ summary: this.summarizeClaudeToolInput(item.input),
579
+ input: item.input,
580
+ toolUseId: item.id,
581
+ createdAt
582
+ });
583
+ }
584
+ return mapped;
585
+ }
586
+
587
+ if (record.type === 'summary' && typeof record.summary === 'string' && record.summary.trim()) {
588
+ return [{ id: uuidv4(), kind: 'event', level: 'info', text: `Summary: ${record.summary.trim()}`, createdAt }];
589
+ }
590
+
591
+ return [];
592
+ }
593
+
594
+ textFromClaudeContent(content) {
595
+ if (typeof content === 'string') return content;
596
+ if (!Array.isArray(content)) return '';
597
+ return content.map(item => {
598
+ if (!item || typeof item !== 'object') return '';
599
+ if (item.type === 'text' && typeof item.text === 'string') return item.text;
600
+ if (item.type === 'tool_result') return this.textFromClaudeContent(item.content);
601
+ return '';
602
+ }).filter(Boolean).join('\n');
603
+ }
604
+
605
+ summarizeClaudeToolInput(input) {
606
+ if (!input || typeof input !== 'object') return '';
607
+ if (typeof input.command === 'string') return input.command;
608
+ if (typeof input.file_path === 'string') return input.file_path;
609
+ if (typeof input.path === 'string') return input.path;
610
+ const serialized = JSON.stringify(input);
611
+ return serialized.length > 240 ? serialized.slice(0, 240) + '...' : serialized;
612
+ }
613
+
614
+ parseClaudeJsonLine(line) {
615
+ try {
616
+ return JSON.parse(line);
617
+ } catch (_) {
618
+ return null;
619
+ }
620
+ }
621
+
622
+ extractClaudeTranscriptText(record) {
623
+ const message = record && record.message;
624
+ const content = message && message.content;
625
+ if (typeof content === 'string') return content.trim();
626
+ if (Array.isArray(content)) {
627
+ return content.map(item => {
628
+ if (!item || typeof item !== 'object') return '';
629
+ if (item.type === 'text' && typeof item.text === 'string') return item.text;
630
+ if (item.type === 'tool_use') return `[tool] ${item.name || 'tool'}`;
631
+ return '';
632
+ }).filter(Boolean).join('\n').trim();
633
+ }
634
+ return '';
635
+ }
636
+
237
637
  logHistoryRequest(id, req) {
238
638
  const session = this.get(id);
239
639
  if (!session) return;
@@ -332,6 +732,7 @@ class SessionManager extends EventEmitter {
332
732
  if (!this.sessions.has(session.id)) return;
333
733
  this.logger.info(`Session ${session.id} (${session.name}) exited.`);
334
734
  clearTimeout(session.completionTimer);
735
+ this.clearTimedInputs(session);
335
736
  this.disposeSessionHistory(session);
336
737
  this.sessions.delete(session.id);
337
738
  this.emit('exit', { sessionId: session.id, session });
@@ -340,6 +741,14 @@ class SessionManager extends EventEmitter {
340
741
  markSessionInput(session, data) {
341
742
  if (typeof data !== 'string' || data.length === 0) return;
342
743
  session.inputSeq = (session.inputSeq || 0) + 1;
744
+ if (session.kind === 'claude-structured') {
745
+ session.hasUnreadCompletion = false;
746
+ this.logSessionDiagnostics('session-input', session, {
747
+ inputSeq: session.inputSeq,
748
+ inputPreview: previewText(data)
749
+ }, { compact: true });
750
+ return;
751
+ }
343
752
  session.awaitingCompletion = true;
344
753
  session.isThinking = false;
345
754
  session.hasUnreadCompletion = false;
@@ -351,13 +760,37 @@ class SessionManager extends EventEmitter {
351
760
  }
352
761
 
353
762
  disposeSessionHistory(session) {
763
+ if (session.kind === 'claude-structured') return;
354
764
  if (session.renderedHistory) {
355
765
  session.renderedHistory.dispose();
356
766
  session.renderedHistory = null;
357
767
  }
358
768
  }
359
769
 
770
+ clearTimedInputs(session) {
771
+ if (!session || !session.timedInputs) return;
772
+ for (const item of session.timedInputs.values()) {
773
+ clearTimeout(item.timer);
774
+ }
775
+ session.timedInputs.clear();
776
+ }
777
+
360
778
  getSessionDiagnostics(session, extra = {}) {
779
+ if (session.kind === 'claude-structured') {
780
+ return {
781
+ sessionId: session.id,
782
+ sessionName: session.name,
783
+ toolKey: session.tool.key,
784
+ kind: session.kind,
785
+ historyMode: 'structured',
786
+ status: session.status,
787
+ workingDirectory: this.getSessionWorkingDirectory(session),
788
+ messages: session.messages.length,
789
+ pendingPermissions: session.pendingPermissions.size,
790
+ timedInputCount: session.timedInputs ? session.timedInputs.size : 0,
791
+ ...extra
792
+ };
793
+ }
361
794
  return {
362
795
  sessionId: session.id,
363
796
  sessionName: session.name,
@@ -372,6 +805,9 @@ class SessionManager extends EventEmitter {
372
805
  }
373
806
 
374
807
  getCompactSessionDiagnostics(session, extra = {}) {
808
+ if (session.kind === 'claude-structured') {
809
+ return this.getSessionDiagnostics(session, extra);
810
+ }
375
811
  const buffer = session.buffer.getDebugSnapshot();
376
812
  const textHistory = session.textHistory.getDebugSnapshot();
377
813
  const renderedHistory = session.renderedHistory ? session.renderedHistory.getDebugSnapshot() : null;
@@ -425,6 +861,10 @@ class SessionManager extends EventEmitter {
425
861
  : this.getSessionDiagnostics(session, extra);
426
862
  this.logger.debugInfo(`[history-debug] ${reason} ${JSON.stringify(payload)}`);
427
863
  }
864
+
865
+ getSessionWorkingDirectory(session) {
866
+ return session.workingDir || (session.ptyManager && session.ptyManager.workingDir) || this.baseDir;
867
+ }
428
868
  }
429
869
 
430
870
  module.exports = SessionManager;