@indigoai-us/hq-cli 5.235.0 → 5.236.0

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.
@@ -34,13 +34,14 @@ export interface BotLocalConfig {
34
34
  autoApprove?: boolean;
35
35
  /** Only the memory *kind* travels — never an absolute path from another Mac. */
36
36
  memory?: BotMemoryMode;
37
+ displayName?: string;
37
38
  intro?: string;
38
39
  kickoff?: string;
39
40
  }
40
41
  /** Walks a value and throws when any key looks like it could carry a secret. */
41
42
  export declare function assertNoSecretKeys(value: unknown, path?: string): void;
42
43
  /** The bot.json fields a localConfig is built from — nothing else is read. */
43
- export type BotLocalConfigSource = Pick<BotConfig, "runtime" | "workerSource" | "workerId" | "companySlug" | "model" | "effort" | "kind" | "companies" | "autoApprove" | "memoryDir" | "intro" | "kickoff">;
44
+ export type BotLocalConfigSource = Pick<BotConfig, "runtime" | "workerSource" | "workerId" | "companySlug" | "model" | "effort" | "kind" | "companies" | "autoApprove" | "memoryDir" | "displayName" | "intro" | "kickoff">;
44
45
  /**
45
46
  * The non-secret settings of a bot, for the cloud to keep. Absolute paths
46
47
  * (hqRoot, memoryDir, workerDir) are deliberately left out: they belong to the
@@ -51,6 +51,7 @@ export function buildBotLocalConfig(config) {
51
51
  ...(kind === "company" ? { companies } : {}),
52
52
  ...(config.autoApprove === false ? { autoApprove: false } : {}),
53
53
  memory: botMemoryMode(config),
54
+ ...(config.displayName ? { displayName: config.displayName } : {}),
54
55
  ...(config.intro ? { intro: config.intro } : {}),
55
56
  ...(config.kickoff ? { kickoff: config.kickoff } : {}),
56
57
  };
@@ -107,6 +108,9 @@ export function parseBotLocalConfig(value) {
107
108
  const memory = str("memory");
108
109
  if (isBotMemoryMode(memory))
109
110
  out.memory = memory;
111
+ const displayName = str("displayName");
112
+ if (displayName)
113
+ out.displayName = displayName;
110
114
  const intro = str("intro");
111
115
  if (intro)
112
116
  out.intro = intro;
@@ -27,6 +27,9 @@ export declare const PROGRESS_COALESCE_MS = 2000;
27
27
  export declare const PROGRESS_MIN_GAP_MS = 4000;
28
28
  export declare const WORKING_NOTICE_MS = 60000;
29
29
  export declare const WORKING_NOTICE_TEXT = "Working on it \u2014 I'll reply here when it's done.";
30
+ /** The setup bot's heads-up, sent once when a turn runs past SETUP_WORKING_NOTICE_MS. */
31
+ export declare const SETUP_WORKING_NOTICE_MS = 30000;
32
+ export declare const SETUP_WORKING_NOTICE_TEXT = "This one takes a minute. I'll reply here as soon as it's done.";
30
33
  export interface ProgressPosterOptions {
31
34
  /** Post one message; resolve true when it was delivered. */
32
35
  send: (body: string) => Promise<boolean>;
@@ -49,6 +52,14 @@ export interface ProgressPosterOptions {
49
52
  * (when the turn is not silent); `abandon` posts nothing.
50
53
  */
51
54
  buffer?: boolean;
55
+ /**
56
+ * Never post a message the model wrote right before a tool call. Those are
57
+ * the model's notes to itself ("Now write the worker."), not replies: the
58
+ * runtime stream shows them as a text block followed by a tool_use. A
59
+ * message is held until the next event says which it was: a tool call drops
60
+ * it, anything else (another message, the end of the turn) keeps it.
61
+ */
62
+ dropPreToolMessages?: boolean;
52
63
  }
53
64
  /** Whitespace-insensitive form used to recognise repeated text. */
54
65
  export declare function normalizeForCompare(text: string): string;
@@ -65,6 +76,9 @@ export declare class ProgressPoster {
65
76
  private readonly workingNoticeMs;
66
77
  private readonly workingNoticeText;
67
78
  private readonly buffer;
79
+ private readonly dropPreTool;
80
+ /** dropPreToolMessages: messages waiting to learn whether a tool call follows. */
81
+ private candidates;
68
82
  private pending;
69
83
  /** In-between questions, held until the turn ends (see the header). */
70
84
  private heldQuestions;
@@ -77,11 +91,22 @@ export declare class ProgressPoster {
77
91
  private finished;
78
92
  private noticeSent;
79
93
  private delivered;
94
+ /**
95
+ * Posts handed to the send chain this turn, counted when queued rather than
96
+ * when delivered. The "(no reply)" fallback checks this: a post still in
97
+ * flight when the turn ends has not bumped `delivered` yet, and counting only
98
+ * deliveries posted the placeholder right after a real message.
99
+ */
100
+ private queued;
80
101
  constructor(opts: ProgressPosterOptions);
81
102
  /** Arm the one-time "working on it" note. Call when the turn starts. */
82
103
  start(): void;
83
104
  /** One finished assistant message from the model. */
84
105
  message(text: string): void;
106
+ /** The model called a tool: whatever it wrote just before was a note to itself. */
107
+ tool(): void;
108
+ private promoteCandidates;
109
+ private accept;
85
110
  /** Posts that actually reached the chat (progress, notice and final). */
86
111
  get postedCount(): number;
87
112
  /** True once any message from the model was said this turn (not counting the notice). */
@@ -27,6 +27,9 @@ export const PROGRESS_COALESCE_MS = 2_000;
27
27
  export const PROGRESS_MIN_GAP_MS = 4_000;
28
28
  export const WORKING_NOTICE_MS = 60_000;
29
29
  export const WORKING_NOTICE_TEXT = "Working on it — I'll reply here when it's done.";
30
+ /** The setup bot's heads-up, sent once when a turn runs past SETUP_WORKING_NOTICE_MS. */
31
+ export const SETUP_WORKING_NOTICE_MS = 30_000;
32
+ export const SETUP_WORKING_NOTICE_TEXT = "This one takes a minute. I'll reply here as soon as it's done.";
30
33
  /** Whitespace-insensitive form used to recognise repeated text. */
31
34
  export function normalizeForCompare(text) {
32
35
  return text.replace(/\s+/g, "");
@@ -46,6 +49,9 @@ export class ProgressPoster {
46
49
  workingNoticeMs;
47
50
  workingNoticeText;
48
51
  buffer;
52
+ dropPreTool;
53
+ /** dropPreToolMessages: messages waiting to learn whether a tool call follows. */
54
+ candidates = [];
49
55
  pending = [];
50
56
  /** In-between questions, held until the turn ends (see the header). */
51
57
  heldQuestions = [];
@@ -58,6 +64,13 @@ export class ProgressPoster {
58
64
  finished = false;
59
65
  noticeSent = false;
60
66
  delivered = 0;
67
+ /**
68
+ * Posts handed to the send chain this turn, counted when queued rather than
69
+ * when delivered. The "(no reply)" fallback checks this: a post still in
70
+ * flight when the turn ends has not bumped `delivered` yet, and counting only
71
+ * deliveries posted the placeholder right after a real message.
72
+ */
73
+ queued = 0;
61
74
  constructor(opts) {
62
75
  this.send = opts.send;
63
76
  this.sendFinalFn = opts.sendFinal;
@@ -69,6 +82,7 @@ export class ProgressPoster {
69
82
  this.workingNoticeMs = opts.workingNoticeMs ?? WORKING_NOTICE_MS;
70
83
  this.workingNoticeText = opts.workingNoticeText ?? WORKING_NOTICE_TEXT;
71
84
  this.buffer = opts.buffer ?? false;
85
+ this.dropPreTool = opts.dropPreToolMessages ?? false;
72
86
  }
73
87
  /** Arm the one-time "working on it" note. Call when the turn starts. */
74
88
  start() {
@@ -93,6 +107,27 @@ export class ProgressPoster {
93
107
  // silence signal, not content, and must never be posted as progress.
94
108
  if (containsNoReplySentinel(t))
95
109
  return;
110
+ if (this.dropPreTool) {
111
+ // The previous message was not followed by a tool call: it was real.
112
+ this.promoteCandidates();
113
+ this.candidates.push(t);
114
+ return;
115
+ }
116
+ this.accept(t);
117
+ }
118
+ /** The model called a tool: whatever it wrote just before was a note to itself. */
119
+ tool() {
120
+ if (this.finished || !this.dropPreTool)
121
+ return;
122
+ this.candidates = [];
123
+ }
124
+ promoteCandidates() {
125
+ const kept = this.candidates;
126
+ this.candidates = [];
127
+ for (const t of kept)
128
+ this.accept(t);
129
+ }
130
+ accept(t) {
96
131
  this.cancelNotice();
97
132
  this.said.push(t);
98
133
  // An unmentioned room turn holds every in-between message: nothing is posted
@@ -124,6 +159,8 @@ export class ProgressPoster {
124
159
  async finish(finalText) {
125
160
  if (this.finished)
126
161
  return this.chain;
162
+ // A message the turn ended on (no tool after it) is real.
163
+ this.promoteCandidates();
127
164
  this.finished = true;
128
165
  this.cancelNotice();
129
166
  this.cancelFlush();
@@ -161,7 +198,7 @@ export class ProgressPoster {
161
198
  const shouldAddFinal = Boolean(final && !this.repeatsSaid(final));
162
199
  if (!shouldAddFinal && held.length > 0)
163
200
  this.pending.push(...held);
164
- if (this.pending.length === 0 && this.delivered === 0 && !shouldAddFinal)
201
+ if (this.pending.length === 0 && this.queued === 0 && !shouldAddFinal)
165
202
  this.pending.push("(no reply)");
166
203
  if (this.sendFinalFn && shouldAddFinal) {
167
204
  // Flush any remaining progress messages first, then the final reply separately.
@@ -229,6 +266,7 @@ export class ProgressPoster {
229
266
  // path built this body.
230
267
  if (containsNoReplySentinel(body))
231
268
  return;
269
+ this.queued += 1;
232
270
  this.lastPostAt = this.now();
233
271
  this.chain = this.chain.then(async () => {
234
272
  try {
@@ -77,6 +77,8 @@ export declare function normalizeInstructions(raw: unknown): string | undefined;
77
77
  export declare function readPromptSources(hqRoot: string, workerDir: string, io?: PromptFs, opts?: ReadPromptSourcesOptions): PromptSources;
78
78
  export interface BuildSystemPromptInput {
79
79
  botName: string;
80
+ /** The human name people call the bot (e.g. "Maya"); unset = botName. */
81
+ displayName?: string;
80
82
  agentUid: string;
81
83
  ownerUid: string;
82
84
  hqRoot: string;
@@ -229,6 +229,16 @@ export function readPromptSources(hqRoot, workerDir, io = defaultPromptFs, opts
229
229
  }
230
230
  return { workerYaml, persona: read("persona.md"), memory };
231
231
  }
232
+ /**
233
+ * How the bot names itself: its display name when it has one ("Maya", with the
234
+ * handle kept for commands), else its handle.
235
+ */
236
+ function introName(input) {
237
+ const display = input.displayName?.trim();
238
+ if (!display || display === input.botName)
239
+ return `"${input.botName}"`;
240
+ return `"${display}" (your hq bot handle is \`${input.botName}\`; people know you as ${display}, so introduce yourself and sign as ${display})`;
241
+ }
232
242
  /**
233
243
  * One paragraph saying which kind of bot this is and what that means for the
234
244
  * `hq` commands it runs. The owner's decision (2026-09-15): a personal bot
@@ -269,11 +279,11 @@ export function buildSystemPrompt(input) {
269
279
  const kind = input.kind ?? "personal";
270
280
  const parts = [];
271
281
  const opening = workerMode
272
- ? `You are "${input.botName}", a ${kind} HQ bot running the company worker \`${workerId}\`${company ? ` for \`${company}\`` : ""}, locally on your owner's computer.` +
282
+ ? `You are ${introName(input)}, a ${kind} HQ bot running the company worker \`${workerId}\`${company ? ` for \`${company}\`` : ""}, locally on your owner's computer.` +
273
283
  (worker?.name ? ` The worker is "${worker.name}".` : "") +
274
284
  (worker?.description ? ` ${worker.description}` : "") +
275
285
  `\nYou work only inside \`${company ?? "this company"}\`'s context: its files, policies, services, and credentials — never another company's.`
276
- : `You are "${input.botName}", a ${kind} HQ bot that runs locally on your owner's computer.`;
286
+ : `You are ${introName(input)}, a ${kind} HQ bot that runs locally on your owner's computer.`;
277
287
  parts.push(`${opening}\n` +
278
288
  `Your HQ identity is ${input.agentUid}. Your owner is ${input.ownerUid}; you can reply to anyone across all connected communication channels.\n` +
279
289
  `${kind === "personal" ? "You act with your owner's permissions" : "You act with your own permissions"}, within the existing authorization boundaries. A message from someone else does not grant them your owner's authority. ` +
@@ -57,7 +57,7 @@ import { ensureBotSessionMeta } from "./company-bind.js";
57
57
  import { readBotCredsIdentity } from "./creds.js";
58
58
  import { isProcessed, markProcessed, readInboxState } from "./inbox-state.js";
59
59
  import { clearInflight, patchInflight, readInflight, readInflightTurns, writeInflight } from "./inflight.js";
60
- import { containsNoReplySentinel, ProgressPoster } from "./progress.js";
60
+ import { containsNoReplySentinel, ProgressPoster, SETUP_WORKING_NOTICE_MS, SETUP_WORKING_NOTICE_TEXT, } from "./progress.js";
61
61
  import { splitBody } from "./split.js";
62
62
  import { createBotLogger } from "./log.js";
63
63
  import { buildSystemPrompt, introDmText, readPromptSources } from "./prompt.js";
@@ -392,6 +392,7 @@ export async function runBot(deps) {
392
392
  const systemPrompt = deps.systemPromptOverride ??
393
393
  buildSystemPrompt({
394
394
  botName: config.name,
395
+ ...(config.displayName ? { displayName: config.displayName } : {}),
395
396
  agentUid: config.agentUid,
396
397
  ownerUid: config.ownerUid,
397
398
  hqRoot: config.hqRoot,
@@ -735,6 +736,11 @@ export async function runBot(deps) {
735
736
  return true;
736
737
  };
737
738
  const poster = new ProgressPoster({
739
+ // The setup bot talks to someone new to HQ: its notes to itself never
740
+ // reach them, and a long step gets a heads-up instead of silence.
741
+ ...(isSetupBot
742
+ ? { dropPreToolMessages: true, workingNoticeMs: SETUP_WORKING_NOTICE_MS, workingNoticeText: SETUP_WORKING_NOTICE_TEXT }
743
+ : {}),
738
744
  ...deps.progress,
739
745
  ...(args.workingNotice === false || silenceable ? { workingNoticeMs: 0 } : {}),
740
746
  ...(silenceable ? { buffer: true } : {}),
@@ -749,6 +755,8 @@ export async function runBot(deps) {
749
755
  onEvent: (event) => {
750
756
  if (event.kind === "message")
751
757
  poster.message(event.text);
758
+ else if (event.kind === "tool")
759
+ poster.tool();
752
760
  },
753
761
  signal: controller.signal,
754
762
  ...(args.readDirs?.length ? { readDirs: args.readDirs } : {}),
@@ -1450,7 +1458,7 @@ export async function runBot(deps) {
1450
1458
  tReceived,
1451
1459
  send,
1452
1460
  sendFinal,
1453
- workingNotice: !isSetupBot,
1461
+ // The setup bot's own notice (SETUP_WORKING_NOTICE_TEXT) is set where the poster is built.
1454
1462
  silenceable: target.kind === "room" && !target.mentioned,
1455
1463
  ...(readDirs.length ? { readDirs } : {}),
1456
1464
  ...(target.kind === "room"
@@ -1,28 +1,19 @@
1
1
  /**
2
2
  * Node network defaults for HQ CLI entry points.
3
3
  *
4
- * The compatibility timeout remains the default. On a canary where IPv6 is
5
- * advertised but unusable, the default-off hq-flags gate can also prefer IPv4
6
- * and disable automatic family racing before the CLI issues its first request.
4
+ * The compatibility timeout remains the default for HQ CLI entry points.
7
5
  *
8
6
  * Keep this module safe to import immediately after the Node version preflight.
9
- * The hq-flags reader loads dynamically so an unsupported runtime exits through
10
- * the preflight rather than failing during module linking.
11
7
  */
12
8
  export declare const HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS = 1000;
13
9
  export interface NodeNetworkCompatibilityOptions {
14
10
  nodeOptions?: string;
15
11
  execArgv?: readonly string[];
16
- forceIpv4?: boolean;
17
12
  /**
18
13
  * Inject the net timeout setter (tests). Pass null to simulate a runtime
19
14
  * that lacks setDefaultAutoSelectFamilyAttemptTimeout.
20
15
  */
21
16
  setAttemptTimeout?: ((milliseconds: number) => void) | null;
22
- /** Inject Node's automatic family selection setter (tests). */
23
- setAutoSelectFamily?: ((enabled: boolean) => void) | null;
24
- /** Inject Node's DNS result-order setter (tests). */
25
- setDefaultResultOrder?: ((order: "ipv4first") => void) | null;
26
17
  }
27
18
  export declare function configureNodeNetworkCompatibility(options?: NodeNetworkCompatibilityOptions): boolean;
28
19
  //# sourceMappingURL=node-network-compat.d.ts.map
@@ -1,15 +1,10 @@
1
1
  /**
2
2
  * Node network defaults for HQ CLI entry points.
3
3
  *
4
- * The compatibility timeout remains the default. On a canary where IPv6 is
5
- * advertised but unusable, the default-off hq-flags gate can also prefer IPv4
6
- * and disable automatic family racing before the CLI issues its first request.
4
+ * The compatibility timeout remains the default for HQ CLI entry points.
7
5
  *
8
6
  * Keep this module safe to import immediately after the Node version preflight.
9
- * The hq-flags reader loads dynamically so an unsupported runtime exits through
10
- * the preflight rather than failing during module linking.
11
7
  */
12
- import * as nodeDns from "node:dns";
13
8
  import * as nodeNet from "node:net";
14
9
  export const HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS = 1000;
15
10
  const ATTEMPT_TIMEOUT_OPTION = "--network-family-autoselection-attempt-timeout";
@@ -19,18 +14,6 @@ function hasExplicitAttemptTimeout(nodeOptions, execArgv) {
19
14
  execArgv.some((argument) => argument === ATTEMPT_TIMEOUT_OPTION ||
20
15
  argument.startsWith(ATTEMPT_TIMEOUT_OPTION + "=")));
21
16
  }
22
- function hasExplicitFamilySelection(nodeOptions, execArgv) {
23
- const optionPattern = /(?:^|\s)(?:--network-family-autoselection|--enable-network-family-autoselection|--no-network-family-autoselection|--dns-result-order)(?:=|\s|$)/;
24
- return (optionPattern.test(nodeOptions) ||
25
- execArgv.some((argument) => argument === "--network-family-autoselection" ||
26
- argument === "--enable-network-family-autoselection" ||
27
- argument === "--no-network-family-autoselection" ||
28
- argument === "--dns-result-order" ||
29
- argument.startsWith("--network-family-autoselection=") ||
30
- argument.startsWith("--enable-network-family-autoselection=") ||
31
- argument.startsWith("--no-network-family-autoselection=") ||
32
- argument.startsWith("--dns-result-order=")));
33
- }
34
17
  function resolveSetAttemptTimeout(override) {
35
18
  if (override === null)
36
19
  return undefined;
@@ -39,69 +22,19 @@ function resolveSetAttemptTimeout(override) {
39
22
  const fn = nodeNet.setDefaultAutoSelectFamilyAttemptTimeout;
40
23
  return typeof fn === "function" ? fn.bind(nodeNet) : undefined;
41
24
  }
42
- function resolveSetAutoSelectFamily(override) {
43
- if (override === null)
44
- return undefined;
45
- if (override)
46
- return override;
47
- const fn = nodeNet.setDefaultAutoSelectFamily;
48
- return typeof fn === "function" ? fn.bind(nodeNet) : undefined;
49
- }
50
- function resolveSetDefaultResultOrder(override) {
51
- if (override === null)
52
- return undefined;
53
- if (override)
54
- return override;
55
- const fn = nodeDns.setDefaultResultOrder;
56
- return typeof fn === "function" ? fn.bind(nodeDns) : undefined;
57
- }
58
- function applyIpv4Preference(options, nodeOptions, execArgv) {
59
- if (!options.forceIpv4 || hasExplicitFamilySelection(nodeOptions, execArgv)) {
60
- return false;
61
- }
62
- const setAutoSelectFamily = resolveSetAutoSelectFamily(options.setAutoSelectFamily);
63
- const setDefaultResultOrder = resolveSetDefaultResultOrder(options.setDefaultResultOrder);
64
- if (!setAutoSelectFamily || !setDefaultResultOrder)
65
- return false;
66
- setDefaultResultOrder("ipv4first");
67
- setAutoSelectFamily(false);
68
- return true;
69
- }
70
25
  export function configureNodeNetworkCompatibility(options = {}) {
71
26
  const nodeOptions = options.nodeOptions ?? process.env.NODE_OPTIONS ?? "";
72
27
  const execArgv = options.execArgv ?? process.execArgv;
73
- const ipv4Applied = applyIpv4Preference(options, nodeOptions, execArgv);
74
28
  if (hasExplicitAttemptTimeout(nodeOptions, execArgv)) {
75
- return ipv4Applied;
29
+ return false;
76
30
  }
77
31
  const setAttemptTimeout = resolveSetAttemptTimeout(options.setAttemptTimeout);
78
32
  if (!setAttemptTimeout) {
79
33
  // Runtime lacks the API; the Node preflight should already have exited.
80
- return ipv4Applied;
34
+ return false;
81
35
  }
82
36
  setAttemptTimeout(HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS);
83
37
  return true;
84
38
  }
85
- async function applyHqFlaggedIpv4Preference() {
86
- try {
87
- const { nodeNetworkForceIpv4Enabled } = await import("./node-network-compat-flag.js");
88
- if (nodeNetworkForceIpv4Enabled &&
89
- (await nodeNetworkForceIpv4Enabled())) {
90
- configureNodeNetworkCompatibility({ forceIpv4: true });
91
- }
92
- }
93
- catch (error) {
94
- const rawName = error instanceof Error ? error.name : "UnknownError";
95
- const errorName = /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(rawName)
96
- ? rawName
97
- : "UnknownError";
98
- process.stderr.write("HQ network IPv4 flag lookup failed (" +
99
- errorName +
100
- "); using default off.\n");
101
- }
102
- }
103
- // Preserve the historical timeout before any flag request. If the hq-flags
104
- // lookup succeeds, the IPv4 preference is applied before command dispatch.
105
39
  configureNodeNetworkCompatibility();
106
- await applyHqFlaggedIpv4Preference();
107
40
  //# sourceMappingURL=node-network-compat.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.235.0",
3
+ "version": "5.236.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -1,19 +0,0 @@
1
- import type { FlagClient, FlagClientOptions } from "@indigoai-us/hq-flags-client";
2
- export declare const NODE_NETWORK_FORCE_IPV4_FLAG = "cli.network.force-ipv4";
3
- export declare const NODE_NETWORK_FORCE_IPV4_FLAG_FALLBACK = false;
4
- export declare const NODE_NETWORK_FORCE_IPV4_FLAG_TIMEOUT_MS = 1500;
5
- type NetworkFlagClient = Pick<FlagClient, "ready" | "isEnabled" | "close">;
6
- export interface NodeNetworkForceIpv4Dependencies {
7
- env?: NodeJS.ProcessEnv;
8
- createClient?: (options: FlagClientOptions) => NetworkFlagClient;
9
- getToken?: () => string | Promise<string>;
10
- reportFailure?: (error: unknown) => void;
11
- }
12
- /**
13
- * Read the opt-in hq-flags gate before CLI dispatch. Local hq-flags overrides
14
- * work without a token or endpoint; service decisions require a company
15
- * identity and use a short request deadline so startup remains bounded.
16
- */
17
- export declare function nodeNetworkForceIpv4Enabled(dependencies?: NodeNetworkForceIpv4Dependencies): Promise<boolean>;
18
- export {};
19
- //# sourceMappingURL=node-network-compat-flag.d.ts.map
@@ -1,87 +0,0 @@
1
- import { createFlagClient, readLocalOverride } from "@indigoai-us/hq-flags-client";
2
- import { resolveFlagRegistryEndpoint } from "./lib/flag-registry-endpoint.js";
3
- export const NODE_NETWORK_FORCE_IPV4_FLAG = "cli.network.force-ipv4";
4
- export const NODE_NETWORK_FORCE_IPV4_FLAG_FALLBACK = false;
5
- // Node waits up to 1s before trying the next address family. Keep this request
6
- // deadline above that wait so a dead IPv6 path can reach the usable IPv4 host.
7
- export const NODE_NETWORK_FORCE_IPV4_FLAG_TIMEOUT_MS = 1_500;
8
- function safeErrorClass(error) {
9
- const name = error instanceof Error ? error.name : "UnknownError";
10
- return /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(name)
11
- ? name
12
- : "UnknownError";
13
- }
14
- function reportFlagFailure(error) {
15
- process.stderr.write("HQ network IPv4 flag lookup failed (" +
16
- safeErrorClass(error) +
17
- "); using default off.\n");
18
- }
19
- async function loadCachedIdToken() {
20
- const { loadCachedTokens } = await import("@indigoai-us/hq-cloud");
21
- return loadCachedTokens()?.idToken ?? "";
22
- }
23
- /**
24
- * Read the opt-in hq-flags gate before CLI dispatch. Local hq-flags overrides
25
- * work without a token or endpoint; service decisions require a company
26
- * identity and use a short request deadline so startup remains bounded.
27
- */
28
- export async function nodeNetworkForceIpv4Enabled(dependencies = {}) {
29
- const env = dependencies.env ?? process.env;
30
- const localOverride = readLocalOverride(NODE_NETWORK_FORCE_IPV4_FLAG, env);
31
- if (localOverride !== null)
32
- return localOverride;
33
- const endpoint = resolveFlagRegistryEndpoint(undefined, env);
34
- const companyUid = env.HQ_COMPANY_UID?.trim() || "";
35
- if (!/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid)) {
36
- return NODE_NETWORK_FORCE_IPV4_FLAG_FALLBACK;
37
- }
38
- const companySlug = env.HQ_COMPANY_SLUG?.trim() || "";
39
- let client;
40
- let failed = false;
41
- let failureReported = false;
42
- let enabled = NODE_NETWORK_FORCE_IPV4_FLAG_FALLBACK;
43
- const reportFailureOnce = (error) => {
44
- failed = true;
45
- if (failureReported)
46
- return;
47
- failureReported = true;
48
- (dependencies.reportFailure ?? reportFlagFailure)(error);
49
- };
50
- try {
51
- const token = await (dependencies.getToken ?? loadCachedIdToken)();
52
- if (!token)
53
- return NODE_NETWORK_FORCE_IPV4_FLAG_FALLBACK;
54
- const createClient = dependencies.createClient ?? createFlagClient;
55
- client = createClient({
56
- endpoint,
57
- companyUid,
58
- ...(companySlug && /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/.test(companySlug)
59
- ? { companyIdentifiers: [companySlug] }
60
- : {}),
61
- env: {},
62
- getToken: () => token,
63
- refreshIntervalMs: 0,
64
- requestTimeoutMs: NODE_NETWORK_FORCE_IPV4_FLAG_TIMEOUT_MS,
65
- onError: reportFailureOnce,
66
- });
67
- await client.ready();
68
- if (!failed) {
69
- enabled = client.isEnabled(NODE_NETWORK_FORCE_IPV4_FLAG, {
70
- fallback: NODE_NETWORK_FORCE_IPV4_FLAG_FALLBACK,
71
- });
72
- }
73
- }
74
- catch (error) {
75
- reportFailureOnce(error);
76
- }
77
- finally {
78
- try {
79
- client?.close();
80
- }
81
- catch (error) {
82
- reportFailureOnce(error);
83
- }
84
- }
85
- return failed ? NODE_NETWORK_FORCE_IPV4_FLAG_FALLBACK : enabled;
86
- }
87
- //# sourceMappingURL=node-network-compat-flag.js.map