@great-arrow/cli 0.1.48 → 0.1.50

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.js CHANGED
@@ -104,7 +104,7 @@ var require_polyfills = __commonJS({
104
104
  }
105
105
  if (platform === "win32") {
106
106
  fs6.rename = typeof fs6.rename !== "function" ? fs6.rename : (function(fs$rename) {
107
- function rename(from, to, cb) {
107
+ function rename2(from, to, cb) {
108
108
  var start = Date.now();
109
109
  var backoff = 0;
110
110
  fs$rename(from, to, function CB(er) {
@@ -124,8 +124,8 @@ var require_polyfills = __commonJS({
124
124
  if (cb) cb(er);
125
125
  });
126
126
  }
127
- if (Object.setPrototypeOf) Object.setPrototypeOf(rename, fs$rename);
128
- return rename;
127
+ if (Object.setPrototypeOf) Object.setPrototypeOf(rename2, fs$rename);
128
+ return rename2;
129
129
  })(fs6.rename);
130
130
  }
131
131
  fs6.read = typeof fs6.read !== "function" ? fs6.read : (function(fs$read) {
@@ -529,8 +529,8 @@ var require_graceful_fs = __commonJS({
529
529
  fs7.createReadStream = createReadStream;
530
530
  fs7.createWriteStream = createWriteStream;
531
531
  var fs$readFile = fs7.readFile;
532
- fs7.readFile = readFile5;
533
- function readFile5(path10, options, cb) {
532
+ fs7.readFile = readFile7;
533
+ function readFile7(path10, options, cb) {
534
534
  if (typeof options === "function")
535
535
  cb = options, options = null;
536
536
  return go$readFile(path10, options, cb);
@@ -546,8 +546,8 @@ var require_graceful_fs = __commonJS({
546
546
  }
547
547
  }
548
548
  var fs$writeFile = fs7.writeFile;
549
- fs7.writeFile = writeFile5;
550
- function writeFile5(path10, data, options, cb) {
549
+ fs7.writeFile = writeFile6;
550
+ function writeFile6(path10, data, options, cb) {
551
551
  if (typeof options === "function")
552
552
  cb = options, options = null;
553
553
  return go$writeFile(path10, data, options, cb);
@@ -582,8 +582,8 @@ var require_graceful_fs = __commonJS({
582
582
  }
583
583
  var fs$copyFile = fs7.copyFile;
584
584
  if (fs$copyFile)
585
- fs7.copyFile = copyFile2;
586
- function copyFile2(src, dest, flags, cb) {
585
+ fs7.copyFile = copyFile3;
586
+ function copyFile3(src, dest, flags, cb) {
587
587
  if (typeof flags === "function") {
588
588
  cb = flags;
589
589
  flags = 0;
@@ -1964,7 +1964,7 @@ var require_minimist = __commonJS({
1964
1964
  var require_rc = __commonJS({
1965
1965
  "node_modules/.pnpm/rc@1.2.8/node_modules/rc/index.js"(exports, module) {
1966
1966
  var cc = require_utils();
1967
- var join9 = __require("path").join;
1967
+ var join10 = __require("path").join;
1968
1968
  var deepExtend = require_deep_extend();
1969
1969
  var etc = "/etc";
1970
1970
  var win = process.platform === "win32";
@@ -1989,15 +1989,15 @@ var require_rc = __commonJS({
1989
1989
  }
1990
1990
  if (!win)
1991
1991
  [
1992
- join9(etc, name, "config"),
1993
- join9(etc, name + "rc")
1992
+ join10(etc, name, "config"),
1993
+ join10(etc, name + "rc")
1994
1994
  ].forEach(addConfigFile);
1995
1995
  if (home)
1996
1996
  [
1997
- join9(home, ".config", name, "config"),
1998
- join9(home, ".config", name),
1999
- join9(home, "." + name, "config"),
2000
- join9(home, "." + name + "rc")
1997
+ join10(home, ".config", name, "config"),
1998
+ join10(home, ".config", name),
1999
+ join10(home, "." + name, "config"),
2000
+ join10(home, "." + name + "rc")
2001
2001
  ].forEach(addConfigFile);
2002
2002
  addConfigFile(cc.find("." + name + "rc"));
2003
2003
  if (env4.config) addConfigFile(env4.config);
@@ -2078,7 +2078,7 @@ var require_polyfills2 = __commonJS({
2078
2078
  }
2079
2079
  if (platform === "win32") {
2080
2080
  fs6.rename = typeof fs6.rename !== "function" ? fs6.rename : (function(fs$rename) {
2081
- function rename(from, to, cb) {
2081
+ function rename2(from, to, cb) {
2082
2082
  var start = Date.now();
2083
2083
  var backoff = 0;
2084
2084
  fs$rename(from, to, function CB(er) {
@@ -2098,8 +2098,8 @@ var require_polyfills2 = __commonJS({
2098
2098
  if (cb) cb(er);
2099
2099
  });
2100
2100
  }
2101
- if (Object.setPrototypeOf) Object.setPrototypeOf(rename, fs$rename);
2102
- return rename;
2101
+ if (Object.setPrototypeOf) Object.setPrototypeOf(rename2, fs$rename);
2102
+ return rename2;
2103
2103
  })(fs6.rename);
2104
2104
  }
2105
2105
  fs6.read = typeof fs6.read !== "function" ? fs6.read : (function(fs$read) {
@@ -2503,8 +2503,8 @@ var require_graceful_fs2 = __commonJS({
2503
2503
  fs7.createReadStream = createReadStream;
2504
2504
  fs7.createWriteStream = createWriteStream;
2505
2505
  var fs$readFile = fs7.readFile;
2506
- fs7.readFile = readFile5;
2507
- function readFile5(path10, options, cb) {
2506
+ fs7.readFile = readFile7;
2507
+ function readFile7(path10, options, cb) {
2508
2508
  if (typeof options === "function")
2509
2509
  cb = options, options = null;
2510
2510
  return go$readFile(path10, options, cb);
@@ -2520,8 +2520,8 @@ var require_graceful_fs2 = __commonJS({
2520
2520
  }
2521
2521
  }
2522
2522
  var fs$writeFile = fs7.writeFile;
2523
- fs7.writeFile = writeFile5;
2524
- function writeFile5(path10, data, options, cb) {
2523
+ fs7.writeFile = writeFile6;
2524
+ function writeFile6(path10, data, options, cb) {
2525
2525
  if (typeof options === "function")
2526
2526
  cb = options, options = null;
2527
2527
  return go$writeFile(path10, data, options, cb);
@@ -2556,8 +2556,8 @@ var require_graceful_fs2 = __commonJS({
2556
2556
  }
2557
2557
  var fs$copyFile = fs7.copyFile;
2558
2558
  if (fs$copyFile)
2559
- fs7.copyFile = copyFile2;
2560
- function copyFile2(src, dest, flags, cb) {
2559
+ fs7.copyFile = copyFile3;
2560
+ function copyFile3(src, dest, flags, cb) {
2561
2561
  if (typeof flags === "function") {
2562
2562
  cb = flags;
2563
2563
  flags = 0;
@@ -3335,11 +3335,11 @@ var require_util = __commonJS({
3335
3335
  if (files.includes("node_modules") || files.includes("package.json") || files.includes("package.json5") || files.includes("package.yaml") || files.includes("pnpm-workspace.yaml")) {
3336
3336
  return name2;
3337
3337
  }
3338
- const dirname6 = path10.dirname(name2);
3339
- if (dirname6 === name2) {
3338
+ const dirname7 = path10.dirname(name2);
3339
+ if (dirname7 === name2) {
3340
3340
  return original;
3341
3341
  }
3342
- return find(dirname6, original);
3342
+ return find(dirname7, original);
3343
3343
  } catch (error) {
3344
3344
  if (name2 === original) {
3345
3345
  if (error.code === "ENOENT") {
@@ -9399,7 +9399,7 @@ function isNodeError(value) {
9399
9399
 
9400
9400
  // src/lib/version.ts
9401
9401
  var CLI_PACKAGE_NAME = "@great-arrow/cli";
9402
- var CLI_VERSION = "0.1.48";
9402
+ var CLI_VERSION = "0.1.50";
9403
9403
  var GAD_INSTALL_VERSION = "2026.08.08";
9404
9404
  var GAD_INSTALL_MIN_VERSION = "2026.06.22";
9405
9405
  var GAD_INSTALL_HEADER = "X-GAD-Install-Version";
@@ -10543,12 +10543,13 @@ var EXIT_SESSION_EXPIRED = 2;
10543
10543
  var EXIT_NOT_OAUTH = 3;
10544
10544
  function authCommand() {
10545
10545
  const cmd = new Command("auth").description("Authentication helpers");
10546
- cmd.command("token").description("Print a fresh OAuth access token, refreshing it if needed").option("--json", "Output {accessToken, accessExpiresAt, apiUrl} as JSON").action(async (options) => {
10546
+ cmd.command("token").description("Print a fresh OAuth access token, refreshing it if needed").option("--json", "Output {accessToken, accessExpiresAt, apiUrl} as JSON").action(async (options, actionCommand) => {
10547
10547
  try {
10548
10548
  const t = await getFreshOAuthAccessToken();
10549
10549
  writeCliLocator();
10550
+ const jsonOutput = options.json || actionCommand.optsWithGlobals().json;
10550
10551
  process.stdout.write(
10551
- options.json ? `${JSON.stringify(t)}
10552
+ jsonOutput ? `${JSON.stringify(t)}
10552
10553
  ` : `${t.accessToken}
10553
10554
  `
10554
10555
  );
@@ -11958,6 +11959,29 @@ import { readFileSync } from "node:fs";
11958
11959
 
11959
11960
  // src/lib/work-status.ts
11960
11961
  import { execFile as execFile2, execFileSync as execFileSync2 } from "node:child_process";
11962
+
11963
+ // ../../src/lib/work-risk/push-state.ts
11964
+ var REMOTE_BRANCH_REF_PREFIX = "refs/remotes/origin/";
11965
+ var REMOTE_BRANCH_TIPS_FORMAT = "%(refname)%09%(objectname)";
11966
+ function parseRemoteBranchTips(raw) {
11967
+ const tips = /* @__PURE__ */ new Map();
11968
+ for (const line of raw.split("\n")) {
11969
+ const [refname, sha] = line.split(" ");
11970
+ if (!refname?.startsWith(REMOTE_BRANCH_REF_PREFIX) || !sha) continue;
11971
+ const branch = refname.slice(REMOTE_BRANCH_REF_PREFIX.length);
11972
+ if (!branch || branch === "HEAD") continue;
11973
+ tips.set(branch, sha.trim());
11974
+ }
11975
+ return tips;
11976
+ }
11977
+ function remoteHasHead(branch, head, tips, isAncestor2) {
11978
+ const tip = tips.get(branch);
11979
+ if (!tip) return false;
11980
+ if (tip === head) return true;
11981
+ return isAncestor2(head, tip);
11982
+ }
11983
+
11984
+ // src/lib/work-status.ts
11961
11985
  var LOCAL_COMMAND_TIMEOUT_MS = 2e4;
11962
11986
  function run(command, args, cwd) {
11963
11987
  try {
@@ -12070,9 +12094,31 @@ function auditLocalBranches(root) {
12070
12094
  root
12071
12095
  ).split("\n").filter(Boolean)
12072
12096
  );
12097
+ const tipsResult = git(
12098
+ [
12099
+ "for-each-ref",
12100
+ `--format=${REMOTE_BRANCH_TIPS_FORMAT}`,
12101
+ "refs/remotes/origin"
12102
+ ],
12103
+ root
12104
+ );
12105
+ const tips = parseRemoteBranchTips(
12106
+ tipsResult.status === 0 ? tipsResult.stdout : ""
12107
+ );
12073
12108
  return raw.split("\n").filter(Boolean).flatMap((line) => {
12074
12109
  const parsed = parseLocalBranchLine(line, exact, unmerged);
12075
- return parsed ? [parsed] : [];
12110
+ if (!parsed) return [];
12111
+ return [
12112
+ {
12113
+ ...parsed,
12114
+ remoteHasHead: remoteHasHead(
12115
+ parsed.branch,
12116
+ parsed.head,
12117
+ tips,
12118
+ (ancestor, descendant) => isAncestor(ancestor, descendant, root)
12119
+ )
12120
+ }
12121
+ ];
12076
12122
  });
12077
12123
  }
12078
12124
  function parseLocalBranchLine(line, exact, unmerged) {
@@ -12131,7 +12177,7 @@ function classifyWorktree(audit) {
12131
12177
  if (audit.path.includes("/.claude/worktrees/")) return "agent-harness";
12132
12178
  if (audit.mergedToMain) return "cleanup-ready";
12133
12179
  if (audit.upstreamGone) return "upstream-gone";
12134
- if (!audit.upstream) return "local-rescue";
12180
+ if (!audit.remoteHasHead) return "local-rescue";
12135
12181
  return "active-unmerged";
12136
12182
  }
12137
12183
  async function auditWorktrees(root, localBranches) {
@@ -12159,6 +12205,7 @@ async function auditWorktrees(root, localBranches) {
12159
12205
  mergedToMain: branch?.mergedToMain ?? false,
12160
12206
  upstream: branch?.upstream ?? null,
12161
12207
  upstreamGone: branch?.upstreamGone ?? false,
12208
+ remoteHasHead: branch?.remoteHasHead ?? false,
12162
12209
  behindMain: branch?.behindMain ?? 0,
12163
12210
  aheadMain: branch?.aheadMain ?? 0,
12164
12211
  countsExact: branch?.countsExact ?? false
@@ -12178,6 +12225,7 @@ function auditDetachedLocalBranches(localBranches, checkedOutBranches) {
12178
12225
  countsExact,
12179
12226
  upstream,
12180
12227
  upstreamGone,
12228
+ remoteHasHead: remoteHasHead2,
12181
12229
  committedAt
12182
12230
  }) => ({
12183
12231
  branch,
@@ -12186,12 +12234,13 @@ function auditDetachedLocalBranches(localBranches, checkedOutBranches) {
12186
12234
  behindMain,
12187
12235
  aheadMain,
12188
12236
  countsExact,
12189
- // `upstream` is what separates "never left this disk" from "pushed,
12190
- // just has no PR yet" — the single bit that decides whether work can
12191
- // actually be lost. It was computed and then dropped here, so the
12192
- // server classified both as one undifferentiated "stranded".
12237
+ // `remoteHasHead` is what separates "never left this disk" from
12238
+ // "pushed, just has no PR yet" — the single bit that decides whether
12239
+ // work can actually be lost. `upstream` alone cannot: a task branch
12240
+ // could carry origin/main as its upstream without ever being pushed.
12193
12241
  upstream,
12194
12242
  upstreamGone,
12243
+ remoteHasHead: remoteHasHead2,
12195
12244
  // Without this the grace window read `ageMs = Infinity` for every
12196
12245
  // branch, so a branch was "stranded" from its first second.
12197
12246
  committedAt
@@ -13025,10 +13074,10 @@ function guardrailsCommand() {
13025
13074
  }
13026
13075
 
13027
13076
  // src/commands/doctor.ts
13028
- import { existsSync as existsSync3 } from "node:fs";
13029
- import { copyFile, mkdir as mkdir4, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
13077
+ import { existsSync as existsSync5 } from "node:fs";
13078
+ import { copyFile as copyFile2, mkdir as mkdir5, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
13030
13079
  import { homedir as homedir5 } from "node:os";
13031
- import { dirname as dirname4, join as join7 } from "node:path";
13080
+ import { dirname as dirname5, join as join8 } from "node:path";
13032
13081
 
13033
13082
  // src/lib/codex-config.ts
13034
13083
  import { execFileSync as execFileSync5 } from "node:child_process";
@@ -13815,6 +13864,285 @@ function inspectConfiguredHooks(hooks, deps) {
13815
13864
  return issues;
13816
13865
  }
13817
13866
 
13867
+ // src/lib/suggest-hook.ts
13868
+ import { existsSync as existsSync3 } from "node:fs";
13869
+ import {
13870
+ chmod,
13871
+ copyFile,
13872
+ mkdir as mkdir4,
13873
+ readFile as readFile3,
13874
+ rename,
13875
+ unlink as unlink2,
13876
+ writeFile as writeFile3
13877
+ } from "node:fs/promises";
13878
+ import { dirname as dirname4, join as join7 } from "node:path";
13879
+ import { fileURLToPath } from "node:url";
13880
+ var SUGGEST_HOOK_FILE = "gad-suggest-tools.mjs";
13881
+ var SUGGEST_HOOK_EVENT = "UserPromptSubmit";
13882
+ var SUGGEST_HOOK_DISCLOSURE = "Tool suggestions (optional, off by default): when on, every prompt you type in Claude Code is sent \u2014 its text only, up to 2,000 characters, never your session id, transcript or working directory \u2014 to greatarrow.ai over your own Great Arrow token, so it can name up to three Great Arrow tools that may help. The prompt is ranked and dropped: it is not stored and not logged. The hook gives up after 1.5 seconds and never blocks a prompt. Turn it off any time with `gad mcp suggest-tools off`.";
13883
+ function asObject2(value) {
13884
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
13885
+ }
13886
+ function isOurs(hook) {
13887
+ return !!hook && typeof hook.command === "string" && hook.command.includes(SUGGEST_HOOK_FILE);
13888
+ }
13889
+ function parseSettings(content) {
13890
+ const parsed = content.trim() ? JSON.parse(content) : {};
13891
+ const cfg = asObject2(parsed);
13892
+ if (!cfg) throw new Error("settings.json is not a JSON object");
13893
+ return cfg;
13894
+ }
13895
+ function promptGroups(cfg) {
13896
+ const groups = asObject2(cfg.hooks)?.[SUGGEST_HOOK_EVENT];
13897
+ return Array.isArray(groups) ? groups : [];
13898
+ }
13899
+ function isSuggestHookEnabled(settingsContent) {
13900
+ try {
13901
+ return promptGroups(parseSettings(settingsContent)).some(
13902
+ (g) => Array.isArray(g?.hooks) && g.hooks.some(isOurs)
13903
+ );
13904
+ } catch {
13905
+ return false;
13906
+ }
13907
+ }
13908
+ function suggestHookCommand(scriptPath) {
13909
+ return `node "${scriptPath}"`;
13910
+ }
13911
+ function planEnableSuggestHook(settingsContent, command) {
13912
+ if (isSuggestHookEnabled(settingsContent)) {
13913
+ return { content: settingsContent, changed: false };
13914
+ }
13915
+ const cfg = parseSettings(settingsContent);
13916
+ const hooks = asObject2(cfg.hooks) ?? {};
13917
+ cfg.hooks = hooks;
13918
+ const groups = promptGroups(cfg);
13919
+ groups.push({ hooks: [{ type: "command", command, timeout: 5 }] });
13920
+ hooks[SUGGEST_HOOK_EVENT] = groups;
13921
+ return { content: JSON.stringify(cfg, null, 2) + "\n", changed: true };
13922
+ }
13923
+ function planDisableSuggestHook(settingsContent) {
13924
+ if (!isSuggestHookEnabled(settingsContent)) {
13925
+ return { content: settingsContent, changed: false };
13926
+ }
13927
+ const cfg = parseSettings(settingsContent);
13928
+ const hooks = asObject2(cfg.hooks);
13929
+ const kept = promptGroups(cfg).map(
13930
+ (g) => Array.isArray(g?.hooks) ? { ...g, hooks: g.hooks.filter((h) => !isOurs(h)) } : g
13931
+ ).filter((g) => !Array.isArray(g?.hooks) || g.hooks.length > 0);
13932
+ if (kept.length > 0) hooks[SUGGEST_HOOK_EVENT] = kept;
13933
+ else delete hooks[SUGGEST_HOOK_EVENT];
13934
+ if (Object.keys(hooks).length === 0) delete cfg.hooks;
13935
+ return { content: JSON.stringify(cfg, null, 2) + "\n", changed: true };
13936
+ }
13937
+ function suggestHookPaths(home) {
13938
+ return {
13939
+ settingsPath: join7(home, ".claude", "settings.json"),
13940
+ scriptPath: join7(home, ".great-arrow", SUGGEST_HOOK_FILE),
13941
+ hookConfigPath: join7(home, ".great-arrow", "hook-config.json")
13942
+ };
13943
+ }
13944
+ function packagedSuggestHookPath() {
13945
+ const here = dirname4(fileURLToPath(import.meta.url));
13946
+ for (const candidate of [
13947
+ join7(here, "hooks", SUGGEST_HOOK_FILE),
13948
+ join7(here, "..", "..", "dist", "hooks", SUGGEST_HOOK_FILE)
13949
+ ]) {
13950
+ if (existsSync3(candidate)) return candidate;
13951
+ }
13952
+ return null;
13953
+ }
13954
+ async function readOrEmpty(path10) {
13955
+ try {
13956
+ return await readFile3(path10, "utf8");
13957
+ } catch {
13958
+ return "";
13959
+ }
13960
+ }
13961
+ async function atomicWrite(path10, content, mode) {
13962
+ await mkdir4(dirname4(path10), { recursive: true });
13963
+ const tmp = `${path10}.tmp-${process.pid}`;
13964
+ await writeFile3(tmp, content, mode === void 0 ? "utf8" : { mode });
13965
+ await rename(tmp, path10);
13966
+ }
13967
+ function hasHookCredential(content) {
13968
+ try {
13969
+ const cfg = asObject2(JSON.parse(content));
13970
+ return typeof cfg?.token === "string" && cfg.token.trim().length > 0;
13971
+ } catch {
13972
+ return false;
13973
+ }
13974
+ }
13975
+ async function suggestHookStatus(home) {
13976
+ const p = suggestHookPaths(home);
13977
+ return {
13978
+ enabled: isSuggestHookEnabled(await readOrEmpty(p.settingsPath)),
13979
+ scriptPresent: existsSync3(p.scriptPath),
13980
+ credentialPresent: hasHookCredential(await readOrEmpty(p.hookConfigPath))
13981
+ };
13982
+ }
13983
+ async function enableSuggestHook(opts) {
13984
+ const p = suggestHookPaths(opts.home);
13985
+ const settings = await readOrEmpty(p.settingsPath);
13986
+ const plan = planEnableSuggestHook(
13987
+ settings,
13988
+ suggestHookCommand(p.scriptPath)
13989
+ );
13990
+ await mkdir4(dirname4(p.scriptPath), { recursive: true, mode: 448 });
13991
+ await copyFile(opts.packagedScript, p.scriptPath);
13992
+ await chmod(p.scriptPath, 493);
13993
+ if (plan.changed) await atomicWrite(p.settingsPath, plan.content);
13994
+ return { changed: plan.changed };
13995
+ }
13996
+ async function disableSuggestHook(opts) {
13997
+ const p = suggestHookPaths(opts.home);
13998
+ const plan = planDisableSuggestHook(await readOrEmpty(p.settingsPath));
13999
+ if (plan.changed) await atomicWrite(p.settingsPath, plan.content);
14000
+ if (existsSync3(p.scriptPath)) await unlink2(p.scriptPath);
14001
+ return { changed: plan.changed };
14002
+ }
14003
+ async function runSuggestHookOptIn(opts) {
14004
+ if (opts.choice === false) {
14005
+ await opts.disable();
14006
+ return "disabled";
14007
+ }
14008
+ if (opts.choice === void 0 && !opts.interactive) return "unchanged";
14009
+ opts.print(SUGGEST_HOOK_DISCLOSURE);
14010
+ if (opts.choice === void 0) {
14011
+ const yes = await opts.confirm("Turn on tool suggestions?");
14012
+ if (!yes) return "unchanged";
14013
+ }
14014
+ await opts.enable();
14015
+ return "enabled";
14016
+ }
14017
+
14018
+ // src/lib/hook-credential.ts
14019
+ import { existsSync as existsSync4 } from "node:fs";
14020
+ import { readFile as readFile4 } from "node:fs/promises";
14021
+ function issuesToChecks(issues) {
14022
+ return issues.map((issue) => {
14023
+ const check = {
14024
+ name: `Hook credential: ${issue.code}`,
14025
+ status: issue.level,
14026
+ message: issue.message
14027
+ };
14028
+ if (issue.hint) check.hint = issue.hint;
14029
+ return check;
14030
+ });
14031
+ }
14032
+ function mergeHookConfig(existing, url, token) {
14033
+ let base = {};
14034
+ if (existing) {
14035
+ try {
14036
+ const parsed = JSON.parse(existing);
14037
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
14038
+ base = parsed;
14039
+ }
14040
+ } catch {
14041
+ base = {};
14042
+ }
14043
+ }
14044
+ return JSON.stringify({ ...base, url, token }, null, 2);
14045
+ }
14046
+ async function ensureHookCredential(opts) {
14047
+ const fetchImpl = opts.fetchImpl ?? fetch;
14048
+ const { hookConfigPath, apiUrl, auth } = opts;
14049
+ const checks = [];
14050
+ const repairs = [];
14051
+ let url = null;
14052
+ let token = null;
14053
+ let existing = null;
14054
+ if (existsSync4(hookConfigPath)) {
14055
+ existing = await readFile4(hookConfigPath, "utf8");
14056
+ const inspection = inspectHookConfig(existing);
14057
+ url = inspection.url;
14058
+ token = inspection.token;
14059
+ checks.push(...issuesToChecks(inspection.issues));
14060
+ } else {
14061
+ checks.push({
14062
+ name: "Hook credential: not found",
14063
+ status: "warn",
14064
+ message: `${hookConfigPath} does not exist \u2014 hooks cannot authenticate`,
14065
+ hint: "Run with --enforce to write it from your gad login credential."
14066
+ });
14067
+ }
14068
+ const probe = async (t, base) => {
14069
+ const res = await fetchImpl(`${base}/api/mcp/ping`, {
14070
+ headers: { Authorization: `Bearer ${t}` },
14071
+ signal: AbortSignal.timeout(1e4)
14072
+ });
14073
+ return res.status;
14074
+ };
14075
+ let tokenOk = false;
14076
+ if (token) {
14077
+ try {
14078
+ const status = await probe(token, url ?? apiUrl);
14079
+ tokenOk = status >= 200 && status < 300;
14080
+ if (status === 401 || status === 403) {
14081
+ checks.push({
14082
+ name: "Hook credential: token rejected",
14083
+ status: "fail",
14084
+ message: `The hook-config.json token was rejected (HTTP ${status}) \u2014 transcript capture and the session glance are silently failing`,
14085
+ hint: "Run with --enforce to rewrite it from your gad login credential."
14086
+ });
14087
+ } else if (!tokenOk) {
14088
+ checks.push({
14089
+ name: "Hook credential: probe inconclusive",
14090
+ status: "warn",
14091
+ message: `Ping returned HTTP ${status} \u2014 could not confirm the hook token`
14092
+ });
14093
+ } else {
14094
+ checks.push({
14095
+ name: "Hook credential: token valid",
14096
+ status: "pass",
14097
+ message: "hook-config.json token authenticates"
14098
+ });
14099
+ }
14100
+ } catch (e) {
14101
+ checks.push({
14102
+ name: "Hook credential: probe failed",
14103
+ status: "warn",
14104
+ message: `Could not reach ${url ?? apiUrl}: ${String(
14105
+ e instanceof Error ? e.message : e
14106
+ )}`
14107
+ });
14108
+ }
14109
+ }
14110
+ let verified = tokenOk;
14111
+ if (opts.repair && !tokenOk) {
14112
+ if (auth?.token) {
14113
+ await opts.write(
14114
+ hookConfigPath,
14115
+ mergeHookConfig(existing, apiUrl, auth.token)
14116
+ );
14117
+ repairs.push("hook-config.json: rewrote from gad credential");
14118
+ try {
14119
+ const status = await probe(auth.token, apiUrl);
14120
+ verified = status >= 200 && status < 300;
14121
+ checks.push({
14122
+ name: "Hook credential: after repair",
14123
+ status: verified ? "pass" : "fail",
14124
+ message: verified ? "Rewritten token authenticates" : `Rewritten token still failing (HTTP ${status})`
14125
+ });
14126
+ } catch (err) {
14127
+ checks.push({
14128
+ name: "Hook credential: after repair",
14129
+ status: "warn",
14130
+ message: `Rewrote the token but could not re-probe it (${err instanceof Error ? err.message : String(err)})`,
14131
+ hint: "Re-run `gad doctor` once the network is reachable to confirm."
14132
+ });
14133
+ }
14134
+ } else {
14135
+ checks.push({
14136
+ name: "Hook credential: no repair source",
14137
+ status: "fail",
14138
+ message: "No gad login credential available to rewrite hook-config.json",
14139
+ hint: "Run `gad login` first, then re-run with --enforce."
14140
+ });
14141
+ }
14142
+ }
14143
+ return { checks, repairs, verified };
14144
+ }
14145
+
13818
14146
  // src/lib/mcp-doctor.ts
13819
14147
  import { createHash as createHash2 } from "node:crypto";
13820
14148
 
@@ -13986,7 +14314,7 @@ function knownConfigLocations(home, cwd, platform = process.platform, appData) {
13986
14314
  ];
13987
14315
  }
13988
14316
  var GREAT_ARROW_NAME = /great[-_ ]?arrow|ai[-_]memory/i;
13989
- function asObject2(value) {
14317
+ function asObject3(value) {
13990
14318
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
13991
14319
  }
13992
14320
  function isGreatArrowServer(name, entry) {
@@ -14007,13 +14335,13 @@ function classifySurface(entry, clientId, hooksInstalled) {
14007
14335
  }
14008
14336
  function serverMapsFromJson(parsed, serversKey) {
14009
14337
  const maps = [];
14010
- const top = asObject2(parsed[serversKey]);
14338
+ const top = asObject3(parsed[serversKey]);
14011
14339
  if (top) maps.push({ servers: top });
14012
- const projects = asObject2(parsed.projects);
14340
+ const projects = asObject3(parsed.projects);
14013
14341
  if (projects) {
14014
14342
  for (const [dir, value] of Object.entries(projects)) {
14015
- const section = asObject2(value);
14016
- const servers = section ? asObject2(section[serversKey]) : null;
14343
+ const section = asObject3(value);
14344
+ const servers = section ? asObject3(section[serversKey]) : null;
14017
14345
  if (servers && Object.keys(servers).length > 0) {
14018
14346
  maps.push({ servers, projectDir: dir });
14019
14347
  }
@@ -14051,7 +14379,7 @@ function findGreatArrowEntries(location, content, options = {}) {
14051
14379
  if (location.format === "toml") return findCodexEntries(location, content);
14052
14380
  let parsed = null;
14053
14381
  try {
14054
- parsed = asObject2(JSON.parse(content));
14382
+ parsed = asObject3(JSON.parse(content));
14055
14383
  } catch {
14056
14384
  return [];
14057
14385
  }
@@ -14063,7 +14391,7 @@ function findGreatArrowEntries(location, content, options = {}) {
14063
14391
  serversKey
14064
14392
  )) {
14065
14393
  for (const [name, value] of Object.entries(servers)) {
14066
- const entry = asObject2(value);
14394
+ const entry = asObject3(value);
14067
14395
  if (!entry) continue;
14068
14396
  if (!isGreatArrowServer(name, entry)) continue;
14069
14397
  entries.push({
@@ -14086,7 +14414,7 @@ function findGreatArrowEntries(location, content, options = {}) {
14086
14414
  function hasGreatArrowHooks(settingsContent) {
14087
14415
  let parsed = null;
14088
14416
  try {
14089
- parsed = asObject2(JSON.parse(settingsContent));
14417
+ parsed = asObject3(JSON.parse(settingsContent));
14090
14418
  } catch {
14091
14419
  return false;
14092
14420
  }
@@ -14160,7 +14488,7 @@ function describeStoredCredential(raw) {
14160
14488
  if (!trimmed) return null;
14161
14489
  if (trimmed.startsWith("{")) {
14162
14490
  try {
14163
- const parsed = asObject2(JSON.parse(trimmed));
14491
+ const parsed = asObject3(JSON.parse(trimmed));
14164
14492
  const refreshToken = parsed?.refreshToken;
14165
14493
  if (typeof refreshToken === "string" && refreshToken.length > 0) {
14166
14494
  return {
@@ -14559,7 +14887,7 @@ async function checkMcp(auth) {
14559
14887
  }
14560
14888
  }
14561
14889
  function defaultCodexConfigPath() {
14562
- return join7(homedir5(), ".codex", "config.toml");
14890
+ return join8(homedir5(), ".codex", "config.toml");
14563
14891
  }
14564
14892
  function codexIssuesToChecks(issues) {
14565
14893
  return issues.map((issue) => {
@@ -14597,16 +14925,16 @@ function codexCommand() {
14597
14925
  });
14598
14926
  const configPath = options.config ?? defaultCodexConfigPath();
14599
14927
  const expectedRepoUrl = options.expectedRepo ?? detectGitOrigin2();
14600
- let content = existsSync3(configPath) ? await readFile3(configPath, "utf8") : "";
14928
+ let content = existsSync5(configPath) ? await readFile5(configPath, "utf8") : "";
14601
14929
  let backupPath = null;
14602
14930
  let removedServers = [];
14603
14931
  if (options.enforce) {
14604
14932
  const apiUrl = extractCodexGreatArrowApiUrl(content) ?? (await resolveAuth()).apiUrl;
14605
14933
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
14606
- await mkdir4(dirname4(configPath), { recursive: true });
14934
+ await mkdir5(dirname5(configPath), { recursive: true });
14607
14935
  if (content.trim()) {
14608
14936
  backupPath = `${configPath}.bak.${timestamp}`;
14609
- await copyFile(configPath, backupPath);
14937
+ await copyFile2(configPath, backupPath);
14610
14938
  }
14611
14939
  content = upsertCodexServerBlock(content, apiUrl, expectedRepoUrl);
14612
14940
  const preRepair = inspectCodexConfig(content, expectedRepoUrl);
@@ -14616,7 +14944,7 @@ function codexCommand() {
14616
14944
  );
14617
14945
  content = repaired.content;
14618
14946
  removedServers = repaired.removedServers;
14619
- await writeFile3(configPath, content, "utf8");
14947
+ await writeFile4(configPath, content, "utf8");
14620
14948
  }
14621
14949
  const inspection = inspectCodexConfig(content, expectedRepoUrl);
14622
14950
  const checks = codexIssuesToChecks(inspection.issues);
@@ -14698,11 +15026,11 @@ function claudeCommand() {
14698
15026
  noColor: g.noColor
14699
15027
  });
14700
15028
  const home = homedir5();
14701
- const gaDir = join7(home, ".great-arrow");
14702
- const userConfigPath = join7(home, ".claude.json");
14703
- const settingsPath = join7(home, ".claude", "settings.json");
14704
- const hookConfigPath = join7(gaDir, "hook-config.json");
14705
- const glanceHookPath = join7(gaDir, "claude-glance-hook.mjs");
15029
+ const gaDir = join8(home, ".great-arrow");
15030
+ const userConfigPath = join8(home, ".claude.json");
15031
+ const settingsPath = join8(home, ".claude", "settings.json");
15032
+ const hookConfigPath = join8(gaDir, "hook-config.json");
15033
+ const glanceHookPath = join8(gaDir, "claude-glance-hook.mjs");
14706
15034
  const checks = [];
14707
15035
  const repairs = [];
14708
15036
  const backups = [];
@@ -14719,13 +15047,13 @@ function claudeCommand() {
14719
15047
  }
14720
15048
  };
14721
15049
  const backupAndWrite = async (path10, content) => {
14722
- if (existsSync3(path10)) {
15050
+ if (existsSync5(path10)) {
14723
15051
  const backupPath = `${path10}.bak.${timestamp}`;
14724
- await copyFile(path10, backupPath);
15052
+ await copyFile2(path10, backupPath);
14725
15053
  backups.push(backupPath);
14726
15054
  }
14727
- await mkdir4(dirname4(path10), { recursive: true });
14728
- await writeFile3(path10, content, "utf8");
15055
+ await mkdir5(dirname5(path10), { recursive: true });
15056
+ await writeFile4(path10, content, "utf8");
14729
15057
  };
14730
15058
  let auth = null;
14731
15059
  try {
@@ -14734,8 +15062,8 @@ function claudeCommand() {
14734
15062
  auth = null;
14735
15063
  }
14736
15064
  const apiUrl = auth?.apiUrl ?? "https://www.greatarrow.ai";
14737
- if (existsSync3(userConfigPath)) {
14738
- let content = await readFile3(userConfigPath, "utf8");
15065
+ if (existsSync5(userConfigPath)) {
15066
+ let content = await readFile5(userConfigPath, "utf8");
14739
15067
  if (options.enforce) {
14740
15068
  const repaired = repairClaudeUserConfig(content);
14741
15069
  if (repaired.changed.length > 0) {
@@ -14755,7 +15083,7 @@ function claudeCommand() {
14755
15083
  });
14756
15084
  }
14757
15085
  {
14758
- let content = existsSync3(settingsPath) ? await readFile3(settingsPath, "utf8") : "";
15086
+ let content = existsSync5(settingsPath) ? await readFile5(settingsPath, "utf8") : "";
14759
15087
  if (options.enforce) {
14760
15088
  const glanceCommand = `node "${glanceHookPath}"`;
14761
15089
  const repaired = repairClaudeHookSettings(content, glanceCommand);
@@ -14764,15 +15092,15 @@ function claudeCommand() {
14764
15092
  content = repaired.content;
14765
15093
  repairs.push(...repaired.changed.map((c) => `settings.json: ${c}`));
14766
15094
  }
14767
- if (!existsSync3(glanceHookPath)) {
15095
+ if (!existsSync5(glanceHookPath)) {
14768
15096
  try {
14769
15097
  const res = await fetch(
14770
15098
  `${apiUrl}/api/install/claude-glance-hook`,
14771
15099
  { signal: AbortSignal.timeout(1e4) }
14772
15100
  );
14773
15101
  if (res.ok) {
14774
- await mkdir4(gaDir, { recursive: true });
14775
- await writeFile3(glanceHookPath, await res.text(), {
15102
+ await mkdir5(gaDir, { recursive: true });
15103
+ await writeFile4(glanceHookPath, await res.text(), {
14776
15104
  mode: 493
14777
15105
  });
14778
15106
  repairs.push(`downloaded ${glanceHookPath}`);
@@ -14793,7 +15121,7 @@ function claudeCommand() {
14793
15121
  }
14794
15122
  }
14795
15123
  toChecks("Claude hooks", inspectClaudeHookSettings(content).issues);
14796
- if (!options.enforce && !existsSync3(glanceHookPath)) {
15124
+ if (!options.enforce && !existsSync5(glanceHookPath)) {
14797
15125
  checks.push({
14798
15126
  name: "Glance hook script: missing",
14799
15127
  status: "warn",
@@ -14803,96 +15131,31 @@ function claudeCommand() {
14803
15131
  }
14804
15132
  }
14805
15133
  {
14806
- let url = null;
14807
- let token = null;
14808
- if (existsSync3(hookConfigPath)) {
14809
- const inspection = inspectHookConfig(
14810
- await readFile3(hookConfigPath, "utf8")
14811
- );
14812
- url = inspection.url;
14813
- token = inspection.token;
14814
- toChecks("Hook credential", inspection.issues);
14815
- } else {
14816
- checks.push({
14817
- name: "Hook credential: not found",
14818
- status: "warn",
14819
- message: `${hookConfigPath} does not exist \u2014 hooks cannot authenticate`,
14820
- hint: "Run with --enforce to write it from your gad login credential."
14821
- });
14822
- }
14823
- const probe = async (t, base) => {
14824
- const res = await fetch(`${base}/api/mcp/ping`, {
14825
- headers: { Authorization: `Bearer ${t}` },
14826
- signal: AbortSignal.timeout(1e4)
14827
- });
14828
- return res.status;
15134
+ const credential = await ensureHookCredential({
15135
+ hookConfigPath,
15136
+ apiUrl,
15137
+ auth,
15138
+ repair: Boolean(options.enforce),
15139
+ write: backupAndWrite
15140
+ });
15141
+ checks.push(...credential.checks);
15142
+ repairs.push(...credential.repairs);
15143
+ }
15144
+ {
15145
+ const suggest = await suggestHookStatus(home);
15146
+ const check = {
15147
+ name: "Tool suggestions (opt-in)",
15148
+ status: "pass",
15149
+ message: suggest.enabled ? "ON \u2014 each prompt's text is sent to Great Arrow for tool suggestions" : "OFF"
14829
15150
  };
14830
- let tokenOk = false;
14831
- if (token) {
14832
- try {
14833
- const status = await probe(token, url ?? apiUrl);
14834
- tokenOk = status >= 200 && status < 300;
14835
- if (status === 401 || status === 403) {
14836
- checks.push({
14837
- name: "Hook credential: token rejected",
14838
- status: "fail",
14839
- message: `The hook-config.json token was rejected (HTTP ${status}) \u2014 transcript capture and the session glance are silently failing`,
14840
- hint: "Run with --enforce to rewrite it from your gad login credential."
14841
- });
14842
- } else if (!tokenOk) {
14843
- checks.push({
14844
- name: "Hook credential: probe inconclusive",
14845
- status: "warn",
14846
- message: `Ping returned HTTP ${status} \u2014 could not confirm the hook token`
14847
- });
14848
- } else {
14849
- checks.push({
14850
- name: "Hook credential: token valid",
14851
- status: "pass",
14852
- message: "hook-config.json token authenticates"
14853
- });
14854
- }
14855
- } catch (e) {
14856
- checks.push({
14857
- name: "Hook credential: probe failed",
14858
- status: "warn",
14859
- message: `Could not reach ${url ?? apiUrl}: ${String(
14860
- e instanceof Error ? e.message : e
14861
- )}`
14862
- });
14863
- }
14864
- }
14865
- if (options.enforce && !tokenOk) {
14866
- if (auth?.token) {
14867
- await backupAndWrite(
14868
- hookConfigPath,
14869
- JSON.stringify({ url: apiUrl, token: auth.token }, null, 2)
14870
- );
14871
- repairs.push("hook-config.json: rewrote from gad credential");
14872
- try {
14873
- const status = await probe(auth.token, apiUrl);
14874
- checks.push({
14875
- name: "Hook credential: after repair",
14876
- status: status >= 200 && status < 300 ? "pass" : "fail",
14877
- message: status >= 200 && status < 300 ? "Rewritten token authenticates" : `Rewritten token still failing (HTTP ${status})`
14878
- });
14879
- } catch (err) {
14880
- checks.push({
14881
- name: "Hook credential: after repair",
14882
- status: "warn",
14883
- message: `Rewrote the token but could not re-probe it (${err instanceof Error ? err.message : String(err)})`,
14884
- hint: "Re-run `gad doctor` once the network is reachable to confirm."
14885
- });
14886
- }
14887
- } else {
14888
- checks.push({
14889
- name: "Hook credential: no repair source",
14890
- status: "fail",
14891
- message: "No gad login credential available to rewrite hook-config.json",
14892
- hint: "Run `gad login` first, then re-run with --enforce."
14893
- });
14894
- }
15151
+ if (suggest.enabled && !suggest.credentialPresent) {
15152
+ check.status = "warn";
15153
+ check.message = "ON but no hook credential \u2014 the hook sends nothing";
15154
+ check.hint = "Run `gad login`, then `gad mcp suggest-tools on`.";
15155
+ } else if (!suggest.enabled) {
15156
+ check.hint = "Opt in with `gad mcp suggest-tools on`.";
14895
15157
  }
15158
+ checks.push(check);
14896
15159
  }
14897
15160
  {
14898
15161
  let root = command;
@@ -14910,23 +15173,23 @@ function claudeCommand() {
14910
15173
  file: "/Library/Application Support/ClaudeCode/managed-settings.json"
14911
15174
  },
14912
15175
  { scope: "user", file: settingsPath },
14913
- { scope: "project", file: join7(cwd, ".claude", "settings.json") },
14914
- { scope: "local", file: join7(cwd, ".claude", "settings.local.json") }
15176
+ { scope: "project", file: join8(cwd, ".claude", "settings.json") },
15177
+ { scope: "local", file: join8(cwd, ".claude", "settings.local.json") }
14915
15178
  ];
14916
15179
  const configured = [];
14917
15180
  for (const { scope, file } of scopes) {
14918
- if (!existsSync3(file)) continue;
15181
+ if (!existsSync5(file)) continue;
14919
15182
  configured.push(
14920
15183
  ...collectConfiguredHooks(
14921
15184
  scope,
14922
15185
  file,
14923
- await readFile3(file, "utf8")
15186
+ await readFile5(file, "utf8")
14924
15187
  )
14925
15188
  );
14926
15189
  }
14927
15190
  const hookIssues = inspectConfiguredHooks(configured, {
14928
15191
  knownGadVerbs,
14929
- fileExists: existsSync3
15192
+ fileExists: existsSync5
14930
15193
  });
14931
15194
  toChecks("Configured hooks", hookIssues);
14932
15195
  if (configured.length > 0 && hookIssues.length === 0) {
@@ -14982,10 +15245,10 @@ function claudeCommand() {
14982
15245
  function defaultJsonClientConfigs() {
14983
15246
  const home = homedir5();
14984
15247
  return [
14985
- { name: "Claude Code", path: join7(home, ".claude.json") },
14986
- { name: "Cursor", path: join7(home, ".cursor", "mcp.json") },
14987
- { name: "Kiro", path: join7(home, ".kiro", "settings", "mcp.json") },
14988
- { name: "Gemini CLI", path: join7(home, ".gemini", "settings.json") }
15248
+ { name: "Claude Code", path: join8(home, ".claude.json") },
15249
+ { name: "Cursor", path: join8(home, ".cursor", "mcp.json") },
15250
+ { name: "Kiro", path: join8(home, ".kiro", "settings", "mcp.json") },
15251
+ { name: "Gemini CLI", path: join8(home, ".gemini", "settings.json") }
14989
15252
  ];
14990
15253
  }
14991
15254
  function clientsCommand() {
@@ -15011,8 +15274,8 @@ function clientsCommand() {
15011
15274
  const checks = [];
15012
15275
  const results = [];
15013
15276
  for (const { name, path: path10 } of defaultJsonClientConfigs()) {
15014
- if (!existsSync3(path10)) continue;
15015
- let content = await readFile3(path10, "utf8");
15277
+ if (!existsSync5(path10)) continue;
15278
+ let content = await readFile5(path10, "utf8");
15016
15279
  let backupPath = null;
15017
15280
  let removedServers = [];
15018
15281
  if (options.enforce) {
@@ -15020,14 +15283,14 @@ function clientsCommand() {
15020
15283
  if (!pre.parseError && pre.staleRepoServers.length > 0) {
15021
15284
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
15022
15285
  backupPath = `${path10}.bak.${timestamp}`;
15023
- await copyFile(path10, backupPath);
15286
+ await copyFile2(path10, backupPath);
15024
15287
  const repaired = repairJsonMcpConfig(
15025
15288
  content,
15026
15289
  pre.staleRepoServers
15027
15290
  );
15028
15291
  content = repaired.content;
15029
15292
  removedServers = repaired.removedServers;
15030
- await writeFile3(path10, content, "utf8");
15293
+ await writeFile4(path10, content, "utf8");
15031
15294
  }
15032
15295
  }
15033
15296
  const inspection = inspectJsonMcpConfig(content, expectedRepoUrl);
@@ -15522,6 +15785,107 @@ async function runStdioBridge(options) {
15522
15785
  });
15523
15786
  }
15524
15787
 
15788
+ // src/commands/suggest-tools.ts
15789
+ import { homedir as homedir6 } from "node:os";
15790
+ var HOOK_CREDENTIAL_FIX_LINE = "Tool suggestions are on, but the hook's Great Arrow token could not be confirmed \u2014 run `gad doctor claude --enforce` (after `gad login` if needed) to fix it.";
15791
+ async function enableSuggestTools(home, deps = {}) {
15792
+ const packagedScript = deps.packagedScript === void 0 ? packagedSuggestHookPath() : deps.packagedScript;
15793
+ if (!packagedScript) {
15794
+ throw validationError(
15795
+ "The tool-suggestion hook script is missing from this @great-arrow/cli install.",
15796
+ "Reinstall with `npm install -g @great-arrow/cli@latest`."
15797
+ );
15798
+ }
15799
+ await enableSuggestHook({ home, packagedScript });
15800
+ let auth = null;
15801
+ try {
15802
+ auth = await (deps.resolveAuth ?? resolveAuth)();
15803
+ } catch {
15804
+ auth = null;
15805
+ }
15806
+ const credential = await ensureHookCredential({
15807
+ hookConfigPath: suggestHookPaths(home).hookConfigPath,
15808
+ apiUrl: auth?.apiUrl ?? DEFAULT_API_URL,
15809
+ auth,
15810
+ repair: true,
15811
+ write: (path10, content) => atomicWrite(path10, content, 384),
15812
+ ...deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}
15813
+ });
15814
+ return { verified: credential.verified };
15815
+ }
15816
+ async function applySuggestToolsChoice(choice, home = homedir6(), deps = {}) {
15817
+ const out = deps.out ?? createFormatter({
15818
+ json: false,
15819
+ quiet: false,
15820
+ isTTY: isTTY(),
15821
+ noColor: false
15822
+ });
15823
+ let verified = true;
15824
+ const outcome = await runSuggestHookOptIn({
15825
+ choice,
15826
+ interactive: deps.interactive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY),
15827
+ print: (line) => out.info(line),
15828
+ confirm: deps.confirm ?? confirm,
15829
+ enable: async () => {
15830
+ verified = (await enableSuggestTools(home, deps)).verified;
15831
+ },
15832
+ disable: () => disableSuggestHook({ home })
15833
+ });
15834
+ const { settingsPath } = suggestHookPaths(home);
15835
+ if (outcome === "enabled") {
15836
+ out.success(`Tool suggestions ON (registered in ${settingsPath}).`);
15837
+ if (!verified) out.warn(HOOK_CREDENTIAL_FIX_LINE);
15838
+ } else if (outcome === "disabled") {
15839
+ out.success("Tool suggestions OFF (our hook entry removed).");
15840
+ }
15841
+ return outcome;
15842
+ }
15843
+ function suggestToolsCommand() {
15844
+ return new Command("suggest-tools").description(
15845
+ "Opt-in Claude Code prompt hook that names Great Arrow tools which may help (sends each prompt's text)"
15846
+ ).argument("[action]", "on | off | status", "status").option("--yes", "Accept the disclosure without an interactive prompt").option("--json", "Output status as JSON").action(
15847
+ async (action, options) => {
15848
+ const home = homedir6();
15849
+ if (action === "status") {
15850
+ const status = await suggestHookStatus(home);
15851
+ if (options.json) {
15852
+ process.stdout.write(JSON.stringify(status) + "\n");
15853
+ return;
15854
+ }
15855
+ process.stdout.write(
15856
+ `Tool suggestions: ${status.enabled ? "ON" : "OFF"}` + (status.enabled && !status.scriptPresent ? " (hook script missing \u2014 run `gad mcp suggest-tools on`)" : "") + (status.enabled && !status.credentialPresent ? " (no hook credential \u2014 run `gad login`, then `gad mcp suggest-tools on`)" : "") + "\n"
15857
+ );
15858
+ return;
15859
+ }
15860
+ if (action === "off") {
15861
+ await applySuggestToolsChoice(false, home);
15862
+ return;
15863
+ }
15864
+ if (action === "on") {
15865
+ const outcome = await applySuggestToolsChoice(
15866
+ options.yes ? true : void 0,
15867
+ home
15868
+ );
15869
+ if (outcome === "unchanged") {
15870
+ if (!(process.stdin.isTTY && process.stdout.isTTY)) {
15871
+ process.stdout.write(`${SUGGEST_HOOK_DISCLOSURE}
15872
+
15873
+ `);
15874
+ }
15875
+ process.stdout.write(
15876
+ "Tool suggestions left OFF. To turn on without a prompt: `gad mcp suggest-tools on --yes`.\n"
15877
+ );
15878
+ }
15879
+ return;
15880
+ }
15881
+ throw validationError(
15882
+ `Unknown action "${action}".`,
15883
+ "Use one of: on, off, status."
15884
+ );
15885
+ }
15886
+ );
15887
+ }
15888
+
15525
15889
  // src/commands/mcp.ts
15526
15890
  var SUPPORTED_CLIENTS = [
15527
15891
  "claude",
@@ -15631,33 +15995,33 @@ async function pathExists(path10) {
15631
15995
  }
15632
15996
  }
15633
15997
  async function installCodexConfig(apiUrl, token, repoUrl) {
15634
- const { homedir: homedir7 } = await import("node:os");
15635
- const { join: join9 } = await import("node:path");
15636
- const { chmod, copyFile: copyFile2, mkdir: mkdir6, readFile: readFile5, writeFile: writeFile5 } = await import("node:fs/promises");
15637
- const codexDir = join9(homedir7(), ".codex");
15638
- const configPath = join9(codexDir, "config.toml");
15639
- const envPath = join9(codexDir, ".env");
15998
+ const { homedir: homedir8 } = await import("node:os");
15999
+ const { join: join10 } = await import("node:path");
16000
+ const { chmod: chmod2, copyFile: copyFile3, mkdir: mkdir7, readFile: readFile7, writeFile: writeFile6 } = await import("node:fs/promises");
16001
+ const codexDir = join10(homedir8(), ".codex");
16002
+ const configPath = join10(codexDir, "config.toml");
16003
+ const envPath = join10(codexDir, ".env");
15640
16004
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
15641
- await mkdir6(codexDir, { recursive: true });
15642
- const existingConfig = await pathExists(configPath) ? await readFile5(configPath, "utf8") : "";
16005
+ await mkdir7(codexDir, { recursive: true });
16006
+ const existingConfig = await pathExists(configPath) ? await readFile7(configPath, "utf8") : "";
15643
16007
  if (existingConfig.trim()) {
15644
- await copyFile2(configPath, `${configPath}.bak.${timestamp}`);
16008
+ await copyFile3(configPath, `${configPath}.bak.${timestamp}`);
15645
16009
  }
15646
- await writeFile5(
16010
+ await writeFile6(
15647
16011
  configPath,
15648
16012
  upsertCodexServerBlock(existingConfig, apiUrl, repoUrl),
15649
16013
  "utf8"
15650
16014
  );
15651
- const existingEnv = await pathExists(envPath) ? await readFile5(envPath, "utf8") : "";
16015
+ const existingEnv = await pathExists(envPath) ? await readFile7(envPath, "utf8") : "";
15652
16016
  if (existingEnv.trim()) {
15653
- await copyFile2(envPath, `${envPath}.bak.${timestamp}`);
16017
+ await copyFile3(envPath, `${envPath}.bak.${timestamp}`);
15654
16018
  }
15655
- await writeFile5(
16019
+ await writeFile6(
15656
16020
  envPath,
15657
16021
  upsertEnvValue(existingEnv, "GREAT_ARROW_TOKEN", token),
15658
16022
  "utf8"
15659
16023
  );
15660
- await chmod(envPath, 384);
16024
+ await chmod2(envPath, 384);
15661
16025
  return { configPath, envPath };
15662
16026
  }
15663
16027
  var startCommand = new Command("start").description(
@@ -15884,6 +16248,12 @@ var installCommand = new Command("install").description("Generate MCP configurat
15884
16248
  ).option("--stdout", "Print config to stdout instead of writing file").option(
15885
16249
  "--repo-url <url>",
15886
16250
  "Repo URL to pin MCP requests to (defaults to current git origin)"
16251
+ ).option(
16252
+ "--suggest-tools",
16253
+ "claude only: turn on the opt-in prompt hook that sends each prompt's text to Great Arrow for tool suggestions"
16254
+ ).option(
16255
+ "--no-suggest-tools",
16256
+ "claude only: turn the tool-suggestion prompt hook off (removes only its entry)"
15887
16257
  ).action(
15888
16258
  async (clientName, options) => {
15889
16259
  const fmt = createFormatter({
@@ -15935,15 +16305,15 @@ var installCommand = new Command("install").description("Generate MCP configurat
15935
16305
  process.stdout.write(configJson2 + "\n");
15936
16306
  return;
15937
16307
  }
15938
- const { homedir: homedir7 } = await import("node:os");
15939
- const { join: join10 } = await import("node:path");
15940
- const { mkdir: mkdir6, readFile: readFile5, writeFile: writeFile6 } = await import("node:fs/promises");
15941
- const geminiDir = join10(homedir7(), ".gemini");
15942
- const mcpPath = join10(geminiDir, "settings.json");
15943
- await mkdir6(geminiDir, { recursive: true });
16308
+ const { homedir: homedir8 } = await import("node:os");
16309
+ const { join: join11 } = await import("node:path");
16310
+ const { mkdir: mkdir7, readFile: readFile7, writeFile: writeFile7 } = await import("node:fs/promises");
16311
+ const geminiDir = join11(homedir8(), ".gemini");
16312
+ const mcpPath = join11(geminiDir, "settings.json");
16313
+ await mkdir7(geminiDir, { recursive: true });
15944
16314
  let existingConfig = {};
15945
16315
  try {
15946
- const content = await readFile5(mcpPath, "utf-8");
16316
+ const content = await readFile7(mcpPath, "utf-8");
15947
16317
  existingConfig = JSON.parse(content);
15948
16318
  } catch {
15949
16319
  existingConfig = {};
@@ -15955,20 +16325,20 @@ var installCommand = new Command("install").description("Generate MCP configurat
15955
16325
  ...config2.mcpServers
15956
16326
  }
15957
16327
  };
15958
- await writeFile6(
16328
+ await writeFile7(
15959
16329
  mcpPath,
15960
16330
  JSON.stringify(existingConfig, null, 2) + "\n",
15961
16331
  "utf-8"
15962
16332
  );
15963
16333
  fmt.success(`Wrote gemini MCP config to ${mcpPath}`);
15964
- const agyMcpDir = join10(
16334
+ const agyMcpDir = join11(
15965
16335
  geminiDir,
15966
16336
  "antigravity-cli",
15967
16337
  "mcp",
15968
16338
  MCP_SERVER_NAME
15969
16339
  );
15970
- await mkdir6(agyMcpDir, { recursive: true });
15971
- const instructionsPath = join10(agyMcpDir, "instructions.md");
16340
+ await mkdir7(agyMcpDir, { recursive: true });
16341
+ const instructionsPath = join11(agyMcpDir, "instructions.md");
15972
16342
  const instructionsContent = `You are connected to greatarrow.ai - the user's private workspace memory + integrations brain.
15973
16343
 
15974
16344
  CRITICAL DIRECTIVE (MANDATORY STANDARD):
@@ -15984,7 +16354,7 @@ THREE-LAYER LIVE TASK TRACKING:
15984
16354
  - Maintain client-native harness plans, Great Arrow Kanban cards (\`todo_create\`, \`todo_update\`), and resume states.
15985
16355
 
15986
16356
  Always assume full autonomy to leverage these tools to their maximum extent.`;
15987
- await writeFile6(instructionsPath, instructionsContent, "utf-8");
16357
+ await writeFile7(instructionsPath, instructionsContent, "utf-8");
15988
16358
  fmt.success(`Wrote gemini MCP instructions to ${instructionsPath}`);
15989
16359
  return;
15990
16360
  }
@@ -15999,25 +16369,28 @@ Always assume full autonomy to leverage these tools to their maximum extent.`;
15999
16369
  process.stdout.write(configJson + "\n");
16000
16370
  return;
16001
16371
  }
16002
- const { writeFile: writeFile5 } = await import("node:fs/promises");
16003
- const { join: join9 } = await import("node:path");
16004
- const outputPath = join9(process.cwd(), ".mcp.json");
16005
- await writeFile5(outputPath, configJson + "\n", { mode: 384 });
16372
+ const { writeFile: writeFile6 } = await import("node:fs/promises");
16373
+ const { join: join10 } = await import("node:path");
16374
+ const outputPath = join10(process.cwd(), ".mcp.json");
16375
+ await writeFile6(outputPath, configJson + "\n", { mode: 384 });
16006
16376
  fmt.success(`Wrote ${clientName} MCP config to .mcp.json`);
16007
16377
  if (await mcpJsonAtRiskOfCommit(process.cwd())) {
16008
16378
  fmt.warn(
16009
16379
  ".mcp.json contains an API token and is NOT gitignored here \u2014 add '.mcp.json' to .gitignore before committing."
16010
16380
  );
16011
16381
  }
16382
+ if (clientName === "claude") {
16383
+ await applySuggestToolsChoice(options.suggestTools);
16384
+ }
16012
16385
  }
16013
16386
  );
16014
16387
  var KEYCHAIN_SERVICE = "GreatArrowDigital";
16015
16388
  var KEYCHAIN_ACCOUNT = "api-token";
16016
16389
  var OAUTH_CRED_FILENAME = "oauth-credential.json";
16017
16390
  async function readIfExists(path10) {
16018
- const { readFile: readFile5 } = await import("node:fs/promises");
16391
+ const { readFile: readFile7 } = await import("node:fs/promises");
16019
16392
  try {
16020
- return await readFile5(path10, "utf8");
16393
+ return await readFile7(path10, "utf8");
16021
16394
  } catch {
16022
16395
  return null;
16023
16396
  }
@@ -16032,13 +16405,13 @@ async function readKeychainOnly() {
16032
16405
  }
16033
16406
  }
16034
16407
  async function resolveProxyScript(override) {
16035
- const { homedir: homedir7 } = await import("node:os");
16036
- const { join: join9 } = await import("node:path");
16408
+ const { homedir: homedir8 } = await import("node:os");
16409
+ const { join: join10 } = await import("node:path");
16037
16410
  const candidates = [
16038
16411
  override,
16039
16412
  process.env.GAD_PROXY_SCRIPT,
16040
- join9(homedir7(), ".great-arrow", "mcp-server.mjs"),
16041
- join9(process.cwd(), "scripts", "mcp-server.mjs")
16413
+ join10(homedir8(), ".great-arrow", "mcp-server.mjs"),
16414
+ join10(process.cwd(), "scripts", "mcp-server.mjs")
16042
16415
  ].filter((c) => Boolean(c));
16043
16416
  for (const candidate of candidates) {
16044
16417
  if (await pathExists(candidate)) return candidate;
@@ -16062,13 +16435,13 @@ async function runProxyDiagnostics(scriptPath) {
16062
16435
  }
16063
16436
  }
16064
16437
  async function findLegacyStores(cwd) {
16065
- const { join: join9 } = await import("node:path");
16438
+ const { join: join10 } = await import("node:path");
16066
16439
  const { readdir } = await import("node:fs/promises");
16067
16440
  const dirs = /* @__PURE__ */ new Set([cwd]);
16068
16441
  try {
16069
- const worktreeRoot = join9(cwd, ".claude", "worktrees");
16442
+ const worktreeRoot = join10(cwd, ".claude", "worktrees");
16070
16443
  for (const entry of await readdir(worktreeRoot, { withFileTypes: true })) {
16071
- if (entry.isDirectory()) dirs.add(join9(worktreeRoot, entry.name));
16444
+ if (entry.isDirectory()) dirs.add(join10(worktreeRoot, entry.name));
16072
16445
  }
16073
16446
  } catch (err) {
16074
16447
  debugSwallow("mcp.findLegacyStores(worktrees)", err);
@@ -16090,18 +16463,18 @@ async function findLegacyStores(cwd) {
16090
16463
  }
16091
16464
  const stores = [];
16092
16465
  for (const dir of dirs) {
16093
- const path10 = join9(dir, ".mcp", OAUTH_CRED_FILENAME);
16466
+ const path10 = join10(dir, ".mcp", OAUTH_CRED_FILENAME);
16094
16467
  const raw = await readIfExists(path10);
16095
16468
  if (raw !== null) stores.push({ path: path10, raw });
16096
16469
  }
16097
16470
  return stores;
16098
16471
  }
16099
16472
  async function scanConnections() {
16100
- const { homedir: homedir7 } = await import("node:os");
16101
- const { join: join9 } = await import("node:path");
16102
- const home = homedir7();
16473
+ const { homedir: homedir8 } = await import("node:os");
16474
+ const { join: join10 } = await import("node:path");
16475
+ const home = homedir8();
16103
16476
  const hookSettings = await readIfExists(
16104
- join9(home, ".claude", "settings.json")
16477
+ join10(home, ".claude", "settings.json")
16105
16478
  );
16106
16479
  const hooksInstalled = hookSettings ? hasGreatArrowHooks(hookSettings) : false;
16107
16480
  const entries = [];
@@ -16134,29 +16507,29 @@ async function probeEndpoint() {
16134
16507
  }
16135
16508
  }
16136
16509
  async function applyMigration(plan) {
16137
- const { homedir: homedir7 } = await import("node:os");
16138
- const { join: join9 } = await import("node:path");
16139
- const { mkdir: mkdir6, rename, writeFile: writeFile5 } = await import("node:fs/promises");
16510
+ const { homedir: homedir8 } = await import("node:os");
16511
+ const { join: join10 } = await import("node:path");
16512
+ const { mkdir: mkdir7, rename: rename2, writeFile: writeFile6 } = await import("node:fs/promises");
16140
16513
  const applied = [];
16141
- const dir = join9(homedir7(), ".great-arrow");
16142
- await mkdir6(dir, { recursive: true, mode: 448 });
16514
+ const dir = join10(homedir8(), ".great-arrow");
16515
+ await mkdir7(dir, { recursive: true, mode: 448 });
16143
16516
  if (plan.seedFrom) {
16144
16517
  const raw = await readIfExists(plan.seedFrom);
16145
16518
  if (raw !== null) {
16146
- const target = join9(dir, OAUTH_CRED_FILENAME);
16519
+ const target = join10(dir, OAUTH_CRED_FILENAME);
16147
16520
  const tmp = `${target}.tmp`;
16148
- await writeFile5(tmp, raw, { mode: 384 });
16149
- await rename(tmp, target);
16521
+ await writeFile6(tmp, raw, { mode: 384 });
16522
+ await rename2(tmp, target);
16150
16523
  applied.push(`Copied ${plan.seedFrom} \u2192 ${target}`);
16151
16524
  }
16152
16525
  }
16153
16526
  if (plan.syncKeychainToFile) {
16154
16527
  const raw = await readKeychainOnly();
16155
16528
  if (raw) {
16156
- const target = join9(dir, "credential");
16529
+ const target = join10(dir, "credential");
16157
16530
  const tmp = `${target}.tmp`;
16158
- await writeFile5(tmp, raw, { mode: 384 });
16159
- await rename(tmp, target);
16531
+ await writeFile6(tmp, raw, { mode: 384 });
16532
+ await rename2(tmp, target);
16160
16533
  applied.push(`Wrote the keychain credential to ${target}`);
16161
16534
  }
16162
16535
  }
@@ -16179,19 +16552,19 @@ var doctorSubcommand = new Command("doctor").description(
16179
16552
  noColor: false
16180
16553
  });
16181
16554
  const c = createColor(jsonFlag);
16182
- const { homedir: homedir7 } = await import("node:os");
16183
- const { join: join9 } = await import("node:path");
16184
- const home = homedir7();
16555
+ const { homedir: homedir8 } = await import("node:os");
16556
+ const { join: join10 } = await import("node:path");
16557
+ const home = homedir8();
16185
16558
  const proxyPath = await resolveProxyScript(options.proxy);
16186
16559
  const diagnostics = proxyPath ? await runProxyDiagnostics(proxyPath) : null;
16187
16560
  const entries = await scanConnections();
16188
16561
  const duplicates = detectDuplicates(entries);
16189
16562
  const keychain = await readKeychainOnly();
16190
16563
  const credentialFile = await readIfExists(
16191
- join9(home, ".great-arrow", "credential")
16564
+ join10(home, ".great-arrow", "credential")
16192
16565
  );
16193
16566
  const userStore = await readIfExists(
16194
- join9(home, ".great-arrow", OAUTH_CRED_FILENAME)
16567
+ join10(home, ".great-arrow", OAUTH_CRED_FILENAME)
16195
16568
  );
16196
16569
  const legacyStores = await findLegacyStores(process.cwd());
16197
16570
  const findings = [
@@ -16294,7 +16667,7 @@ ${c.dim("Credential migration")}
16294
16667
  }
16295
16668
  }
16296
16669
  );
16297
- var mcpCommand = new Command("mcp").description("Manage and diagnose the MCP server connection").addCommand(startCommand).addCommand(statusCommand).addCommand(logsCommand).addCommand(testCommand).addCommand(doctorSubcommand).addCommand(installCommand);
16670
+ var mcpCommand = new Command("mcp").description("Manage and diagnose the MCP server connection").addCommand(startCommand).addCommand(statusCommand).addCommand(logsCommand).addCommand(testCommand).addCommand(doctorSubcommand).addCommand(installCommand).addCommand(suggestToolsCommand());
16298
16671
  var mcp_default = mcpCommand;
16299
16672
 
16300
16673
  // src/commands/smoke.ts
@@ -16615,9 +16988,9 @@ function askCommand() {
16615
16988
  }
16616
16989
 
16617
16990
  // src/lib/glance-footer.ts
16618
- import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir5 } from "node:fs/promises";
16619
- import { homedir as homedir6 } from "node:os";
16620
- import { join as join8, dirname as dirname5 } from "node:path";
16991
+ import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir6 } from "node:fs/promises";
16992
+ import { homedir as homedir7 } from "node:os";
16993
+ import { join as join9, dirname as dirname6 } from "node:path";
16621
16994
 
16622
16995
  // src/lib/seen.ts
16623
16996
  var SEEN_TIMEOUT_MS = 1e3;
@@ -17004,11 +17377,11 @@ function shouldRequestTip(input) {
17004
17377
  return now - state.lastTipAt >= TIP_THROTTLE_INTERVAL_MS;
17005
17378
  }
17006
17379
  function getGlanceStatePath() {
17007
- return join8(homedir6(), ".gad", "glance-state.json");
17380
+ return join9(homedir7(), ".gad", "glance-state.json");
17008
17381
  }
17009
17382
  async function loadGlanceState() {
17010
17383
  try {
17011
- const content = await readFile4(getGlanceStatePath(), "utf-8");
17384
+ const content = await readFile6(getGlanceStatePath(), "utf-8");
17012
17385
  const parsed = JSON.parse(content);
17013
17386
  if (typeof parsed === "object" && parsed !== null) {
17014
17387
  return parsed;
@@ -17020,8 +17393,8 @@ async function loadGlanceState() {
17020
17393
  }
17021
17394
  async function saveGlanceState(state) {
17022
17395
  const path10 = getGlanceStatePath();
17023
- await mkdir5(dirname5(path10), { recursive: true });
17024
- await writeFile4(path10, JSON.stringify(state), "utf-8");
17396
+ await mkdir6(dirname6(path10), { recursive: true });
17397
+ await writeFile5(path10, JSON.stringify(state), "utf-8");
17025
17398
  }
17026
17399
  function glanceItemUrl(href, apiUrl) {
17027
17400
  if (!href) return null;
@@ -18903,7 +19276,7 @@ function completionsCommand() {
18903
19276
  // node_modules/.pnpm/update-notifier@7.3.1/node_modules/update-notifier/update-notifier.js
18904
19277
  import process11 from "node:process";
18905
19278
  import { spawn } from "node:child_process";
18906
- import { fileURLToPath as fileURLToPath2 } from "node:url";
19279
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
18907
19280
  import path9 from "node:path";
18908
19281
  import { format } from "node:util";
18909
19282
 
@@ -21054,7 +21427,7 @@ var isNpmOrYarn = isNpm || isYarn;
21054
21427
  // node_modules/.pnpm/is-installed-globally@1.0.0/node_modules/is-installed-globally/index.js
21055
21428
  import fs5 from "node:fs";
21056
21429
  import path8 from "node:path";
21057
- import { fileURLToPath } from "node:url";
21430
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
21058
21431
 
21059
21432
  // node_modules/.pnpm/global-directory@4.0.1/node_modules/global-directory/index.js
21060
21433
  var import_ini = __toESM(require_ini2(), 1);
@@ -21158,7 +21531,7 @@ function isPathInside(childPath, parentPath) {
21158
21531
  }
21159
21532
 
21160
21533
  // node_modules/.pnpm/is-installed-globally@1.0.0/node_modules/is-installed-globally/index.js
21161
- var __dirname2 = path8.dirname(fileURLToPath(import.meta.url));
21534
+ var __dirname2 = path8.dirname(fileURLToPath2(import.meta.url));
21162
21535
  var isInstalledGlobally = (() => {
21163
21536
  try {
21164
21537
  return isPathInside(__dirname2, global_directory_default.yarn.packages) || isPathInside(__dirname2, fs5.realpathSync(global_directory_default.npm.packages));
@@ -22140,7 +22513,7 @@ function pupa(template, data, { ignoreMissing = false, transform = ({ value }) =
22140
22513
  }
22141
22514
 
22142
22515
  // node_modules/.pnpm/update-notifier@7.3.1/node_modules/update-notifier/update-notifier.js
22143
- var __dirname3 = path9.dirname(fileURLToPath2(import.meta.url));
22516
+ var __dirname3 = path9.dirname(fileURLToPath3(import.meta.url));
22144
22517
  var ONE_DAY = 1e3 * 60 * 60 * 24;
22145
22518
  var UpdateNotifier = class {
22146
22519
  // Public