@indigoai-us/hq-cli 5.345.162 → 5.345.164

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 (35) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/command-catalog.generated.d.ts +341 -1
  3. package/dist/command-catalog.generated.js +422 -1
  4. package/dist/command-registration-plan.d.ts +6 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/billing-usage.d.ts +16 -2
  7. package/dist/commands/billing-usage.js +63 -9
  8. package/dist/commands/billing.d.ts +34 -0
  9. package/dist/commands/billing.js +108 -1
  10. package/dist/commands/deploy-domains.d.ts +53 -0
  11. package/dist/commands/deploy-domains.js +802 -0
  12. package/dist/commands/feedback-triage.d.ts +67 -0
  13. package/dist/commands/feedback-triage.js +294 -0
  14. package/dist/commands/feedback.d.ts +13 -1
  15. package/dist/commands/feedback.js +32 -8
  16. package/dist/lib/billing/billing-status-lines.d.ts +14 -0
  17. package/dist/lib/billing/billing-status-lines.js +43 -0
  18. package/dist/lib/billing/usage-only.d.ts +18 -0
  19. package/dist/lib/billing/usage-only.js +18 -1
  20. package/dist/lib/doctor/checks/sync-runtime.d.ts +34 -0
  21. package/dist/lib/doctor/checks/sync-runtime.js +217 -8
  22. package/dist/lib/doctor/fix/apply.js +1 -0
  23. package/dist/lib/doctor/fix/refresh-sync-outcome.d.ts +4 -1
  24. package/dist/lib/doctor/fix/refresh-sync-outcome.js +15 -0
  25. package/dist/lib/doctor/fix/refresh-sync-progress.d.ts +42 -0
  26. package/dist/lib/doctor/fix/refresh-sync-progress.js +100 -0
  27. package/dist/lib/doctor/json-output.d.ts +7 -0
  28. package/dist/lib/doctor/json-output.js +8 -1
  29. package/dist/lib/doctor/types.d.ts +1 -1
  30. package/dist/lib/doctor/types.js +1 -0
  31. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +1 -1
  32. package/dist/lib/mesh/live/daemon/work-subscribe.js +121 -38
  33. package/dist/lib/mesh/live/paths.d.ts +1 -0
  34. package/dist/lib/mesh/live/paths.js +1 -0
  35. package/package.json +7 -7
