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 CHANGED
@@ -63,7 +63,7 @@ Requirements:
63
63
  - Node.js `>=18`
64
64
 
65
65
  ```bash
66
- git clone https://github.com/Next2012/Glad.git
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 [next2012](https://github.com/Next2012/Glad).
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/Next2012/Glad.git
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 协议,由 [next2012](https://github.com/Next2012/Glad) 维护。
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/next2012/glad');
60
+ console.log('Source: https://gitee.com/anonymous/glad');
61
61
  console.log('');
62
62
  });
63
63
 
@@ -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/next2012/glad',
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.permissionMode)).catch(error => {
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
- respondPermission(id, approved) {
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 publicRequest = { ...pending.public, status: approved ? 'approved' : 'denied' };
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
- ? { behavior: 'allow', toolUseID: pending.toolUseID, decisionClassification: 'user_temporary' }
309
- : { behavior: 'deny', message: 'Denied by user', interrupt: true, toolUseID: pending.toolUseID, decisionClassification: 'user_reject' });
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.permissionMode,
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
- reason: options.decisionReason || '',
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) {