glad-web 1.0.20 → 1.0.22
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/bin/cli.js +1 -1
- package/lib/ai-tools/registry.js +1 -1
- package/lib/claude/structured-session.js +198 -7
- package/lib/codex/structured-session.js +588 -0
- package/lib/commands/web.js +64 -3
- package/lib/session/session-manager.js +76 -13
- package/lib/web/index.html +589 -50
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Requirements:
|
|
|
63
63
|
- Node.js `>=18`
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
git clone https://github.com/
|
|
66
|
+
git clone https://github.com/Anonymous/Glad.git
|
|
67
67
|
cd glad
|
|
68
68
|
npm install
|
|
69
69
|
node bin/cli.js
|
|
@@ -192,4 +192,4 @@ See [SECURITY.md](./SECURITY.md) for details.
|
|
|
192
192
|
|
|
193
193
|
## License
|
|
194
194
|
|
|
195
|
-
MIT. Glad is maintained by [
|
|
195
|
+
MIT. Glad is maintained by [anonymous](https://github.com/Anonymous/Glad).
|
package/README.zh-CN.md
CHANGED
|
@@ -63,7 +63,7 @@ glad
|
|
|
63
63
|
- Node.js `>=18`
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
git clone https://github.com/
|
|
66
|
+
git clone https://github.com/Anonymous/Glad.git
|
|
67
67
|
cd glad
|
|
68
68
|
npm install
|
|
69
69
|
node bin/cli.js
|
|
@@ -192,4 +192,4 @@ Glad 面向受信任的本机或局域网环境使用。
|
|
|
192
192
|
|
|
193
193
|
## 开源协议
|
|
194
194
|
|
|
195
|
-
本项目使用 MIT 协议,由 [
|
|
195
|
+
本项目使用 MIT 协议,由 [anonymous](https://github.com/Anonymous/Glad) 维护。
|
package/bin/cli.js
CHANGED
|
@@ -57,7 +57,7 @@ program.on('--help', () => {
|
|
|
57
57
|
console.log('Supported AI Tools:');
|
|
58
58
|
console.log(' • Claude Code, Aider, GitHub Copilot, Gemini CLI, and more...');
|
|
59
59
|
console.log('');
|
|
60
|
-
console.log('Source: https://gitee.com/
|
|
60
|
+
console.log('Source: https://gitee.com/anonymous/glad');
|
|
61
61
|
console.log('');
|
|
62
62
|
});
|
|
63
63
|
|
package/lib/ai-tools/registry.js
CHANGED
|
@@ -200,7 +200,7 @@ const AI_TOOLS = {
|
|
|
200
200
|
args: [require('path').join(__dirname, 'demo', 'index.js')],
|
|
201
201
|
displayName: 'Demo',
|
|
202
202
|
description: 'Interactive demo for testing (no AI installation required)',
|
|
203
|
-
website: 'https://gitee.com/
|
|
203
|
+
website: 'https://gitee.com/anonymous/glad',
|
|
204
204
|
checkInstalled: async () => true // Always available
|
|
205
205
|
}
|
|
206
206
|
};
|
|
@@ -3,6 +3,9 @@ const crypto = require('crypto');
|
|
|
3
3
|
const { normalizeEffort, normalizeModel, resolveClaudeModel } = require('./config');
|
|
4
4
|
|
|
5
5
|
const PERMISSION_MODES = new Set(['default', 'acceptEdits', 'bypassPermissions', 'plan']);
|
|
6
|
+
const EDIT_TOOLS = new Set(['Edit', 'MultiEdit', 'Write', 'NotebookEdit']);
|
|
7
|
+
const EXIT_PLAN_TOOLS = new Set(['exit_plan_mode', 'ExitPlanMode']);
|
|
8
|
+
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.";
|
|
6
9
|
|
|
7
10
|
function normalizePermissionMode(value) {
|
|
8
11
|
const mode = String(value || 'default');
|
|
@@ -88,10 +91,14 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
88
91
|
this.permissionMode = normalizePermissionMode(options.permissionMode);
|
|
89
92
|
this.model = normalizeModel(options.model);
|
|
90
93
|
this.effort = normalizeEffort(options.effort);
|
|
94
|
+
this.allowedTools = new Set();
|
|
95
|
+
this.allowedBashLiterals = new Set();
|
|
96
|
+
this.allowedBashPrefixes = new Set();
|
|
91
97
|
this.resumeSessionId = options.resume || null;
|
|
92
98
|
this.claudeSessionId = options.resume || null;
|
|
93
99
|
this.activeOptionSignature = null;
|
|
94
100
|
this.contextRemaining = null;
|
|
101
|
+
this.latestUsage = null;
|
|
95
102
|
|
|
96
103
|
// Compatibility with existing session-scoped Git/file APIs.
|
|
97
104
|
this.ptyManager = {
|
|
@@ -140,6 +147,7 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
140
147
|
claudeSessionId: this.claudeSessionId || null,
|
|
141
148
|
resumeSessionId: this.resumeSessionId || null,
|
|
142
149
|
contextRemaining: this.contextRemaining,
|
|
150
|
+
latestUsage: this.latestUsage,
|
|
143
151
|
canAbort: this.status === 'thinking',
|
|
144
152
|
pendingPermissionCount: this.pendingPermissions.size
|
|
145
153
|
};
|
|
@@ -238,7 +246,7 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
238
246
|
|
|
239
247
|
if (changed) {
|
|
240
248
|
if (this.query && typeof this.query.setPermissionMode === 'function') {
|
|
241
|
-
Promise.resolve(this.query.setPermissionMode(this.
|
|
249
|
+
Promise.resolve(this.query.setPermissionMode(this.getSdkPermissionMode())).catch(error => {
|
|
242
250
|
this.logger.debugInfo?.(`[claude-structured] setPermissionMode failed: ${error.message}`);
|
|
243
251
|
});
|
|
244
252
|
}
|
|
@@ -298,18 +306,161 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
298
306
|
this.abortController = null;
|
|
299
307
|
}
|
|
300
308
|
|
|
301
|
-
|
|
309
|
+
finishRunner() {
|
|
310
|
+
this.runnerStarted = false;
|
|
311
|
+
this.inputQueue?.close();
|
|
312
|
+
this.inputQueue = null;
|
|
313
|
+
this.query = null;
|
|
314
|
+
this.abortController = null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
respondPermission(id, approved, action = null) {
|
|
302
318
|
const pending = this.pendingPermissions.get(id);
|
|
303
319
|
if (!pending) return false;
|
|
304
320
|
this.pendingPermissions.delete(id);
|
|
305
|
-
const
|
|
321
|
+
const normalizedAction = this.normalizePermissionAction(action, approved);
|
|
322
|
+
const allowedTools = this.getAllowedToolsForAction(normalizedAction, pending);
|
|
323
|
+
const nextMode = this.getPermissionModeForAction(normalizedAction);
|
|
324
|
+
if (allowedTools.length > 0) this.addAllowedTools(allowedTools);
|
|
325
|
+
if (nextMode) {
|
|
326
|
+
this.permissionMode = nextMode;
|
|
327
|
+
if (this.query && typeof this.query.setPermissionMode === 'function') {
|
|
328
|
+
Promise.resolve(this.query.setPermissionMode(this.getSdkPermissionMode())).catch(error => {
|
|
329
|
+
this.logger.debugInfo?.(`[claude-structured] setPermissionMode failed: ${error.message}`);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const publicRequest = {
|
|
334
|
+
...pending.public,
|
|
335
|
+
status: approved ? 'approved' : 'denied',
|
|
336
|
+
action: normalizedAction,
|
|
337
|
+
mode: nextMode || undefined,
|
|
338
|
+
allowedTools: allowedTools.length > 0 ? allowedTools : undefined
|
|
339
|
+
};
|
|
306
340
|
this.emitEvent({ type: 'permission-updated', request: publicRequest });
|
|
307
341
|
pending.resolve(approved
|
|
308
|
-
? {
|
|
309
|
-
|
|
342
|
+
? {
|
|
343
|
+
behavior: 'allow',
|
|
344
|
+
updatedInput: pending.input || {},
|
|
345
|
+
updatedPermissions: this.getPermissionUpdatesForAction(normalizedAction, pending),
|
|
346
|
+
toolUseID: pending.toolUseID,
|
|
347
|
+
decisionClassification: normalizedAction === 'allow-once' ? 'user_temporary' : 'user_permanent'
|
|
348
|
+
}
|
|
349
|
+
: { behavior: 'deny', message: DENY_PERMISSION_MESSAGE, interrupt: true, toolUseID: pending.toolUseID, decisionClassification: 'user_reject' });
|
|
350
|
+
this.emitEvent({ type: 'state', state: this.getControlState() });
|
|
310
351
|
return true;
|
|
311
352
|
}
|
|
312
353
|
|
|
354
|
+
normalizePermissionAction(action, approved) {
|
|
355
|
+
if (!approved) return 'deny';
|
|
356
|
+
const value = String(action || '').trim();
|
|
357
|
+
if (['allow-once', 'allow-tool', 'allow-edits', 'bypass'].includes(value)) return value;
|
|
358
|
+
return 'allow-once';
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
getPermissionModeForAction(action) {
|
|
362
|
+
if (action === 'allow-edits') return 'acceptEdits';
|
|
363
|
+
if (action === 'bypass') return 'bypassPermissions';
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
getSdkPermissionMode() {
|
|
368
|
+
// Claude CLI refuses --dangerously-skip-permissions under root/sudo. Glad
|
|
369
|
+
// keeps bypass as local state and auto-allows through canUseTool instead.
|
|
370
|
+
if (this.permissionMode === 'bypassPermissions') return 'default';
|
|
371
|
+
return this.permissionMode;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
getAllowedToolsForAction(action, pending) {
|
|
375
|
+
if (action !== 'allow-tool') return [];
|
|
376
|
+
const toolName = pending.toolName || pending.public.toolName;
|
|
377
|
+
if (!toolName) return [];
|
|
378
|
+
if (toolName === 'Bash') {
|
|
379
|
+
const command = pending.input && typeof pending.input.command === 'string'
|
|
380
|
+
? pending.input.command
|
|
381
|
+
: '';
|
|
382
|
+
return command ? [`Bash(${command})`] : ['Bash'];
|
|
383
|
+
}
|
|
384
|
+
return [toolName];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
getPermissionUpdatesForAction(action, pending) {
|
|
388
|
+
const mode = this.getPermissionModeForAction(action);
|
|
389
|
+
const allowedTools = this.getAllowedToolsForAction(action, pending);
|
|
390
|
+
const updates = [];
|
|
391
|
+
if (allowedTools.length > 0) {
|
|
392
|
+
updates.push({
|
|
393
|
+
type: 'addRules',
|
|
394
|
+
rules: allowedTools.map(tool => this.permissionRuleFromTool(tool)),
|
|
395
|
+
behavior: 'allow',
|
|
396
|
+
destination: 'session'
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
if (mode === 'acceptEdits') {
|
|
400
|
+
updates.push({
|
|
401
|
+
type: 'addRules',
|
|
402
|
+
rules: Array.from(EDIT_TOOLS).map(toolName => ({ toolName })),
|
|
403
|
+
behavior: 'allow',
|
|
404
|
+
destination: 'session'
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return updates.length > 0 ? updates : undefined;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
permissionRuleFromTool(tool) {
|
|
411
|
+
const match = String(tool || '').match(/^Bash\(([\s\S]*)\)$/);
|
|
412
|
+
if (match) return { toolName: 'Bash', ruleContent: match[1] };
|
|
413
|
+
return { toolName: String(tool || '') };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
addAllowedTools(tools) {
|
|
417
|
+
for (const tool of tools) {
|
|
418
|
+
if (tool === 'Bash') {
|
|
419
|
+
this.allowedTools.add(tool);
|
|
420
|
+
} else if (tool.startsWith('Bash(')) {
|
|
421
|
+
this.parseBashPermission(tool);
|
|
422
|
+
} else {
|
|
423
|
+
this.allowedTools.add(tool);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
parseBashPermission(permission) {
|
|
429
|
+
const match = String(permission || '').match(/^Bash\(([\s\S]*)\)$/);
|
|
430
|
+
if (!match) return;
|
|
431
|
+
const command = match[1];
|
|
432
|
+
if (command.endsWith(':*')) {
|
|
433
|
+
this.allowedBashPrefixes.add(command.slice(0, -2));
|
|
434
|
+
} else {
|
|
435
|
+
this.allowedBashLiterals.add(command);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
isToolAllowed(toolName, input) {
|
|
440
|
+
if (toolName === 'Bash') {
|
|
441
|
+
if (this.allowedTools.has('Bash')) return true;
|
|
442
|
+
const command = input && typeof input.command === 'string' ? input.command : '';
|
|
443
|
+
if (command && this.allowedBashLiterals.has(command)) return true;
|
|
444
|
+
for (const prefix of this.allowedBashPrefixes) {
|
|
445
|
+
if (command.startsWith(prefix)) return true;
|
|
446
|
+
}
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
return this.allowedTools.has(toolName);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
shouldAutoAllowTool(toolName, input) {
|
|
453
|
+
if (this.isToolAllowed(toolName, input)) return true;
|
|
454
|
+
if (this.permissionMode === 'bypassPermissions' && !EXIT_PLAN_TOOLS.has(toolName)) return true;
|
|
455
|
+
if (this.permissionMode === 'acceptEdits' && EDIT_TOOLS.has(toolName)) return true;
|
|
456
|
+
if (this.permissionMode === 'plan' && !this.isDangerousTool(toolName)) return true;
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
isDangerousTool(toolName) {
|
|
461
|
+
return toolName === 'Bash' || EDIT_TOOLS.has(toolName) || EXIT_PLAN_TOOLS.has(toolName);
|
|
462
|
+
}
|
|
463
|
+
|
|
313
464
|
markCompletionRead() {
|
|
314
465
|
this.hasUnreadCompletion = false;
|
|
315
466
|
this.completionReadInputSeq = this.inputSeq || 0;
|
|
@@ -330,7 +481,8 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
330
481
|
const options = {
|
|
331
482
|
cwd: this.workingDir,
|
|
332
483
|
resume: this.resumeSessionId || undefined,
|
|
333
|
-
permissionMode: this.
|
|
484
|
+
permissionMode: this.getSdkPermissionMode(),
|
|
485
|
+
allowDangerouslySkipPermissions: false,
|
|
334
486
|
effort: this.effort,
|
|
335
487
|
tools: { type: 'preset', preset: 'claude_code' },
|
|
336
488
|
env: {
|
|
@@ -349,10 +501,12 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
349
501
|
for await (const message of this.query) {
|
|
350
502
|
this.handleSdkMessage(message);
|
|
351
503
|
}
|
|
504
|
+
this.finishRunner();
|
|
352
505
|
this.setStatus('idle');
|
|
353
506
|
} catch (error) {
|
|
354
507
|
if (!this.running) return;
|
|
355
508
|
if (this.abortRequested) {
|
|
509
|
+
this.finishRunner();
|
|
356
510
|
this.setStatus('idle');
|
|
357
511
|
return;
|
|
358
512
|
}
|
|
@@ -375,12 +529,26 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
375
529
|
}
|
|
376
530
|
|
|
377
531
|
requestPermission(toolName, input, options = {}) {
|
|
532
|
+
if (toolName !== 'AskUserQuestion' && this.shouldAutoAllowTool(toolName, input)) {
|
|
533
|
+
return Promise.resolve({
|
|
534
|
+
behavior: 'allow',
|
|
535
|
+
updatedInput: input || {},
|
|
536
|
+
toolUseID: options.toolUseID,
|
|
537
|
+
decisionClassification: 'user_permanent'
|
|
538
|
+
});
|
|
539
|
+
}
|
|
378
540
|
const id = crypto.randomUUID();
|
|
379
541
|
const request = {
|
|
380
542
|
id,
|
|
381
543
|
toolName,
|
|
382
544
|
title: options.title || `${toolName} requires approval`,
|
|
383
|
-
|
|
545
|
+
displayName: options.displayName || '',
|
|
546
|
+
description: options.description || '',
|
|
547
|
+
reason: options.decisionReason || options.description || '',
|
|
548
|
+
blockedPath: options.blockedPath || null,
|
|
549
|
+
canAllowTool: Boolean(toolName && !EDIT_TOOLS.has(toolName) && !EXIT_PLAN_TOOLS.has(toolName)),
|
|
550
|
+
canAllowEdits: EDIT_TOOLS.has(toolName) || EXIT_PLAN_TOOLS.has(toolName),
|
|
551
|
+
canBypass: EXIT_PLAN_TOOLS.has(toolName),
|
|
384
552
|
input,
|
|
385
553
|
createdAt: Date.now(),
|
|
386
554
|
status: 'pending'
|
|
@@ -390,6 +558,8 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
390
558
|
this.pendingPermissions.set(id, {
|
|
391
559
|
public: request,
|
|
392
560
|
resolve,
|
|
561
|
+
input,
|
|
562
|
+
toolName,
|
|
393
563
|
toolUseID: options.toolUseID
|
|
394
564
|
});
|
|
395
565
|
});
|
|
@@ -412,6 +582,8 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
412
582
|
}
|
|
413
583
|
|
|
414
584
|
if (message.type === 'assistant') {
|
|
585
|
+
const usage = message.message && message.message.usage;
|
|
586
|
+
if (usage) this.updateLatestUsageFromClaudeUsage(usage, message.message.model);
|
|
415
587
|
const content = message.message && message.message.content;
|
|
416
588
|
const text = textFromContent(content).trim();
|
|
417
589
|
const toolBlocks = Array.isArray(content)
|
|
@@ -467,6 +639,25 @@ class ClaudeStructuredSession extends EventEmitter {
|
|
|
467
639
|
const usage = message && (message.usage || message.total_usage || message.result && message.result.usage);
|
|
468
640
|
const context = usage && (usage.context_remaining || usage.contextRemaining || usage.remaining_context || usage.remainingContext);
|
|
469
641
|
this.contextRemaining = typeof context === 'number' ? context : null;
|
|
642
|
+
if (usage) this.updateLatestUsageFromClaudeUsage(usage, message.model || message.result && message.result.model);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
updateLatestUsageFromClaudeUsage(usage, model = null) {
|
|
646
|
+
const inputTokens = Number(usage.input_tokens || usage.inputTokens || 0);
|
|
647
|
+
const outputTokens = Number(usage.output_tokens || usage.outputTokens || 0);
|
|
648
|
+
const cacheCreation = Number(usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0);
|
|
649
|
+
const cacheRead = Number(usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0);
|
|
650
|
+
const contextSize = inputTokens + cacheCreation + cacheRead;
|
|
651
|
+
this.latestUsage = {
|
|
652
|
+
inputTokens,
|
|
653
|
+
outputTokens,
|
|
654
|
+
cacheCreation,
|
|
655
|
+
cacheRead,
|
|
656
|
+
totalTokens: inputTokens + outputTokens + cacheCreation + cacheRead,
|
|
657
|
+
contextSize,
|
|
658
|
+
model: model || null,
|
|
659
|
+
updatedAt: Date.now()
|
|
660
|
+
};
|
|
470
661
|
}
|
|
471
662
|
|
|
472
663
|
summarizeToolInput(input) {
|