@@ -0,0 +1,67 @@
1
+ import { Command } from "commander";
2
+ /**
3
+ * Staff triage commands on the feedback queue: `hq feedback list`,
4
+ * `hq feedback triage` and `hq feedback close`. `hq feedback assign` lives in
5
+ * feedback.ts next to the shared helpers.
6
+ *
7
+ * The server owns triagedAt, triagedBy and priority. These commands never
8
+ * send them.
9
+ */
10
+ export declare const TRIAGE_KINDS: readonly ["bug", "feature", "question"];
11
+ export type TriageKind = (typeof TRIAGE_KINDS)[number];
12
+ export declare const MAX_TRIAGE_SUMMARY = 2000;
13
+ export declare const DEFAULT_LIST_LIMIT = 50;
14
+ export interface FeedbackListRow {
15
+ id: string;
16
+ title?: string;
17
+ plan?: string;
18
+ submittedAt?: string;
19
+ createdAt?: string;
20
+ [key: string]: unknown;
21
+ }
22
+ export type FeedbackListSelector = {
23
+ kind: "untriaged";
24
+ } | {
25
+ kind: "assignee";
26
+ assignee: string;
27
+ };
28
+ /** Email claim of the caller's staff token, used by `list --mine`. */
29
+ export declare function emailFromToken(token: string): string | null;
30
+ /** Compact age such as `5m`, `3h` or `2d`. Unknown or unparsable times print `-`. */
31
+ export declare function formatFeedbackAge(iso: string | undefined, now?: number): string;
32
+ /**
33
+ * hq-pro's `GET /v1/feedback` returns the next page as `nextCursor` on every
34
+ * path (the untriaged queue also mirrors it as `cursor`) and reads it back from
35
+ * `?cursor=`. `nextToken` is accepted for older servers. The value always goes
36
+ * back as `?cursor=`.
37
+ */
38
+ export declare function cursorFrom(data: unknown): string | null;
39
+ /** Page through `GET /v1/feedback` until `limit` rows (or every row with `limit: null`). */
40
+ export declare function listFeedback(opts: {
41
+ token: string;
42
+ selector: FeedbackListSelector;
43
+ status?: string;
44
+ limit: number | null;
45
+ }): Promise<FeedbackListRow[]>;
46
+ export declare function formatFeedbackTable(rows: FeedbackListRow[], now?: number): string;
47
+ export declare function parseListLimit(raw: string): number;
48
+ export declare function buildTriagePatch(opts: {
49
+ id: string;
50
+ kind: string;
51
+ area: string;
52
+ summary: string;
53
+ duplicateOf?: string;
54
+ }): Record<string, string>;
55
+ export declare function buildClosePatch(opts: {
56
+ reason: string;
57
+ wontFix?: boolean;
58
+ }): Record<string, string>;
59
+ /** PATCH one feedback row; returns the server's JSON, or the sent fields when the body is empty. */
60
+ export declare function patchFeedback(opts: {
61
+ id: string;
62
+ token: string;
63
+ body: Record<string, string>;
64
+ action: string;
65
+ }): Promise<unknown>;
66
+ export declare function registerFeedbackTriageCommands(feedbackCmd: Command): void;
67
+ //# sourceMappingURL=feedback-triage.d.ts.map
@@ -0,0 +1,294 @@
1
+ import chalk from "chalk";
2
+ import { assertVaultApiRequestSupported, vaultApiFetch } from "../utils/vault-api.js";
3
+ import { decodeJwtPayload } from "../utils/person-token-cache.js";
4
+ import { assertFeedbackId, normalizeFeedbackAssignee, readFeedbackError, resolveStaffToken, } from "./feedback.js";
5
+ /**
6
+ * Staff triage commands on the feedback queue: `hq feedback list`,
7
+ * `hq feedback triage` and `hq feedback close`. `hq feedback assign` lives in
8
+ * feedback.ts next to the shared helpers.
9
+ *
10
+ * The server owns triagedAt, triagedBy and priority. These commands never
11
+ * send them.
12
+ */
13
+ export const TRIAGE_KINDS = ["bug", "feature", "question"];
14
+ export const MAX_TRIAGE_SUMMARY = 2000;
15
+ export const DEFAULT_LIST_LIMIT = 50;
16
+ /** Safety stop for --all so a server that never ends its cursor can't loop forever. */
17
+ const MAX_LIST_PAGES = 100;
18
+ function fail(err) {
19
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
20
+ process.exit(1);
21
+ }
22
+ /** Email claim of the caller's staff token, used by `list --mine`. */
23
+ export function emailFromToken(token) {
24
+ const claims = decodeJwtPayload(token);
25
+ const email = claims?.email;
26
+ return typeof email === "string" && email.includes("@") ? email.toLowerCase() : null;
27
+ }
28
+ /** Compact age such as `5m`, `3h` or `2d`. Unknown or unparsable times print `-`. */
29
+ export function formatFeedbackAge(iso, now = Date.now()) {
30
+ if (!iso)
31
+ return "-";
32
+ const at = Date.parse(iso);
33
+ if (Number.isNaN(at))
34
+ return "-";
35
+ const minutes = Math.max(0, Math.floor((now - at) / 60_000));
36
+ if (minutes < 60)
37
+ return `${minutes}m`;
38
+ const hours = Math.floor(minutes / 60);
39
+ if (hours < 48)
40
+ return `${hours}h`;
41
+ return `${Math.floor(hours / 24)}d`;
42
+ }
43
+ function rowsFrom(data) {
44
+ if (!data || typeof data !== "object" || Array.isArray(data))
45
+ return [];
46
+ const items = data.items;
47
+ if (!Array.isArray(items))
48
+ return [];
49
+ return items.filter((row) => !!row && typeof row === "object" && typeof row.id === "string");
50
+ }
51
+ /**
52
+ * hq-pro's `GET /v1/feedback` returns the next page as `nextCursor` on every
53
+ * path (the untriaged queue also mirrors it as `cursor`) and reads it back from
54
+ * `?cursor=`. `nextToken` is accepted for older servers. The value always goes
55
+ * back as `?cursor=`.
56
+ */
57
+ export function cursorFrom(data) {
58
+ if (!data || typeof data !== "object" || Array.isArray(data))
59
+ return null;
60
+ for (const name of ["nextCursor", "cursor", "nextToken"]) {
61
+ const value = data[name];
62
+ if (typeof value === "string" && value.length > 0)
63
+ return value;
64
+ }
65
+ return null;
66
+ }
67
+ /** Server page cap (FEEDBACK_LIST_PAGE_MAX). The plain list path rejects a cursor without a limit. */
68
+ const LIST_PAGE_SIZE = 200;
69
+ /** Page through `GET /v1/feedback` until `limit` rows (or every row with `limit: null`). */
70
+ export async function listFeedback(opts) {
71
+ const base = opts.selector.kind === "untriaged"
72
+ ? { triage: "untriaged" }
73
+ : { assignee: opts.selector.assignee };
74
+ if (opts.status)
75
+ base.status = opts.status;
76
+ const rows = [];
77
+ let cursor = null;
78
+ for (let page = 0; page < MAX_LIST_PAGES; page++) {
79
+ const pageSize = opts.limit === null ? LIST_PAGE_SIZE : Math.min(LIST_PAGE_SIZE, opts.limit - rows.length);
80
+ const query = { ...base, limit: String(pageSize) };
81
+ if (cursor)
82
+ query.cursor = cursor;
83
+ const res = await vaultApiFetch({ token: opts.token, path: "/v1/feedback", query });
84
+ if (!res.ok) {
85
+ throw new Error(`Failed to list feedback: ${await readFeedbackError(res)}`);
86
+ }
87
+ const data = (await res.json());
88
+ rows.push(...rowsFrom(data));
89
+ if (opts.limit !== null && rows.length >= opts.limit)
90
+ return rows.slice(0, opts.limit);
91
+ cursor = cursorFrom(data);
92
+ if (!cursor)
93
+ return rows;
94
+ }
95
+ return rows;
96
+ }
97
+ export function formatFeedbackTable(rows, now = Date.now()) {
98
+ const header = ["ID", "AGE", "PLAN", "TITLE"];
99
+ const body = rows.map((row) => [
100
+ row.id,
101
+ formatFeedbackAge(row.submittedAt ?? row.createdAt, now),
102
+ typeof row.plan === "string" && row.plan.length > 0 ? row.plan : "-",
103
+ typeof row.title === "string" ? row.title : "",
104
+ ]);
105
+ const widths = [0, 1, 2].map((col) => Math.max(header[col].length, ...body.map((cells) => cells[col].length)));
106
+ return [header, ...body]
107
+ .map((cells) => cells.map((cell, col) => (col < 3 ? cell.padEnd(widths[col]) : cell)).join(" ").trimEnd())
108
+ .join("\n");
109
+ }
110
+ export function parseListLimit(raw) {
111
+ const n = Number(raw);
112
+ if (!Number.isInteger(n) || n < 1) {
113
+ throw new Error(`Invalid --limit '${raw}': expected a positive whole number.`);
114
+ }
115
+ return n;
116
+ }
117
+ export function buildTriagePatch(opts) {
118
+ if (!TRIAGE_KINDS.includes(opts.kind)) {
119
+ throw new Error(`Invalid --kind '${opts.kind}': expected one of ${TRIAGE_KINDS.join(", ")}.`);
120
+ }
121
+ const area = opts.area.trim();
122
+ if (!area)
123
+ throw new Error("--area must not be empty.");
124
+ const summary = opts.summary.trim();
125
+ if (!summary)
126
+ throw new Error("--summary must not be empty.");
127
+ if (summary.length > MAX_TRIAGE_SUMMARY) {
128
+ throw new Error(`--summary is ${summary.length} characters; the limit is ${MAX_TRIAGE_SUMMARY}.`);
129
+ }
130
+ const body = {
131
+ triageStatus: opts.duplicateOf === undefined ? "triaged" : "duplicate",
132
+ triageKind: opts.kind,
133
+ area,
134
+ summary,
135
+ };
136
+ if (opts.duplicateOf !== undefined) {
137
+ assertFeedbackId(opts.duplicateOf);
138
+ if (opts.duplicateOf === opts.id) {
139
+ throw new Error("--duplicate-of must name a different feedback item.");
140
+ }
141
+ body.duplicateOf = opts.duplicateOf;
142
+ }
143
+ return body;
144
+ }
145
+ export function buildClosePatch(opts) {
146
+ const reason = opts.reason.trim();
147
+ if (!reason)
148
+ throw new Error("--reason must not be empty.");
149
+ return { status: opts.wontFix ? "wont_fix" : "resolved", resolution: reason };
150
+ }
151
+ /** PATCH one feedback row; returns the server's JSON, or the sent fields when the body is empty. */
152
+ export async function patchFeedback(opts) {
153
+ const res = await vaultApiFetch({
154
+ token: opts.token,
155
+ path: `/v1/feedback/${opts.id}`,
156
+ method: "PATCH",
157
+ body: opts.body,
158
+ });
159
+ if (!res.ok) {
160
+ throw new Error(`Failed to ${opts.action} ${opts.id}: ${await readFeedbackError(res)}`);
161
+ }
162
+ const data = (await res.json().catch(() => null));
163
+ return data ?? { id: opts.id, ...opts.body };
164
+ }
165
+ function registerListSubcommand(feedbackCmd) {
166
+ feedbackCmd
167
+ .command("list")
168
+ .description("List feedback: untriaged (default), yours, or one person's")
169
+ .option("--untriaged", "Items nobody has triaged yet (the default)")
170
+ .option("--mine", "Items assigned to you")
171
+ .option("--assignee <person>", "Items assigned to this email address or prs_ uid")
172
+ .option("--status <status>", "Only items with this status")
173
+ .option("--limit <n>", `Stop after this many items (default ${DEFAULT_LIST_LIMIT})`)
174
+ .option("--all", "Fetch every page")
175
+ .option("--json", "Print { items, count } as JSON")
176
+ .action(async (opts) => {
177
+ try {
178
+ const picked = [opts.untriaged, opts.mine, opts.assignee !== undefined].filter(Boolean);
179
+ if (picked.length > 1) {
180
+ throw new Error("Use only one of --untriaged, --mine and --assignee.");
181
+ }
182
+ if (opts.all && opts.limit !== undefined) {
183
+ throw new Error("Use --limit or --all, not both.");
184
+ }
185
+ if (opts.status !== undefined && !opts.status.trim()) {
186
+ throw new Error("--status must not be empty.");
187
+ }
188
+ const limit = opts.all
189
+ ? null
190
+ : opts.limit === undefined
191
+ ? DEFAULT_LIST_LIMIT
192
+ : parseListLimit(opts.limit);
193
+ const token = await resolveStaffToken();
194
+ let selector = { kind: "untriaged" };
195
+ if (opts.mine) {
196
+ const email = emailFromToken(token);
197
+ if (!email) {
198
+ throw new Error("--mine needs a person login with an email (hq login). Use --assignee <person> instead.");
199
+ }
200
+ selector = { kind: "assignee", assignee: email };
201
+ }
202
+ else if (opts.assignee !== undefined) {
203
+ selector = {
204
+ kind: "assignee",
205
+ assignee: normalizeFeedbackAssignee(opts.assignee, "--assignee"),
206
+ };
207
+ }
208
+ assertVaultApiRequestSupported({ token, path: "/v1/feedback" });
209
+ const items = await listFeedback({
210
+ token,
211
+ selector,
212
+ status: opts.status?.trim(),
213
+ limit,
214
+ });
215
+ if (opts.json) {
216
+ console.log(JSON.stringify({ items, count: items.length }));
217
+ }
218
+ else if (items.length === 0) {
219
+ console.log("No feedback items match.");
220
+ }
221
+ else {
222
+ console.log(formatFeedbackTable(items));
223
+ }
224
+ }
225
+ catch (err) {
226
+ fail(err);
227
+ }
228
+ });
229
+ }
230
+ function registerTriageSubcommand(feedbackCmd) {
231
+ feedbackCmd
232
+ .command("triage")
233
+ .description("Label a feedback item with its kind, area and summary, or mark it a duplicate")
234
+ .argument("<id>", "Feedback id (feedback_<uuid>)")
235
+ .requiredOption("--kind <kind>", `One of ${TRIAGE_KINDS.join(", ")}`)
236
+ .requiredOption("--area <area>", "Product area, for example sync or desktop")
237
+ .requiredOption("--summary <text>", `One-line triage summary (at most ${MAX_TRIAGE_SUMMARY} characters)`)
238
+ .option("--duplicate-of <id>", "Mark this item a duplicate of another feedback id")
239
+ .option("--json", "Print the updated item as JSON")
240
+ .action(async (id, opts) => {
241
+ try {
242
+ assertFeedbackId(id);
243
+ const body = buildTriagePatch({ id, ...opts });
244
+ const token = await resolveStaffToken();
245
+ assertVaultApiRequestSupported({ token, path: `/v1/feedback/${id}` });
246
+ const result = await patchFeedback({ id, token, body, action: "triage" });
247
+ if (opts.json) {
248
+ console.log(JSON.stringify(result));
249
+ }
250
+ else if (body.duplicateOf) {
251
+ console.log(`Marked duplicate: ${id} -> ${body.duplicateOf}`);
252
+ }
253
+ else {
254
+ console.log(`Triaged: ${id} (${body.triageKind}, ${body.area})`);
255
+ }
256
+ }
257
+ catch (err) {
258
+ fail(err);
259
+ }
260
+ });
261
+ }
262
+ function registerCloseSubcommand(feedbackCmd) {
263
+ feedbackCmd
264
+ .command("close")
265
+ .description("Close a feedback item as resolved (or won't fix) with a reason")
266
+ .argument("<id>", "Feedback id (feedback_<uuid>)")
267
+ .requiredOption("--reason <text>", "Why it is closed: the fix, PR or version, or the decision")
268
+ .option("--wont-fix", "Close as won't fix instead of resolved")
269
+ .option("--json", "Print the updated item as JSON")
270
+ .action(async (id, opts) => {
271
+ try {
272
+ assertFeedbackId(id);
273
+ const body = buildClosePatch(opts);
274
+ const token = await resolveStaffToken();
275
+ assertVaultApiRequestSupported({ token, path: `/v1/feedback/${id}` });
276
+ const result = await patchFeedback({ id, token, body, action: "close" });
277
+ if (opts.json) {
278
+ console.log(JSON.stringify(result));
279
+ }
280
+ else {
281
+ console.log(`Closed: ${id} (${body.status})`);
282
+ }
283
+ }
284
+ catch (err) {
285
+ fail(err);
286
+ }
287
+ });
288
+ }
289
+ export function registerFeedbackTriageCommands(feedbackCmd) {
290
+ registerListSubcommand(feedbackCmd);
291
+ registerTriageSubcommand(feedbackCmd);
292
+ registerCloseSubcommand(feedbackCmd);
293
+ }
294
+ //# sourceMappingURL=feedback-triage.js.map
@@ -76,6 +76,13 @@ export declare function submitFeedback(opts: FeedbackSubmitOptions): Promise<Fee
76
76
  export declare const PUBLIC_TICKET_STATES: readonly ["triaged", "in-progress", "shipped", "verified", "resolved"];
