@indigoai-us/hq-cli 5.322.0 → 5.324.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/assets/scaffold/core/scripts/derive-trigger-facts.sh +263 -0
  3. package/assets/scaffold/core/scripts/lib/transcript-tail.sh +98 -0
  4. package/assets/scaffold/core/scripts/lib/trigger-fact-text.awk +86 -0
  5. package/dist/bin/hq-auth-refresh.d.ts +5 -1
  6. package/dist/bin/hq-auth-refresh.js +33 -1
  7. package/dist/command-catalog.generated.d.ts +138 -6
  8. package/dist/command-catalog.generated.js +178 -6
  9. package/dist/command-registration-plan.d.ts +24 -0
  10. package/dist/command-registration-plan.js +4 -0
  11. package/dist/commands/anywhere-runtime-flag.d.ts +9 -0
  12. package/dist/commands/anywhere-runtime-flag.js +22 -0
  13. package/dist/commands/cloud.js +23 -4
  14. package/dist/commands/core-derive-trigger-facts-command.d.ts +3 -0
  15. package/dist/commands/core-derive-trigger-facts-command.js +47 -0
  16. package/dist/commands/core-derive-trigger-facts-native.d.ts +21 -0
  17. package/dist/commands/core-derive-trigger-facts-native.js +492 -0
  18. package/dist/commands/core.js +6 -0
  19. package/dist/commands/daemon.js +39 -2
  20. package/dist/commands/db-provision.js +6 -1
  21. package/dist/commands/doctor.js +8 -4
  22. package/dist/commands/feedback.d.ts +2 -0
  23. package/dist/commands/feedback.js +10 -0
  24. package/dist/commands/install-global.d.ts +46 -0
  25. package/dist/commands/install-global.js +674 -0
  26. package/dist/commands/link.d.ts +30 -0
  27. package/dist/commands/link.js +184 -0
  28. package/dist/commands/mcp-status.d.ts +6 -0
  29. package/dist/commands/mcp-status.js +21 -4
  30. package/dist/commands/pkg-install.js +20 -1
  31. package/dist/commands/update.d.ts +19 -0
  32. package/dist/commands/update.js +52 -6
  33. package/dist/fast-core.d.ts +2 -1
  34. package/dist/fast-core.js +12 -8
  35. package/dist/fast-scaffold.js +13 -0
  36. package/dist/index.js +9 -7
  37. package/dist/lib/cli-invite-nudge.d.ts +25 -0
  38. package/dist/lib/cli-invite-nudge.js +152 -0
  39. package/dist/lib/core-utils/timeout-guard-native-flags.d.ts +6 -0
  40. package/dist/lib/core-utils/timeout-guard-native-flags.js +30 -0
  41. package/dist/lib/core-utils/timeout-guard-native.d.ts +0 -3
  42. package/dist/lib/core-utils/timeout-guard-native.js +4 -30
  43. package/dist/lib/core-utils/timeout-guard.d.ts +2 -0
  44. package/dist/lib/core-utils/timeout-guard.js +2 -1
  45. package/dist/lib/daemon/convergence.d.ts +42 -0
  46. package/dist/lib/daemon/convergence.js +66 -0
  47. package/dist/lib/daemon/heartbeat.d.ts +43 -0
  48. package/dist/lib/daemon/heartbeat.js +70 -0
  49. package/dist/lib/daemon/ops/journal.d.ts +29 -0
  50. package/dist/lib/daemon/ops/journal.js +141 -0
  51. package/dist/lib/daemon/ops/memory.d.ts +20 -0
  52. package/dist/lib/daemon/ops/memory.js +65 -0
  53. package/dist/lib/daemon/ops/mesh.d.ts +38 -0
  54. package/dist/lib/daemon/ops/mesh.js +70 -0
  55. package/dist/lib/daemon/ops/policy.d.ts +78 -0
  56. package/dist/lib/daemon/ops/policy.js +283 -0
  57. package/dist/lib/daemon/protocol.d.ts +68 -0
  58. package/dist/lib/daemon/protocol.js +154 -0
  59. package/dist/lib/daemon/run.d.ts +10 -0
  60. package/dist/lib/daemon/run.js +85 -3
  61. package/dist/lib/daemon/server.d.ts +36 -0
  62. package/dist/lib/daemon/server.js +306 -0
  63. package/dist/lib/daemon/session-registry.d.ts +30 -0
  64. package/dist/lib/daemon/session-registry.js +42 -0
  65. package/dist/lib/doctor/checks/anywhere.d.ts +38 -0
  66. package/dist/lib/doctor/checks/anywhere.js +182 -0
  67. package/dist/lib/doctor/registry.js +6 -0
  68. package/dist/lib/first-sync-next-step.d.ts +10 -1
  69. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  70. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  71. package/dist/lib/install/claude-targets.d.ts +47 -0
  72. package/dist/lib/install/claude-targets.js +203 -0
  73. package/dist/lib/install/codex-targets.d.ts +57 -0
  74. package/dist/lib/install/codex-targets.js +187 -0
  75. package/dist/lib/mesh/live/session-seq.js +62 -9
  76. package/dist/lib/registry/normalize-remote.d.ts +21 -0
  77. package/dist/lib/registry/normalize-remote.js +68 -0
  78. package/dist/lib/registry/registry.d.ts +75 -0
  79. package/dist/lib/registry/registry.js +261 -0
  80. package/dist/main.js +3 -1
  81. package/dist/utils/cli-telemetry.d.ts +11 -0
  82. package/dist/utils/cli-telemetry.js +76 -0
  83. package/dist/utils/cognito-session.d.ts +5 -1
  84. package/dist/utils/cognito-session.js +8 -12
  85. package/dist/utils/feedback-diagnostics.d.ts +2 -0
  86. package/dist/utils/run-bundled-script.d.ts +7 -4
  87. package/dist/utils/run-bundled-script.js +47 -12
  88. package/dist/utils/version-check.d.ts +2 -0
  89. package/dist/utils/version-check.js +7 -0
  90. package/package.json +2 -2
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Repo-to-company registry (hq-anywhere US-004).
3
+ *
4
+ * `~/.hq/registry.json` maps a folder to the HQ company it belongs to so a
5
+ * session started outside the HQ tree can bind without the user naming the
6
+ * company. Keys are either a normalised git remote (`remote:github.com/o/n`)
7
+ * or an absolute folder path (`path:/abs/dir`). Resolution never guesses:
8
+ * a folder with no matching entry resolves to `company: null` with a reason.
9
+ */
10
+ import { type CompaniesManifestMap } from "../work-context/repo-remote.js";
11
+ export type RegistrySource = "link" | "manifest" | "prompt";
12
+ export interface RegistryEntry {
13
+ company: string;
14
+ source: RegistrySource;
15
+ linkedAt: string;
16
+ }
17
+ export interface Registry {
18
+ version: 1;
19
+ entries: Record<string, RegistryEntry>;
20
+ }
21
+ export type ResolveResult = {
22
+ company: string;
23
+ key: string;
24
+ source: RegistrySource;
25
+ } | {
26
+ company: null;
27
+ reason: string;
28
+ };
29
+ export declare class RegistryFormatError extends Error {
30
+ constructor(file: string, detail: string);
31
+ }
32
+ /** Directory holding registry.json: HQ_REGISTRY_DIR, else ~/.hq (same parent as the work-mesh cache). */
33
+ export declare function registryDir(home?: string, env?: NodeJS.ProcessEnv): string;
34
+ export declare function registryFile(dir?: string): string;
35
+ export declare function emptyRegistry(): Registry;
36
+ /** Read the registry. A missing file is an empty registry; a malformed one throws. */
37
+ export declare function readRegistry(file?: string): Registry;
38
+ export declare function writeRegistry(registry: Registry, file?: string): void;
39
+ export declare function remoteKey(normalized: string): string;
40
+ export declare function pathKey(dir: string): string;
41
+ /** Absolute, symlink-resolved directory path (falls back to path.resolve when missing). */
42
+ export declare function canonicalDir(dir: string): string;
43
+ /** Normalised origin remote for the repo enclosing `dir`, or null. */
44
+ export declare function remoteForDir(dir: string): string | null;
45
+ /**
46
+ * The key `hq link` writes for `dir`: the remote key when inside a git repo
47
+ * with a recognised origin, otherwise the path key (work-tree root when inside
48
+ * a repo without an origin, else the folder itself).
49
+ */
50
+ export declare function keyForDir(dir: string): string;
51
+ /** Every key that could describe `dir`, most specific first. */
52
+ export declare function candidateKeys(dir: string): string[];
53
+ /**
54
+ * Resolve the company for `dir`. Among matching keys the highest-ranked source
55
+ * wins (link > prompt > manifest); ties go to the more specific key.
56
+ */
57
+ export declare function resolveCompanyForDir(registry: Registry, dir: string): ResolveResult;
58
+ export declare function setEntry(registry: Registry, key: string, company: string, source: RegistrySource, now?: Date): RegistryEntry;
59
+ /** Remove every key that describes `dir`. Returns the removed keys. */
60
+ export declare function removeEntriesForDir(registry: Registry, dir: string): string[];
61
+ export interface SeedResult {
62
+ added: string[];
63
+ owned: string[];
64
+ skippedLinked: string[];
65
+ ambiguous: string[];
66
+ }
67
+ /**
68
+ * Seed manifest-sourced entries from companies/manifest.yaml repos[]. Entries
69
+ * may be HQ-relative paths (`repos/private/x`), `owner/name`, or remote URLs.
70
+ * A local path seeds both its path key and, when it has an origin, its remote
71
+ * key. Never overwrites a `link` or `prompt` entry, and skips any key claimed
72
+ * by more than one company.
73
+ */
74
+ export declare function seedFromManifest(registry: Registry, hqRoot: string, companies: CompaniesManifestMap, now?: Date): SeedResult;
75
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Repo-to-company registry (hq-anywhere US-004).
3
+ *
4
+ * `~/.hq/registry.json` maps a folder to the HQ company it belongs to so a
5
+ * session started outside the HQ tree can bind without the user naming the
6
+ * company. Keys are either a normalised git remote (`remote:github.com/o/n`)
7
+ * or an absolute folder path (`path:/abs/dir`). Resolution never guesses:
8
+ * a folder with no matching entry resolves to `company: null` with a reason.
9
+ */
10
+ import * as fs from "node:fs";
11
+ import * as os from "node:os";
12
+ import * as path from "node:path";
13
+ import { atomicWriteJson } from "../work-context/atomic.js";
14
+ import { findEnclosingGitDir, findWorkTreeRoot, readOriginRemoteUrl, } from "../work-context/repo-remote.js";
15
+ import { normalizeManifestRepo, normalizeRemoteUrl } from "./normalize-remote.js";
16
+ /** Higher wins. An explicit `hq link` beats a prompt answer beats the manifest seed. */
17
+ const SOURCE_RANK = { link: 3, prompt: 2, manifest: 1 };
18
+ export class RegistryFormatError extends Error {
19
+ constructor(file, detail) {
20
+ super(`HQ registry at ${file} is unreadable: ${detail}`);
21
+ this.name = "RegistryFormatError";
22
+ }
23
+ }
24
+ /** Directory holding registry.json: HQ_REGISTRY_DIR, else ~/.hq (same parent as the work-mesh cache). */
25
+ export function registryDir(home = os.homedir(), env = process.env) {
26
+ const override = env.HQ_REGISTRY_DIR?.trim();
27
+ if (override)
28
+ return path.resolve(override);
29
+ return path.join(home, ".hq");
30
+ }
31
+ export function registryFile(dir = registryDir()) {
32
+ return path.join(dir, "registry.json");
33
+ }
34
+ export function emptyRegistry() {
35
+ return { version: 1, entries: {} };
36
+ }
37
+ function isSource(value) {
38
+ return value === "link" || value === "manifest" || value === "prompt";
39
+ }
40
+ /** Read the registry. A missing file is an empty registry; a malformed one throws. */
41
+ export function readRegistry(file = registryFile()) {
42
+ let text;
43
+ try {
44
+ text = fs.readFileSync(file, "utf8");
45
+ }
46
+ catch (err) {
47
+ if (err.code === "ENOENT")
48
+ return emptyRegistry();
49
+ throw err;
50
+ }
51
+ let parsed;
52
+ try {
53
+ parsed = JSON.parse(text);
54
+ }
55
+ catch (err) {
56
+ throw new RegistryFormatError(file, err.message);
57
+ }
58
+ const rawEntries = parsed?.entries;
59
+ if (!rawEntries || typeof rawEntries !== "object" || Array.isArray(rawEntries)) {
60
+ throw new RegistryFormatError(file, "missing `entries` object");
61
+ }
62
+ const entries = {};
63
+ for (const [key, value] of Object.entries(rawEntries)) {
64
+ const rec = value;
65
+ if (!rec || typeof rec.company !== "string" || !rec.company || !isSource(rec.source)) {
66
+ throw new RegistryFormatError(file, `invalid entry for ${key}`);
67
+ }
68
+ entries[key] = {
69
+ company: rec.company,
70
+ source: rec.source,
71
+ linkedAt: typeof rec.linkedAt === "string" ? rec.linkedAt : "",
72
+ };
73
+ }
74
+ return { version: 1, entries };
75
+ }
76
+ export function writeRegistry(registry, file = registryFile()) {
77
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
78
+ const sorted = {};
79
+ for (const key of Object.keys(registry.entries).sort())
80
+ sorted[key] = registry.entries[key];
81
+ atomicWriteJson(file, { version: 1, entries: sorted });
82
+ }
83
+ export function remoteKey(normalized) {
84
+ return `remote:${normalized}`;
85
+ }
86
+ export function pathKey(dir) {
87
+ return `path:${canonicalDir(dir)}`;
88
+ }
89
+ /** Absolute, symlink-resolved directory path (falls back to path.resolve when missing). */
90
+ export function canonicalDir(dir) {
91
+ const abs = path.resolve(dir);
92
+ try {
93
+ return fs.realpathSync(abs);
94
+ }
95
+ catch {
96
+ return abs;
97
+ }
98
+ }
99
+ /** Normalised origin remote for the repo enclosing `dir`, or null. */
100
+ export function remoteForDir(dir) {
101
+ const gitDir = findEnclosingGitDir(dir);
102
+ if (!gitDir)
103
+ return null;
104
+ // Linked worktrees keep their config in the common dir.
105
+ const commonFile = path.join(gitDir, "commondir");
106
+ let configDir = gitDir;
107
+ try {
108
+ const common = fs.readFileSync(commonFile, "utf8").trim();
109
+ if (common)
110
+ configDir = path.resolve(gitDir, common);
111
+ }
112
+ catch {
113
+ // Not a linked worktree.
114
+ }
115
+ const origin = readOriginRemoteUrl(configDir) ?? readOriginRemoteUrl(gitDir);
116
+ return origin ? normalizeRemoteUrl(origin) : null;
117
+ }
118
+ /**
119
+ * The key `hq link` writes for `dir`: the remote key when inside a git repo
120
+ * with a recognised origin, otherwise the path key (work-tree root when inside
121
+ * a repo without an origin, else the folder itself).
122
+ */
123
+ export function keyForDir(dir) {
124
+ const remote = remoteForDir(dir);
125
+ if (remote)
126
+ return remoteKey(remote);
127
+ return pathKey(findWorkTreeRoot(dir) ?? dir);
128
+ }
129
+ /** Every key that could describe `dir`, most specific first. */
130
+ export function candidateKeys(dir) {
131
+ const keys = [pathKey(dir)];
132
+ const workTree = findWorkTreeRoot(dir);
133
+ if (workTree)
134
+ keys.push(pathKey(workTree));
135
+ const remote = remoteForDir(dir);
136
+ if (remote)
137
+ keys.push(remoteKey(remote));
138
+ return [...new Set(keys)];
139
+ }
140
+ /**
141
+ * Resolve the company for `dir`. Among matching keys the highest-ranked source
142
+ * wins (link > prompt > manifest); ties go to the more specific key.
143
+ */
144
+ export function resolveCompanyForDir(registry, dir) {
145
+ let best = null;
146
+ for (const key of candidateKeys(dir)) {
147
+ const entry = registry.entries[key];
148
+ if (!entry)
149
+ continue;
150
+ if (!best || SOURCE_RANK[entry.source] > SOURCE_RANK[best.entry.source]) {
151
+ best = { key, entry };
152
+ }
153
+ }
154
+ if (!best) {
155
+ return {
156
+ company: null,
157
+ reason: `No HQ company is linked to ${canonicalDir(dir)}. Run \`hq link <company>\` there.`,
158
+ };
159
+ }
160
+ return { company: best.entry.company, key: best.key, source: best.entry.source };
161
+ }
162
+ export function setEntry(registry, key, company, source, now = new Date()) {
163
+ const entry = { company, source, linkedAt: now.toISOString() };
164
+ registry.entries[key] = entry;
165
+ return entry;
166
+ }
167
+ /** Remove every key that describes `dir`. Returns the removed keys. */
168
+ export function removeEntriesForDir(registry, dir) {
169
+ const removed = [];
170
+ for (const key of candidateKeys(dir)) {
171
+ if (registry.entries[key]) {
172
+ delete registry.entries[key];
173
+ removed.push(key);
174
+ }
175
+ }
176
+ return removed;
177
+ }
178
+ /**
179
+ * Seed manifest-sourced entries from companies/manifest.yaml repos[]. Entries
180
+ * may be HQ-relative paths (`repos/private/x`), `owner/name`, or remote URLs.
181
+ * A local path seeds both its path key and, when it has an origin, its remote
182
+ * key. Never overwrites a `link` or `prompt` entry, and skips any key claimed
183
+ * by more than one company.
184
+ */
185
+ export function seedFromManifest(registry, hqRoot, companies, now = new Date()) {
186
+ const claimGroups = [];
187
+ for (const [company, entry] of Object.entries(companies)) {
188
+ const repos = entry && typeof entry === "object" ? entry.repos : undefined;
189
+ if (!Array.isArray(repos))
190
+ continue;
191
+ for (const raw of repos) {
192
+ if (typeof raw !== "string" || !raw.trim())
193
+ continue;
194
+ const trimmed = raw.trim();
195
+ const local = path.isAbsolute(trimmed) ? trimmed : path.join(hqRoot, trimmed);
196
+ let isDir;
197
+ try {
198
+ isDir = fs.statSync(local).isDirectory();
199
+ }
200
+ catch {
201
+ isDir = false;
202
+ }
203
+ if (isDir) {
204
+ const keys = [pathKey(local)];
205
+ const remote = remoteForDir(local);
206
+ if (remote)
207
+ keys.push(remoteKey(remote));
208
+ claimGroups.push({ company, keys });
209
+ continue;
210
+ }
211
+ const normalized = normalizeManifestRepo(trimmed);
212
+ if (normalized)
213
+ claimGroups.push({ company, keys: [remoteKey(normalized)] });
214
+ }
215
+ }
216
+ const claims = new Map();
217
+ for (const group of claimGroups) {
218
+ for (const key of group.keys) {
219
+ const set = claims.get(key) ?? new Set();
220
+ set.add(group.company);
221
+ claims.set(key, set);
222
+ }
223
+ }
224
+ const ambiguous = new Set([...claims].filter(([, companies]) => companies.size > 1).map(([key]) => key));
225
+ let changed = true;
226
+ while (changed) {
227
+ changed = false;
228
+ for (const group of claimGroups) {
229
+ if (group.keys.some((key) => ambiguous.has(key))) {
230
+ for (const key of group.keys) {
231
+ if (!ambiguous.has(key)) {
232
+ ambiguous.add(key);
233
+ changed = true;
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
239
+ const result = { added: [], owned: [], skippedLinked: [], ambiguous: [...ambiguous].sort() };
240
+ const unambiguousClaims = new Map();
241
+ for (const group of claimGroups) {
242
+ if (group.keys.some((key) => ambiguous.has(key)))
243
+ continue;
244
+ for (const key of group.keys)
245
+ unambiguousClaims.set(key, group.company);
246
+ }
247
+ for (const [key, company] of unambiguousClaims) {
248
+ const existing = registry.entries[key];
249
+ if (existing && existing.source !== "manifest") {
250
+ result.skippedLinked.push(key);
251
+ continue;
252
+ }
253
+ if (existing?.company !== company) {
254
+ setEntry(registry, key, company, "manifest", now);
255
+ result.added.push(key);
256
+ }
257
+ result.owned.push(key);
258
+ }
259
+ return result;
260
+ }
261
+ //# sourceMappingURL=registry.js.map
package/dist/main.js CHANGED
@@ -44,7 +44,7 @@ import { enforceVersionGate, shouldSkipGate, } from "./utils/version-gate.js";
44
44
  import { autoUpdateAndReexec } from "./utils/self-update.js";
45
45
  import { CLI_VERSION } from "./cli-version.js";
46
46
  import { findLazyCommand, registerCommandCatalog } from "./lazy-commands.js";
47
- import { emitCliFirstSyncNextStepOutcome, emitCliLimitNoticeEngagement, emitCliLimitNoticeExposure, emitCliSessionStarted, emitPlanLimitInviteTeammateExposure, isCliLimitResourceKind, queueCliLimitNoticeEngagement, queueCliLimitNoticeExposure, takeFirstSyncStepOutcomes, takeCliLimitNoticeEngagements, takeCliLimitNoticeExposures, takePlanLimitInviteTeammateExposures, maybeShowFirstSyncNextStep, } from "./utils/cli-telemetry.js";
47
+ import { emitCliFirstSyncNextStepOutcome, emitCliLimitNoticeEngagement, emitCliLimitNoticeExposure, emitCliInviteNudgeEvent, emitCliSessionStarted, emitPlanLimitInviteTeammateExposure, isCliLimitResourceKind, queueCliLimitNoticeEngagement, queueCliLimitNoticeExposure, takeFirstSyncStepOutcomes, takeCliLimitNoticeEngagements, takeCliLimitNoticeExposures, takeCliInviteNudgeEvents, takePlanLimitInviteTeammateExposures, maybeShowFirstSyncNextStep, } from "./utils/cli-telemetry.js";
48
48
  import { reportCliClientHealthInvocation } from "./utils/client-health.js";
49
49
  import { settleWithin } from "./utils/settle-with-timeout.js";
50
50
  import { emitPlanLimitNag } from "./lib/plan-limit-nag.js";
@@ -313,6 +313,7 @@ export async function runCli() {
313
313
  // slice even when the command itself was long-running.
314
314
  const releaseHealthTimeoutMs = foregroundNetworkBudget.timeoutFor(FINALIZATION_FOREGROUND_WAIT_MS);
315
315
  const firstSyncOutcomeFlushes = takeFirstSyncStepOutcomes().map((outcome) => emitCliFirstSyncNextStepOutcome(outcome, releaseHealthTimeoutMs));
316
+ const inviteNudgeOutcomeFlushes = takeCliInviteNudgeEvents().map((event) => emitCliInviteNudgeEvent(event, releaseHealthTimeoutMs));
316
317
  const inviteTeammateExposureFlushes = takePlanLimitInviteTeammateExposures().map((companyUid) => emitPlanLimitInviteTeammateExposure(companyUid, releaseHealthTimeoutMs));
317
318
  const limitNoticeExposureFlushes = takeCliLimitNoticeExposures().map((exposure) => emitCliLimitNoticeExposure(exposure, releaseHealthTimeoutMs));
318
319
  const limitNoticeEngagementFlushes = takeCliLimitNoticeEngagements().map((engagement) => emitCliLimitNoticeEngagement(engagement, releaseHealthTimeoutMs));
@@ -322,6 +323,7 @@ export async function runCli() {
322
323
  : [refreshVersionCache({ timeoutMs: releaseHealthTimeoutMs })]),
323
324
  Sentry.flush(releaseHealthTimeoutMs),
324
325
  ...firstSyncOutcomeFlushes,
326
+ ...inviteNudgeOutcomeFlushes,
325
327
  ...inviteTeammateExposureFlushes,
326
328
  ...limitNoticeExposureFlushes,
327
329
  ...limitNoticeEngagementFlushes,
@@ -9,8 +9,14 @@ export declare const TELEMETRY_TIMEOUT_MS = 1200;
9
9
  */
10
10
  export declare const TELEMETRY_DISCLOSURE_LINE = "HQ sends a usage event when you run a signed-in command (CLI version and a timestamp, never command names, file contents, or secrets) to improve the CLI. Set HQ_TELEMETRY=0 to turn this off.";
11
11
  export declare const FIRST_SYNC_TELEMETRY_DISCLOSURE_LINE = "HQ sends a usage event when you run a signed-in command (CLI version and a timestamp; first-sync guidance shown or followed, with the identifier cli.first-sync-next-step; never command names, file contents, or secrets) to improve the CLI. Set HQ_TELEMETRY=0 to turn this off.";
12
+ export declare const CLI_INVITE_NUDGE_TELEMETRY_DISCLOSURE_LINE = "HQ sends a usage event when it shows the teammate invite prompt (your company UID and whether you saw, clicked, or dismissed the prompt; no invitee details, command names, file contents, or secrets). Set HQ_TELEMETRY=0 to turn this off.";
12
13
  export declare const CLI_FEATURE_USED_TELEMETRY_FLAG = "cli.feature-used-telemetry-v1";
13
14
  export declare const CLI_FEATURE_USED_TELEMETRY_DISCLOSURE_LINE = "HQ sends a feature-use event when you run a supported CLI feature (feature kind and outcome, plus your company UID when available; never command names, file contents, or secrets). The hq-flags key cli.feature-used-telemetry-v1 controls these events. Set HQ_TELEMETRY=0 to turn this off.";
15
+ export type CliInviteNudgeOutcome = "shown" | "clicked" | "dismissed";
16
+ export interface CliInviteNudgeEvent {
17
+ companyUid: string;
18
+ outcome: CliInviteNudgeOutcome;
19
+ }
14
20
  export declare const CLI_LIMIT_AT_ACTION_PROMPT_FLAG = "billing.limit-at-action-prompt";
15
21
  export type CliLimitResourceKind = "users" | "integrations" | "secrets" | "agents" | "deployments" | "storageBytes";
16
22
  export type CliLimitPresentationKind = "advisory" | "refusal";
@@ -33,6 +39,11 @@ export declare function takeCliLimitNoticeEngagements(): CliLimitNoticeEngagemen
33
39
  export declare function queuePlanLimitInviteTeammateExposure(companyUid: string): void;
34
40
  /** Drain invite-suggestion exposures for the bounded command-finalization flush. */
35
41
  export declare function takePlanLimitInviteTeammateExposures(): string[];
42
+ /** Queue one company-keyed CLI invite prompt outcome; no person UID is included in the event. */
43
+ export declare function queueCliInviteNudgeEvent(companyUid: string, outcome: CliInviteNudgeOutcome): void;
44
+ export declare function takeCliInviteNudgeEvents(): CliInviteNudgeEvent[];
45
+ /** Emit a bounded CLI session fact through the existing product-telemetry path. */
46
+ export declare function emitCliInviteNudgeEvent(event: CliInviteNudgeEvent, timeoutMs?: number): Promise<void>;
36
47
  export type CliFeatureKind = "secret_read" | "lane_create" | "lane_resume";
37
48
  export type CliFeatureOutcome = "success" | "failure";
38
49
  export interface CliFeatureUsage {
@@ -17,14 +17,17 @@ export const TELEMETRY_TIMEOUT_MS = 1_200;
17
17
  */
18
18
  export const TELEMETRY_DISCLOSURE_LINE = "HQ sends a usage event when you run a signed-in command (CLI version and a timestamp, never command names, file contents, or secrets) to improve the CLI. Set HQ_TELEMETRY=0 to turn this off.";
19
19
  export const FIRST_SYNC_TELEMETRY_DISCLOSURE_LINE = "HQ sends a usage event when you run a signed-in command (CLI version and a timestamp; first-sync guidance shown or followed, with the identifier cli.first-sync-next-step; never command names, file contents, or secrets) to improve the CLI. Set HQ_TELEMETRY=0 to turn this off.";
20
+ export const CLI_INVITE_NUDGE_TELEMETRY_DISCLOSURE_LINE = "HQ sends a usage event when it shows the teammate invite prompt (your company UID and whether you saw, clicked, or dismissed the prompt; no invitee details, command names, file contents, or secrets). Set HQ_TELEMETRY=0 to turn this off.";
20
21
  export const CLI_FEATURE_USED_TELEMETRY_FLAG = "cli.feature-used-telemetry-v1";
21
22
  export const CLI_FEATURE_USED_TELEMETRY_DISCLOSURE_LINE = "HQ sends a feature-use event when you run a supported CLI feature (feature kind and outcome, plus your company UID when available; never command names, file contents, or secrets). The hq-flags key cli.feature-used-telemetry-v1 controls these events. Set HQ_TELEMETRY=0 to turn this off.";
22
23
  const TELEMETRY_DISABLED_VALUES = new Set(["0", "false", "off"]);
23
24
  const DISCLOSURE_MARKER = "cli-telemetry-disclosed";
24
25
  const FIRST_SYNC_DISCLOSURE_MARKER = "cli-telemetry-disclosed-first-sync-v1";
26
+ const INVITE_NUDGE_DISCLOSURE_MARKER = "cli-telemetry-disclosed-invite-nudge-v1";
25
27
  const FEATURE_USED_DISCLOSURE_MARKER = "cli-telemetry-disclosed-feature-used-v1";
26
28
  let cliSessionStartedPromise;
27
29
  const pendingPlanLimitInviteTeammateExposures = [];
30
+ const pendingCliInviteNudgeEvents = [];
28
31
  export const CLI_LIMIT_AT_ACTION_PROMPT_FLAG = "billing.limit-at-action-prompt";
29
32
  const pendingCliLimitNoticeExposures = [];
30
33
  const pendingCliLimitNoticeEngagements = [];
@@ -68,6 +71,73 @@ export function queuePlanLimitInviteTeammateExposure(companyUid) {
68
71
  export function takePlanLimitInviteTeammateExposures() {
69
72
  return pendingPlanLimitInviteTeammateExposures.splice(0, pendingPlanLimitInviteTeammateExposures.length);
70
73
  }
74
+ /** Queue one company-keyed CLI invite prompt outcome; no person UID is included in the event. */
75
+ export function queueCliInviteNudgeEvent(companyUid, outcome) {
76
+ if (!/^cmp_[A-Za-z0-9_-]{1,128}$/.test(companyUid))
77
+ return;
78
+ pendingCliInviteNudgeEvents.push({ companyUid, outcome });
79
+ }
80
+ export function takeCliInviteNudgeEvents() {
81
+ return pendingCliInviteNudgeEvents.splice(0, pendingCliInviteNudgeEvents.length);
82
+ }
83
+ /** Emit a bounded CLI session fact through the existing product-telemetry path. */
84
+ export async function emitCliInviteNudgeEvent(event, timeoutMs = TELEMETRY_TIMEOUT_MS) {
85
+ try {
86
+ if (isCliTelemetryDisabled())
87
+ return;
88
+ const cached = loadCachedTokens();
89
+ if (!cached || isExpiring(cached, 120) || !cached.accessToken ||
90
+ !isHumanInteractiveCliSession({
91
+ stdoutIsTTY: Boolean(process.stdout.isTTY),
92
+ argv: process.argv,
93
+ env: process.env,
94
+ machineIdentity: isMachineIdentity(),
95
+ }) || process.stdin.isTTY !== true)
96
+ return;
97
+ if (!maybeDiscloseInviteNudgeTelemetry())
98
+ return;
99
+ const controller = new AbortController();
100
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
101
+ try {
102
+ const response = await vaultApiFetch({
103
+ token: cached.accessToken,
104
+ path: "/v1/telemetry/events",
105
+ method: "POST",
106
+ body: {
107
+ events: [{
108
+ eventName: "cli_session_started",
109
+ app: "hq-cli",
110
+ source: "cli",
111
+ occurredAt: new Date().toISOString(),
112
+ consentBasis: "disclosed",
113
+ schemaVersion: 1,
114
+ companyUid: event.companyUid,
115
+ properties: {
116
+ version: CLI_VERSION,
117
+ step: "invite_teammate_nudge",
118
+ outcome: event.outcome,
119
+ },
120
+ }],
121
+ },
122
+ signal: controller.signal,
123
+ });
124
+ if (!response.ok) {
125
+ console.error(`hq: Could not send invite prompt telemetry (http_${response.status}).`);
126
+ }
127
+ }
128
+ catch (error) {
129
+ const errorClass = error instanceof Error ? error.name : "unknown";
130
+ console.error(`hq: Could not send invite prompt telemetry (${errorClass}).`);
131
+ }
132
+ finally {
133
+ clearTimeout(timeout);
134
+ }
135
+ }
136
+ catch (error) {
137
+ const errorClass = error instanceof Error ? error.name : "unknown";
138
+ console.error(`hq: Could not prepare invite prompt telemetry (${errorClass}).`);
139
+ }
140
+ }
71
141
  const COMPANY_UID_PATTERN = /^cmp_[A-Za-z0-9_-]{1,128}$/;
72
142
  export function isCliLimitResourceKind(value) {
73
143
  return ["users", "integrations", "secrets", "agents", "deployments", "storageBytes"].includes(value);
@@ -172,6 +242,9 @@ export function telemetryDisclosureMarkerPath() {
172
242
  function firstSyncTelemetryDisclosureMarkerPath() {
173
243
  return path.join(hqConfigDir(), FIRST_SYNC_DISCLOSURE_MARKER);
174
244
  }
245
+ function inviteNudgeTelemetryDisclosureMarkerPath() {
246
+ return path.join(hqConfigDir(), INVITE_NUDGE_DISCLOSURE_MARKER);
247
+ }
175
248
  function featureUsedTelemetryDisclosureMarkerPath() {
176
249
  return path.join(hqConfigDir(), FEATURE_USED_DISCLOSURE_MARKER);
177
250
  }
@@ -212,6 +285,9 @@ export function maybeDiscloseCliTelemetry(stderr = process.stderr, stdout = proc
212
285
  function maybeDiscloseFirstSyncTelemetry() {
213
286
  return maybeDiscloseTelemetry(FIRST_SYNC_TELEMETRY_DISCLOSURE_LINE, firstSyncTelemetryDisclosureMarkerPath());
214
287
  }
288
+ function maybeDiscloseInviteNudgeTelemetry() {
289
+ return maybeDiscloseTelemetry(CLI_INVITE_NUDGE_TELEMETRY_DISCLOSURE_LINE, inviteNudgeTelemetryDisclosureMarkerPath());
290
+ }
215
291
  function maybeDiscloseFeatureUsedTelemetry() {
216
292
  return maybeDiscloseTelemetry(CLI_FEATURE_USED_TELEMETRY_DISCLOSURE_LINE, featureUsedTelemetryDisclosureMarkerPath());
217
293
  }
@@ -237,6 +237,9 @@ export declare function withMachineTokenStateDir<T>(fn: () => Promise<T>, opts?:
237
237
  home?: string;
238
238
  env?: NodeJS.ProcessEnv;
239
239
  }): Promise<T>;
240
+ type MachineTokenValidityOptions = {
241
+ minValiditySeconds?: number;
242
+ };
240
243
  /**
241
244
  * Ensure the current machine identity has a valid session and return only the
242
245
  * access/ID token pair needed by supported machine-session handoff scripts.
@@ -294,7 +297,8 @@ export declare function buildVaultConfig(authToken: string): VaultServiceConfig;
294
297
  * means no valid session could be ensured non-interactively; the caller gets a
295
298
  * reason string and can decide what to do next. Never opens a browser.
296
299
  */
297
- export declare function refreshCachedSession(options?: Pick<EnsureCognitoTokenOptions, "tokenSource">): Promise<{
300
+ type MachineRefreshOptions = Pick<EnsureCognitoTokenOptions, "tokenSource"> & MachineTokenValidityOptions;
301
+ export declare function refreshCachedSession(options?: MachineRefreshOptions): Promise<{
298
302
  refreshed: boolean;
299
303
  reason?: string;
300
304
  }>;
@@ -837,8 +837,12 @@ function withTokenStateDirSection(fn) {
837
837
  tokenStateDirTail = run.catch(() => undefined);
838
838
  return run;
839
839
  }
840
- async function ensureMachineTokens() {
841
- return withMachineTokenStateDir(() => getValidMachineTokens(DEFAULT_COGNITO));
840
+ const MAX_MACHINE_TOKEN_MIN_VALIDITY_SECONDS = 1800;
841
+ async function ensureMachineTokens(minValiditySeconds) {
842
+ const resolverMinimum = minValiditySeconds === undefined
843
+ ? undefined
844
+ : Math.min(minValiditySeconds, MAX_MACHINE_TOKEN_MIN_VALIDITY_SECONDS);
845
+ return withMachineTokenStateDir(() => getValidMachineTokens(DEFAULT_COGNITO, resolverMinimum === undefined ? undefined : { minValiditySeconds: resolverMinimum }));
842
846
  }
843
847
  /**
844
848
  * Ensure the current machine identity has a valid session and return only the
@@ -857,7 +861,7 @@ async function ensureMachineTokensMaybeFallback(options = {}) {
857
861
  if (isDefaultMachineCredsFallback(options)) {
858
862
  logDefaultMachineCredsFallback();
859
863
  }
860
- return ensureMachineTokens();
864
+ return ensureMachineTokens(options.minValiditySeconds);
861
865
  }
862
866
  /**
863
867
  * After a person-cache miss or refresh failure: if the default machine-creds
@@ -872,7 +876,7 @@ async function tryDefaultMachineCredsFallback(options = {}) {
872
876
  if (!isMachineIdentity())
873
877
  return null;
874
878
  logDefaultMachineCredsFallback();
875
- return ensureMachineTokens();
879
+ return ensureMachineTokens(options.minValiditySeconds);
876
880
  }
877
881
  /**
878
882
  * True when a person-token refresh failed with a definitive Cognito auth
@@ -1114,14 +1118,6 @@ export function buildVaultConfig(authToken) {
1114
1118
  clientInfo: CLI_CLIENT_INFO,
1115
1119
  };
1116
1120
  }
1117
- /**
1118
- * Ensure a valid cached Cognito session exists right now, non-interactively,
1119
- * and return the result. Used by `hq auth refresh` and the `hq-auth-refresh`
1120
- * bin. `refreshed: true` means a valid session is ensured now from a healthy
1121
- * cache hit, a token refresh, or a machine-token mint. `refreshed: false`
1122
- * means no valid session could be ensured non-interactively; the caller gets a
1123
- * reason string and can decide what to do next. Never opens a browser.
1124
- */
1125
1121
  export async function refreshCachedSession(options = {}) {
1126
1122
  if (wantsMachineCognitoTokens(options))
1127
1123
  return refreshCachedSessionUnserialized(options);
@@ -8,6 +8,8 @@ interface GitContext {
8
8
  }
9
9
  export interface DiagnosticsBlob {
10
10
  cliVersion: string;
11
+ /** Current npm release, included only when the feedback.latest-version flag is enabled. */
12
+ latestVersion?: string;
11
13
  /**
12
14
  * The hq-cli, hq-core, and hq-sync versions from the submitter's
13
15
  * environment. `cliVersion` above is retained for back-compat; new
@@ -8,10 +8,11 @@
8
8
  *
9
9
  * Deliberate choices:
10
10
  *
11
- * - `stdio: "inherit"`. Not piped. Piping would buffer output that callers
12
- * stream, reorder stdout against stderr, and require re-emitting bytes we
13
- * have no reason to touch. Inheriting also means a script that reads stdin
14
- * (the hook payload pattern) gets the real fd, unbuffered and unconsumed.
11
+ * - stdio inherits by default. Captured runs pipe only stdout/stderr, and
12
+ * inherit stdin unless the caller supplies an explicit payload. Piping
13
+ * output would buffer streams, reorder stdout against stderr, and require
14
+ * re-emitting bytes, so capture remains an explicit opt-in for native
15
+ * parity fallbacks that need the shell's exact output bytes.
15
16
  *
16
17
  * - an explicit interpreter (`bash` or `node`) rather than executing the file
17
18
  * directly. The executable bit does survive `npm publish` for files that
@@ -52,6 +53,8 @@ export type RunBundledScriptOptions = {
52
53
  env?: Record<string, string | undefined>;
53
54
  /** Capture stdout and stderr as bytes instead of inheriting those streams. */
54
55
  captureOutput?: boolean;
56
+ /** Bytes supplied on stdin. With no value, stdin is inherited as before. */
57
+ stdin?: string | Buffer;
55
58
  };
56
59
  export type RunBundledScriptResult = {
57
60
  /** The child's exit code, or 128+signal if it was terminated by one. */