@indigoai-us/hq-cli 5.115.3 → 5.115.5

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.
@@ -2,6 +2,7 @@ import { Option } from 'commander';
2
2
  import { deriveCollections, listRegisteredCollections, packageLocalBin, reconcileCollections, resolveQmdBin, resolveQmdHome, resolveQmdVersion, runQmd, } from '../lib/search-index/index.js';
3
3
  import { backgroundStatus, defaultBackgroundDependencies, runBackgroundLauncher, runBackgroundWorker, } from '../lib/search-index/background.js';
4
4
  import { defaultEmbedLockDependencies, runWithEmbedLock, } from '../lib/search-index/embed-lock.js';
5
+ import { checkEmbedLoad, defaultLoadGateDependencies, recordSuccessfulEmbed, } from '../lib/search-index/load-gate.js';
5
6
  import { applyIndexSizeLimit } from '../lib/search-index/max-doc-bytes.js';
6
7
  import { findHqRoot } from '../utils/manifest.js';
7
8
  import { QMD_NATIVE_BINDING_REMEDY, isQmdNativeBindingError, } from '../utils/qmd-native-binding-error.js';
@@ -38,10 +39,22 @@ export function syncSearchIndex(hqRoot, embed, dependencies = defaults) {
38
39
  dependencies.runQmd(['update'], { cwd: hqRoot });
39
40
  if (!embed)
40
41
  return { embedded: false };
41
- const runEmbed = () => dependencies.runQmd(['embed'], { cwd: hqRoot });
42
42
  const lockDependencies = dependencies.embedLockDependencies ?? defaultEmbedLockDependencies();
43
- const outcome = runWithEmbedLock(resolveQmdHome(lockDependencies.env), lockDependencies, runEmbed);
44
43
  const writeStderr = dependencies.writeStderr ?? ((text) => process.stderr.write(text));
44
+ const home = resolveQmdHome(lockDependencies.env);
45
+ const loadGateDependencies = dependencies.loadGateDependencies ?? defaultLoadGateDependencies(lockDependencies.env);
46
+ const outcome = runWithEmbedLock(home, lockDependencies, () => {
47
+ const decision = checkEmbedLoad(home, loadGateDependencies);
48
+ if (decision.action === 'skip') {
49
+ writeStderr(decision.notice);
50
+ return 'skipped';
51
+ }
52
+ if (decision.forced)
53
+ writeStderr(decision.notice);
54
+ dependencies.runQmd(['embed'], { cwd: hqRoot });
55
+ recordSuccessfulEmbed(home, loadGateDependencies);
56
+ return undefined;
57
+ });
45
58
  if (outcome === 'busy')
46
59
  writeStderr('hq: qmd embed is already running; skipping this pass.\n');
47
60
  if (outcome === 'unavailable')
@@ -38,6 +38,24 @@ const HOOK_CHECK_RELATIVE_PATH = path.join('core', 'scripts', 'check-hq-hooks.sh
38
38
  const WORKTREE_STALE_AFTER_MS = 12 * 60 * 60 * 1_000;
39
39
  const WORKTREE_GIT_TIMEOUT_MS = 2_000;
40
40
  const WORKTREE_HOOK_SWEEP_BUDGET_MS = 5_000;
41
+ /**
42
+ * Ceiling for the shipped hook checker. Sized for the SLOWEST legitimate case,
43
+ * not the fast one: an older core whose checker still runs an unscoped
44
+ * `hq doctor`, on a loaded host, takes tens of seconds. Undercutting that would
45
+ * convert a working checker into a permanent timeout. A wedged checker is still
46
+ * bounded, which is the point.
47
+ */
48
+ const HOOK_CHECK_TIMEOUT_MS = 60_000;
49
+ /** Ceiling override, in milliseconds. Present so tests need not wait a minute. */
50
+ function hookCheckTimeoutMs() {
51
+ const raw = process.env.HQ_HOOK_CHECK_TIMEOUT_MS?.trim();
52
+ if (raw) {
53
+ const parsed = Number(raw);
54
+ if (Number.isFinite(parsed) && parsed > 0)
55
+ return parsed;
56
+ }
57
+ return HOOK_CHECK_TIMEOUT_MS;
58
+ }
41
59
  /** Resolve the same root the repair/check commands must operate on. */
42
60
  function resolveHqRoot(repoRoot) {
43
61
  const root = repoRoot ?? findHqRoot();
@@ -335,16 +353,37 @@ function hasCommandHook(value) {
335
353
  * Run the release health checker when it is available. Its diagnostics cover
336
354
  * runtime/configuration issues outside the safe repair scope; the JSON check
337
355
  * below additionally recognizes UserPromptSubmit, which older checkers omit.
356
+ *
357
+ * Bounded, because reindex runs from Stop / PostToolUse lifecycle hooks and the
358
+ * checker shells into `hq doctor`: an unbounded wait here is an unbounded stall
359
+ * in front of the agent.
360
+ *
361
+ * An overrun is reported as a FAILED check, not as an absent one. Those two
362
+ * look identical to `spawnSync` (both surface as an error) but mean opposite
363
+ * things: absent is "this tree ships no checker, nothing more to learn", while
364
+ * an overrun is "the checker exists and we did not hear back". Collapsing the
365
+ * second into the first would let the slowest trees — exactly the ones running
366
+ * an older, unscoped checker — report healthy while the drift only this checker
367
+ * detects goes unseen.
338
368
  */
339
369
  function runShippedHookCheck(hqRoot) {
340
370
  const checker = path.join(hqRoot, HOOK_CHECK_RELATIVE_PATH);
341
371
  if (!fs.existsSync(checker))
342
372
  return undefined;
373
+ const budgetMs = hookCheckTimeoutMs();
343
374
  try {
344
375
  const result = spawnSync('bash', [checker, '--root', hqRoot], {
345
376
  encoding: 'utf8',
346
377
  stdio: 'pipe',
378
+ timeout: budgetMs,
347
379
  });
380
+ if (result.error?.code === 'ETIMEDOUT') {
381
+ return {
382
+ status: 1,
383
+ output: `the shipped ${HOOK_CHECK_RELATIVE_PATH} check did not finish within ` +
384
+ `${Math.round(budgetMs / 1000)}s, so hook health could not be determined`,
385
+ };
386
+ }
348
387
  if (result.error)
349
388
  return undefined;
350
389
  return {
@@ -356,8 +395,17 @@ function runShippedHookCheck(hqRoot) {
356
395
  return undefined;
357
396
  }
358
397
  }
398
+ /**
399
+ * Classify the tree's hook configuration, cheapest signal first.
400
+ *
401
+ * The shipped checker runs LAST and only when everything else looks healthy.
402
+ * That ordering is not a micro-optimisation: its result is consulted in exactly
403
+ * one branch — the final healthy/minor decision — so on a tree that is already
404
+ * extreme or already minor it is a subprocess whose answer is discarded. Since
405
+ * reindex fires on every Stop / PostToolUse hook, that was the single most
406
+ * expensive discarded result in the command.
407
+ */
359
408
  function inspectHookHealth(hqRoot) {
360
- const checkerResult = runShippedHookCheck(hqRoot);
361
409
  const settingsPath = path.join(hqRoot, '.claude', 'settings.json');
362
410
  if (!fs.existsSync(settingsPath)) {
363
411
  return { state: 'extreme', reason: '.claude/settings.json is missing' };
@@ -386,6 +434,7 @@ function inspectHookHealth(hqRoot) {
386
434
  reason: `missing command hook wiring for ${HOOK_EVENTS.filter((event) => !commandEvents.includes(event)).join(', ')}`,
387
435
  };
388
436
  }
437
+ const checkerResult = runShippedHookCheck(hqRoot);
389
438
  if (checkerResult !== undefined && checkerResult.status !== 0) {
390
439
  return {
391
440
  state: 'minor',
@@ -23,6 +23,12 @@ export function messagesStreamEvents(line) {
23
23
  if (ev.parent_tool_use_id !== null && ev.parent_tool_use_id !== undefined)
24
24
  return [];
25
25
  const message = ev.message && typeof ev.message === "object" ? ev.message : null;
26
+ // Claude Code wraps its own notices (a dead login: "Not logged in", "Failed
27
+ // to authenticate…") in an assistant message from model "<synthetic>". That
28
+ // is the tool talking, not the model: never post it as progress, so a
29
+ // sign-in failure is still recognised as one (the result event carries it).
30
+ if (message?.model === "<synthetic>")
31
+ return [];
26
32
  const content = Array.isArray(message?.content) ? message.content : [];
27
33
  const out = [];
28
34
  for (const block of content) {
@@ -1,4 +1,5 @@
1
1
  import { type EmbedLockDependencies } from "../search-index/embed-lock.js";
2
+ import { type LoadGateDependencies } from "../search-index/load-gate.js";
2
3
  import { type UtilityIo } from "./common.js";
3
4
  export type QmdReindexOptions = UtilityIo & {
4
5
  cwd?: string;
@@ -14,6 +15,8 @@ export type QmdReindexOptions = UtilityIo & {
14
15
  sizeLimit?: (hqRoot: string) => unknown;
15
16
  /** Test seams for the lock shared by every hq-cli embed entry point. */
16
17
  embedLockDependencies?: EmbedLockDependencies;
18
+ /** Test seams for the host load gate consulted while the embed lock is held. */
19
+ loadGateDependencies?: LoadGateDependencies;
17
20
  };
18
21
  export declare function qmdReindexAfterSync(args?: string[], options?: QmdReindexOptions): number;
19
22
  //# sourceMappingURL=qmd-reindex-after-sync.d.ts.map
@@ -14,6 +14,7 @@ import * as path from "node:path";
14
14
  import { reconcileCollections, resolveQmdBin, resolveQmdHome, runQmd } from "../search-index/index.js";
15
15
  import { applyIndexSizeLimit } from "../search-index/max-doc-bytes.js";
16
16
  import { defaultEmbedLockDependencies, runWithEmbedLock, } from "../search-index/embed-lock.js";
17
+ import { LoadGateConfigurationError, checkEmbedLoad, defaultLoadGateDependencies, recordSuccessfulEmbed, } from "../search-index/load-gate.js";
17
18
  import { ioFor, line } from "./common.js";
18
19
  export function qmdReindexAfterSync(args = [], options = {}) {
19
20
  let hqRoot = "";
@@ -66,15 +67,30 @@ export function qmdReindexAfterSync(args = [], options = {}) {
66
67
  }
67
68
  if (embed) {
68
69
  const lockDependencies = options.embedLockDependencies ?? defaultEmbedLockDependencies();
70
+ const home = resolveQmdHome(lockDependencies.env);
71
+ const loadGateDependencies = options.loadGateDependencies ?? defaultLoadGateDependencies(lockDependencies.env);
69
72
  try {
70
- const outcome = runWithEmbedLock(resolveQmdHome(lockDependencies.env), lockDependencies, () => { run(["embed"], { bin, cwd: hqRoot }); });
73
+ const outcome = runWithEmbedLock(home, lockDependencies, () => {
74
+ const decision = checkEmbedLoad(home, loadGateDependencies);
75
+ if (decision.action === "skip") {
76
+ line(stderr, decision.notice.trimEnd());
77
+ return "skipped";
78
+ }
79
+ if (decision.forced)
80
+ line(stderr, decision.notice.trimEnd());
81
+ run(["embed"], { bin, cwd: hqRoot });
82
+ recordSuccessfulEmbed(home, loadGateDependencies);
83
+ return undefined;
84
+ });
71
85
  if (outcome === "busy") {
72
86
  line(stderr, "hq: qmd embed is already running; skipping this pass.");
73
87
  }
74
88
  if (outcome === "unavailable")
75
89
  line(stderr, "hq: cannot access the qmd embed lock; skipping this pass.");
76
90
  }
77
- catch {
91
+ catch (error) {
92
+ if (error instanceof LoadGateConfigurationError)
93
+ line(stderr, `hq: ${error.message}`);
78
94
  /* embeddings are deferred-cost and optional */
79
95
  }
80
96
  }
@@ -71,6 +71,14 @@ export interface DoctorJsonDocument {
71
71
  summary: StatusCounts;
72
72
  /** The exit code this run produced: 0, or 1 when any FAIL/UNKNOWN is present. */
73
73
  exitCode: number;
74
+ /**
75
+ * The check families this run was restricted to (`--only`), in registration
76
+ * order. ABSENT on a full run, which is the only reading a consumer may treat
77
+ * as a verdict on the whole tree: a scoped document reports nothing at all
78
+ * about the families that never ran, and "no FAILs" in it must not be
79
+ * mistaken for a healthy tree.
80
+ */
81
+ scope?: string[];
74
82
  /** The full, flattened result list in family/registration order. */
75
83
  results: DoctorJsonResult[];
76
84
  }
@@ -82,6 +90,8 @@ export interface BuildDoctorJsonInput {
82
90
  families: FamilyRun[];
83
91
  /** Detected platform. Defaults to {@link UNKNOWN_PLATFORM}. */
84
92
  platform?: DoctorPlatform;
93
+ /** Families this run was restricted to. Omitted on a full run. */
94
+ scope?: readonly string[];
85
95
  }
86
96
  /** Assemble the machine-readable document from a completed run. */
87
97
  export declare function buildDoctorJson(input: BuildDoctorJsonInput): DoctorJsonDocument;
@@ -43,6 +43,7 @@ export function buildDoctorJson(input) {
43
43
  hqRoot: input.hqRoot,
44
44
  summary: summarize(input.families),
45
45
  exitCode: computeExitCode(input.families),
46
+ ...(input.scope && input.scope.length > 0 ? { scope: [...input.scope] } : {}),
46
47
  results,
47
48
  };
48
49
  }
@@ -27,6 +27,20 @@ export declare class DoctorRegistry {
27
27
  families(): CheckFamily[];
28
28
  /** Whether a family with this id is registered. */
29
29
  has(id: string): boolean;
30
+ /** The registered family ids, in registration order. */
31
+ ids(): string[];
32
+ /**
33
+ * A registry holding only the named families, still in REGISTRATION order —
34
+ * the caller's argument order is a selection, not a reordering, so a scoped
35
+ * report reads the same way as a full one.
36
+ *
37
+ * Unknown ids are returned rather than ignored: running zero checks and
38
+ * reporting success would describe a tree nothing looked at.
39
+ */
40
+ select(ids: readonly string[]): {
41
+ registry: DoctorRegistry;
42
+ unknown: string[];
43
+ };
30
44
  /** Run every family in order and collect grouped results. */
31
45
  run(context: CheckContext): Promise<FamilyRun[]>;
32
46
  }
@@ -43,6 +43,28 @@ export class DoctorRegistry {
43
43
  has(id) {
44
44
  return this.familiesById.has(id);
45
45
  }
46
+ /** The registered family ids, in registration order. */
47
+ ids() {
48
+ return [...this.familiesById.keys()];
49
+ }
50
+ /**
51
+ * A registry holding only the named families, still in REGISTRATION order —
52
+ * the caller's argument order is a selection, not a reordering, so a scoped
53
+ * report reads the same way as a full one.
54
+ *
55
+ * Unknown ids are returned rather than ignored: running zero checks and
56
+ * reporting success would describe a tree nothing looked at.
57
+ */
58
+ select(ids) {
59
+ const wanted = new Set(ids);
60
+ const unknown = [...wanted].filter((id) => !this.familiesById.has(id));
61
+ const registry = new DoctorRegistry();
62
+ for (const family of this.families()) {
63
+ if (wanted.has(family.id))
64
+ registry.register(family);
65
+ }
66
+ return { registry, unknown };
67
+ }
46
68
  /** Run every family in order and collect grouped results. */
47
69
  async run(context) {
48
70
  const runs = [];
@@ -1,7 +1,8 @@
1
1
  import { type StdioOptions } from 'node:child_process';
2
2
  import { type QmdProcessResult, type RunQmdOptions } from './index.js';
3
+ import { type LoadGateDependencies } from './load-gate.js';
3
4
  export type BackgroundResult = {
4
- state: 'skipped-agent' | 'skipped' | 'quiet' | 'busy' | 'completed' | 'update-failed' | 'terminated';
5
+ state: 'skipped-agent' | 'skipped' | 'quiet' | 'busy' | 'deferred' | 'completed' | 'update-failed' | 'terminated';
5
6
  } | {
6
7
  state: 'launched';
7
8
  pid: number;
@@ -27,6 +28,9 @@ export type BackgroundDependencies = {
27
28
  /** Test seams for signal delivery; production uses the real process. */
28
29
  processEvents?: Pick<NodeJS.Process, 'once'>;
29
30
  exit?: (code: number) => void;
31
+ /** Test seams for the host load gate consulted immediately before qmd embed. */
32
+ loadGateDependencies?: LoadGateDependencies;
33
+ writeStderr?: (text: string) => void;
30
34
  };
31
35
  export type BackgroundStatus = {
32
36
  lock: 'held' | 'stale' | 'free';
@@ -5,6 +5,7 @@ import { Sentry } from '../../sentry.js';
5
5
  import { reconcileCollections as defaultReconcileCollections, resolveQmdBin as defaultResolveQmdBin, runQmd as defaultRunQmd, } from './index.js';
6
6
  import { applyIndexSizeLimit as defaultApplyIndexSizeLimit } from './max-doc-bytes.js';
7
7
  import { acquireEmbedLock as acquireLock, installEmbedLockCleanup, lockPath, lockRoot, lockState, releaseEmbedLock as releaseLock, } from './embed-lock.js';
8
+ import { LoadGateConfigurationError, checkEmbedLoad, defaultLoadGateDependencies, recordSuccessfulEmbed, } from './load-gate.js';
8
9
  const COMPLETE_NAME = 'qmd-reindex-bg.completed';
9
10
  function errnoInfo(error) {
10
11
  const e = error;
@@ -123,6 +124,7 @@ export function defaultBackgroundDependencies(hqRoot) {
123
124
  applyIndexSizeLimit: defaultApplyIndexSizeLimit,
124
125
  runQmd: defaultRunQmd,
125
126
  spawnWorker: defaultSpawnWorker,
127
+ writeStderr: (text) => process.stderr.write(text),
126
128
  };
127
129
  }
128
130
  /** Match the shell forwarder's hosted-agent markers before looking up qmd. */
@@ -350,8 +352,30 @@ export async function runBackgroundWorker(dependencies) {
350
352
  }
351
353
  if (await signalWindow())
352
354
  return { state: 'terminated' };
355
+ const loadGateDependencies = dependencies.loadGateDependencies ?? defaultLoadGateDependencies(dependencies.env);
356
+ let loadDecision;
357
+ try {
358
+ loadDecision = checkEmbedLoad(home, loadGateDependencies);
359
+ }
360
+ catch (error) {
361
+ if (error instanceof LoadGateConfigurationError) {
362
+ (dependencies.writeStderr ?? ((text) => process.stderr.write(text)))(`hq: ${error.message}\n`);
363
+ cleanup();
364
+ return { state: 'deferred' };
365
+ }
366
+ throw error;
367
+ }
368
+ if (loadDecision.action === 'skip') {
369
+ (dependencies.writeStderr ?? ((text) => process.stderr.write(text)))(loadDecision.notice);
370
+ writeCompletion(home, dependencies);
371
+ cleanup();
372
+ return { state: 'deferred' };
373
+ }
374
+ if (loadDecision.forced)
375
+ (dependencies.writeStderr ?? ((text) => process.stderr.write(text)))(loadDecision.notice);
353
376
  try {
354
377
  appendWorkerLog(logPath, stepOutput(dependencies.runQmd(['embed'], { cwd: dependencies.hqRoot })));
378
+ recordSuccessfulEmbed(home, loadGateDependencies);
355
379
  }
356
380
  catch (error) {
357
381
  appendWorkerLog(logPath, errorOutput(error)); // a completed embed attempt still permits the stamp
@@ -10,7 +10,7 @@ export type EmbedLockDependencies = {
10
10
  export type EmbedLockProcessEvents = Pick<NodeJS.Process, 'once'> & {
11
11
  removeListener?: NodeJS.Process['removeListener'];
12
12
  };
13
- export type EmbedLockRunResult = 'ran' | 'busy' | 'unavailable';
13
+ export type EmbedLockRunResult = 'ran' | 'skipped' | 'busy' | 'unavailable';
14
14
  /**
15
15
  * Serializes embeddings started through hq-cli only. A bare `qmd embed` (or
16
16
  * another tool that does not call this module) cannot participate, because qmd
@@ -34,5 +34,5 @@ export declare function installEmbedLockCleanup(cleanup: () => void, processEven
34
34
  * termination. An interrupted owner is safely reclaimed through the lock's
35
35
  * existing staleness and owner-grace protocol.
36
36
  */
37
- export declare function runWithEmbedLock(home: string, dependencies: EmbedLockDependencies, run: () => void): EmbedLockRunResult;
37
+ export declare function runWithEmbedLock(home: string, dependencies: EmbedLockDependencies, run: () => 'skipped' | void): EmbedLockRunResult;
38
38
  //# sourceMappingURL=embed-lock.d.ts.map
@@ -276,8 +276,7 @@ export function runWithEmbedLock(home, dependencies, run) {
276
276
  if (acquired === 'busy')
277
277
  return 'busy';
278
278
  try {
279
- run();
280
- return 'ran';
279
+ return run() === 'skipped' ? 'skipped' : 'ran';
281
280
  }
282
281
  finally {
283
282
  releaseEmbedLock(home, dependencies);
@@ -0,0 +1,65 @@
1
+ import * as os from 'node:os';
2
+ export declare const DEFAULT_MAX_LOAD_PERCENT = 50;
3
+ /**
4
+ * Twelve requests is enough to avoid quietly starving a frequently requested
5
+ * index, while still leaving several opportunities for a busy machine to cool
6
+ * down before we spend CPU on an embed. The thirteenth request is forced.
7
+ */
8
+ export declare const MAX_CONSECUTIVE_DEFERRED_EMBEDS = 12;
9
+ /** Six hours bounds deferral even when indexing requests are infrequent. */
10
+ export declare const MAX_EMBED_DEFERRAL_SECONDS: number;
11
+ export type LoadGateDependencies = {
12
+ env: NodeJS.ProcessEnv;
13
+ now: () => number;
14
+ readFile: (file: string) => string;
15
+ writeFile: (file: string, contents: string) => void;
16
+ rename: (source: string, destination: string) => void;
17
+ mkdir: (directory: string) => void;
18
+ /** A synchronous seam so the existing synchronous qmd entry points stay synchronous. */
19
+ sleep: (milliseconds: number) => void;
20
+ cpus: () => os.CpuInfo[];
21
+ /** Only used outside Linux, where /proc/meminfo is unavailable. */
22
+ freemem: () => number;
23
+ totalmem: () => number;
24
+ };
25
+ type CpuCounters = {
26
+ total: number;
27
+ idle: number;
28
+ };
29
+ export type EmbedLoadDecision = {
30
+ action: 'embed';
31
+ forced: false;
32
+ } | {
33
+ action: 'embed';
34
+ forced: true;
35
+ notice: string;
36
+ } | {
37
+ action: 'skip';
38
+ signal: 'cpu' | 'memory';
39
+ percent: number;
40
+ limit: number;
41
+ notice: string;
42
+ };
43
+ export declare class LoadGateConfigurationError extends Error {
44
+ constructor(value: string);
45
+ }
46
+ export declare function defaultLoadGateDependencies(env?: NodeJS.ProcessEnv): LoadGateDependencies;
47
+ export declare function parseProcStatAggregate(contents: string): CpuCounters | undefined;
48
+ export declare function cpuBusyPercent(before: CpuCounters, after: CpuCounters): number | undefined;
49
+ /** Sample aggregate CPU busy time over 200 ms, falling back only without /proc/stat. */
50
+ export declare function measureCpuPercent(dependencies: LoadGateDependencies): number;
51
+ export declare function memAvailableFraction(contents: string): number | undefined;
52
+ /** Prefer Linux MemAvailable, because MemFree alone treats reclaimable cache as used memory. */
53
+ export declare function measureMemoryPercent(dependencies: LoadGateDependencies): number;
54
+ export declare function parseLoadThreshold(env: NodeJS.ProcessEnv): number | undefined;
55
+ export declare function loadGateStatePath(home: string): string;
56
+ /**
57
+ * Decide immediately before qmd embed whether this host can absorb it. The
58
+ * caller must already own the shared embed lock, so state updates are atomic
59
+ * with respect to hq-cli's other embed entry points.
60
+ */
61
+ export declare function checkEmbedLoad(home: string, dependencies: LoadGateDependencies): EmbedLoadDecision;
62
+ /** Reset the starvation counter only after qmd embed has actually returned successfully. */
63
+ export declare function recordSuccessfulEmbed(home: string, dependencies: LoadGateDependencies): void;
64
+ export {};
65
+ //# sourceMappingURL=load-gate.d.ts.map