77
77
  export type PublicTicketState = (typeof PUBLIC_TICKET_STATES)[number];
78
78
  export declare function assertFeedbackId(id: string): void;
79
+ /**
80
+ * The staff feedback routes gate on the id token's email claim, so send the
81
+ * cached id token when a person session has one. Machine identities already
82
+ * get their id token back from ensureCognitoToken.
83
+ */
84
+ export declare function resolveStaffToken(): Promise<string>;
85
+ export declare function readFeedbackError(res: Response): Promise<string>;
79
86
  /** Resolve (and lazily mint) the public tracking link for a ticket. */
80
87
  export declare function fetchFeedbackLink(opts: {
81
88
  id: string;
@@ -92,7 +99,7 @@ export declare function postFeedbackUpdate(opts: {
92
99
  slack: string | null;
93
100
  }>;
94
101
  /** Mirror of hq-pro normalizeAssignee: a prs_ uid as-is, an email lowercased. */
95
- export declare function normalizeFeedbackAssignee(value: string): string;
102
+ export declare function normalizeFeedbackAssignee(value: string, label?: string): string;
96
103
  /**
97
104
  * Reassign a ticket. A person's own login may move a ticket that another
98
105
  * person claimed; agent and operator service tokens get 409
@@ -103,5 +110,10 @@ export declare function assignFeedback(opts: {
103
110
  token: string;
104
111
  assignee: string;
105
112
  }): Promise<void>;
113
+ /**
114
+ * Pick the new assignee from the positional `[person]` or the `--to` flag.
115
+ * Either form works; naming two different people is an error.
116
+ */
117
+ export declare function resolveAssignTarget(person: string | undefined, to: string | undefined): string;
106
118
  export declare function registerFeedbackCommand(program: Command): void;
107
119
  //# sourceMappingURL=feedback.d.ts.map
@@ -9,6 +9,7 @@ import { uploadLogBundle } from "../utils/feedback-log-bundle.js";
9
9
  import { resolveProcessFlagGate } from "../lib/flag-registry.js";
10
10
  import { cachedLatestVersion } from "../utils/version-check.js";
11
11
  import { registerFeedbackShowCommand } from "./feedback-show.js";
12
+ import { registerFeedbackTriageCommands } from "./feedback-triage.js";
12
13
  export const FEEDBACK_LATEST_VERSION_FLAG = "feedback.latest-version";
13
14
  export const FEEDBACK_LATEST_VERSION_FLAG_FALLBACK = false;
14
15
  export const FEEDBACK_REPORTER_KIND_FLAG = "feedback.reporter-kind";
@@ -229,14 +230,14 @@ export function assertFeedbackId(id) {
229
230
  * cached id token when a person session has one. Machine identities already
230
231
  * get their id token back from ensureCognitoToken.
231
232
  */
232
- async function resolveStaffToken() {
233
+ export async function resolveStaffToken() {
233
234
  const token = await ensureCognitoToken({ interactive: false });
234
235
  const cached = loadCachedTokens();
235
236
  return typeof cached?.idToken === "string" && cached.idToken.length > 0
236
237
  ? cached.idToken
237
238
  : token;
238
239
  }
239
- async function readFeedbackError(res) {
240
+ export async function readFeedbackError(res) {
240
241
  const data = await res.json().catch(() => ({}));
241
242
  return data &&
242
243
  typeof data === "object" &&
@@ -287,10 +288,10 @@ export async function postFeedbackUpdate(opts) {
287
288
  }
288
289
  const ASSIGNEE_RE = /^(?:prs_[A-Za-z0-9]+|[^\s@]+@[^\s@]+\.[^\s@]+)$/;
289
290
  /** Mirror of hq-pro normalizeAssignee: a prs_ uid as-is, an email lowercased. */
290
- export function normalizeFeedbackAssignee(value) {
291
+ export function normalizeFeedbackAssignee(value, label = "--to") {
291
292
  const trimmed = value.trim();
292
293
  if (!ASSIGNEE_RE.test(trimmed)) {
293
- throw new Error(`Invalid --to '${value}': expected an email address or a prs_ uid.`);
294
+ throw new Error(`Invalid ${label} '${value}': expected an email address or a prs_ uid.`);
294
295
  }
295
296
  return trimmed.startsWith("prs_") ? trimmed : trimmed.toLowerCase();
296
297
  }
@@ -315,20 +316,42 @@ export async function assignFeedback(opts) {
315
316
  }
316
317
  throw new Error(`Failed to assign ${opts.id}: ${message}`);
317
318
  }
319
+ /**
320
+ * Pick the new assignee from the positional `[person]` or the `--to` flag.
321
+ * Either form works; naming two different people is an error.
322
+ */
323
+ export function resolveAssignTarget(person, to) {
324
+ if (person === undefined && to === undefined) {
325
+ throw new Error("Name the new assignee: hq feedback assign <id> <person> (or --to <person>).");
326
+ }
327
+ const fromPerson = person === undefined ? undefined : normalizeFeedbackAssignee(person, "person");
328
+ const fromTo = to === undefined ? undefined : normalizeFeedbackAssignee(to);
329
+ if (fromPerson !== undefined && fromTo !== undefined && fromPerson !== fromTo) {
330
+ throw new Error(`Conflicting assignees '${person}' and --to '${to}': give only one.`);
331
+ }
332
+ return (fromPerson ?? fromTo);
333
+ }
318
334
  function registerAssignSubcommand(feedbackCmd) {
319
335
  feedbackCmd
320
336
  .command("assign")
321
337
  .description("Reassign a ticket to a person, including one another person has claimed")
322
338
  .argument("<id>", "Feedback id (feedback_<uuid>)")
323
- .requiredOption("--to <assignee>", "New assignee: an email address or a prs_ uid")
324
- .action(async (id, opts) => {
339
+ .argument("[person]", "New assignee: an email address or a prs_ uid")
340
+ .option("--to <assignee>", "New assignee: an email address or a prs_ uid")
341
+ .option("--json", "Print the result as JSON")
342
+ .action(async (id, person, opts) => {
325
343
  try {
326
344
  assertFeedbackId(id);
327
- const assignee = normalizeFeedbackAssignee(opts.to);
345
+ const assignee = resolveAssignTarget(person, opts.to);
328
346
  const token = await resolveStaffToken();
329
347
  assertVaultApiRequestSupported({ token, path: `/v1/feedback/${id}` });
330
348
  await assignFeedback({ id, token, assignee });
331
- console.log(`Assigned: ${id} -> ${assignee}`);
349
+ if (opts.json) {
350
+ console.log(JSON.stringify({ id, assignee }));
351
+ }
352
+ else {
353
+ console.log(`Assigned: ${id} -> ${assignee}`);
354
+ }
332
355
  }
333
356
  catch (err) {
334
357
  console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
@@ -499,5 +522,6 @@ export function registerFeedbackCommand(program) {
499
522
  registerUpdateSubcommand(feedbackCmd);
500
523
  registerFeedbackShowCommand(feedbackCmd);
501
524
  registerAssignSubcommand(feedbackCmd);
525
+ registerFeedbackTriageCommands(feedbackCmd);
502
526
  }
503
527
  //# sourceMappingURL=feedback.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The account lines `hq billing status`, `hq billing usage` and
3
+ * `hq billing usage --card` print from hq-pro's shared `billingState` (US-025).
4
+ * One helper so every surface shows the same trial usage, included usage,
5
+ * spend limit and card prompt for the same state.
6
+ */
7
+ import { type UsageCard } from "./usage-card.js";
8
+ import type { SharedBillingState } from "./usage-only.js";
9
+ /**
10
+ * Lines for one billing state. Legacy, enterprise and comped accounts get no
11
+ * lines. The member view carries no money figures, so it gets none either.
12
+ */
13
+ export declare function accountStatusLines(state: SharedBillingState | null, card?: UsageCard): string[];
14
+ //# sourceMappingURL=billing-status-lines.d.ts.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The account lines `hq billing status`, `hq billing usage` and
3
+ * `hq billing usage --card` print from hq-pro's shared `billingState` (US-025).
4
+ * One helper so every surface shows the same trial usage, included usage,
5
+ * spend limit and card prompt for the same state.
6
+ */
7
+ import { BUNDLED_USAGE_CARD, formatCardUsd } from "./usage-card.js";
8
+ function usd(cents) {
9
+ return `$${(cents / 100).toFixed(2)}`;
10
+ }
11
+ function onUsagePricing(state) {
12
+ return state.kind === "usage" || state.kind === "usage-trial";
13
+ }
14
+ /**
15
+ * Lines for one billing state. Legacy, enterprise and comped accounts get no
16
+ * lines. The member view carries no money figures, so it gets none either.
17
+ */
18
+ export function accountStatusLines(state, card = BUNDLED_USAGE_CARD) {
19
+ if (!state || !onUsagePricing(state))
20
+ return [];
21
+ const lines = [];
22
+ if (state.kind === "usage-trial" && state.trial) {
23
+ lines.push(`Trial: ${state.trial.daysLeft} days left (ends ${state.trial.endsAt.slice(0, 10)})`);
24
+ if (state.view === "full" && state.trial.usedCents !== null) {
25
+ const capCents = state.trial.capCents ?? card.trial.capUsd * 100;
26
+ lines.push(`Trial usage: ${usd(state.trial.usedCents)} of the ${usd(capCents)} trial cap`);
27
+ }
28
+ }
29
+ if (state.view !== "full")
30
+ return lines;
31
+ lines.push(`Included usage: ${formatCardUsd(card.includedUsd)} each month`);
32
+ if (state.spendLimit) {
33
+ lines.push(state.spendLimit.cents === null
34
+ ? "Spend limit: none set"
35
+ : `Spend limit: ${usd(state.spendLimit.cents)} a month`);
36
+ }
37
+ if (state.cardPrompt) {
38
+ lines.push(`Add a card: usage is past the ${formatCardUsd(card.includedUsd)} included this month. ` +
39
+ "Run `hq billing checkout` to keep working past it.");
40
+ }
41
+ return lines;
42
+ }
43
+ //# sourceMappingURL=billing-status-lines.js.map
@@ -35,10 +35,28 @@ export interface SharedBillingState {
35
35
  kind: BillingStateKind;
36
36
  scope: "company" | "person";
37
37
  view: "full" | "member";
38
+ /**
39
+ * The usage trial, or null. `capCents` is the trial's usage cap and
40
+ * `usedCents` what the trial has used so far; both are null when the server
41
+ * does not say (member view, older server).
42
+ */
38
43
  trial: {
39
44
  daysLeft: number;
40
45
  endsAt: string;
46
+ capCents: number | null;
47
+ usedCents: number | null;
48
+ } | null;
49
+ /**
50
+ * The monthly spend limit. `cents: null` means no limit is set. The whole
51
+ * field is null off usage pricing and in the member view.
52
+ */
53
+ spendLimit: {
54
+ cents: number | null;
41
55
  } | null;
56
+ /** Whether a card is on file. Null when the server does not say. */
57
+ cardOnFile: boolean | null;
58
+ /** Usage passed the included allowance with no card on file. */
59
+ cardPrompt: boolean;
42
60
  /** A pause the server is applying now, or null. */
43
61
  paused: {
44
62
  reason: string;
@@ -65,7 +65,21 @@ function readTrial(value) {
65
65
  !Number.isFinite(Date.parse(trial.endsAt))) {
66
66
  return undefined;
67
67
  }
68
- return { daysLeft: trial.daysLeft, endsAt: trial.endsAt };
68
+ return {
69
+ daysLeft: trial.daysLeft,
70
+ endsAt: trial.endsAt,
71
+ capCents: centsOrNull(trial.capCents),
72
+ usedCents: centsOrNull(trial.usedCents),
73
+ };
74
+ }
75
+ function centsOrNull(value) {
76
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
77
+ }
78
+ function readSpendLimit(value) {
79
+ const limit = asRecord(value);
80
+ if (!limit)
81
+ return null;
82
+ return { cents: centsOrNull(limit.cents) };
69
83
  }
70
84
  function readPaused(value) {
71
85
  const paused = asRecord(value);
@@ -102,6 +116,9 @@ export function readBillingState(body) {
102
116
  scope: state.scope,
103
117
  view: state.view,
104
118
  trial,
119
+ spendLimit: readSpendLimit(state.spendLimit),
120
+ cardOnFile: typeof state.cardOnFile === "boolean" ? state.cardOnFile : null,
121
+ cardPrompt: state.cardPrompt === true,
105
122
  paused: readPaused(state.paused),
106
123
  actions: {
107
124
  addCard: actions?.addCard === true,
@@ -1,13 +1,47 @@
1
+ import { readSessionCompanyLock } from "../../session-company-lock.js";
1
2
  import type { CheckResult } from "../types.js";
2
3
  export declare const SYNC_RUNTIME_WINDOW_MS: number;
3
4
  /** Ignore timing and restart signals until a newly started receiver has settled. */
4
5
  export declare const SYNC_RUNTIME_RESTART_WARMUP_MS: number;
6
+ /** Most refused vault keys named by `sync.runtime.pull-access-denied`. */
7
+ export declare const PULL_DENIED_KEY_CAP = 10;
8
+ /** A key refused on at least this many pulls in the hour is a standing condition, not a one-off. */
9
+ export declare const PULL_DENIED_MIN_REPEATS = 2;
5
10
  export interface SyncRuntimeOptions {
6
11
  now: number;
7
12
  conflictCount?: number;
13
+ /**
14
+ * Company scopes whose refused vault keys may be named. `undefined` means
15
+ * the session holds no company lock, so every scope may be named. Keys in
16
+ * any other scope (including `personal` when it is not listed) are counted
17
+ * but never named, so `hq doctor` in a session locked to company A cannot
18
+ * print company B's file names from the machine-wide sync log.
19
+ */
20
+ nameableCompanies?: readonly string[];
8
21
  }
22
+ /**
23
+ * Which cloud call a receiver permission refusal came from. Closed vocabulary
24
+ * derived from the hq-cloud receiver's event names, so the breakdown can be
25
+ * reported without the error prose.
26
+ *
27
+ * - `subscribe`: `POST /v1/sync/subscribe` itself answered 403 while the
28
+ * receiver re-vended its queue credentials.
29
+ * - `sqs-receive` / `sqs-visibility` / `sqs-delete`: the per-device queue
30
+ * credentials vended by subscribe were refused by SQS. One bad vend
31
+ * repeats on every poll and lease heartbeat until the runner restarts.
32
+ * - `cloudwatch-metric`: the latency metric publisher was refused.
33
+ * - `targeted-pull`: the object pull after a change event failed; the
34
+ * refused keys are named by `sync.runtime.pull-access-denied`.
35
+ * - `runner-error`: a runner `error` record outside the receiver.
36
+ */
37
+ export type ReceiverRefusalCall = "subscribe" | "sqs-receive" | "sqs-visibility" | "sqs-delete" | "cloudwatch-metric" | "targeted-pull" | "runner-error" | "other";
9
38
  /** Classify the last hour of sync log lines into privacy-safe results. */
10
39
  export declare function analyzeSyncRuntimeLog(lines: readonly string[], options: SyncRuntimeOptions): CheckResult[];
40
+ /**
41
+ * Company scopes this session may see named: `undefined` (all) when there is
42
+ * no session or no company lock; nothing when the lock cannot be verified.
43
+ */
44
+ export declare function sessionNameableCompanies(env: NodeJS.ProcessEnv, readLock?: typeof readSessionCompanyLock): readonly string[] | undefined;
11
45
  /** Read the machine's sync logs and client health file. */
12
46
  export declare function defaultSyncRuntimeResults(now: Date, hqHome?: string, env?: NodeJS.ProcessEnv): CheckResult[];
13
47
  //# sourceMappingURL=sync-runtime.d.ts.map