@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 CHANGED
@@ -3970,7 +3970,7 @@ var cliVersion, defaultApiUrl, BUILD_CONSTANTS, constants_default;
3970
3970
  var init_constants = __esm({
3971
3971
  "src/constants.ts"() {
3972
3972
  "use strict";
3973
- cliVersion = true ? "0.0.406" : "0.0.0-dev";
3973
+ cliVersion = true ? "0.0.407" : "0.0.0-dev";
3974
3974
  defaultApiUrl = true ? "https://api.gal.run" : "http://localhost:3000";
3975
3975
  BUILD_CONSTANTS = Object.freeze([cliVersion, defaultApiUrl]);
3976
3976
  constants_default = BUILD_CONSTANTS;
@@ -4880,7 +4880,7 @@ function detectEnvironment() {
4880
4880
  return "dev";
4881
4881
  }
4882
4882
  try {
4883
- const version2 = true ? "0.0.406" : void 0;
4883
+ const version2 = true ? "0.0.407" : void 0;
4884
4884
  if (version2 && version2.includes("-local")) {
4885
4885
  return "dev";
4886
4886
  }
@@ -4919,11 +4919,11 @@ var init_config_manager = __esm({
4919
4919
  */
4920
4920
  static load() {
4921
4921
  const env2 = detectEnvironment();
4922
- const defaultApiUrl18 = API_URLS[env2];
4922
+ const defaultApiUrl19 = API_URLS[env2];
4923
4923
  const envApiUrl = process.env.GAL_API_URL;
4924
4924
  if (!(0, import_fs4.existsSync)(CONFIG_FILE)) {
4925
4925
  return {
4926
- apiUrl: envApiUrl || defaultApiUrl18
4926
+ apiUrl: envApiUrl || defaultApiUrl19
4927
4927
  };
4928
4928
  }
4929
4929
  try {
@@ -4934,7 +4934,7 @@ var init_config_manager = __esm({
4934
4934
  return config2;
4935
4935
  }
4936
4936
  if (!config2.apiUrl) {
4937
- config2.apiUrl = defaultApiUrl18;
4937
+ config2.apiUrl = defaultApiUrl19;
4938
4938
  }
4939
4939
  if (env2 === "test") {
4940
4940
  return config2;
@@ -4942,20 +4942,20 @@ var init_config_manager = __esm({
4942
4942
  if (env2 === "prod") {
4943
4943
  const isValidUrl = config2.apiUrl === API_URLS.prod;
4944
4944
  if (!isValidUrl) {
4945
- config2.apiUrl = defaultApiUrl18;
4945
+ config2.apiUrl = defaultApiUrl19;
4946
4946
  }
4947
4947
  }
4948
4948
  if (env2 === "dev") {
4949
- config2.apiUrl = defaultApiUrl18;
4949
+ config2.apiUrl = defaultApiUrl19;
4950
4950
  }
4951
4951
  if (env2 === "dev" && config2.apiUrl.includes("localhost")) {
4952
- config2.apiUrl = defaultApiUrl18;
4952
+ config2.apiUrl = defaultApiUrl19;
4953
4953
  }
4954
4954
  return config2;
4955
4955
  } catch (error3) {
4956
4956
  console.error("Error loading config:", error3);
4957
4957
  return {
4958
- apiUrl: envApiUrl || defaultApiUrl18
4958
+ apiUrl: envApiUrl || defaultApiUrl19
4959
4959
  };
4960
4960
  }
4961
4961
  }
@@ -5249,7 +5249,7 @@ function getId() {
5249
5249
  }
5250
5250
  function getCliVersion() {
5251
5251
  try {
5252
- return true ? "0.0.406" : "0.0.0-dev";
5252
+ return true ? "0.0.407" : "0.0.0-dev";
5253
5253
  } catch {
5254
5254
  return "0.0.0-dev";
5255
5255
  }
@@ -13596,6 +13596,9 @@ var init_HttpScanResultRepository = __esm({
13596
13596
  async saveDiscoveredConfigsCache(_orgName, _configs) {
13597
13597
  throw new Error("saveDiscoveredConfigsCache is a server-side operation not available from HTTP clients");
13598
13598
  }
13599
+ async getDiscoveredConfigsContentBatch(_orgName, _items) {
13600
+ throw new Error("getDiscoveredConfigsContentBatch is a server-side operation not available from HTTP clients");
13601
+ }
13599
13602
  async invalidateDiscoveredConfigsCache(orgName) {
13600
13603
  await this.fetch(`/organizations/${orgName}/discovered-configs-cache`, {
13601
13604
  method: "DELETE"
@@ -46525,10 +46528,12 @@ const fs = require('fs');
46525
46528
  const path = require('path');
46526
46529
  const { execSync, spawn } = require('child_process');
46527
46530
  const os = require('os');
46531
+ const crypto = require('crypto');
46528
46532
 
46529
46533
  const GAL_DIR = '.gal';
46530
46534
  const SYNC_STATE_FILE = 'sync-state.json';
46531
46535
  const GAL_CONFIG_FILE = path.join(os.homedir(), '.gal', 'config.json');
46536
+ const SESSION_USAGE_DIR = path.join(os.homedir(), '.gal', 'claude-session-usage');
46532
46537
 
46533
46538
  function showMessage(message, status) {
46534
46539
  queueTelemetryEvent(status);
@@ -46552,7 +46557,7 @@ function queueTelemetryEvent(status) {
46552
46557
  } catch {}
46553
46558
 
46554
46559
  pending.push({
46555
- id: require('crypto').randomUUID(),
46560
+ id: crypto.randomUUID(),
46556
46561
  eventType: 'hook_triggered',
46557
46562
  timestamp: new Date().toISOString(),
46558
46563
  payload: {
@@ -46587,6 +46592,45 @@ function isGalInstalled() {
46587
46592
  }
46588
46593
  }
46589
46594
 
46595
+ function readHookInput() {
46596
+ try {
46597
+ if (process.stdin.isTTY) return {};
46598
+ const raw = fs.readFileSync(0, 'utf-8');
46599
+ if (!raw || !raw.trim()) return {};
46600
+ return JSON.parse(raw);
46601
+ } catch {
46602
+ return {};
46603
+ }
46604
+ }
46605
+
46606
+ function getSessionUsagePath(inputData) {
46607
+ const identity =
46608
+ (typeof inputData.session_id === 'string' && inputData.session_id) ||
46609
+ (typeof inputData.transcript_path === 'string' && inputData.transcript_path) ||
46610
+ process.cwd();
46611
+ const sessionKey = crypto.createHash('sha256').update(identity).digest('hex');
46612
+ return path.join(SESSION_USAGE_DIR, \`\${sessionKey}.json\`);
46613
+ }
46614
+
46615
+ function rememberSessionStart(inputData) {
46616
+ try {
46617
+ const filePath = getSessionUsagePath(inputData);
46618
+ if (!fs.existsSync(SESSION_USAGE_DIR)) {
46619
+ fs.mkdirSync(SESSION_USAGE_DIR, { recursive: true });
46620
+ }
46621
+ fs.writeFileSync(
46622
+ filePath,
46623
+ JSON.stringify({
46624
+ sessionId: inputData.session_id || null,
46625
+ transcriptPath: inputData.transcript_path || null,
46626
+ source: inputData.source || null,
46627
+ startedAt: new Date().toISOString(),
46628
+ }),
46629
+ 'utf-8',
46630
+ );
46631
+ } catch {}
46632
+ }
46633
+
46590
46634
  function selfClean() {
46591
46635
  const hookPath = __filename;
46592
46636
  const claudeDir = path.join(os.homedir(), '.claude');
@@ -46599,7 +46643,7 @@ function selfClean() {
46599
46643
  try {
46600
46644
  if (fs.existsSync(settingsPath)) {
46601
46645
  const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
46602
- const hookEvents = ['SessionStart', 'UserPromptSubmit'];
46646
+ const hookEvents = ['SessionStart', 'Stop', 'UserPromptSubmit'];
46603
46647
 
46604
46648
  for (const event of hookEvents) {
46605
46649
  if (settings.hooks?.[event]) {
@@ -46618,6 +46662,9 @@ function selfClean() {
46618
46662
  } catch {}
46619
46663
  }
46620
46664
 
46665
+ const hookInput = readHookInput();
46666
+ rememberSessionStart(hookInput);
46667
+
46621
46668
  if (!isGalInstalled()) {
46622
46669
  selfClean();
46623
46670
  process.exit(0);
@@ -46820,6 +46867,144 @@ try {
46820
46867
  showMessage(syncMessage, 'synced');
46821
46868
  `;
46822
46869
  }
46870
+ function generateStopHookContent() {
46871
+ return `#!/usr/bin/env node
46872
+ /**
46873
+ * GAL Local Usage Report Hook for Claude Code (Stop)
46874
+ * Version: ${HOOK_VERSION}
46875
+ * Installed by: gal hooks install (v${cliVersion3})
46876
+ *
46877
+ * Records local Claude session duration into GAL's provider-usage system so
46878
+ * token management can combine local and background-agent consumption.
46879
+ */
46880
+
46881
+ // GAL_HOOK_VERSION = "${HOOK_VERSION}"
46882
+
46883
+ const fs = require('fs');
46884
+ const path = require('path');
46885
+ const os = require('os');
46886
+ const crypto = require('crypto');
46887
+ const { execSync } = require('child_process');
46888
+
46889
+ const SESSION_USAGE_DIR = path.join(os.homedir(), '.gal', 'claude-session-usage');
46890
+ const GAL_CONFIG_FILE = path.join(os.homedir(), '.gal', 'config.json');
46891
+
46892
+ function readHookInput() {
46893
+ try {
46894
+ if (process.stdin.isTTY) return {};
46895
+ const raw = fs.readFileSync(0, 'utf-8');
46896
+ if (!raw || !raw.trim()) return {};
46897
+ return JSON.parse(raw);
46898
+ } catch {
46899
+ return {};
46900
+ }
46901
+ }
46902
+
46903
+ function getSessionUsagePath(inputData) {
46904
+ const identity =
46905
+ (typeof inputData.session_id === 'string' && inputData.session_id) ||
46906
+ (typeof inputData.transcript_path === 'string' && inputData.transcript_path) ||
46907
+ process.cwd();
46908
+ const sessionKey = crypto.createHash('sha256').update(identity).digest('hex');
46909
+ return path.join(SESSION_USAGE_DIR, \`\${sessionKey}.json\`);
46910
+ }
46911
+
46912
+ function readGalConfig() {
46913
+ if (!fs.existsSync(GAL_CONFIG_FILE)) return null;
46914
+ try {
46915
+ return JSON.parse(fs.readFileSync(GAL_CONFIG_FILE, 'utf-8'));
46916
+ } catch {
46917
+ return null;
46918
+ }
46919
+ }
46920
+
46921
+ function isGalInstalled() {
46922
+ try {
46923
+ execSync('which gal', { stdio: 'ignore' });
46924
+ return true;
46925
+ } catch {
46926
+ return false;
46927
+ }
46928
+ }
46929
+
46930
+ function selfClean() {
46931
+ const hookPath = __filename;
46932
+ const claudeDir = path.join(os.homedir(), '.claude');
46933
+ const settingsPath = path.join(claudeDir, 'settings.json');
46934
+
46935
+ try { fs.unlinkSync(hookPath); } catch {}
46936
+
46937
+ try {
46938
+ if (fs.existsSync(settingsPath)) {
46939
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
46940
+ const hookEvents = ['SessionStart', 'Stop', 'UserPromptSubmit'];
46941
+
46942
+ for (const event of hookEvents) {
46943
+ if (settings.hooks?.[event]) {
46944
+ settings.hooks[event] = settings.hooks[event].filter(entry => {
46945
+ if (!entry.hooks) return true;
46946
+ entry.hooks = entry.hooks.filter(h => !h.command?.includes('gal-'));
46947
+ return entry.hooks.length > 0;
46948
+ });
46949
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
46950
+ }
46951
+ }
46952
+
46953
+ if (Object.keys(settings.hooks || {}).length === 0) delete settings.hooks;
46954
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
46955
+ }
46956
+ } catch {}
46957
+ }
46958
+
46959
+ if (!isGalInstalled()) {
46960
+ selfClean();
46961
+ process.exit(0);
46962
+ }
46963
+
46964
+ const hookInput = readHookInput();
46965
+ const usagePath = getSessionUsagePath(hookInput);
46966
+
46967
+ if (!fs.existsSync(usagePath)) {
46968
+ process.exit(0);
46969
+ }
46970
+
46971
+ let startedAt = null;
46972
+ try {
46973
+ const sessionState = JSON.parse(fs.readFileSync(usagePath, 'utf-8'));
46974
+ startedAt = sessionState.startedAt || null;
46975
+ } catch {}
46976
+
46977
+ try {
46978
+ fs.unlinkSync(usagePath);
46979
+ } catch {}
46980
+
46981
+ if (!startedAt) {
46982
+ process.exit(0);
46983
+ }
46984
+
46985
+ const startedMs = new Date(startedAt).getTime();
46986
+ if (!Number.isFinite(startedMs) || startedMs <= 0) {
46987
+ process.exit(0);
46988
+ }
46989
+
46990
+ const durationSeconds = Math.max(1, Math.round((Date.now() - startedMs) / 1000));
46991
+ const galConfig = readGalConfig();
46992
+ const orgName = galConfig?.defaultOrg;
46993
+
46994
+ if (!orgName) {
46995
+ process.exit(0);
46996
+ }
46997
+
46998
+ try {
46999
+ execSync(
47000
+ \`gal report-usage --provider claude --seconds \${durationSeconds} --org \${JSON.stringify(orgName)} --json\`,
47001
+ { stdio: 'pipe', timeout: 15000, shell: true },
47002
+ );
47003
+ } catch {}
47004
+
47005
+ process.exit(0);
47006
+ `;
47007
+ }
46823
47008
  function generateRulesContent() {
46824
47009
  return `# GAL CLI
46825
47010
 
@@ -46861,6 +47046,13 @@ function isHookRegistered() {
46861
47046
  (entry) => entry.hooks?.some((h) => h.command?.includes("gal-sync-reminder"))
46862
47047
  );
46863
47048
  }
47049
+ function isStopHookRegistered() {
47050
+ const settings = readSettings2();
47051
+ if (!settings.hooks?.Stop) return false;
47052
+ return settings.hooks.Stop.some(
47053
+ (entry) => entry.hooks?.some((h) => h.command?.includes("gal-usage-report"))
47054
+ );
47055
+ }
46864
47056
  function getHookFileStatus() {
46865
47057
  if (!(0, import_fs17.existsSync)(HOOK_PATH)) {
46866
47058
  return { exists: false, version: null, current: false };
@@ -46874,6 +47066,19 @@ function getHookFileStatus() {
46874
47066
  return { exists: true, version: null, current: false };
46875
47067
  }
46876
47068
  }
47069
+ function getStopHookFileStatus() {
47070
+ if (!(0, import_fs17.existsSync)(STOP_HOOK_PATH)) {
47071
+ return { exists: false, version: null, current: false };
47072
+ }
47073
+ try {
47074
+ const content = (0, import_fs17.readFileSync)(STOP_HOOK_PATH, "utf-8");
47075
+ const versionMatch = content.match(/GAL_HOOK_VERSION = "([^"]+)"/);
47076
+ const version2 = versionMatch ? versionMatch[1] : null;
47077
+ return { exists: true, version: version2, current: version2 === HOOK_VERSION };
47078
+ } catch {
47079
+ return { exists: true, version: null, current: false };
47080
+ }
47081
+ }
46877
47082
  function getRulesFileStatus() {
46878
47083
  if (!(0, import_fs17.existsSync)(RULES_PATH)) {
46879
47084
  return { exists: false, version: null, current: false };
@@ -46902,6 +47107,12 @@ function installHooks() {
46902
47107
  (0, import_fs17.chmodSync)(HOOK_PATH, "755");
46903
47108
  hookInstalled = true;
46904
47109
  }
47110
+ const stopHookStatus = getStopHookFileStatus();
47111
+ if (!stopHookStatus.exists || !stopHookStatus.current) {
47112
+ (0, import_fs17.writeFileSync)(STOP_HOOK_PATH, generateStopHookContent(), "utf-8");
47113
+ (0, import_fs17.chmodSync)(STOP_HOOK_PATH, "755");
47114
+ hookInstalled = true;
47115
+ }
46905
47116
  const settings = readSettings2();
46906
47117
  if (settings.hooks?.UserPromptSubmit) {
46907
47118
  settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter((entry) => {
@@ -46917,6 +47128,7 @@ function installHooks() {
46917
47128
  }
46918
47129
  if (!settings.hooks) settings.hooks = {};
46919
47130
  if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
47131
+ if (!settings.hooks.Stop) settings.hooks.Stop = [];
46920
47132
  const alreadyRegistered = settings.hooks.SessionStart.some(
46921
47133
  (entry) => entry.hooks?.some((h) => h.command?.includes("gal-sync-reminder"))
46922
47134
  );
@@ -46927,6 +47139,16 @@ function installHooks() {
46927
47139
  });
46928
47140
  hookInstalled = true;
46929
47141
  }
47142
+ const stopAlreadyRegistered = settings.hooks.Stop.some(
47143
+ (entry) => entry.hooks?.some((h) => h.command?.includes("gal-usage-report"))
47144
+ );
47145
+ if (!stopAlreadyRegistered) {
47146
+ const stopHookCommand = `node "${STOP_HOOK_PATH}"`;
47147
+ settings.hooks.Stop.push({
47148
+ hooks: [{ type: "command", command: stopHookCommand }]
47149
+ });
47150
+ hookInstalled = true;
47151
+ }
46930
47152
  writeSettings(settings);
46931
47153
  } catch (error3) {
46932
47154
  errors.push(`Hook installation failed: ${error3 instanceof Error ? error3.message : String(error3)}`);
@@ -46985,13 +47207,17 @@ function uninstallHooks() {
46985
47207
  (0, import_fs17.unlinkSync)(HOOK_PATH);
46986
47208
  hookRemoved = true;
46987
47209
  }
47210
+ if ((0, import_fs17.existsSync)(STOP_HOOK_PATH)) {
47211
+ (0, import_fs17.unlinkSync)(STOP_HOOK_PATH);
47212
+ hookRemoved = true;
47213
+ }
46988
47214
  } catch (error3) {
46989
47215
  errors.push(`Failed to remove hook file: ${error3 instanceof Error ? error3.message : String(error3)}`);
46990
47216
  }
46991
47217
  try {
46992
47218
  if ((0, import_fs17.existsSync)(SETTINGS_PATH)) {
46993
47219
  const settings = readSettings2();
46994
- const hookEvents = ["SessionStart", "UserPromptSubmit"];
47220
+ const hookEvents = ["SessionStart", "Stop", "UserPromptSubmit"];
46995
47221
  for (const event of hookEvents) {
46996
47222
  if (settings.hooks?.[event]) {
46997
47223
  const originalLength = settings.hooks[event].length;
@@ -47110,6 +47336,7 @@ function createHooksCommand() {
47110
47336
  if (options.force) {
47111
47337
  try {
47112
47338
  if ((0, import_fs17.existsSync)(HOOK_PATH)) (0, import_fs17.unlinkSync)(HOOK_PATH);
47339
+ if ((0, import_fs17.existsSync)(STOP_HOOK_PATH)) (0, import_fs17.unlinkSync)(STOP_HOOK_PATH);
47113
47340
  if ((0, import_fs17.existsSync)(RULES_PATH)) (0, import_fs17.unlinkSync)(RULES_PATH);
47114
47341
  } catch {
47115
47342
  }
@@ -47121,10 +47348,11 @@ function createHooksCommand() {
47121
47348
  }
47122
47349
  }
47123
47350
  if (result.hookInstalled) {
47124
- console.log(source_default.green(" v Claude Code SessionStart hook installed"));
47351
+ console.log(source_default.green(" v Claude Code SessionStart + Stop hooks installed"));
47125
47352
  console.log(source_default.dim(` ${HOOK_PATH}`));
47353
+ console.log(source_default.dim(` ${STOP_HOOK_PATH}`));
47126
47354
  } else if (result.errors.length === 0) {
47127
- console.log(source_default.dim(" - Claude Code SessionStart hook already up to date"));
47355
+ console.log(source_default.dim(" - Claude Code SessionStart + Stop hooks already up to date"));
47128
47356
  }
47129
47357
  if (result.rulesInstalled) {
47130
47358
  console.log(source_default.green(" v GAL CLI rules installed"));
@@ -47160,6 +47388,7 @@ function createHooksCommand() {
47160
47388
  }
47161
47389
  console.log(source_default.dim("What these hooks do:"));
47162
47390
  console.log(source_default.dim(" - Auto-sync org config at session start (gal sync --pull)"));
47391
+ console.log(source_default.dim(" - Record local Claude usage at session stop (gal report-usage)"));
47163
47392
  console.log(source_default.dim(" - Show sync status notifications"));
47164
47393
  console.log("");
47165
47394
  });
@@ -47172,9 +47401,9 @@ function createHooksCommand() {
47172
47401
  }
47173
47402
  }
47174
47403
  if (result.hookRemoved) {
47175
- console.log(source_default.green(" v SessionStart hook removed"));
47404
+ console.log(source_default.green(" v SessionStart + Stop hooks removed"));
47176
47405
  } else {
47177
- console.log(source_default.dim(" - No SessionStart hook found"));
47406
+ console.log(source_default.dim(" - No SessionStart/Stop hooks found"));
47178
47407
  }
47179
47408
  if (result.rulesRemoved) {
47180
47409
  console.log(source_default.green(" v GAL CLI rules removed"));
@@ -47189,6 +47418,8 @@ function createHooksCommand() {
47189
47418
  command.command("status").description("Show current hook installation status").option("--json", "Output status as JSON").action((options) => {
47190
47419
  const hookFile = getHookFileStatus();
47191
47420
  const hookRegistered = isHookRegistered();
47421
+ const stopHookFile = getStopHookFileStatus();
47422
+ const stopHookRegistered = isStopHookRegistered();
47192
47423
  const rulesFile = getRulesFileStatus();
47193
47424
  const multiPlatformStatus = [];
47194
47425
  const cursorDetected = isAgentInstalled("cursor");
@@ -47243,14 +47474,21 @@ function createHooksCommand() {
47243
47474
  registeredInSettings: hookRegistered,
47244
47475
  path: HOOK_PATH
47245
47476
  },
47477
+ stopHook: {
47478
+ fileExists: stopHookFile.exists,
47479
+ fileVersion: stopHookFile.version,
47480
+ fileCurrent: stopHookFile.current,
47481
+ registeredInSettings: stopHookRegistered,
47482
+ path: STOP_HOOK_PATH
47483
+ },
47246
47484
  rules: {
47247
47485
  fileExists: rulesFile.exists,
47248
47486
  fileVersion: rulesFile.version,
47249
47487
  fileCurrent: rulesFile.current,
47250
47488
  path: RULES_PATH
47251
47489
  },
47252
- installed: hookFile.exists && hookRegistered && rulesFile.exists,
47253
- upToDate: hookFile.current && rulesFile.current && hookRegistered
47490
+ installed: hookFile.exists && hookRegistered && stopHookFile.exists && stopHookRegistered && rulesFile.exists,
47491
+ upToDate: hookFile.current && hookRegistered && stopHookFile.current && stopHookRegistered && rulesFile.current
47254
47492
  },
47255
47493
  multiPlatform: multiPlatformStatus
47256
47494
  }, null, 2));
@@ -47268,6 +47506,16 @@ function createHooksCommand() {
47268
47506
  console.log(source_default.yellow(" Warning: Hook file exists but not registered in settings.json"));
47269
47507
  }
47270
47508
  }
47509
+ const stopHookIcon = stopHookFile.exists && stopHookRegistered ? source_default.green("v") : source_default.red("x");
47510
+ const stopHookLabel = stopHookFile.exists && stopHookRegistered ? "Installed" : "Not installed";
47511
+ console.log(` ${stopHookIcon} Stop usage hook: ${stopHookLabel}`);
47512
+ if (stopHookFile.exists) {
47513
+ console.log(source_default.dim(` Version: ${stopHookFile.version || "unknown"}${stopHookFile.current ? " (current)" : " (update available)"}`));
47514
+ console.log(source_default.dim(` Path: ${STOP_HOOK_PATH}`));
47515
+ if (!stopHookRegistered) {
47516
+ console.log(source_default.yellow(" Warning: Stop hook file exists but not registered in settings.json"));
47517
+ }
47518
+ }
47271
47519
  const rulesIcon = rulesFile.exists ? source_default.green("v") : source_default.red("x");
47272
47520
  const rulesLabel = rulesFile.exists ? "Installed" : "Not installed";
47273
47521
  console.log(` ${rulesIcon} GAL CLI rules: ${rulesLabel}`);
@@ -47289,8 +47537,8 @@ function createHooksCommand() {
47289
47537
  }
47290
47538
  }
47291
47539
  console.log("");
47292
- const allInstalled = hookFile.exists && hookRegistered && rulesFile.exists;
47293
- const allCurrent = hookFile.current && rulesFile.current && hookRegistered;
47540
+ const allInstalled = hookFile.exists && hookRegistered && stopHookFile.exists && stopHookRegistered && rulesFile.exists;
47541
+ const allCurrent = hookFile.current && stopHookFile.current && rulesFile.current && hookRegistered && stopHookRegistered;
47294
47542
  if (!allInstalled) {
47295
47543
  console.log(source_default.yellow(" Run `gal hooks install` to set up AI agent integration.\n"));
47296
47544
  } else if (!allCurrent) {
@@ -47408,7 +47656,7 @@ fi
47408
47656
  });
47409
47657
  return command;
47410
47658
  }
47411
- var import_fs17, import_path16, import_os14, import_child_process6, CLAUDE_DIR, HOOKS_DIR, SETTINGS_PATH, RULES_DIR, cliVersion3, HOOK_FILENAME, HOOK_PATH, RULES_PATH, HOOK_VERSION, RULES_VERSION;
47659
+ var import_fs17, import_path16, import_os14, import_child_process6, CLAUDE_DIR, HOOKS_DIR, SETTINGS_PATH, RULES_DIR, cliVersion3, HOOK_FILENAME, STOP_HOOK_FILENAME, HOOK_PATH, STOP_HOOK_PATH, RULES_PATH, HOOK_VERSION, RULES_VERSION;
47412
47660
  var init_hooks = __esm({
47413
47661
  "src/commands/hooks.ts"() {
47414
47662
  "use strict";
@@ -47425,9 +47673,11 @@ var init_hooks = __esm({
47425
47673
  RULES_DIR = (0, import_path16.join)(CLAUDE_DIR, "rules");
47426
47674
  cliVersion3 = constants_default[0];
47427
47675
  HOOK_FILENAME = "gal-sync-reminder.js";
47676
+ STOP_HOOK_FILENAME = "gal-usage-report.js";
47428
47677
  HOOK_PATH = (0, import_path16.join)(HOOKS_DIR, HOOK_FILENAME);
47678
+ STOP_HOOK_PATH = (0, import_path16.join)(HOOKS_DIR, STOP_HOOK_FILENAME);
47429
47679
  RULES_PATH = (0, import_path16.join)(RULES_DIR, "gal-cli.md");
47430
- HOOK_VERSION = "4.1.1";
47680
+ HOOK_VERSION = "4.2.0";
47431
47681
  RULES_VERSION = "1.0.0";
47432
47682
  }
47433
47683
  });
@@ -63178,6 +63428,141 @@ var init_queue_seed = __esm({
63178
63428
  }
63179
63429
  });
63180
63430
 
63431
+ // src/commands/report-usage.ts
63432
+ async function fetchWithAuth3(url, authToken, method = "GET", body) {
63433
+ const controller = new AbortController();
63434
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
63435
+ try {
63436
+ return await fetch(url, {
63437
+ method,
63438
+ headers: {
63439
+ Authorization: `Bearer ${authToken}`,
63440
+ "Content-Type": "application/json"
63441
+ },
63442
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {},
63443
+ signal: controller.signal
63444
+ });
63445
+ } finally {
63446
+ clearTimeout(timeoutId);
63447
+ }
63448
+ }
63449
+ function createReportUsageCommand() {
63450
+ return new Command("report-usage").description("Record local provider usage for token-management workflows").requiredOption("--provider <provider>", "Provider name (currently only claude)").requiredOption("--seconds <seconds>", "Duration to record in seconds").option("--org <org>", "Organization name (defaults to config.defaultOrg)").option("--json", "Output as JSON").action(async (options) => {
63451
+ const config2 = ConfigManager.load();
63452
+ const authToken = config2.authToken || config2.apiKey;
63453
+ if (!authToken) {
63454
+ const message = "Not authenticated. Run: gal auth login";
63455
+ if (options.json) {
63456
+ console.log(JSON.stringify({ success: false, error: message }));
63457
+ } else {
63458
+ console.error(source_default.red(message));
63459
+ }
63460
+ process.exit(1);
63461
+ }
63462
+ const orgName = options.org || config2.defaultOrg;
63463
+ if (!orgName) {
63464
+ const message = "No organization specified. Use --org or set defaultOrg.";
63465
+ if (options.json) {
63466
+ console.log(JSON.stringify({ success: false, error: message }));
63467
+ } else {
63468
+ console.error(source_default.red(message));
63469
+ }
63470
+ process.exit(1);
63471
+ }
63472
+ if (options.provider !== "claude") {
63473
+ const message = "Only claude usage reporting is currently supported.";
63474
+ if (options.json) {
63475
+ console.log(JSON.stringify({ success: false, error: message }));
63476
+ } else {
63477
+ console.error(source_default.red(message));
63478
+ }
63479
+ process.exit(1);
63480
+ }
63481
+ const totalSeconds = Math.round(Number(options.seconds));
63482
+ if (!Number.isFinite(totalSeconds) || totalSeconds <= 0) {
63483
+ const message = "--seconds must be a positive number.";
63484
+ if (options.json) {
63485
+ console.log(JSON.stringify({ success: false, error: message }));
63486
+ } else {
63487
+ console.error(source_default.red(message));
63488
+ }
63489
+ process.exit(1);
63490
+ }
63491
+ const apiUrl = config2.apiUrl || defaultApiUrl14;
63492
+ let remaining = totalSeconds;
63493
+ let recordedSeconds = 0;
63494
+ let chunkCount = 0;
63495
+ let latestPayload = null;
63496
+ try {
63497
+ while (remaining > 0) {
63498
+ const durationSeconds = Math.min(remaining, MAX_USAGE_CHUNK_SECONDS);
63499
+ const response = await fetchWithAuth3(
63500
+ `${apiUrl}/api/usage/providers/local`,
63501
+ authToken,
63502
+ "POST",
63503
+ {
63504
+ organizationId: orgName,
63505
+ provider: options.provider,
63506
+ durationSeconds
63507
+ }
63508
+ );
63509
+ const payload = await response.json().catch(() => ({}));
63510
+ if (!response.ok) {
63511
+ throw new Error(
63512
+ typeof payload?.message === "string" ? payload.message : `HTTP ${response.status}`
63513
+ );
63514
+ }
63515
+ recordedSeconds += durationSeconds;
63516
+ remaining -= durationSeconds;
63517
+ chunkCount += 1;
63518
+ latestPayload = payload;
63519
+ }
63520
+ if (options.json) {
63521
+ console.log(
63522
+ JSON.stringify(
63523
+ {
63524
+ success: true,
63525
+ provider: options.provider,
63526
+ organizationId: orgName,
63527
+ recordedSeconds,
63528
+ chunkCount,
63529
+ result: latestPayload
63530
+ },
63531
+ null,
63532
+ 2
63533
+ )
63534
+ );
63535
+ } else {
63536
+ console.log(
63537
+ source_default.green(
63538
+ `Recorded ${recordedSeconds}s of ${options.provider} usage for ${orgName}${chunkCount > 1 ? ` across ${chunkCount} chunks` : ""}.`
63539
+ )
63540
+ );
63541
+ }
63542
+ } catch (error3) {
63543
+ const message = error3 instanceof Error ? error3.message : String(error3);
63544
+ if (options.json) {
63545
+ console.log(JSON.stringify({ success: false, error: message }));
63546
+ } else {
63547
+ console.error(source_default.red(`Failed to record usage: ${message}`));
63548
+ }
63549
+ process.exit(1);
63550
+ }
63551
+ });
63552
+ }
63553
+ var defaultApiUrl14, MAX_USAGE_CHUNK_SECONDS;
63554
+ var init_report_usage = __esm({
63555
+ "src/commands/report-usage.ts"() {
63556
+ "use strict";
63557
+ init_esm();
63558
+ init_source();
63559
+ init_config_manager();
63560
+ init_constants();
63561
+ defaultApiUrl14 = constants_default[1];
63562
+ MAX_USAGE_CHUNK_SECONDS = 18e3;
63563
+ }
63564
+ });
63565
+
63181
63566
  // src/parser/command-parser.ts
63182
63567
  function parseFrontmatter(content) {
63183
63568
  const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
@@ -65970,7 +66355,7 @@ function createSecurityCommand() {
65970
66355
  console.error(source_default.red("No default organization set. Run: gal auth login"));
65971
66356
  process.exit(1);
65972
66357
  }
65973
- const apiUrl = config2.apiUrl || defaultApiUrl14;
66358
+ const apiUrl = config2.apiUrl || defaultApiUrl15;
65974
66359
  const spinner = options.json ? null : ora("Fetching security standards...").start();
65975
66360
  try {
65976
66361
  const res = await fetchApi4(
@@ -66013,7 +66398,7 @@ function createSecurityCommand() {
66013
66398
  });
66014
66399
  return command;
66015
66400
  }
66016
- var defaultApiUrl14;
66401
+ var defaultApiUrl15;
66017
66402
  var init_security = __esm({
66018
66403
  "src/commands/security.ts"() {
66019
66404
  "use strict";
@@ -66022,7 +66407,7 @@ var init_security = __esm({
66022
66407
  init_ora();
66023
66408
  init_config_manager();
66024
66409
  init_constants();
66025
- defaultApiUrl14 = constants_default[1];
66410
+ defaultApiUrl15 = constants_default[1];
66026
66411
  }
66027
66412
  });
66028
66413
 
@@ -66055,7 +66440,7 @@ async function runHealthCheck() {
66055
66440
  };
66056
66441
  if (authToken) {
66057
66442
  try {
66058
- const apiUrl = config2.apiUrl || defaultApiUrl15;
66443
+ const apiUrl = config2.apiUrl || defaultApiUrl16;
66059
66444
  const provider = new CoreServiceProvider({
66060
66445
  apiUrl,
66061
66446
  authToken
@@ -66362,7 +66747,7 @@ function createStatusCommand3() {
66362
66747
  });
66363
66748
  return command;
66364
66749
  }
66365
- var import_path37, import_os24, import_fs39, cliVersion6, defaultApiUrl15, PLATFORM_LABELS2, RATE_LIMIT_MS, DRIFT_REPORT_CACHE_PATH;
66750
+ var import_path37, import_os24, import_fs39, cliVersion6, defaultApiUrl16, PLATFORM_LABELS2, RATE_LIMIT_MS, DRIFT_REPORT_CACHE_PATH;
66366
66751
  var init_status = __esm({
66367
66752
  "src/commands/status.ts"() {
66368
66753
  "use strict";
@@ -66380,7 +66765,7 @@ var init_status = __esm({
66380
66765
  init_constants();
66381
66766
  init_dist2();
66382
66767
  cliVersion6 = constants_default[0];
66383
- defaultApiUrl15 = constants_default[1];
66768
+ defaultApiUrl16 = constants_default[1];
66384
66769
  PLATFORM_LABELS2 = Object.fromEntries(
66385
66770
  Object.keys(PLATFORM_REGISTRY).map((id) => [
66386
66771
  id,
@@ -69756,7 +70141,7 @@ function formatTime(dateStr) {
69756
70141
  }
69757
70142
  function createWorkItemRepo() {
69758
70143
  const config2 = ConfigManager.load();
69759
- const apiUrl = config2.apiUrl || process.env.GAL_API_URL || defaultApiUrl16;
70144
+ const apiUrl = config2.apiUrl || process.env.GAL_API_URL || defaultApiUrl17;
69760
70145
  return {
69761
70146
  apiUrl,
69762
70147
  authToken: config2.authToken,
@@ -70212,7 +70597,7 @@ ${source_default.dim(`Claim: gal work claim ${item.id.slice(0, 8)}`)}`);
70212
70597
  );
70213
70598
  return cmd;
70214
70599
  }
70215
- var defaultApiUrl16, PRIORITY_COLORS2, STATUS_COLORS2;
70600
+ var defaultApiUrl17, PRIORITY_COLORS2, STATUS_COLORS2;
70216
70601
  var init_work = __esm({
70217
70602
  "src/commands/work.ts"() {
70218
70603
  "use strict";
@@ -70222,7 +70607,7 @@ var init_work = __esm({
70222
70607
  init_constants();
70223
70608
  init_config_manager();
70224
70609
  init_client();
70225
- defaultApiUrl16 = constants_default[1];
70610
+ defaultApiUrl17 = constants_default[1];
70226
70611
  PRIORITY_COLORS2 = {
70227
70612
  0: source_default.red,
70228
70613
  1: source_default.yellow,
@@ -70243,7 +70628,7 @@ var init_work = __esm({
70243
70628
  function createWorkflowCommand() {
70244
70629
  const workflowCmd = new Command("workflow").description("Test workflow configurations (commands and hooks) in E2B sandbox");
70245
70630
  workflowCmd.command("test").description("Test a workflow file in sandbox before deployment").argument("<file>", "Path to command (.md) or hook (.json) file").option("-p, --platform <platform>", "Platform: claude, cursor, gemini, codex, windsurf", "claude").option("--type <type>", "Type: command or hook (auto-detected if not specified)").option("--test-cases <cases>", "Comma-separated test cases to run").option("--max-iterations <n>", "Maximum improvement iterations (default: 3)", "3").option("-o, --org <org>", "Organization name").option("--json", "Output results as JSON", false).action(async (filePath, options) => {
70246
- const apiUrl = process.env.GAL_API_URL || defaultApiUrl17;
70631
+ const apiUrl = process.env.GAL_API_URL || defaultApiUrl18;
70247
70632
  try {
70248
70633
  const spinner = ora(`Reading ${filePath}...`).start();
70249
70634
  const absolutePath = import_path42.default.resolve(filePath);
@@ -70296,7 +70681,7 @@ function createWorkflowCommand() {
70296
70681
  }
70297
70682
  });
70298
70683
  workflowCmd.command("test-batch").description("Test multiple workflow files in batch").argument("<directory>", "Directory containing workflow files").option("-p, --platform <platform>", "Platform: claude, cursor, gemini, codex, windsurf", "claude").option("-o, --org <org>", "Organization name").option("--report <path>", "Save detailed report to file").action(async (directory, options) => {
70299
- const apiUrl = process.env.GAL_API_URL || defaultApiUrl17;
70684
+ const apiUrl = process.env.GAL_API_URL || defaultApiUrl18;
70300
70685
  const spinner = ora("Scanning directory...").start();
70301
70686
  try {
70302
70687
  const org = options.org || await getDefaultOrg(apiUrl);
@@ -70414,7 +70799,7 @@ function displayTestResult(result) {
70414
70799
  }
70415
70800
  console.log();
70416
70801
  }
70417
- var import_promises9, import_path42, defaultApiUrl17;
70802
+ var import_promises9, import_path42, defaultApiUrl18;
70418
70803
  var init_workflow2 = __esm({
70419
70804
  "src/commands/workflow.ts"() {
70420
70805
  "use strict";
@@ -70425,7 +70810,7 @@ var init_workflow2 = __esm({
70425
70810
  import_path42 = __toESM(require("path"), 1);
70426
70811
  init_constants();
70427
70812
  init_client();
70428
- defaultApiUrl17 = constants_default[1];
70813
+ defaultApiUrl18 = constants_default[1];
70429
70814
  }
70430
70815
  });
70431
70816
 
@@ -70648,6 +71033,7 @@ var init_command_registry = __esm({
70648
71033
  init_protect();
70649
71034
  init_quality();
70650
71035
  init_queue_seed();
71036
+ init_report_usage();
70651
71037
  init_run();
70652
71038
  init_run_design();
70653
71039
  init_scan();
@@ -70699,6 +71085,7 @@ var init_command_registry = __esm({
70699
71085
  { name: "protect", create: createProtectCommand },
70700
71086
  { name: "quality", create: createQualityCommand },
70701
71087
  { name: "queue", create: createQueueCommand },
71088
+ { name: "report-usage", create: createReportUsageCommand },
70702
71089
  { name: "run", create: createRunCommand },
70703
71090
  { name: "run-design", create: createRunDesignCommand },
70704
71091
  { name: "scan", create: createScanCommand },
@@ -71284,7 +71671,7 @@ var init_index = __esm({
71284
71671
  });
71285
71672
 
71286
71673
  // src/bootstrap.ts
71287
- var cliVersion10 = true ? "0.0.406" : "0.0.0-dev";
71674
+ var cliVersion10 = true ? "0.0.407" : "0.0.0-dev";
71288
71675
  var args = process.argv.slice(2);
71289
71676
  var requestedGlobalHelp = args.length === 1 && (args[0] === "--help" || args[0] === "-h");
71290
71677
  var requestedVersion = args.length === 1 && (args[0] === "--version" || args[0] === "-V");