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,884 +0,0 @@
|
|
|
1
|
-
const { EventEmitter } = require('events');
|
|
2
|
-
const crypto = require('crypto');
|
|
3
|
-
const { normalizeEffort, normalizeModel, resolveClaudeModel } = require('./config');
|
|
4
|
-
const { parseClaudeContextOutput, parseClaudeUsageOutput } = require('./cli-usage');
|
|
5
|
-
|
|
6
|
-
const PERMISSION_MODES = new Set(['default', 'acceptEdits', 'bypassPermissions', 'plan']);
|
|
7
|
-
const EDIT_TOOLS = new Set(['Edit', 'MultiEdit', 'Write', 'NotebookEdit']);
|
|
8
|
-
const EXIT_PLAN_TOOLS = new Set(['exit_plan_mode', 'ExitPlanMode']);
|
|
9
|
-
const DENY_PERMISSION_MESSAGE = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
10
|
-
const LOCAL_COMMAND_TIMEOUT_MS = 30_000;
|
|
11
|
-
|
|
12
|
-
function normalizePermissionMode(value) {
|
|
13
|
-
const mode = String(value || 'default');
|
|
14
|
-
return PERMISSION_MODES.has(mode) ? mode : 'default';
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function textFromContent(content) {
|
|
18
|
-
if (typeof content === 'string') return content;
|
|
19
|
-
if (!Array.isArray(content)) return '';
|
|
20
|
-
return content
|
|
21
|
-
.map(item => {
|
|
22
|
-
if (!item || typeof item !== 'object') return '';
|
|
23
|
-
if (item.type === 'text' && typeof item.text === 'string') return item.text;
|
|
24
|
-
if (item.type === 'tool_result') {
|
|
25
|
-
if (typeof item.content === 'string') return item.content;
|
|
26
|
-
if (Array.isArray(item.content)) return textFromContent(item.content);
|
|
27
|
-
}
|
|
28
|
-
return '';
|
|
29
|
-
})
|
|
30
|
-
.filter(Boolean)
|
|
31
|
-
.join('\n');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
class AsyncMessageQueue {
|
|
35
|
-
constructor() {
|
|
36
|
-
this.items = [];
|
|
37
|
-
this.waiters = [];
|
|
38
|
-
this.closed = false;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
push(item) {
|
|
42
|
-
if (this.closed) return false;
|
|
43
|
-
const waiter = this.waiters.shift();
|
|
44
|
-
if (waiter) waiter({ value: item, done: false });
|
|
45
|
-
else this.items.push(item);
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
close() {
|
|
50
|
-
this.closed = true;
|
|
51
|
-
while (this.waiters.length > 0) {
|
|
52
|
-
this.waiters.shift()({ value: undefined, done: true });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
[Symbol.asyncIterator]() {
|
|
57
|
-
return {
|
|
58
|
-
next: () => {
|
|
59
|
-
if (this.items.length > 0) {
|
|
60
|
-
return Promise.resolve({ value: this.items.shift(), done: false });
|
|
61
|
-
}
|
|
62
|
-
if (this.closed) return Promise.resolve({ value: undefined, done: true });
|
|
63
|
-
return new Promise(resolve => this.waiters.push(resolve));
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
class ClaudeStructuredSession extends EventEmitter {
|
|
70
|
-
constructor({
|
|
71
|
-
id,
|
|
72
|
-
tool,
|
|
73
|
-
workingDir,
|
|
74
|
-
name,
|
|
75
|
-
logger,
|
|
76
|
-
options = {},
|
|
77
|
-
localCommandRunner = null
|
|
78
|
-
}) {
|
|
79
|
-
super();
|
|
80
|
-
this.id = id;
|
|
81
|
-
this.tool = tool;
|
|
82
|
-
this.name = name || tool.displayName;
|
|
83
|
-
this.workingDir = workingDir;
|
|
84
|
-
this.logger = logger || console;
|
|
85
|
-
this.kind = 'claude-structured';
|
|
86
|
-
this.startTime = Date.now();
|
|
87
|
-
this.running = true;
|
|
88
|
-
this.status = 'idle';
|
|
89
|
-
this.messages = [];
|
|
90
|
-
this.pendingPermissions = new Map();
|
|
91
|
-
this.pendingInput = '';
|
|
92
|
-
this.inputSeq = 0;
|
|
93
|
-
this.hasUnreadCompletion = false;
|
|
94
|
-
this.completionReadInputSeq = 0;
|
|
95
|
-
this.timedInputs = new Map();
|
|
96
|
-
this.abortController = null;
|
|
97
|
-
this.inputQueue = null;
|
|
98
|
-
this.query = null;
|
|
99
|
-
this.runnerStarted = false;
|
|
100
|
-
this.runnerReadyPromise = null;
|
|
101
|
-
this.runnerInitializationAnnounced = false;
|
|
102
|
-
this.abortRequested = false;
|
|
103
|
-
this.localCommandRunner = localCommandRunner;
|
|
104
|
-
this.localCommandChain = Promise.resolve();
|
|
105
|
-
this.pendingLocalCommand = null;
|
|
106
|
-
this.permissionMode = normalizePermissionMode(options.permissionMode);
|
|
107
|
-
this.model = normalizeModel(options.model);
|
|
108
|
-
this.effort = normalizeEffort(options.effort);
|
|
109
|
-
this.allowedTools = new Set();
|
|
110
|
-
this.allowedBashLiterals = new Set();
|
|
111
|
-
this.allowedBashPrefixes = new Set();
|
|
112
|
-
this.resumeSessionId = options.resume || null;
|
|
113
|
-
this.claudeSessionId = options.resume || null;
|
|
114
|
-
this.activeOptionSignature = null;
|
|
115
|
-
this.turnQueue = [];
|
|
116
|
-
|
|
117
|
-
// Compatibility with existing session-scoped Git/file APIs.
|
|
118
|
-
this.ptyManager = {
|
|
119
|
-
workingDir,
|
|
120
|
-
isRunning: () => this.isRunning(),
|
|
121
|
-
write: data => this.write(data),
|
|
122
|
-
kill: () => this.kill(),
|
|
123
|
-
resize: () => {},
|
|
124
|
-
redraw: () => false
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
toListItem() {
|
|
129
|
-
return {
|
|
130
|
-
id: this.id,
|
|
131
|
-
name: this.name,
|
|
132
|
-
tool: this.tool.displayName,
|
|
133
|
-
startTime: this.startTime,
|
|
134
|
-
toolKey: this.tool.key,
|
|
135
|
-
workingDirectory: this.workingDir,
|
|
136
|
-
hasUnreadCompletion: Boolean(this.hasUnreadCompletion),
|
|
137
|
-
timedInputCount: Array.from(this.timedInputs.values()).filter(item => item.sendAt > Date.now()).length,
|
|
138
|
-
mode: 'structured'
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
snapshot() {
|
|
143
|
-
return {
|
|
144
|
-
id: this.id,
|
|
145
|
-
name: this.name,
|
|
146
|
-
tool: this.tool.displayName,
|
|
147
|
-
toolKey: this.tool.key,
|
|
148
|
-
status: this.status,
|
|
149
|
-
state: this.getControlState(),
|
|
150
|
-
messages: this.messages,
|
|
151
|
-
pendingPermissions: Array.from(this.pendingPermissions.values()).map(item => item.public)
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
getControlState() {
|
|
156
|
-
return {
|
|
157
|
-
permissionMode: this.permissionMode,
|
|
158
|
-
model: this.model,
|
|
159
|
-
effort: this.effort,
|
|
160
|
-
status: this.status,
|
|
161
|
-
claudeSessionId: this.claudeSessionId || null,
|
|
162
|
-
resumeSessionId: this.resumeSessionId || null,
|
|
163
|
-
canAbort: this.status === 'thinking',
|
|
164
|
-
pendingPermissionCount: this.pendingPermissions.size
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
getHistory() {
|
|
169
|
-
const lines = [];
|
|
170
|
-
for (const message of this.messages) {
|
|
171
|
-
if (message.kind === 'user') lines.push(`User: ${message.text}`);
|
|
172
|
-
if (message.kind === 'assistant') lines.push(`Claude: ${message.text}`);
|
|
173
|
-
if (message.kind === 'tool') lines.push(`Tool ${message.name}: ${message.summary}`);
|
|
174
|
-
if (message.kind === 'event') lines.push(message.text);
|
|
175
|
-
}
|
|
176
|
-
const text = lines.join('\n\n');
|
|
177
|
-
return {
|
|
178
|
-
success: true,
|
|
179
|
-
sessionId: this.id,
|
|
180
|
-
sessionName: this.name,
|
|
181
|
-
tool: this.tool.displayName,
|
|
182
|
-
historyMode: 'structured',
|
|
183
|
-
text,
|
|
184
|
-
updatedAt: Date.now(),
|
|
185
|
-
truncated: false,
|
|
186
|
-
bytes: Buffer.byteLength(text, 'utf8'),
|
|
187
|
-
lines: text ? text.split('\n').length : 0
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
getCatchupOutput() {
|
|
192
|
-
return {
|
|
193
|
-
source: 'claude-structured',
|
|
194
|
-
items: this.messages.length,
|
|
195
|
-
data: ''
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
write(data) {
|
|
200
|
-
if (!this.running) return false;
|
|
201
|
-
const text = String(data || '');
|
|
202
|
-
if (!text) return true;
|
|
203
|
-
|
|
204
|
-
this.pendingInput += text.replace(/\r/g, '\n');
|
|
205
|
-
if (!this.pendingInput.includes('\n')) return true;
|
|
206
|
-
|
|
207
|
-
const parts = this.pendingInput.split('\n');
|
|
208
|
-
this.pendingInput = parts.pop() || '';
|
|
209
|
-
const prompt = parts.join('\n').trim();
|
|
210
|
-
if (prompt) this.sendUserMessage(prompt);
|
|
211
|
-
return true;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
sendUserMessage(text, attachments = [], options = {}) {
|
|
215
|
-
if (!this.running) return false;
|
|
216
|
-
const prompt = String(text || '').trim();
|
|
217
|
-
const agentPrompt = String(options.agentText ?? prompt).trim();
|
|
218
|
-
const images = Array.isArray(attachments) ? attachments.filter(item => item?.data && item?.mediaType) : [];
|
|
219
|
-
const displayAttachments = Array.isArray(options.displayAttachments) ? options.displayAttachments : [];
|
|
220
|
-
if (!agentPrompt && images.length === 0) return false;
|
|
221
|
-
this.hasUnreadCompletion = false;
|
|
222
|
-
const turn = this.beginTurn(prompt, [
|
|
223
|
-
...images.map(item => ({ name: item.name, size: item.size })),
|
|
224
|
-
...displayAttachments
|
|
225
|
-
]);
|
|
226
|
-
this.setStatus('thinking');
|
|
227
|
-
|
|
228
|
-
const content = images.length > 0 ? [
|
|
229
|
-
...(agentPrompt ? [{ type: 'text', text: agentPrompt }] : []),
|
|
230
|
-
...images.map(item => ({
|
|
231
|
-
type: 'image',
|
|
232
|
-
source: { type: 'base64', media_type: item.mediaType, data: item.data }
|
|
233
|
-
}))
|
|
234
|
-
] : agentPrompt;
|
|
235
|
-
|
|
236
|
-
const sdkMessage = {
|
|
237
|
-
type: 'user',
|
|
238
|
-
parent_tool_use_id: null,
|
|
239
|
-
message: {
|
|
240
|
-
role: 'user',
|
|
241
|
-
content
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
if (!this.runnerStarted) {
|
|
246
|
-
this.startRunner(sdkMessage);
|
|
247
|
-
} else if (this.inputQueue) {
|
|
248
|
-
this.inputQueue.push(sdkMessage);
|
|
249
|
-
}
|
|
250
|
-
return true;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
beginTurn(prompt, attachments = []) {
|
|
254
|
-
const turn = { id: crypto.randomUUID(), startedAt: Date.now() };
|
|
255
|
-
this.turnQueue.push(turn);
|
|
256
|
-
this.appendMessage({ kind: 'turn-start', turnId: turn.id, createdAt: turn.startedAt });
|
|
257
|
-
this.appendMessage({ kind: 'user', text: prompt, attachments, turnId: turn.id, createdAt: turn.startedAt });
|
|
258
|
-
return turn;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
currentTurn() {
|
|
262
|
-
return this.turnQueue[0] || null;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
completeCurrentTurn(status = 'completed', reportedDurationMs = null) {
|
|
266
|
-
const turn = this.turnQueue.shift();
|
|
267
|
-
if (!turn) return null;
|
|
268
|
-
const completedAt = Date.now();
|
|
269
|
-
return this.appendMessage({
|
|
270
|
-
kind: 'turn-end',
|
|
271
|
-
turnId: turn.id,
|
|
272
|
-
turnStatus: status,
|
|
273
|
-
durationMs: Number.isFinite(Number(reportedDurationMs))
|
|
274
|
-
? Math.max(0, Number(reportedDurationMs))
|
|
275
|
-
: Math.max(0, completedAt - turn.startedAt),
|
|
276
|
-
createdAt: completedAt
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
sealTurns(status = 'cancelled') {
|
|
281
|
-
while (this.turnQueue.length > 0) this.completeCurrentTurn(status);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
createMessageItem(message) {
|
|
285
|
-
return {
|
|
286
|
-
id: crypto.randomUUID(),
|
|
287
|
-
createdAt: Date.now(),
|
|
288
|
-
...message
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
updateSettings(settings = {}) {
|
|
293
|
-
const next = {
|
|
294
|
-
permissionMode: settings.permissionMode === undefined ? this.permissionMode : normalizePermissionMode(settings.permissionMode),
|
|
295
|
-
model: settings.model === undefined ? this.model : normalizeModel(settings.model),
|
|
296
|
-
effort: settings.effort === undefined ? this.effort : normalizeEffort(settings.effort)
|
|
297
|
-
};
|
|
298
|
-
const changed = next.permissionMode !== this.permissionMode
|
|
299
|
-
|| next.model !== this.model
|
|
300
|
-
|| next.effort !== this.effort;
|
|
301
|
-
this.permissionMode = next.permissionMode;
|
|
302
|
-
this.model = next.model;
|
|
303
|
-
this.effort = next.effort;
|
|
304
|
-
|
|
305
|
-
if (changed) {
|
|
306
|
-
if (this.query && typeof this.query.setPermissionMode === 'function') {
|
|
307
|
-
Promise.resolve(this.query.setPermissionMode(this.getSdkPermissionMode())).catch(error => {
|
|
308
|
-
this.logger.debugInfo?.(`[claude-structured] setPermissionMode failed: ${error.message}`);
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
if (this.runnerStarted && this.status === 'idle' && this.activeOptionSignature !== this.getOptionSignature()) {
|
|
312
|
-
this.resetRunnerForNextTurn();
|
|
313
|
-
}
|
|
314
|
-
this.emitEvent({ type: 'state', state: this.getControlState() });
|
|
315
|
-
}
|
|
316
|
-
return this.getControlState();
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
selectResumeSession(resumeSessionId, historyMessages = null) {
|
|
320
|
-
const id = String(resumeSessionId || '').trim();
|
|
321
|
-
if (!id) return false;
|
|
322
|
-
if (this.status === 'thinking') this.abort('Switching resume target');
|
|
323
|
-
this.resumeSessionId = id;
|
|
324
|
-
this.claudeSessionId = id;
|
|
325
|
-
this.resetRunnerForNextTurn();
|
|
326
|
-
const eventMessage = this.createMessageItem({
|
|
327
|
-
kind: 'event',
|
|
328
|
-
level: 'info',
|
|
329
|
-
text: `Resume target selected: ${id}`
|
|
330
|
-
});
|
|
331
|
-
if (Array.isArray(historyMessages)) {
|
|
332
|
-
this.messages = [...historyMessages, eventMessage];
|
|
333
|
-
this.emitEvent({ type: 'history-reset', messages: this.messages });
|
|
334
|
-
} else {
|
|
335
|
-
this.messages.push(eventMessage);
|
|
336
|
-
this.emitEvent({ type: 'message', message: eventMessage });
|
|
337
|
-
}
|
|
338
|
-
this.emitEvent({ type: 'state', state: this.getControlState() });
|
|
339
|
-
return true;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
abort(reason = 'Aborted by user') {
|
|
343
|
-
if (!this.runnerStarted && this.status !== 'thinking') return false;
|
|
344
|
-
this.abortRequested = true;
|
|
345
|
-
this.inputQueue?.close();
|
|
346
|
-
this.query?.close?.();
|
|
347
|
-
this.abortController?.abort();
|
|
348
|
-
this.rejectPendingLocalCommand(new Error(reason));
|
|
349
|
-
this.runnerStarted = false;
|
|
350
|
-
this.runnerReadyPromise = null;
|
|
351
|
-
this.runnerInitializationAnnounced = false;
|
|
352
|
-
this.inputQueue = null;
|
|
353
|
-
this.query = null;
|
|
354
|
-
this.abortController = null;
|
|
355
|
-
this.sealTurns('cancelled');
|
|
356
|
-
this.setStatus('idle');
|
|
357
|
-
this.appendMessage({ kind: 'event', level: 'info', text: reason });
|
|
358
|
-
this.emitEvent({ type: 'state', state: this.getControlState() });
|
|
359
|
-
return true;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
resetRunnerForNextTurn() {
|
|
363
|
-
this.inputQueue?.close();
|
|
364
|
-
this.query?.close?.();
|
|
365
|
-
this.rejectPendingLocalCommand(new Error('Claude CLI session was reset'));
|
|
366
|
-
this.runnerStarted = false;
|
|
367
|
-
this.runnerReadyPromise = null;
|
|
368
|
-
this.runnerInitializationAnnounced = false;
|
|
369
|
-
this.inputQueue = null;
|
|
370
|
-
this.query = null;
|
|
371
|
-
this.abortController = null;
|
|
372
|
-
this.turnQueue = [];
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
finishRunner() {
|
|
376
|
-
this.rejectPendingLocalCommand(new Error('Claude CLI session ended before returning the command output'));
|
|
377
|
-
this.runnerStarted = false;
|
|
378
|
-
this.runnerReadyPromise = null;
|
|
379
|
-
this.runnerInitializationAnnounced = false;
|
|
380
|
-
this.inputQueue?.close();
|
|
381
|
-
this.inputQueue = null;
|
|
382
|
-
this.query = null;
|
|
383
|
-
this.abortController = null;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
respondPermission(id, approved, action = null) {
|
|
387
|
-
const pending = this.pendingPermissions.get(id);
|
|
388
|
-
if (!pending) return false;
|
|
389
|
-
this.pendingPermissions.delete(id);
|
|
390
|
-
const normalizedAction = this.normalizePermissionAction(action, approved);
|
|
391
|
-
const allowedTools = this.getAllowedToolsForAction(normalizedAction, pending);
|
|
392
|
-
const nextMode = this.getPermissionModeForAction(normalizedAction);
|
|
393
|
-
if (allowedTools.length > 0) this.addAllowedTools(allowedTools);
|
|
394
|
-
if (nextMode) {
|
|
395
|
-
this.permissionMode = nextMode;
|
|
396
|
-
if (this.query && typeof this.query.setPermissionMode === 'function') {
|
|
397
|
-
Promise.resolve(this.query.setPermissionMode(this.getSdkPermissionMode())).catch(error => {
|
|
398
|
-
this.logger.debugInfo?.(`[claude-structured] setPermissionMode failed: ${error.message}`);
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
const publicRequest = {
|
|
403
|
-
...pending.public,
|
|
404
|
-
status: approved ? 'approved' : 'denied',
|
|
405
|
-
action: normalizedAction,
|
|
406
|
-
mode: nextMode || undefined,
|
|
407
|
-
allowedTools: allowedTools.length > 0 ? allowedTools : undefined
|
|
408
|
-
};
|
|
409
|
-
this.emitEvent({ type: 'permission-updated', request: publicRequest });
|
|
410
|
-
pending.resolve(approved
|
|
411
|
-
? {
|
|
412
|
-
behavior: 'allow',
|
|
413
|
-
updatedInput: pending.input || {},
|
|
414
|
-
updatedPermissions: this.getPermissionUpdatesForAction(normalizedAction, pending),
|
|
415
|
-
toolUseID: pending.toolUseID,
|
|
416
|
-
decisionClassification: normalizedAction === 'allow-once' ? 'user_temporary' : 'user_permanent'
|
|
417
|
-
}
|
|
418
|
-
: { behavior: 'deny', message: DENY_PERMISSION_MESSAGE, interrupt: true, toolUseID: pending.toolUseID, decisionClassification: 'user_reject' });
|
|
419
|
-
this.emitEvent({ type: 'state', state: this.getControlState() });
|
|
420
|
-
return true;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
normalizePermissionAction(action, approved) {
|
|
424
|
-
if (!approved) return 'deny';
|
|
425
|
-
const value = String(action || '').trim();
|
|
426
|
-
if (['allow-once', 'allow-tool', 'allow-edits', 'bypass'].includes(value)) return value;
|
|
427
|
-
return 'allow-once';
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
getPermissionModeForAction(action) {
|
|
431
|
-
if (action === 'allow-edits') return 'acceptEdits';
|
|
432
|
-
if (action === 'bypass') return 'bypassPermissions';
|
|
433
|
-
return null;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
getSdkPermissionMode() {
|
|
437
|
-
// Claude CLI refuses --dangerously-skip-permissions under root/sudo. Glad
|
|
438
|
-
// keeps bypass as local state and auto-allows through canUseTool instead.
|
|
439
|
-
if (this.permissionMode === 'bypassPermissions') return 'default';
|
|
440
|
-
return this.permissionMode;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
getAllowedToolsForAction(action, pending) {
|
|
444
|
-
if (action !== 'allow-tool') return [];
|
|
445
|
-
const toolName = pending.toolName || pending.public.toolName;
|
|
446
|
-
if (!toolName) return [];
|
|
447
|
-
if (toolName === 'Bash') {
|
|
448
|
-
const command = pending.input && typeof pending.input.command === 'string'
|
|
449
|
-
? pending.input.command
|
|
450
|
-
: '';
|
|
451
|
-
return command ? [`Bash(${command})`] : ['Bash'];
|
|
452
|
-
}
|
|
453
|
-
return [toolName];
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
getPermissionUpdatesForAction(action, pending) {
|
|
457
|
-
const mode = this.getPermissionModeForAction(action);
|
|
458
|
-
const allowedTools = this.getAllowedToolsForAction(action, pending);
|
|
459
|
-
const updates = [];
|
|
460
|
-
if (allowedTools.length > 0) {
|
|
461
|
-
updates.push({
|
|
462
|
-
type: 'addRules',
|
|
463
|
-
rules: allowedTools.map(tool => this.permissionRuleFromTool(tool)),
|
|
464
|
-
behavior: 'allow',
|
|
465
|
-
destination: 'session'
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
if (mode === 'acceptEdits') {
|
|
469
|
-
updates.push({
|
|
470
|
-
type: 'addRules',
|
|
471
|
-
rules: Array.from(EDIT_TOOLS).map(toolName => ({ toolName })),
|
|
472
|
-
behavior: 'allow',
|
|
473
|
-
destination: 'session'
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
return updates.length > 0 ? updates : undefined;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
permissionRuleFromTool(tool) {
|
|
480
|
-
const match = String(tool || '').match(/^Bash\(([\s\S]*)\)$/);
|
|
481
|
-
if (match) return { toolName: 'Bash', ruleContent: match[1] };
|
|
482
|
-
return { toolName: String(tool || '') };
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
addAllowedTools(tools) {
|
|
486
|
-
for (const tool of tools) {
|
|
487
|
-
if (tool === 'Bash') {
|
|
488
|
-
this.allowedTools.add(tool);
|
|
489
|
-
} else if (tool.startsWith('Bash(')) {
|
|
490
|
-
this.parseBashPermission(tool);
|
|
491
|
-
} else {
|
|
492
|
-
this.allowedTools.add(tool);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
parseBashPermission(permission) {
|
|
498
|
-
const match = String(permission || '').match(/^Bash\(([\s\S]*)\)$/);
|
|
499
|
-
if (!match) return;
|
|
500
|
-
const command = match[1];
|
|
501
|
-
if (command.endsWith(':*')) {
|
|
502
|
-
this.allowedBashPrefixes.add(command.slice(0, -2));
|
|
503
|
-
} else {
|
|
504
|
-
this.allowedBashLiterals.add(command);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
isToolAllowed(toolName, input) {
|
|
509
|
-
if (toolName === 'Bash') {
|
|
510
|
-
if (this.allowedTools.has('Bash')) return true;
|
|
511
|
-
const command = input && typeof input.command === 'string' ? input.command : '';
|
|
512
|
-
if (command && this.allowedBashLiterals.has(command)) return true;
|
|
513
|
-
for (const prefix of this.allowedBashPrefixes) {
|
|
514
|
-
if (command.startsWith(prefix)) return true;
|
|
515
|
-
}
|
|
516
|
-
return false;
|
|
517
|
-
}
|
|
518
|
-
return this.allowedTools.has(toolName);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
shouldAutoAllowTool(toolName, input) {
|
|
522
|
-
if (this.isToolAllowed(toolName, input)) return true;
|
|
523
|
-
if (this.permissionMode === 'bypassPermissions' && !EXIT_PLAN_TOOLS.has(toolName)) return true;
|
|
524
|
-
if (this.permissionMode === 'acceptEdits' && EDIT_TOOLS.has(toolName)) return true;
|
|
525
|
-
if (this.permissionMode === 'plan' && !this.isDangerousTool(toolName)) return true;
|
|
526
|
-
return false;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
isDangerousTool(toolName) {
|
|
530
|
-
return toolName === 'Bash' || EDIT_TOOLS.has(toolName) || EXIT_PLAN_TOOLS.has(toolName);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
markCompletionRead() {
|
|
534
|
-
this.hasUnreadCompletion = false;
|
|
535
|
-
this.completionReadInputSeq = this.inputSeq || 0;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
startRunner(initialMessage = null) {
|
|
539
|
-
if (this.runnerStarted) {
|
|
540
|
-
if (initialMessage && this.inputQueue) this.inputQueue.push(initialMessage);
|
|
541
|
-
return this.runnerReadyPromise || Promise.resolve(this.query);
|
|
542
|
-
}
|
|
543
|
-
this.runnerStarted = true;
|
|
544
|
-
this.runnerInitializationAnnounced = false;
|
|
545
|
-
this.inputQueue = new AsyncMessageQueue();
|
|
546
|
-
if (initialMessage) this.inputQueue.push(initialMessage);
|
|
547
|
-
this.abortController = new AbortController();
|
|
548
|
-
this.abortRequested = false;
|
|
549
|
-
this.activeOptionSignature = this.getOptionSignature();
|
|
550
|
-
if (initialMessage) this.setStatus('thinking');
|
|
551
|
-
|
|
552
|
-
let resolveReady;
|
|
553
|
-
this.runnerReadyPromise = new Promise(resolve => { resolveReady = resolve; });
|
|
554
|
-
void this.runRunner(resolveReady);
|
|
555
|
-
return this.runnerReadyPromise;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
async runRunner(resolveReady) {
|
|
559
|
-
try {
|
|
560
|
-
const sdk = await import('@anthropic-ai/claude-agent-sdk');
|
|
561
|
-
const resolvedModel = resolveClaudeModel(this.model, process.env);
|
|
562
|
-
const options = {
|
|
563
|
-
cwd: this.workingDir,
|
|
564
|
-
resume: this.resumeSessionId || undefined,
|
|
565
|
-
permissionMode: this.getSdkPermissionMode(),
|
|
566
|
-
allowDangerouslySkipPermissions: false,
|
|
567
|
-
effort: this.effort,
|
|
568
|
-
tools: { type: 'preset', preset: 'claude_code' },
|
|
569
|
-
env: {
|
|
570
|
-
...process.env,
|
|
571
|
-
CLAUDE_AGENT_SDK_CLIENT_APP: 'glad-web'
|
|
572
|
-
},
|
|
573
|
-
abortController: this.abortController,
|
|
574
|
-
canUseTool: (toolName, input, options) => this.requestPermission(toolName, input, options)
|
|
575
|
-
};
|
|
576
|
-
if (resolvedModel) options.model = resolvedModel;
|
|
577
|
-
this.query = sdk.query({
|
|
578
|
-
prompt: this.inputQueue,
|
|
579
|
-
options
|
|
580
|
-
});
|
|
581
|
-
if (typeof this.query.initializationResult === 'function') {
|
|
582
|
-
await this.query.initializationResult();
|
|
583
|
-
}
|
|
584
|
-
resolveReady(this.query);
|
|
585
|
-
|
|
586
|
-
for await (const message of this.query) {
|
|
587
|
-
this.handleSdkMessage(message);
|
|
588
|
-
}
|
|
589
|
-
this.finishRunner();
|
|
590
|
-
this.sealTurns('completed');
|
|
591
|
-
this.setStatus('idle');
|
|
592
|
-
} catch (error) {
|
|
593
|
-
resolveReady(null);
|
|
594
|
-
const wasAborted = this.abortRequested;
|
|
595
|
-
this.inputQueue?.close();
|
|
596
|
-
this.query?.close?.();
|
|
597
|
-
this.finishRunner();
|
|
598
|
-
if (!this.running) return;
|
|
599
|
-
if (wasAborted) {
|
|
600
|
-
this.setStatus('idle');
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
this.appendMessage({
|
|
604
|
-
kind: 'event',
|
|
605
|
-
level: 'error',
|
|
606
|
-
text: `Claude session error: ${error && error.message ? error.message : String(error)}`
|
|
607
|
-
});
|
|
608
|
-
this.sealTurns('failed');
|
|
609
|
-
this.setStatus('error');
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
async showUsage() {
|
|
614
|
-
try {
|
|
615
|
-
const output = await this.runLocalCommand('/usage');
|
|
616
|
-
const usage = { source: 'claude-cli-command', session: parseClaudeUsageOutput(output), fetchedAt: Date.now() };
|
|
617
|
-
this.appendMessage({ kind: 'usage', title: 'Claude usage', usage });
|
|
618
|
-
return true;
|
|
619
|
-
} catch (error) {
|
|
620
|
-
this.appendMessage({
|
|
621
|
-
kind: 'usage',
|
|
622
|
-
title: 'Claude usage',
|
|
623
|
-
error: error && error.message ? error.message : String(error)
|
|
624
|
-
});
|
|
625
|
-
return false;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
async showContext() {
|
|
630
|
-
try {
|
|
631
|
-
const output = await this.runLocalCommand('/context');
|
|
632
|
-
this.appendMessage({ kind: 'context', title: 'Claude context', context: parseClaudeContextOutput(output) });
|
|
633
|
-
return true;
|
|
634
|
-
} catch (error) {
|
|
635
|
-
this.appendMessage({
|
|
636
|
-
kind: 'context',
|
|
637
|
-
title: 'Claude context',
|
|
638
|
-
error: error && error.message ? error.message : String(error)
|
|
639
|
-
});
|
|
640
|
-
return false;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
runLocalCommand(command) {
|
|
645
|
-
const task = this.localCommandChain.then(() => this.executeLocalCommand(command));
|
|
646
|
-
this.localCommandChain = task.catch(() => {});
|
|
647
|
-
return task;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
async executeLocalCommand(command) {
|
|
651
|
-
if (this.status === 'thinking') throw new Error('Wait for Claude to finish before running a local command');
|
|
652
|
-
if (this.localCommandRunner) return this.localCommandRunner(command);
|
|
653
|
-
|
|
654
|
-
const query = await this.startRunner();
|
|
655
|
-
if (!query || !this.inputQueue) throw new Error('Claude CLI session is not available');
|
|
656
|
-
if (this.pendingLocalCommand) throw new Error('Another Claude local command is already running');
|
|
657
|
-
|
|
658
|
-
return new Promise((resolve, reject) => {
|
|
659
|
-
const timeout = setTimeout(() => {
|
|
660
|
-
if (this.pendingLocalCommand?.command !== command) return;
|
|
661
|
-
this.pendingLocalCommand = null;
|
|
662
|
-
reject(new Error(`Claude ${command} command timed out`));
|
|
663
|
-
}, LOCAL_COMMAND_TIMEOUT_MS);
|
|
664
|
-
this.pendingLocalCommand = {
|
|
665
|
-
command,
|
|
666
|
-
resolve: output => {
|
|
667
|
-
clearTimeout(timeout);
|
|
668
|
-
this.pendingLocalCommand = null;
|
|
669
|
-
resolve(output);
|
|
670
|
-
},
|
|
671
|
-
reject: error => {
|
|
672
|
-
clearTimeout(timeout);
|
|
673
|
-
this.pendingLocalCommand = null;
|
|
674
|
-
reject(error);
|
|
675
|
-
}
|
|
676
|
-
};
|
|
677
|
-
this.inputQueue.push({
|
|
678
|
-
type: 'user',
|
|
679
|
-
parent_tool_use_id: null,
|
|
680
|
-
message: { role: 'user', content: command }
|
|
681
|
-
});
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
rejectPendingLocalCommand(error) {
|
|
686
|
-
this.pendingLocalCommand?.reject(error);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
getOptionSignature() {
|
|
690
|
-
return JSON.stringify({
|
|
691
|
-
permissionMode: this.permissionMode,
|
|
692
|
-
model: this.model,
|
|
693
|
-
effort: this.effort,
|
|
694
|
-
resume: this.resumeSessionId || null
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
requestPermission(toolName, input, options = {}) {
|
|
699
|
-
if (toolName !== 'AskUserQuestion' && this.shouldAutoAllowTool(toolName, input)) {
|
|
700
|
-
return Promise.resolve({
|
|
701
|
-
behavior: 'allow',
|
|
702
|
-
updatedInput: input || {},
|
|
703
|
-
toolUseID: options.toolUseID,
|
|
704
|
-
decisionClassification: 'user_permanent'
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
const id = crypto.randomUUID();
|
|
708
|
-
const request = {
|
|
709
|
-
id,
|
|
710
|
-
toolName,
|
|
711
|
-
title: options.title || `${toolName} requires approval`,
|
|
712
|
-
displayName: options.displayName || '',
|
|
713
|
-
description: options.description || '',
|
|
714
|
-
reason: options.decisionReason || options.description || '',
|
|
715
|
-
blockedPath: options.blockedPath || null,
|
|
716
|
-
canAllowTool: Boolean(toolName && !EDIT_TOOLS.has(toolName) && !EXIT_PLAN_TOOLS.has(toolName)),
|
|
717
|
-
canAllowEdits: EDIT_TOOLS.has(toolName) || EXIT_PLAN_TOOLS.has(toolName),
|
|
718
|
-
canBypass: EXIT_PLAN_TOOLS.has(toolName),
|
|
719
|
-
input,
|
|
720
|
-
toolUseId: options.toolUseID || null,
|
|
721
|
-
createdAt: Date.now(),
|
|
722
|
-
status: 'pending'
|
|
723
|
-
};
|
|
724
|
-
this.emitEvent({ type: 'permission-request', request });
|
|
725
|
-
return new Promise(resolve => {
|
|
726
|
-
this.pendingPermissions.set(id, {
|
|
727
|
-
public: request,
|
|
728
|
-
resolve,
|
|
729
|
-
input,
|
|
730
|
-
toolName,
|
|
731
|
-
toolUseID: options.toolUseID
|
|
732
|
-
});
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
handleSdkMessage(message) {
|
|
737
|
-
if (!message || typeof message !== 'object') return;
|
|
738
|
-
|
|
739
|
-
if (message.type === 'system' && message.subtype === 'init') {
|
|
740
|
-
const signatureBeforeInit = this.getOptionSignature();
|
|
741
|
-
this.claudeSessionId = message.session_id || this.claudeSessionId;
|
|
742
|
-
this.resumeSessionId = this.claudeSessionId || this.resumeSessionId;
|
|
743
|
-
if (message.model) this.model = String(message.model);
|
|
744
|
-
// Resolving runtime session/model values is not a user settings change
|
|
745
|
-
// and must not recycle the live runner after the turn.
|
|
746
|
-
if (this.activeOptionSignature === signatureBeforeInit) {
|
|
747
|
-
this.activeOptionSignature = this.getOptionSignature();
|
|
748
|
-
}
|
|
749
|
-
if (!this.runnerInitializationAnnounced) {
|
|
750
|
-
this.runnerInitializationAnnounced = true;
|
|
751
|
-
this.appendMessage({
|
|
752
|
-
kind: 'event',
|
|
753
|
-
level: 'info',
|
|
754
|
-
text: `Claude ready${message.model ? ` (${message.model})` : ''}`
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
this.emitEvent({ type: 'state', state: this.getControlState() });
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
if (message.type === 'system' && message.subtype === 'local_command_output') {
|
|
762
|
-
if (this.pendingLocalCommand) {
|
|
763
|
-
this.pendingLocalCommand.resolve(String(message.content || ''));
|
|
764
|
-
}
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
if (this.pendingLocalCommand && message.type === 'assistant') return;
|
|
769
|
-
|
|
770
|
-
if (this.pendingLocalCommand && message.type === 'result') {
|
|
771
|
-
const output = typeof message.result === 'string' ? message.result : '';
|
|
772
|
-
if (message.is_error || !output) {
|
|
773
|
-
this.pendingLocalCommand.reject(new Error(output || `Claude ${this.pendingLocalCommand.command} command failed`));
|
|
774
|
-
} else {
|
|
775
|
-
this.pendingLocalCommand.resolve(output);
|
|
776
|
-
}
|
|
777
|
-
this.setStatus('idle');
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (message.type === 'assistant') {
|
|
782
|
-
const turn = this.currentTurn();
|
|
783
|
-
const content = message.message && message.message.content;
|
|
784
|
-
const text = textFromContent(content).trim();
|
|
785
|
-
const toolBlocks = Array.isArray(content)
|
|
786
|
-
? content.filter(item => item && item.type === 'tool_use')
|
|
787
|
-
: [];
|
|
788
|
-
if (text) {
|
|
789
|
-
this.appendMessage({ kind: 'assistant', text, raw: message, turnId: turn?.id || null });
|
|
790
|
-
}
|
|
791
|
-
for (const block of toolBlocks) {
|
|
792
|
-
this.appendMessage({
|
|
793
|
-
kind: 'tool',
|
|
794
|
-
name: block.name || 'tool',
|
|
795
|
-
summary: this.summarizeToolInput(block.input),
|
|
796
|
-
input: block.input,
|
|
797
|
-
toolUseId: block.id,
|
|
798
|
-
turnId: turn?.id || null,
|
|
799
|
-
startedAtMs: Date.now()
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
this.setStatus('thinking');
|
|
803
|
-
return;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
if (message.type === 'user') {
|
|
807
|
-
const turn = this.currentTurn();
|
|
808
|
-
const content = message.message && message.message.content;
|
|
809
|
-
if (Array.isArray(content)) {
|
|
810
|
-
for (const item of content) {
|
|
811
|
-
if (item && item.type === 'tool_result') {
|
|
812
|
-
this.appendMessage({
|
|
813
|
-
kind: 'tool-result',
|
|
814
|
-
toolUseId: item.tool_use_id,
|
|
815
|
-
text: textFromContent([item]).trim(),
|
|
816
|
-
isError: Boolean(item.is_error),
|
|
817
|
-
turnId: turn?.id || null,
|
|
818
|
-
completedAtMs: Date.now()
|
|
819
|
-
});
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
if (message.type === 'result') {
|
|
827
|
-
this.completeCurrentTurn(message.is_error || message.subtype !== 'success' ? 'failed' : 'completed', message.duration_ms);
|
|
828
|
-
this.setStatus(this.turnQueue.length > 0 ? 'thinking' : 'idle');
|
|
829
|
-
if (this.inputSeq > this.completionReadInputSeq) {
|
|
830
|
-
this.hasUnreadCompletion = true;
|
|
831
|
-
}
|
|
832
|
-
this.emit('complete');
|
|
833
|
-
if (this.activeOptionSignature !== this.getOptionSignature()) {
|
|
834
|
-
this.resetRunnerForNextTurn();
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
summarizeToolInput(input) {
|
|
840
|
-
if (!input || typeof input !== 'object') return '';
|
|
841
|
-
if (typeof input.command === 'string') return input.command;
|
|
842
|
-
if (typeof input.file_path === 'string') return input.file_path;
|
|
843
|
-
if (typeof input.path === 'string') return input.path;
|
|
844
|
-
const serialized = JSON.stringify(input);
|
|
845
|
-
return serialized.length > 240 ? serialized.slice(0, 240) + '...' : serialized;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
appendMessage(message) {
|
|
849
|
-
const item = this.createMessageItem(message);
|
|
850
|
-
this.messages.push(item);
|
|
851
|
-
this.emitEvent({ type: 'message', message: item });
|
|
852
|
-
return item;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
setStatus(status) {
|
|
856
|
-
if (this.status === status) return;
|
|
857
|
-
this.status = status;
|
|
858
|
-
this.emitEvent({ type: 'status', status });
|
|
859
|
-
this.emitEvent({ type: 'state', state: this.getControlState() });
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
emitEvent(event) {
|
|
863
|
-
this.emit('event', event);
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
isRunning() {
|
|
867
|
-
return this.running;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
kill() {
|
|
871
|
-
this.running = false;
|
|
872
|
-
this.inputQueue?.close();
|
|
873
|
-
this.query?.close?.();
|
|
874
|
-
this.abortController?.abort();
|
|
875
|
-
for (const pending of this.pendingPermissions.values()) {
|
|
876
|
-
pending.resolve({ behavior: 'deny', message: 'Session ended', interrupt: true, toolUseID: pending.toolUseID });
|
|
877
|
-
}
|
|
878
|
-
this.pendingPermissions.clear();
|
|
879
|
-
this.setStatus('stopped');
|
|
880
|
-
this.emit('exit', { exitCode: 0 });
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
module.exports = ClaudeStructuredSession;
|