@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.
- package/CHANGELOG.md +47 -0
- package/assets/bot-workers/setup/skills/agents-and-team.md +154 -0
- package/assets/bot-workers/setup/skills/first-company.md +4 -1
- package/assets/bot-workers/setup/skills/pricing.md +92 -0
- package/assets/bot-workers/setup/skills/standing-help.md +7 -0
- package/assets/bot-workers/setup/worker.yaml +244 -63
- package/dist/command-catalog.generated.d.ts +3 -0
- package/dist/command-catalog.generated.js +4 -0
- package/dist/commands/bot.d.ts +2 -0
- package/dist/commands/bot.js +14 -2
- package/dist/lib/bot/config.d.ts +12 -0
- package/dist/lib/bot/config.js +25 -0
- package/dist/lib/bot/local-config.d.ts +2 -1
- package/dist/lib/bot/local-config.js +4 -0
- package/dist/lib/bot/progress.d.ts +25 -0
- package/dist/lib/bot/progress.js +39 -1
- package/dist/lib/bot/prompt.d.ts +2 -0
- package/dist/lib/bot/prompt.js +12 -2
- package/dist/lib/bot/run.js +10 -2
- package/dist/node-network-compat.d.ts +1 -10
- package/dist/node-network-compat.js +3 -70
- package/package.json +1 -1
- package/dist/node-network-compat-flag.d.ts +0 -19
- package/dist/node-network-compat-flag.js +0 -87
|
@@ -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). */
|
package/dist/lib/bot/progress.js
CHANGED
|
@@ -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.
|
|
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 {
|
package/dist/lib/bot/prompt.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/bot/prompt.js
CHANGED
|
@@ -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
|
|
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
|
|
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. ` +
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,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
|