@scheduler-systems/gal-run 0.0.406 → 0.0.407
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +424 -37
- package/dist/postinstall.cjs +202 -5
- package/dist/preuninstall.cjs +24 -17
- package/package.json +1 -1
- package/scripts/postinstall.cjs +202 -5
- package/scripts/preuninstall.cjs +24 -17
package/dist/postinstall.cjs
CHANGED
|
@@ -56,7 +56,7 @@ const cliVersion = cliPackageJson.version;
|
|
|
56
56
|
|
|
57
57
|
// Version markers for idempotency checks
|
|
58
58
|
// Bump these to force updates to installed files
|
|
59
|
-
const HOOK_VERSION = '4.
|
|
59
|
+
const HOOK_VERSION = '4.2.0'; // SessionStart + Stop hooks with local usage reporting (#4673)
|
|
60
60
|
const STATUS_LINE_VERSION = '1.0.0'; // Status line script
|
|
61
61
|
const RULES_VERSION = '1.0.0'; // GAL CLI rules
|
|
62
62
|
|
|
@@ -114,10 +114,12 @@ const fs = require('fs');
|
|
|
114
114
|
const path = require('path');
|
|
115
115
|
const { execSync, spawn } = require('child_process');
|
|
116
116
|
const os = require('os');
|
|
117
|
+
const crypto = require('crypto');
|
|
117
118
|
|
|
118
119
|
const GAL_DIR = '.gal';
|
|
119
120
|
const SYNC_STATE_FILE = 'sync-state.json';
|
|
120
121
|
const GAL_CONFIG_FILE = path.join(os.homedir(), '.gal', 'config.json');
|
|
122
|
+
const SESSION_USAGE_DIR = path.join(os.homedir(), '.gal', 'claude-session-usage');
|
|
121
123
|
|
|
122
124
|
function showMessage(message, status) {
|
|
123
125
|
// Queue telemetry event before showing message
|
|
@@ -156,7 +158,7 @@ function queueTelemetryEvent(status) {
|
|
|
156
158
|
|
|
157
159
|
// Add session start hook event
|
|
158
160
|
pending.push({
|
|
159
|
-
id:
|
|
161
|
+
id: crypto.randomUUID(),
|
|
160
162
|
eventType: 'hook_triggered',
|
|
161
163
|
timestamp: new Date().toISOString(),
|
|
162
164
|
payload: {
|
|
@@ -180,6 +182,45 @@ function queueTelemetryEvent(status) {
|
|
|
180
182
|
}
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
function readHookInput() {
|
|
186
|
+
try {
|
|
187
|
+
if (process.stdin.isTTY) return {};
|
|
188
|
+
const raw = fs.readFileSync(0, 'utf-8');
|
|
189
|
+
if (!raw || !raw.trim()) return {};
|
|
190
|
+
return JSON.parse(raw);
|
|
191
|
+
} catch {
|
|
192
|
+
return {};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function getSessionUsagePath(inputData) {
|
|
197
|
+
const identity =
|
|
198
|
+
(typeof inputData.session_id === 'string' && inputData.session_id) ||
|
|
199
|
+
(typeof inputData.transcript_path === 'string' && inputData.transcript_path) ||
|
|
200
|
+
process.cwd();
|
|
201
|
+
const sessionKey = crypto.createHash('sha256').update(identity).digest('hex');
|
|
202
|
+
return path.join(SESSION_USAGE_DIR, \`\${sessionKey}.json\`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function rememberSessionStart(inputData) {
|
|
206
|
+
try {
|
|
207
|
+
const filePath = getSessionUsagePath(inputData);
|
|
208
|
+
if (!fs.existsSync(SESSION_USAGE_DIR)) {
|
|
209
|
+
fs.mkdirSync(SESSION_USAGE_DIR, { recursive: true });
|
|
210
|
+
}
|
|
211
|
+
fs.writeFileSync(
|
|
212
|
+
filePath,
|
|
213
|
+
JSON.stringify({
|
|
214
|
+
sessionId: inputData.session_id || null,
|
|
215
|
+
transcriptPath: inputData.transcript_path || null,
|
|
216
|
+
source: inputData.source || null,
|
|
217
|
+
startedAt: new Date().toISOString(),
|
|
218
|
+
}),
|
|
219
|
+
'utf-8',
|
|
220
|
+
);
|
|
221
|
+
} catch {}
|
|
222
|
+
}
|
|
223
|
+
|
|
183
224
|
// =============================================================================
|
|
184
225
|
// Self-cleaning: Remove hook if GAL CLI is uninstalled
|
|
185
226
|
// =============================================================================
|
|
@@ -209,7 +250,7 @@ function selfClean() {
|
|
|
209
250
|
try {
|
|
210
251
|
if (fs.existsSync(settingsPath)) {
|
|
211
252
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
212
|
-
const hookEvents = ['SessionStart', 'UserPromptSubmit'];
|
|
253
|
+
const hookEvents = ['SessionStart', 'Stop', 'UserPromptSubmit'];
|
|
213
254
|
|
|
214
255
|
for (const event of hookEvents) {
|
|
215
256
|
if (settings.hooks?.[event]) {
|
|
@@ -228,6 +269,9 @@ function selfClean() {
|
|
|
228
269
|
} catch {}
|
|
229
270
|
}
|
|
230
271
|
|
|
272
|
+
const hookInput = readHookInput();
|
|
273
|
+
rememberSessionStart(hookInput);
|
|
274
|
+
|
|
231
275
|
// Check if GAL is installed, self-clean if not
|
|
232
276
|
if (!isGalInstalled()) {
|
|
233
277
|
selfClean();
|
|
@@ -423,6 +467,137 @@ try {
|
|
|
423
467
|
showMessage(syncMessage, 'synced');
|
|
424
468
|
`;
|
|
425
469
|
|
|
470
|
+
const STOP_HOOK_CONTENT = `#!/usr/bin/env node
|
|
471
|
+
/**
|
|
472
|
+
* GAL Local Usage Report Hook for Claude Code (Stop)
|
|
473
|
+
* Version: ${HOOK_VERSION}
|
|
474
|
+
*/
|
|
475
|
+
|
|
476
|
+
// GAL_HOOK_VERSION = "${HOOK_VERSION}"
|
|
477
|
+
|
|
478
|
+
const fs = require('fs');
|
|
479
|
+
const path = require('path');
|
|
480
|
+
const os = require('os');
|
|
481
|
+
const crypto = require('crypto');
|
|
482
|
+
const { execSync } = require('child_process');
|
|
483
|
+
|
|
484
|
+
const SESSION_USAGE_DIR = path.join(os.homedir(), '.gal', 'claude-session-usage');
|
|
485
|
+
const GAL_CONFIG_FILE = path.join(os.homedir(), '.gal', 'config.json');
|
|
486
|
+
|
|
487
|
+
function readHookInput() {
|
|
488
|
+
try {
|
|
489
|
+
if (process.stdin.isTTY) return {};
|
|
490
|
+
const raw = fs.readFileSync(0, 'utf-8');
|
|
491
|
+
if (!raw || !raw.trim()) return {};
|
|
492
|
+
return JSON.parse(raw);
|
|
493
|
+
} catch {
|
|
494
|
+
return {};
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function getSessionUsagePath(inputData) {
|
|
499
|
+
const identity =
|
|
500
|
+
(typeof inputData.session_id === 'string' && inputData.session_id) ||
|
|
501
|
+
(typeof inputData.transcript_path === 'string' && inputData.transcript_path) ||
|
|
502
|
+
process.cwd();
|
|
503
|
+
const sessionKey = crypto.createHash('sha256').update(identity).digest('hex');
|
|
504
|
+
return path.join(SESSION_USAGE_DIR, \`\${sessionKey}.json\`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function readGalConfig() {
|
|
508
|
+
if (!fs.existsSync(GAL_CONFIG_FILE)) return null;
|
|
509
|
+
try {
|
|
510
|
+
return JSON.parse(fs.readFileSync(GAL_CONFIG_FILE, 'utf-8'));
|
|
511
|
+
} catch { return null; }
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function isGalInstalled() {
|
|
515
|
+
try {
|
|
516
|
+
execSync('which gal', { stdio: 'ignore' });
|
|
517
|
+
return true;
|
|
518
|
+
} catch {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function selfClean() {
|
|
524
|
+
const hookPath = __filename;
|
|
525
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
526
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
527
|
+
|
|
528
|
+
try { fs.unlinkSync(hookPath); } catch {}
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
if (fs.existsSync(settingsPath)) {
|
|
532
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
533
|
+
const hookEvents = ['SessionStart', 'Stop', 'UserPromptSubmit'];
|
|
534
|
+
|
|
535
|
+
for (const event of hookEvents) {
|
|
536
|
+
if (settings.hooks?.[event]) {
|
|
537
|
+
settings.hooks[event] = settings.hooks[event].filter(entry => {
|
|
538
|
+
if (!entry.hooks) return true;
|
|
539
|
+
entry.hooks = entry.hooks.filter(h => !h.command?.includes('gal-'));
|
|
540
|
+
return entry.hooks.length > 0;
|
|
541
|
+
});
|
|
542
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (Object.keys(settings.hooks || {}).length === 0) delete settings.hooks;
|
|
547
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
548
|
+
}
|
|
549
|
+
} catch {}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (!isGalInstalled()) {
|
|
553
|
+
selfClean();
|
|
554
|
+
process.exit(0);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const hookInput = readHookInput();
|
|
558
|
+
const usagePath = getSessionUsagePath(hookInput);
|
|
559
|
+
|
|
560
|
+
if (!fs.existsSync(usagePath)) {
|
|
561
|
+
process.exit(0);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
let startedAt = null;
|
|
565
|
+
try {
|
|
566
|
+
const sessionState = JSON.parse(fs.readFileSync(usagePath, 'utf-8'));
|
|
567
|
+
startedAt = sessionState.startedAt || null;
|
|
568
|
+
} catch {}
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
fs.unlinkSync(usagePath);
|
|
572
|
+
} catch {}
|
|
573
|
+
|
|
574
|
+
if (!startedAt) {
|
|
575
|
+
process.exit(0);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const startedMs = new Date(startedAt).getTime();
|
|
579
|
+
if (!Number.isFinite(startedMs) || startedMs <= 0) {
|
|
580
|
+
process.exit(0);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const durationSeconds = Math.max(1, Math.round((Date.now() - startedMs) / 1000));
|
|
584
|
+
const galConfig = readGalConfig();
|
|
585
|
+
const orgName = galConfig?.defaultOrg;
|
|
586
|
+
|
|
587
|
+
if (!orgName) {
|
|
588
|
+
process.exit(0);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
execSync(
|
|
593
|
+
\`gal report-usage --provider claude --seconds \${durationSeconds} --org \${JSON.stringify(orgName)} --json\`,
|
|
594
|
+
{ stdio: 'pipe', timeout: 15000, shell: true },
|
|
595
|
+
);
|
|
596
|
+
} catch {}
|
|
597
|
+
|
|
598
|
+
process.exit(0);
|
|
599
|
+
`;
|
|
600
|
+
|
|
426
601
|
// =============================================================================
|
|
427
602
|
// Status Line Script Content
|
|
428
603
|
// =============================================================================
|
|
@@ -615,6 +790,7 @@ function installHook() {
|
|
|
615
790
|
const claudeDir = path.join(os.homedir(), '.claude');
|
|
616
791
|
const hooksDir = path.join(claudeDir, 'hooks');
|
|
617
792
|
const hookPath = path.join(hooksDir, 'gal-sync-reminder.js');
|
|
793
|
+
const stopHookPath = path.join(hooksDir, 'gal-usage-report.js');
|
|
618
794
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
619
795
|
|
|
620
796
|
try {
|
|
@@ -632,12 +808,24 @@ function installHook() {
|
|
|
632
808
|
needsUpdate = false;
|
|
633
809
|
}
|
|
634
810
|
}
|
|
811
|
+
let needsStopUpdate = true;
|
|
812
|
+
if (fs.existsSync(stopHookPath)) {
|
|
813
|
+
const existingContent = fs.readFileSync(stopHookPath, 'utf-8');
|
|
814
|
+
const versionMatch = existingContent.match(/GAL_HOOK_VERSION = "([^"]+)"/);
|
|
815
|
+
if (versionMatch && versionMatch[1] === HOOK_VERSION) {
|
|
816
|
+
needsStopUpdate = false;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
635
819
|
|
|
636
820
|
// Write the hook file if needed
|
|
637
821
|
if (needsUpdate) {
|
|
638
822
|
fs.writeFileSync(hookPath, HOOK_CONTENT, 'utf-8');
|
|
639
823
|
fs.chmodSync(hookPath, '755');
|
|
640
824
|
}
|
|
825
|
+
if (needsStopUpdate) {
|
|
826
|
+
fs.writeFileSync(stopHookPath, STOP_HOOK_CONTENT, 'utf-8');
|
|
827
|
+
fs.chmodSync(stopHookPath, '755');
|
|
828
|
+
}
|
|
641
829
|
|
|
642
830
|
// Update settings.json
|
|
643
831
|
let settings = {};
|
|
@@ -669,22 +857,31 @@ function installHook() {
|
|
|
669
857
|
const hookCommand = `node ${hookPath}`;
|
|
670
858
|
if (!settings.hooks) settings.hooks = {};
|
|
671
859
|
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
860
|
+
if (!settings.hooks.Stop) settings.hooks.Stop = [];
|
|
672
861
|
|
|
673
862
|
const alreadyRegistered = settings.hooks.SessionStart.some(entry =>
|
|
674
863
|
entry.hooks?.some(h => h.command?.includes('gal-sync-reminder'))
|
|
675
864
|
);
|
|
865
|
+
const stopAlreadyRegistered = settings.hooks.Stop.some(entry =>
|
|
866
|
+
entry.hooks?.some(h => h.command?.includes('gal-usage-report'))
|
|
867
|
+
);
|
|
676
868
|
|
|
677
869
|
if (!alreadyRegistered) {
|
|
678
870
|
settings.hooks.SessionStart.push({
|
|
679
871
|
hooks: [{ type: 'command', command: hookCommand }]
|
|
680
872
|
});
|
|
681
873
|
}
|
|
874
|
+
if (!stopAlreadyRegistered) {
|
|
875
|
+
settings.hooks.Stop.push({
|
|
876
|
+
hooks: [{ type: 'command', command: `node ${stopHookPath}` }]
|
|
877
|
+
});
|
|
878
|
+
}
|
|
682
879
|
|
|
683
880
|
// Write settings
|
|
684
881
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
685
882
|
|
|
686
|
-
if (needsUpdate) {
|
|
687
|
-
console.log('✓ GAL
|
|
883
|
+
if (needsUpdate || needsStopUpdate) {
|
|
884
|
+
console.log('✓ GAL Claude hooks installed');
|
|
688
885
|
}
|
|
689
886
|
return true;
|
|
690
887
|
} catch (error) {
|
package/dist/preuninstall.cjs
CHANGED
|
@@ -45,7 +45,7 @@ const os = require('os');
|
|
|
45
45
|
* This function surgically removes only GAL-specific hook entries while
|
|
46
46
|
* preserving the user's other hooks and settings. It handles:
|
|
47
47
|
* - Filtering GAL hooks from UserPromptSubmit array
|
|
48
|
-
* - Filtering GAL hooks from SessionStart
|
|
48
|
+
* - Filtering GAL hooks from SessionStart and Stop arrays
|
|
49
49
|
* - Removing empty hook arrays after filtering
|
|
50
50
|
* - Preserving the settings.json file structure
|
|
51
51
|
*
|
|
@@ -60,24 +60,31 @@ function removeGalHookEntries(settingsPath) {
|
|
|
60
60
|
|
|
61
61
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
62
62
|
|
|
63
|
-
if (!settings.hooks
|
|
63
|
+
if (!settings.hooks) {
|
|
64
64
|
return false;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
67
|
+
let removedAny = false;
|
|
68
|
+
|
|
69
|
+
for (const event of ['UserPromptSubmit', 'SessionStart', 'Stop']) {
|
|
70
|
+
if (!settings.hooks[event]) continue;
|
|
71
|
+
|
|
72
|
+
const originalLength = settings.hooks[event].length;
|
|
73
|
+
settings.hooks[event] = settings.hooks[event].filter((entry) => {
|
|
74
|
+
if (!entry.hooks) return true;
|
|
75
|
+
entry.hooks = entry.hooks.filter((hook) =>
|
|
76
|
+
!hook.command?.includes('gal-') && !hook.command?.includes('/gal/')
|
|
77
|
+
);
|
|
78
|
+
return entry.hooks.length > 0;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (settings.hooks[event].length === 0) {
|
|
82
|
+
delete settings.hooks[event];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if ((settings.hooks[event]?.length || 0) !== originalLength) {
|
|
86
|
+
removedAny = true;
|
|
87
|
+
}
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
// Remove empty hooks object
|
|
@@ -85,7 +92,7 @@ function removeGalHookEntries(settingsPath) {
|
|
|
85
92
|
delete settings.hooks;
|
|
86
93
|
}
|
|
87
94
|
|
|
88
|
-
if (
|
|
95
|
+
if (removedAny) {
|
|
89
96
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
90
97
|
return true;
|
|
91
98
|
}
|
package/package.json
CHANGED
package/scripts/postinstall.cjs
CHANGED
|
@@ -56,7 +56,7 @@ const cliVersion = cliPackageJson.version;
|
|
|
56
56
|
|
|
57
57
|
// Version markers for idempotency checks
|
|
58
58
|
// Bump these to force updates to installed files
|
|
59
|
-
const HOOK_VERSION = '4.
|
|
59
|
+
const HOOK_VERSION = '4.2.0'; // SessionStart + Stop hooks with local usage reporting (#4673)
|
|
60
60
|
const STATUS_LINE_VERSION = '1.0.0'; // Status line script
|
|
61
61
|
const RULES_VERSION = '1.0.0'; // GAL CLI rules
|
|
62
62
|
|
|
@@ -114,10 +114,12 @@ const fs = require('fs');
|
|
|
114
114
|
const path = require('path');
|
|
115
115
|
const { execSync, spawn } = require('child_process');
|
|
116
116
|
const os = require('os');
|
|
117
|
+
const crypto = require('crypto');
|
|
117
118
|
|
|
118
119
|
const GAL_DIR = '.gal';
|
|
119
120
|
const SYNC_STATE_FILE = 'sync-state.json';
|
|
120
121
|
const GAL_CONFIG_FILE = path.join(os.homedir(), '.gal', 'config.json');
|
|
122
|
+
const SESSION_USAGE_DIR = path.join(os.homedir(), '.gal', 'claude-session-usage');
|
|
121
123
|
|
|
122
124
|
function showMessage(message, status) {
|
|
123
125
|
// Queue telemetry event before showing message
|
|
@@ -156,7 +158,7 @@ function queueTelemetryEvent(status) {
|
|
|
156
158
|
|
|
157
159
|
// Add session start hook event
|
|
158
160
|
pending.push({
|
|
159
|
-
id:
|
|
161
|
+
id: crypto.randomUUID(),
|
|
160
162
|
eventType: 'hook_triggered',
|
|
161
163
|
timestamp: new Date().toISOString(),
|
|
162
164
|
payload: {
|
|
@@ -180,6 +182,45 @@ function queueTelemetryEvent(status) {
|
|
|
180
182
|
}
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
function readHookInput() {
|
|
186
|
+
try {
|
|
187
|
+
if (process.stdin.isTTY) return {};
|
|
188
|
+
const raw = fs.readFileSync(0, 'utf-8');
|
|
189
|
+
if (!raw || !raw.trim()) return {};
|
|
190
|
+
return JSON.parse(raw);
|
|
191
|
+
} catch {
|
|
192
|
+
return {};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function getSessionUsagePath(inputData) {
|
|
197
|
+
const identity =
|
|
198
|
+
(typeof inputData.session_id === 'string' && inputData.session_id) ||
|
|
199
|
+
(typeof inputData.transcript_path === 'string' && inputData.transcript_path) ||
|
|
200
|
+
process.cwd();
|
|
201
|
+
const sessionKey = crypto.createHash('sha256').update(identity).digest('hex');
|
|
202
|
+
return path.join(SESSION_USAGE_DIR, \`\${sessionKey}.json\`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function rememberSessionStart(inputData) {
|
|
206
|
+
try {
|
|
207
|
+
const filePath = getSessionUsagePath(inputData);
|
|
208
|
+
if (!fs.existsSync(SESSION_USAGE_DIR)) {
|
|
209
|
+
fs.mkdirSync(SESSION_USAGE_DIR, { recursive: true });
|
|
210
|
+
}
|
|
211
|
+
fs.writeFileSync(
|
|
212
|
+
filePath,
|
|
213
|
+
JSON.stringify({
|
|
214
|
+
sessionId: inputData.session_id || null,
|
|
215
|
+
transcriptPath: inputData.transcript_path || null,
|
|
216
|
+
source: inputData.source || null,
|
|
217
|
+
startedAt: new Date().toISOString(),
|
|
218
|
+
}),
|
|
219
|
+
'utf-8',
|
|
220
|
+
);
|
|
221
|
+
} catch {}
|
|
222
|
+
}
|
|
223
|
+
|
|
183
224
|
// =============================================================================
|
|
184
225
|
// Self-cleaning: Remove hook if GAL CLI is uninstalled
|
|
185
226
|
// =============================================================================
|
|
@@ -209,7 +250,7 @@ function selfClean() {
|
|
|
209
250
|
try {
|
|
210
251
|
if (fs.existsSync(settingsPath)) {
|
|
211
252
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
212
|
-
const hookEvents = ['SessionStart', 'UserPromptSubmit'];
|
|
253
|
+
const hookEvents = ['SessionStart', 'Stop', 'UserPromptSubmit'];
|
|
213
254
|
|
|
214
255
|
for (const event of hookEvents) {
|
|
215
256
|
if (settings.hooks?.[event]) {
|
|
@@ -228,6 +269,9 @@ function selfClean() {
|
|
|
228
269
|
} catch {}
|
|
229
270
|
}
|
|
230
271
|
|
|
272
|
+
const hookInput = readHookInput();
|
|
273
|
+
rememberSessionStart(hookInput);
|
|
274
|
+
|
|
231
275
|
// Check if GAL is installed, self-clean if not
|
|
232
276
|
if (!isGalInstalled()) {
|
|
233
277
|
selfClean();
|
|
@@ -423,6 +467,137 @@ try {
|
|
|
423
467
|
showMessage(syncMessage, 'synced');
|
|
424
468
|
`;
|
|
425
469
|
|
|
470
|
+
const STOP_HOOK_CONTENT = `#!/usr/bin/env node
|
|
471
|
+
/**
|
|
472
|
+
* GAL Local Usage Report Hook for Claude Code (Stop)
|
|
473
|
+
* Version: ${HOOK_VERSION}
|
|
474
|
+
*/
|
|
475
|
+
|
|
476
|
+
// GAL_HOOK_VERSION = "${HOOK_VERSION}"
|
|
477
|
+
|
|
478
|
+
const fs = require('fs');
|
|
479
|
+
const path = require('path');
|
|
480
|
+
const os = require('os');
|
|
481
|
+
const crypto = require('crypto');
|
|
482
|
+
const { execSync } = require('child_process');
|
|
483
|
+
|
|
484
|
+
const SESSION_USAGE_DIR = path.join(os.homedir(), '.gal', 'claude-session-usage');
|
|
485
|
+
const GAL_CONFIG_FILE = path.join(os.homedir(), '.gal', 'config.json');
|
|
486
|
+
|
|
487
|
+
function readHookInput() {
|
|
488
|
+
try {
|
|
489
|
+
if (process.stdin.isTTY) return {};
|
|
490
|
+
const raw = fs.readFileSync(0, 'utf-8');
|
|
491
|
+
if (!raw || !raw.trim()) return {};
|
|
492
|
+
return JSON.parse(raw);
|
|
493
|
+
} catch {
|
|
494
|
+
return {};
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function getSessionUsagePath(inputData) {
|
|
499
|
+
const identity =
|
|
500
|
+
(typeof inputData.session_id === 'string' && inputData.session_id) ||
|
|
501
|
+
(typeof inputData.transcript_path === 'string' && inputData.transcript_path) ||
|
|
502
|
+
process.cwd();
|
|
503
|
+
const sessionKey = crypto.createHash('sha256').update(identity).digest('hex');
|
|
504
|
+
return path.join(SESSION_USAGE_DIR, \`\${sessionKey}.json\`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function readGalConfig() {
|
|
508
|
+
if (!fs.existsSync(GAL_CONFIG_FILE)) return null;
|
|
509
|
+
try {
|
|
510
|
+
return JSON.parse(fs.readFileSync(GAL_CONFIG_FILE, 'utf-8'));
|
|
511
|
+
} catch { return null; }
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function isGalInstalled() {
|
|
515
|
+
try {
|
|
516
|
+
execSync('which gal', { stdio: 'ignore' });
|
|
517
|
+
return true;
|
|
518
|
+
} catch {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function selfClean() {
|
|
524
|
+
const hookPath = __filename;
|
|
525
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
526
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
527
|
+
|
|
528
|
+
try { fs.unlinkSync(hookPath); } catch {}
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
if (fs.existsSync(settingsPath)) {
|
|
532
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
533
|
+
const hookEvents = ['SessionStart', 'Stop', 'UserPromptSubmit'];
|
|
534
|
+
|
|
535
|
+
for (const event of hookEvents) {
|
|
536
|
+
if (settings.hooks?.[event]) {
|
|
537
|
+
settings.hooks[event] = settings.hooks[event].filter(entry => {
|
|
538
|
+
if (!entry.hooks) return true;
|
|
539
|
+
entry.hooks = entry.hooks.filter(h => !h.command?.includes('gal-'));
|
|
540
|
+
return entry.hooks.length > 0;
|
|
541
|
+
});
|
|
542
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (Object.keys(settings.hooks || {}).length === 0) delete settings.hooks;
|
|
547
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
548
|
+
}
|
|
549
|
+
} catch {}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (!isGalInstalled()) {
|
|
553
|
+
selfClean();
|
|
554
|
+
process.exit(0);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const hookInput = readHookInput();
|
|
558
|
+
const usagePath = getSessionUsagePath(hookInput);
|
|
559
|
+
|
|
560
|
+
if (!fs.existsSync(usagePath)) {
|
|
561
|
+
process.exit(0);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
let startedAt = null;
|
|
565
|
+
try {
|
|
566
|
+
const sessionState = JSON.parse(fs.readFileSync(usagePath, 'utf-8'));
|
|
567
|
+
startedAt = sessionState.startedAt || null;
|
|
568
|
+
} catch {}
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
fs.unlinkSync(usagePath);
|
|
572
|
+
} catch {}
|
|
573
|
+
|
|
574
|
+
if (!startedAt) {
|
|
575
|
+
process.exit(0);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const startedMs = new Date(startedAt).getTime();
|
|
579
|
+
if (!Number.isFinite(startedMs) || startedMs <= 0) {
|
|
580
|
+
process.exit(0);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const durationSeconds = Math.max(1, Math.round((Date.now() - startedMs) / 1000));
|
|
584
|
+
const galConfig = readGalConfig();
|
|
585
|
+
const orgName = galConfig?.defaultOrg;
|
|
586
|
+
|
|
587
|
+
if (!orgName) {
|
|
588
|
+
process.exit(0);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
execSync(
|
|
593
|
+
\`gal report-usage --provider claude --seconds \${durationSeconds} --org \${JSON.stringify(orgName)} --json\`,
|
|
594
|
+
{ stdio: 'pipe', timeout: 15000, shell: true },
|
|
595
|
+
);
|
|
596
|
+
} catch {}
|
|
597
|
+
|
|
598
|
+
process.exit(0);
|
|
599
|
+
`;
|
|
600
|
+
|
|
426
601
|
// =============================================================================
|
|
427
602
|
// Status Line Script Content
|
|
428
603
|
// =============================================================================
|
|
@@ -615,6 +790,7 @@ function installHook() {
|
|
|
615
790
|
const claudeDir = path.join(os.homedir(), '.claude');
|
|
616
791
|
const hooksDir = path.join(claudeDir, 'hooks');
|
|
617
792
|
const hookPath = path.join(hooksDir, 'gal-sync-reminder.js');
|
|
793
|
+
const stopHookPath = path.join(hooksDir, 'gal-usage-report.js');
|
|
618
794
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
619
795
|
|
|
620
796
|
try {
|
|
@@ -632,12 +808,24 @@ function installHook() {
|
|
|
632
808
|
needsUpdate = false;
|
|
633
809
|
}
|
|
634
810
|
}
|
|
811
|
+
let needsStopUpdate = true;
|
|
812
|
+
if (fs.existsSync(stopHookPath)) {
|
|
813
|
+
const existingContent = fs.readFileSync(stopHookPath, 'utf-8');
|
|
814
|
+
const versionMatch = existingContent.match(/GAL_HOOK_VERSION = "([^"]+)"/);
|
|
815
|
+
if (versionMatch && versionMatch[1] === HOOK_VERSION) {
|
|
816
|
+
needsStopUpdate = false;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
635
819
|
|
|
636
820
|
// Write the hook file if needed
|
|
637
821
|
if (needsUpdate) {
|
|
638
822
|
fs.writeFileSync(hookPath, HOOK_CONTENT, 'utf-8');
|
|
639
823
|
fs.chmodSync(hookPath, '755');
|
|
640
824
|
}
|
|
825
|
+
if (needsStopUpdate) {
|
|
826
|
+
fs.writeFileSync(stopHookPath, STOP_HOOK_CONTENT, 'utf-8');
|
|
827
|
+
fs.chmodSync(stopHookPath, '755');
|
|
828
|
+
}
|
|
641
829
|
|
|
642
830
|
// Update settings.json
|
|
643
831
|
let settings = {};
|
|
@@ -669,22 +857,31 @@ function installHook() {
|
|
|
669
857
|
const hookCommand = `node ${hookPath}`;
|
|
670
858
|
if (!settings.hooks) settings.hooks = {};
|
|
671
859
|
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
860
|
+
if (!settings.hooks.Stop) settings.hooks.Stop = [];
|
|
672
861
|
|
|
673
862
|
const alreadyRegistered = settings.hooks.SessionStart.some(entry =>
|
|
674
863
|
entry.hooks?.some(h => h.command?.includes('gal-sync-reminder'))
|
|
675
864
|
);
|
|
865
|
+
const stopAlreadyRegistered = settings.hooks.Stop.some(entry =>
|
|
866
|
+
entry.hooks?.some(h => h.command?.includes('gal-usage-report'))
|
|
867
|
+
);
|
|
676
868
|
|
|
677
869
|
if (!alreadyRegistered) {
|
|
678
870
|
settings.hooks.SessionStart.push({
|
|
679
871
|
hooks: [{ type: 'command', command: hookCommand }]
|
|
680
872
|
});
|
|
681
873
|
}
|
|
874
|
+
if (!stopAlreadyRegistered) {
|
|
875
|
+
settings.hooks.Stop.push({
|
|
876
|
+
hooks: [{ type: 'command', command: `node ${stopHookPath}` }]
|
|
877
|
+
});
|
|
878
|
+
}
|
|
682
879
|
|
|
683
880
|
// Write settings
|
|
684
881
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
685
882
|
|
|
686
|
-
if (needsUpdate) {
|
|
687
|
-
console.log('✓ GAL
|
|
883
|
+
if (needsUpdate || needsStopUpdate) {
|
|
884
|
+
console.log('✓ GAL Claude hooks installed');
|
|
688
885
|
}
|
|
689
886
|
return true;
|
|
690
887
|
} catch (error) {
|