@indigoai-us/hq-cli 5.234.2 → 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 +56 -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/commands/lanes-checks.d.ts +0 -3
- package/dist/commands/lanes-checks.js +22 -40
- package/dist/commands/lanes-import.d.ts +0 -2
- package/dist/commands/lanes-import.js +3 -7
- package/dist/commands/lanes-report.js +5 -12
- package/dist/commands/lanes-tracker.d.ts +0 -3
- package/dist/commands/lanes-tracker.js +41 -17
- package/dist/commands/lanes.d.ts +0 -5
- package/dist/commands/lanes.js +3 -329
- 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/lib/lanes/reconcile.d.ts +0 -2
- package/dist/lib/lanes/reconcile.js +0 -16
- package/dist/lib/lanes/store.js +1 -9
- package/dist/lib/lanes/tracker-store.d.ts +2 -6
- package/dist/lib/lanes/tracker-store.js +1 -1
- package/dist/lib/lanes/types.d.ts +0 -4
- package/dist/node-network-compat.d.ts +1 -10
- package/dist/node-network-compat.js +3 -70
- package/package.json +1 -1
- package/dist/lib/lanes/tracker-mesh-questions.d.ts +0 -54
- package/dist/lib/lanes/tracker-mesh-questions.js +0 -138
- package/dist/lib/lanes/tracker-mesh.d.ts +0 -19
- package/dist/lib/lanes/tracker-mesh.js +0 -251
- package/dist/node-network-compat-flag.d.ts +0 -19
- package/dist/node-network-compat-flag.js +0 -87
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"
|
|
@@ -81,8 +81,6 @@ export interface ReconcileDeps {
|
|
|
81
81
|
readRegistryFlag?: typeof readLanesRegistryFlag;
|
|
82
82
|
/** Active-company fallback for questions raised from an unscoped lane. */
|
|
83
83
|
resolveQuestionActiveCompany?: () => Promise<LaneCompanyRef>;
|
|
84
|
-
/** Index local questions and poll remote answers before the normal answer-wake pass. */
|
|
85
|
-
syncTrackerQuestions?: (hqRoot: string) => Promise<void>;
|
|
86
84
|
/** Resume queued messages after this pass wins a local dead-worker CAS. */
|
|
87
85
|
recoverQueuedMessages?: (lane: LaneRecord) => Promise<void>;
|
|
88
86
|
/** Detached mesh seam for dead-watcher recovery. */
|
|
@@ -632,14 +632,6 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
632
632
|
const livenessDeps = deps.livenessDeps ?? defaultLivenessDeps();
|
|
633
633
|
let lanes;
|
|
634
634
|
let questions;
|
|
635
|
-
if (!deps.dryRun) {
|
|
636
|
-
try {
|
|
637
|
-
await deps.syncTrackerQuestions?.(hqRoot);
|
|
638
|
-
}
|
|
639
|
-
catch (err) {
|
|
640
|
-
markError(summary, err);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
635
|
try {
|
|
644
636
|
lanes = loadLaneRecords(hqRoot, summary);
|
|
645
637
|
questions = loadQuestionRecords(hqRoot, summary);
|
|
@@ -843,14 +835,6 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
843
835
|
await admitQueued(hqRoot, drainLanes, summary, deps, now);
|
|
844
836
|
await resumeAwaiting(hqRoot, drainLanes, freshQuestions, summary, deps);
|
|
845
837
|
});
|
|
846
|
-
if (!deps.dryRun && deps.syncTrackerQuestions) {
|
|
847
|
-
try {
|
|
848
|
-
await deps.syncTrackerQuestions(hqRoot);
|
|
849
|
-
}
|
|
850
|
-
catch (err) {
|
|
851
|
-
markError(summary, err);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
838
|
if (!deps.dryRun && summary.errors.length === 0) {
|
|
855
839
|
try {
|
|
856
840
|
writeHeartbeatFile(hqRoot, summary, nowIso);
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -738,15 +738,7 @@ export function parseQuestionRecord(raw, filePath) {
|
|
|
738
738
|
value: delivered_at,
|
|
739
739
|
});
|
|
740
740
|
}
|
|
741
|
-
|
|
742
|
-
if (mesh_question_id !== undefined && (typeof mesh_question_id !== "string" || !mesh_question_id.trim())) {
|
|
743
|
-
throw new LaneSchemaError("mesh_question_id must be a non-empty string when present", { path: filePath, value: mesh_question_id });
|
|
744
|
-
}
|
|
745
|
-
const mesh_delivered_at = raw.mesh_delivered_at === undefined ? null : raw.mesh_delivered_at;
|
|
746
|
-
if (mesh_delivered_at !== null && (typeof mesh_delivered_at !== "string" || !Number.isFinite(Date.parse(mesh_delivered_at)))) {
|
|
747
|
-
throw new LaneSchemaError("mesh_delivered_at must be a valid timestamp or null", { path: filePath, value: mesh_delivered_at });
|
|
748
|
-
}
|
|
749
|
-
return { ...raw, delivered_at, mesh_delivered_at };
|
|
741
|
+
return { ...raw, delivered_at };
|
|
750
742
|
}
|
|
751
743
|
/**
|
|
752
744
|
* HQ Sync sibling-twin grammar from `@indigoai-us/hq-cloud` `buildConflictPath`:
|
|
@@ -22,17 +22,13 @@ export interface TrackerStore {
|
|
|
22
22
|
read(kind: TrackerKind, id: string): StoreValue<TrackerRecord | null>;
|
|
23
23
|
list(kind: TrackerKind): StoreValue<TrackerRecord[]>;
|
|
24
24
|
create(record: TrackerRecordInput): StoreValue<TrackerRecord>;
|
|
25
|
-
update(kind: TrackerKind, id: string, expectedVersion: number, change: (current: TrackerRecord) => TrackerRecord
|
|
26
|
-
approvalQuestionId?: string;
|
|
27
|
-
}): StoreValue<TrackerRecord>;
|
|
25
|
+
update(kind: TrackerKind, id: string, expectedVersion: number, change: (current: TrackerRecord) => TrackerRecord): StoreValue<TrackerRecord>;
|
|
28
26
|
}
|
|
29
27
|
export interface LocalTrackerStore extends TrackerStore {
|
|
30
28
|
read(kind: TrackerKind, id: string): TrackerRecord | null;
|
|
31
29
|
list(kind: TrackerKind): TrackerRecord[];
|
|
32
30
|
create(record: TrackerRecordInput): TrackerRecord;
|
|
33
|
-
update(kind: TrackerKind, id: string, expectedVersion: number, change: (current: TrackerRecord) => TrackerRecord
|
|
34
|
-
approvalQuestionId?: string;
|
|
35
|
-
}): TrackerRecord;
|
|
31
|
+
update(kind: TrackerKind, id: string, expectedVersion: number, change: (current: TrackerRecord) => TrackerRecord): TrackerRecord;
|
|
36
32
|
}
|
|
37
33
|
export declare function createLocalTrackerStore(projectDirectory: string, now?: () => Date): LocalTrackerStore;
|
|
38
34
|
//# sourceMappingURL=tracker-store.d.ts.map
|
|
@@ -238,7 +238,7 @@ export function createLocalTrackerStore(projectDirectory, now = () => new Date()
|
|
|
238
238
|
writeExclusive(path.join(directory, `${id}.json`), `${JSON.stringify(record, null, 2)}\n`);
|
|
239
239
|
return record;
|
|
240
240
|
},
|
|
241
|
-
update(kind, id, expectedVersion, change
|
|
241
|
+
update(kind, id, expectedVersion, change) {
|
|
242
242
|
const file = recordPath(root, kind, id);
|
|
243
243
|
const directory = ensureKindDirectory(root, kind);
|
|
244
244
|
return withRecordLock(file, () => {
|
|
@@ -662,10 +662,6 @@ export interface QuestionRecord {
|
|
|
662
662
|
version: number;
|
|
663
663
|
/** Null until the owner starts a resume carrying this answer; absent on old records. */
|
|
664
664
|
delivered_at?: string | null;
|
|
665
|
-
/** Work Mesh's server-issued id; absent for pre-TRK-9 local questions. */
|
|
666
|
-
mesh_question_id?: string;
|
|
667
|
-
/** Local checkpoint that the server accepted this question's delivery stamp. */
|
|
668
|
-
mesh_delivered_at?: string | null;
|
|
669
665
|
body: QuestionBodyRef;
|
|
670
666
|
routing_history: RoutingHistoryEntry[];
|
|
671
667
|
answer?: QuestionAnswer;
|
|
@@ -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,54 +0,0 @@
|
|
|
1
|
-
import type { LaneCompanyRef, QuestionRecord } from "./types.js";
|
|
2
|
-
import { type TrackerMeshRequest } from "./tracker-mesh.js";
|
|
3
|
-
export type TrackerMeshQuestionAudience = "owner" | "operator" | "lane";
|
|
4
|
-
export type TrackerMeshQuestionStatus = "pending" | "answered" | "cancelled";
|
|
5
|
-
export interface TrackerMeshQuestionRow {
|
|
6
|
-
questionId: string;
|
|
7
|
-
threadId: string;
|
|
8
|
-
companyUid: string;
|
|
9
|
-
projectId: string;
|
|
10
|
-
laneId: string;
|
|
11
|
-
host: {
|
|
12
|
-
kind: string;
|
|
13
|
-
id: string;
|
|
14
|
-
};
|
|
15
|
-
audience: TrackerMeshQuestionAudience;
|
|
16
|
-
audienceChain: string[];
|
|
17
|
-
routingHistory: Array<Record<string, string>>;
|
|
18
|
-
question: string;
|
|
19
|
-
context?: string;
|
|
20
|
-
options: string[];
|
|
21
|
-
recommended?: string;
|
|
22
|
-
askedAt: string;
|
|
23
|
-
status: TrackerMeshQuestionStatus;
|
|
24
|
-
choice?: string;
|
|
25
|
-
notes?: string;
|
|
26
|
-
answeredBy?: string;
|
|
27
|
-
answeredAt?: string;
|
|
28
|
-
deliveredAt?: string;
|
|
29
|
-
}
|
|
30
|
-
export interface TrackerMeshQuestionInput {
|
|
31
|
-
laneId: string;
|
|
32
|
-
threadId?: string;
|
|
33
|
-
projectId: string;
|
|
34
|
-
audience: TrackerMeshQuestionAudience;
|
|
35
|
-
audienceChain: string[];
|
|
36
|
-
routingHistory: Array<Record<string, string>>;
|
|
37
|
-
question: string;
|
|
38
|
-
context: string;
|
|
39
|
-
options: string[];
|
|
40
|
-
recommended?: string;
|
|
41
|
-
}
|
|
42
|
-
export interface TrackerMeshQuestionClient {
|
|
43
|
-
create(input: TrackerMeshQuestionInput): Promise<TrackerMeshQuestionRow>;
|
|
44
|
-
list(statuses?: readonly TrackerMeshQuestionStatus[]): Promise<TrackerMeshQuestionRow[]>;
|
|
45
|
-
answer(questionId: string, choice: string, notes?: string): Promise<TrackerMeshQuestionRow>;
|
|
46
|
-
delivered(questionId: string): Promise<TrackerMeshQuestionRow>;
|
|
47
|
-
}
|
|
48
|
-
/** Persist a local delivery checkpoint only after the Work Mesh stamp succeeds. */
|
|
49
|
-
export declare function deliverTrackerMeshQuestionOnce(question: QuestionRecord, company: LaneCompanyRef, client: Pick<TrackerMeshQuestionClient, "delivered">, persist: (deliveredAt: string) => QuestionRecord): Promise<QuestionRecord>;
|
|
50
|
-
export declare function createTrackerMeshQuestionClient(options: {
|
|
51
|
-
company: LaneCompanyRef;
|
|
52
|
-
request?: TrackerMeshRequest;
|
|
53
|
-
}): TrackerMeshQuestionClient;
|
|
54
|
-
//# sourceMappingURL=tracker-mesh-questions.d.ts.map
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { meshJson, requireToken } from "../mesh/client.js";
|
|
2
|
-
import { LanesError } from "./errors.js";
|
|
3
|
-
import { mapWorkMeshTrackerError } from "./tracker-mesh.js";
|
|
4
|
-
/** Persist a local delivery checkpoint only after the Work Mesh stamp succeeds. */
|
|
5
|
-
export async function deliverTrackerMeshQuestionOnce(question, company, client, persist) {
|
|
6
|
-
if (!question.mesh_question_id || question.status !== "answered" ||
|
|
7
|
-
!question.delivered_at || question.mesh_delivered_at)
|
|
8
|
-
return question;
|
|
9
|
-
const delivered = await client.delivered(question.mesh_question_id);
|
|
10
|
-
if (delivered.questionId !== question.mesh_question_id ||
|
|
11
|
-
delivered.companyUid !== company.uid ||
|
|
12
|
-
delivered.laneId !== question.lane_id ||
|
|
13
|
-
delivered.projectId !== question.project_id ||
|
|
14
|
-
!delivered.deliveredAt) {
|
|
15
|
-
throw new LanesError("tracker_mesh_response_invalid", "Work Mesh did not stamp the expected question as delivered", {
|
|
16
|
-
mesh_question_id: question.mesh_question_id,
|
|
17
|
-
question_id: question.question_id,
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
return persist(delivered.deliveredAt);
|
|
21
|
-
}
|
|
22
|
-
const ROOT = "/v1/work-mesh/tracker/questions";
|
|
23
|
-
const AUDIENCES = ["owner", "operator", "lane"];
|
|
24
|
-
const STATUSES = ["pending", "answered", "cancelled"];
|
|
25
|
-
const MAX_PAGES_PER_FILTER = 10;
|
|
26
|
-
function invalidResponse(field) {
|
|
27
|
-
throw new LanesError("tracker_mesh_response_invalid", `Work Mesh returned an invalid tracker question ${field}`, { field });
|
|
28
|
-
}
|
|
29
|
-
function parseQuestion(raw, company) {
|
|
30
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
31
|
-
return invalidResponse("row");
|
|
32
|
-
const row = raw;
|
|
33
|
-
if (typeof row.questionId !== "string" || !row.questionId ||
|
|
34
|
-
typeof row.threadId !== "string" || !row.threadId ||
|
|
35
|
-
row.companyUid !== company.uid ||
|
|
36
|
-
typeof row.projectId !== "string" || !row.projectId ||
|
|
37
|
-
typeof row.laneId !== "string" || !row.laneId ||
|
|
38
|
-
!AUDIENCES.includes(row.audience) ||
|
|
39
|
-
!STATUSES.includes(row.status) ||
|
|
40
|
-
typeof row.question !== "string" ||
|
|
41
|
-
!Array.isArray(row.options) || row.options.some((entry) => typeof entry !== "string") ||
|
|
42
|
-
!Array.isArray(row.audienceChain) || row.audienceChain.some((entry) => typeof entry !== "string") ||
|
|
43
|
-
!Array.isArray(row.routingHistory) ||
|
|
44
|
-
typeof row.askedAt !== "string" || !Number.isFinite(Date.parse(row.askedAt)))
|
|
45
|
-
return invalidResponse("row");
|
|
46
|
-
if (row.context !== undefined && typeof row.context !== "string")
|
|
47
|
-
return invalidResponse("context");
|
|
48
|
-
if (row.recommended !== undefined && typeof row.recommended !== "string")
|
|
49
|
-
return invalidResponse("recommended");
|
|
50
|
-
if (row.choice !== undefined && typeof row.choice !== "string")
|
|
51
|
-
return invalidResponse("choice");
|
|
52
|
-
if (row.notes !== undefined && typeof row.notes !== "string")
|
|
53
|
-
return invalidResponse("notes");
|
|
54
|
-
if (row.answeredBy !== undefined && typeof row.answeredBy !== "string")
|
|
55
|
-
return invalidResponse("answeredBy");
|
|
56
|
-
if (row.answeredAt !== undefined && (typeof row.answeredAt !== "string" || !Number.isFinite(Date.parse(row.answeredAt))))
|
|
57
|
-
return invalidResponse("answeredAt");
|
|
58
|
-
if (row.deliveredAt !== undefined && (typeof row.deliveredAt !== "string" || !Number.isFinite(Date.parse(row.deliveredAt))))
|
|
59
|
-
return invalidResponse("deliveredAt");
|
|
60
|
-
if (!row.host || typeof row.host !== "object" || Array.isArray(row.host) || typeof row.host.kind !== "string" || typeof row.host.id !== "string")
|
|
61
|
-
return invalidResponse("host");
|
|
62
|
-
return row;
|
|
63
|
-
}
|
|
64
|
-
export function createTrackerMeshQuestionClient(options) {
|
|
65
|
-
const { company } = options;
|
|
66
|
-
if (!company.slug.trim() || !company.uid.trim()) {
|
|
67
|
-
throw new LanesError("tracker_company_unresolved", "A resolved company slug and uid are required for Work Mesh tracker questions", {});
|
|
68
|
-
}
|
|
69
|
-
const request = options.request ?? (async (endpoint, init) => {
|
|
70
|
-
const token = await requireToken({ interactive: false });
|
|
71
|
-
return meshJson(token, endpoint, init);
|
|
72
|
-
});
|
|
73
|
-
const call = async (endpoint, init) => {
|
|
74
|
-
try {
|
|
75
|
-
const result = await request(endpoint, init);
|
|
76
|
-
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
77
|
-
return invalidResponse("response");
|
|
78
|
-
return result;
|
|
79
|
-
}
|
|
80
|
-
catch (err) {
|
|
81
|
-
throw mapWorkMeshTrackerError(err);
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
return {
|
|
85
|
-
async create(input) {
|
|
86
|
-
const result = await call(ROOT, { method: "POST", body: { companyUid: company.uid, ...input } });
|
|
87
|
-
return parseQuestion(result.question, company);
|
|
88
|
-
},
|
|
89
|
-
async list(requestedStatuses = STATUSES) {
|
|
90
|
-
const uniqueStatuses = [...new Set(requestedStatuses)];
|
|
91
|
-
if (uniqueStatuses.some((status) => !STATUSES.includes(status))) {
|
|
92
|
-
throw new LanesError("tracker_mesh_query_invalid", "Unsupported Work Mesh tracker question status filter", { statuses: uniqueStatuses });
|
|
93
|
-
}
|
|
94
|
-
const rows = new Map();
|
|
95
|
-
for (const audience of AUDIENCES) {
|
|
96
|
-
for (const status of uniqueStatuses) {
|
|
97
|
-
let cursor;
|
|
98
|
-
for (let page = 0; page < MAX_PAGES_PER_FILTER; page += 1) {
|
|
99
|
-
const query = { companyUid: company.uid, audience, status, limit: "100", ...(cursor ? { cursor } : {}) };
|
|
100
|
-
const result = await call(ROOT, { method: "GET", query });
|
|
101
|
-
if (!Array.isArray(result.questions))
|
|
102
|
-
return invalidResponse("questions list");
|
|
103
|
-
for (const raw of result.questions) {
|
|
104
|
-
const row = parseQuestion(raw, company);
|
|
105
|
-
if (row.audience !== audience || row.status !== status)
|
|
106
|
-
return invalidResponse("filter scope");
|
|
107
|
-
rows.set(row.questionId, row);
|
|
108
|
-
}
|
|
109
|
-
if (result.nextCursor === undefined)
|
|
110
|
-
break;
|
|
111
|
-
if (typeof result.nextCursor !== "string" || !result.nextCursor || result.nextCursor === cursor)
|
|
112
|
-
return invalidResponse("cursor");
|
|
113
|
-
if (page === MAX_PAGES_PER_FILTER - 1) {
|
|
114
|
-
throw new LanesError("tracker_mesh_response_incomplete", "Work Mesh tracker question list exceeded its bounded page limit", { audience, status, max_pages: MAX_PAGES_PER_FILTER });
|
|
115
|
-
}
|
|
116
|
-
cursor = result.nextCursor;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return [...rows.values()].sort((a, b) => b.askedAt.localeCompare(a.askedAt) || a.questionId.localeCompare(b.questionId));
|
|
121
|
-
},
|
|
122
|
-
async answer(questionId, choice, notes) {
|
|
123
|
-
const result = await call(`${ROOT}/${encodeURIComponent(questionId)}/answer`, {
|
|
124
|
-
method: "POST",
|
|
125
|
-
body: { companyUid: company.uid, choice, ...(notes === undefined ? {} : { notes }) },
|
|
126
|
-
});
|
|
127
|
-
return parseQuestion(result.question, company);
|
|
128
|
-
},
|
|
129
|
-
async delivered(questionId) {
|
|
130
|
-
const result = await call(`${ROOT}/${encodeURIComponent(questionId)}/delivered`, {
|
|
131
|
-
method: "POST",
|
|
132
|
-
body: { companyUid: company.uid },
|
|
133
|
-
});
|
|
134
|
-
return parseQuestion(result.question, company);
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
//# sourceMappingURL=tracker-mesh-questions.js.map
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { LanesError } from "./errors.js";
|
|
2
|
-
import type { LaneCompanyRef } from "./types.js";
|
|
3
|
-
import type { TrackerStore } from "./tracker-store.js";
|
|
4
|
-
export interface TrackerMeshRequestInit {
|
|
5
|
-
method?: string;
|
|
6
|
-
body?: Record<string, unknown>;
|
|
7
|
-
query?: Record<string, string>;
|
|
8
|
-
}
|
|
9
|
-
export type TrackerMeshRequest = (path: string, init: TrackerMeshRequestInit) => Promise<unknown>;
|
|
10
|
-
export interface WorkMeshTrackerStoreOptions {
|
|
11
|
-
company: LaneCompanyRef;
|
|
12
|
-
projectId: string;
|
|
13
|
-
request?: TrackerMeshRequest;
|
|
14
|
-
now?: () => Date;
|
|
15
|
-
clientRecordId?: () => string;
|
|
16
|
-
}
|
|
17
|
-
export declare function mapWorkMeshTrackerError(err: unknown): LanesError;
|
|
18
|
-
export declare function createWorkMeshTrackerStore(options: WorkMeshTrackerStoreOptions): TrackerStore;
|
|
19
|
-
//# sourceMappingURL=tracker-mesh.d.ts.map
|