@indigoai-us/hq-cli 5.345.156 → 5.345.160
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 +255 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
- package/assets/scaffold/core/scripts/worktree.sh +4 -0
- package/dist/command-catalog.generated.d.ts +535 -10
- package/dist/command-catalog.generated.js +678 -31
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/_patterns.d.ts +17 -0
- package/dist/commands/_patterns.js +22 -0
- package/dist/commands/access.d.ts +6 -0
- package/dist/commands/access.js +18 -1
- package/dist/commands/acl-lock.d.ts +41 -0
- package/dist/commands/acl-lock.js +28 -0
- package/dist/commands/agent-enroll.d.ts +2 -0
- package/dist/commands/agent-enroll.js +9 -4
- package/dist/commands/agent-kit.js +8 -6
- package/dist/commands/agents.js +5 -11
- package/dist/commands/billing-sponsor.d.ts +4 -4
- package/dist/commands/billing-sponsor.js +11 -11
- package/dist/commands/billing-usage.d.ts +22 -8
- package/dist/commands/billing-usage.js +57 -22
- package/dist/commands/billing.d.ts +7 -7
- package/dist/commands/billing.js +13 -13
- package/dist/commands/bot.js +7 -4
- package/dist/commands/channel-suggest.d.ts +56 -0
- package/dist/commands/channel-suggest.js +170 -0
- package/dist/commands/channels.js +7 -1
- package/dist/commands/cloud-provision.js +2 -2
- package/dist/commands/cloud.d.ts +45 -0
- package/dist/commands/cloud.js +157 -17
- package/dist/commands/company.js +4 -3
- package/dist/commands/core-checkpoint-native.d.ts +18 -0
- package/dist/commands/core-checkpoint-native.js +67 -2
- package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
- package/dist/commands/core-pipeline-driver-native.js +131 -5
- package/dist/commands/core.js +12 -0
- package/dist/commands/crm.js +83 -6
- package/dist/commands/db-remote-deps.js +12 -1
- package/dist/commands/deploy-access.d.ts +33 -0
- package/dist/commands/deploy-access.js +322 -0
- package/dist/commands/dm-attach.d.ts +106 -0
- package/dist/commands/dm-attach.js +184 -0
- package/dist/commands/dm.d.ts +37 -0
- package/dist/commands/dm.js +172 -10
- package/dist/commands/feedback-show.d.ts +40 -0
- package/dist/commands/feedback-show.js +261 -0
- package/dist/commands/feedback.d.ts +21 -0
- package/dist/commands/feedback.js +152 -2
- package/dist/commands/files.d.ts +24 -0
- package/dist/commands/files.js +71 -8
- package/dist/commands/integrations-connect.js +7 -3
- package/dist/commands/lanes-accounts.js +16 -1
- package/dist/commands/lanes-loop.js +14 -4
- package/dist/commands/lanes-message.js +6 -4
- package/dist/commands/lanes-resume.js +36 -13
- package/dist/commands/lanes-shared.d.ts +5 -0
- package/dist/commands/lanes-shared.js +13 -3
- package/dist/commands/lanes-watch.js +1 -0
- package/dist/commands/lanes.d.ts +20 -1
- package/dist/commands/lanes.js +201 -51
- package/dist/commands/meetings.d.ts +4 -4
- package/dist/commands/meetings.js +17 -8
- package/dist/commands/mesh.js +149 -1
- package/dist/commands/monitor.d.ts +1 -0
- package/dist/commands/monitor.js +4 -1
- package/dist/commands/repo.d.ts +41 -0
- package/dist/commands/repo.js +331 -0
- package/dist/commands/scan.d.ts +3 -1
- package/dist/commands/scan.js +7 -3
- package/dist/commands/secrets.d.ts +21 -0
- package/dist/commands/secrets.js +114 -14
- package/dist/commands/signals.js +17 -5
- package/dist/commands/skill.js +6 -6
- package/dist/commands/staff/listings.d.ts +41 -0
- package/dist/commands/staff/listings.js +160 -0
- package/dist/commands/staff/reset-test-account.d.ts +3 -2
- package/dist/commands/staff/reset-test-account.js +4 -3
- package/dist/commands/whoami.js +1 -1
- package/dist/hook-engine-path.js +13 -2
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3392 -1243
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/run/inbox.js +2 -1
- package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
- package/dist/lib/agent-kit/wake-secrets.js +32 -0
- package/dist/lib/agent-kit/wake.d.ts +4 -0
- package/dist/lib/agent-kit/wake.js +49 -3
- package/dist/lib/billing/plan-limit-refusals.d.ts +13 -3
- package/dist/lib/billing/plan-limit-refusals.js +26 -3
- package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
- package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
- package/dist/lib/billing/plan-lock.d.ts +22 -6
- package/dist/lib/billing/plan-lock.js +58 -30
- package/dist/lib/billing/usage-cost.d.ts +24 -2
- package/dist/lib/billing/usage-cost.js +103 -3
- package/dist/lib/billing/usage-only.d.ts +37 -1
- package/dist/lib/billing/usage-only.js +89 -4
- package/dist/lib/billing/usage-pause.d.ts +27 -0
- package/dist/lib/billing/usage-pause.js +50 -0
- package/dist/lib/bot/inflight.d.ts +7 -1
- package/dist/lib/bot/inflight.js +10 -2
- package/dist/lib/bot/prompt.js +3 -0
- package/dist/lib/bot/run.js +72 -20
- package/dist/lib/bot/session.d.ts +14 -0
- package/dist/lib/bot/session.js +70 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
- package/dist/lib/capacity/lane-login.d.ts +6 -0
- package/dist/lib/capacity/lane-login.js +5 -0
- package/dist/lib/core-utils/worktree.js +2 -0
- package/dist/lib/daemon/exit-cause.d.ts +43 -0
- package/dist/lib/daemon/exit-cause.js +88 -0
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +6 -1
- package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
- package/dist/lib/daemon/paths.d.ts +2 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/run.js +13 -0
- package/dist/lib/daemon/supervisor.d.ts +54 -1
- package/dist/lib/daemon/supervisor.js +297 -21
- package/dist/lib/daemon/unit-holds.d.ts +22 -0
- package/dist/lib/daemon/unit-holds.js +86 -0
- package/dist/lib/db/control-plane.d.ts +5 -0
- package/dist/lib/db/control-plane.js +10 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/hook-engine/resolve.d.ts +14 -0
- package/dist/lib/hook-engine/resolve.js +55 -1
- package/dist/lib/index-render/company-knowledge.js +1 -3
- package/dist/lib/index-render/projects.js +1 -3
- package/dist/lib/lanes/account-company.d.ts +28 -0
- package/dist/lib/lanes/account-company.js +53 -0
- package/dist/lib/lanes/accounts.d.ts +22 -3
- package/dist/lib/lanes/accounts.js +38 -2
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +6 -1
- package/dist/lib/lanes/claude-credential.d.ts +78 -0
- package/dist/lib/lanes/claude-credential.js +163 -0
- package/dist/lib/lanes/compaction.d.ts +2 -0
- package/dist/lib/lanes/compaction.js +9 -0
- package/dist/lib/lanes/config.js +6 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
- package/dist/lib/lanes/hooks-install.js +2 -1
- package/dist/lib/lanes/host-capacity.d.ts +2 -0
- package/dist/lib/lanes/host-capacity.js +1 -0
- package/dist/lib/lanes/keep-alive.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.js +3 -1
- package/dist/lib/lanes/links.js +10 -4
- package/dist/lib/lanes/loop.js +16 -1
- package/dist/lib/lanes/node-binary.d.ts +17 -0
- package/dist/lib/lanes/node-binary.js +30 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.js +2 -0
- package/dist/lib/lanes/spawn.d.ts +3 -0
- package/dist/lib/lanes/spawn.js +119 -9
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +107 -18
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/watch.js +28 -5
- package/dist/lib/lanes/watcher-main.js +4 -3
- package/dist/lib/lanes/watcher.d.ts +4 -0
- package/dist/lib/lanes/watcher.js +110 -21
- package/dist/lib/mesh/api.d.ts +32 -0
- package/dist/lib/mesh/api.js +75 -0
- package/dist/lib/plan-limit-nag.d.ts +0 -16
- package/dist/lib/plan-limit-nag.js +17 -18
- package/dist/lib/secret-scan/detect.js +9 -0
- package/dist/lib/secret-scan/repo-config.d.ts +41 -0
- package/dist/lib/secret-scan/repo-config.js +237 -0
- package/dist/lib/secret-scan/rules.d.ts +7 -1
- package/dist/lib/secret-scan/rules.js +3 -1
- package/dist/lib/secret-scan/staged.d.ts +5 -0
- package/dist/lib/secret-scan/staged.js +19 -2
- package/dist/lib/secret-scan/types.d.ts +2 -0
- package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
- package/dist/lib/sync/server-owned-local-only.js +75 -0
- package/dist/main.d.ts +0 -2
- package/dist/main.js +19 -57
- package/dist/utils/package-use-lease.js +27 -9
- package/dist/utils/plan-gate-error.d.ts +30 -14
- package/dist/utils/plan-gate-error.js +60 -46
- package/dist/utils/team-upgrade.d.ts +19 -6
- package/dist/utils/team-upgrade.js +37 -39
- package/dist/utils/version-gate.d.ts +7 -0
- package/dist/utils/version-gate.js +66 -0
- package/package.json +8 -8
package/dist/commands/dm.js
CHANGED
|
@@ -7,6 +7,8 @@ import { peekIdToken } from "../utils/id-token.js";
|
|
|
7
7
|
import { redactAgentInboxItem as redactInboxCredentials } from "../lib/agent-kit/inbox-redact.js";
|
|
8
8
|
import { EMAIL_PATTERN, RECIPIENT_UID_PATTERN } from "../lib/notify-recipient.js";
|
|
9
9
|
import { dmForwardCliEnabled } from "./dm-forward-flag.js";
|
|
10
|
+
import { attachmentScopeForChannel, formatUploadedAttachments, personalScopeForCaller, uploadDmAttachments, validateAttachmentPaths, } from "./dm-attach.js";
|
|
11
|
+
import { ChannelNotFoundError, suggestChannels, withChannelSuggestions, } from "./channel-suggest.js";
|
|
10
12
|
/**
|
|
11
13
|
* Classify a recipient arg as an email or a person/agent uid. Mirrors the
|
|
12
14
|
* email/uid heuristic used by `hq members`. Returns null for neither.
|
|
@@ -108,7 +110,57 @@ export function matchChannelsByName(channels, name) {
|
|
|
108
110
|
return false;
|
|
109
111
|
});
|
|
110
112
|
}
|
|
111
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Find the single contact whose display name (case-insensitive, or by slug)
|
|
115
|
+
* equals a bare recipient name. Email local parts are not matched: they are not
|
|
116
|
+
* the name the user sees, and matching them would divert channel posts. A bare word like `setup` used
|
|
117
|
+
* to go straight to a channel lookup, so `hq dm send setup` landed in a channel
|
|
118
|
+
* whose slug was `setup` even when a person by that name existed (feedback
|
|
119
|
+
* eba06f59). Returns null when nothing or more than one contact matches. Pure.
|
|
120
|
+
*/
|
|
121
|
+
export function matchContactByName(contacts, name) {
|
|
122
|
+
const target = name.trim().toLowerCase();
|
|
123
|
+
const targetSlug = channelSlug(name);
|
|
124
|
+
if (!target)
|
|
125
|
+
return null;
|
|
126
|
+
const hits = contacts.filter((c) => {
|
|
127
|
+
if (!c.personUid)
|
|
128
|
+
return false;
|
|
129
|
+
const dn = c.displayName?.trim().toLowerCase();
|
|
130
|
+
return Boolean(dn && (dn === target || (targetSlug && channelSlug(dn) === targetSlug)));
|
|
131
|
+
});
|
|
132
|
+
const unique = [...new Map(hits.map((c) => [c.personUid, c])).values()];
|
|
133
|
+
return unique.length === 1 ? unique[0] : null;
|
|
134
|
+
}
|
|
135
|
+
async function fetchContacts(token) {
|
|
136
|
+
try {
|
|
137
|
+
const res = await vaultApiFetch({ token, path: "/v1/notify/contacts" });
|
|
138
|
+
if (!res.ok)
|
|
139
|
+
return [];
|
|
140
|
+
const data = (await res.json());
|
|
141
|
+
return Array.isArray(data.contacts) ? data.contacts : [];
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The confirmation line for a channel send. When the typed target is not the
|
|
149
|
+
* channel's own display name (for example `hq dm setup` matching a channel
|
|
150
|
+
* whose slug is `setup` but whose display name is `welcome`), say what it
|
|
151
|
+
* resolved to: the typed text, the channel's scope, and its id. Pure.
|
|
152
|
+
*/
|
|
153
|
+
export function formatChannelPostedLine(channel, typed) {
|
|
154
|
+
const bareTyped = typed.trim().replace(/^#/, "");
|
|
155
|
+
const base = `Message posted to #${channel.name ?? bareTyped}`;
|
|
156
|
+
const typedSlug = channelSlug(bareTyped);
|
|
157
|
+
const nameSlug = channel.name ? channelSlug(channel.name) : typedSlug;
|
|
158
|
+
if (typedSlug === nameSlug)
|
|
159
|
+
return `${base}.`;
|
|
160
|
+
const scope = channel.scope ? `${channel.scope} channel` : "channel";
|
|
161
|
+
return `${base} (resolved '${bareTyped}' to ${scope} ${channel.channelId}).`;
|
|
162
|
+
}
|
|
163
|
+
// ─── Channel @mentions───────────────────────────────────────────────────────
|
|
112
164
|
//
|
|
113
165
|
// HQ Rooms mentions are STRUCTURED ONLY: the server never parses `@name` out of
|
|
114
166
|
// the message body (see hq-pro src/vault-service/lib/room-agent-delivery.ts and
|
|
@@ -298,13 +350,18 @@ export function buildDmBody(args) {
|
|
|
298
350
|
if (!rcpt) {
|
|
299
351
|
throw new Error(`Invalid recipient '${args.recipient}': must be an email address, a personUid (prs_…), or an agentUid (agt_…).`);
|
|
300
352
|
}
|
|
353
|
+
const attachments = args.attachments ?? [];
|
|
301
354
|
const body = (args.message ?? "").trim();
|
|
302
|
-
|
|
355
|
+
// The server accepts an empty body when the message carries attachments.
|
|
356
|
+
if (!body && attachments.length === 0) {
|
|
303
357
|
throw new Error("A message body is required: hq dm <recipient> <message>");
|
|
304
358
|
}
|
|
305
359
|
if (args.at && args.inDelay) {
|
|
306
360
|
throw new Error("Use only one of --at or --in, not both.");
|
|
307
361
|
}
|
|
362
|
+
if ((args.at || args.inDelay) && attachments.length > 0) {
|
|
363
|
+
throw new Error("--attach cannot be combined with a scheduled send (--at/--in); send the file now or schedule the text alone.");
|
|
364
|
+
}
|
|
308
365
|
let deliverAt;
|
|
309
366
|
if (args.at) {
|
|
310
367
|
const when = new Date(args.at);
|
|
@@ -336,8 +393,36 @@ export function buildDmBody(args) {
|
|
|
336
393
|
...(details ? { details } : {}),
|
|
337
394
|
...(deliverAt ? { deliverAt } : {}),
|
|
338
395
|
...(audience ? { audience } : {}),
|
|
396
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
339
397
|
};
|
|
340
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* The other participant's uid for a 1:1 DM attachment key. A `prs_`/`agt_`
|
|
401
|
+
* recipient is already a uid; an email is looked up in the caller's contacts
|
|
402
|
+
* (GET /v1/notify/contacts), the same route `dm forward --from <email>` uses.
|
|
403
|
+
* There is no separate "person by email" route on the vault API, and a DM to
|
|
404
|
+
* someone outside your contacts is a connection request anyway, which the
|
|
405
|
+
* server holds without delivering files.
|
|
406
|
+
*/
|
|
407
|
+
export async function resolveDmPeerUid(token, recipient) {
|
|
408
|
+
const rcpt = detectRecipient(recipient);
|
|
409
|
+
if (!rcpt)
|
|
410
|
+
throw new Error(`Invalid recipient '${recipient}'.`);
|
|
411
|
+
if (rcpt.toPersonUid)
|
|
412
|
+
return rcpt.toPersonUid;
|
|
413
|
+
const email = rcpt.toEmail.toLowerCase();
|
|
414
|
+
const res = await vaultApiFetch({ token, path: "/v1/notify/contacts" });
|
|
415
|
+
if (!res.ok) {
|
|
416
|
+
const err = (await res.json().catch(() => ({})));
|
|
417
|
+
throw new Error(friendlyDmError(res.status, err.code, err.error ?? err.message ?? res.statusText));
|
|
418
|
+
}
|
|
419
|
+
const data = (await res.json());
|
|
420
|
+
const peer = (data.contacts ?? []).find((c) => (c.email ?? "").toLowerCase() === email);
|
|
421
|
+
if (!peer?.personUid) {
|
|
422
|
+
throw new Error(`--attach needs ${recipient}'s person uid to place the file, and they are not in your contacts yet. Send the DM without --attach first (it becomes a connection request), or use their prs_ uid.`);
|
|
423
|
+
}
|
|
424
|
+
return peer.personUid;
|
|
425
|
+
}
|
|
341
426
|
function friendlyDmError(status, code, fallback) {
|
|
342
427
|
if (status === 401)
|
|
343
428
|
return "Not authenticated — run `hq login` and try again.";
|
|
@@ -596,7 +681,10 @@ async function fetchChannelMessages(token, channelId, query = {}) {
|
|
|
596
681
|
async function runChannelSend(channelName, message, opts) {
|
|
597
682
|
try {
|
|
598
683
|
let body = (message ?? "").trim();
|
|
599
|
-
|
|
684
|
+
// Validate attachments before anything touches the network; an empty body
|
|
685
|
+
// is allowed when files are attached.
|
|
686
|
+
const localFiles = validateAttachmentPaths(opts.attach ?? []);
|
|
687
|
+
if (!body && localFiles.length === 0) {
|
|
600
688
|
console.error(chalk.red(`A message body is required: hq dm ${channelName} "<message>" (or hq dm --channel ${channelName} "<message>").`));
|
|
601
689
|
process.exit(1);
|
|
602
690
|
}
|
|
@@ -619,7 +707,7 @@ async function runChannelSend(channelName, message, opts) {
|
|
|
619
707
|
const channels = await fetchChannels(token);
|
|
620
708
|
const matches = matchChannelsByName(channels, channelName);
|
|
621
709
|
if (matches.length === 0) {
|
|
622
|
-
console.error(chalk.red(`No channel named '${channelName}' — run \`hq channels\` to see your channels
|
|
710
|
+
console.error(chalk.red(withChannelSuggestions(`No channel named '${channelName}' — run \`hq channels\` to see your channels.`, suggestChannels(channels, channelName))));
|
|
623
711
|
process.exit(1);
|
|
624
712
|
}
|
|
625
713
|
if (matches.length > 1) {
|
|
@@ -677,6 +765,24 @@ async function runChannelSend(channelName, message, opts) {
|
|
|
677
765
|
payload.rootEventId = rootEventId;
|
|
678
766
|
if (audience)
|
|
679
767
|
payload.audience = audience;
|
|
768
|
+
// Upload after the channel and thread resolved (so a bad name never
|
|
769
|
+
// uploads anything) and before the post (so the post never names bytes
|
|
770
|
+
// that are not there). Company channels store in the company vault;
|
|
771
|
+
// personal channels in the caller's personal vault.
|
|
772
|
+
let uploaded = [];
|
|
773
|
+
if (localFiles.length > 0) {
|
|
774
|
+
const callerUid = callerFromToken(token).personUid;
|
|
775
|
+
const scopeUid = channel.companyUid?.trim()
|
|
776
|
+
? attachmentScopeForChannel(channel, callerUid ?? "")
|
|
777
|
+
: personalScopeForCaller(callerUid);
|
|
778
|
+
uploaded = await uploadDmAttachments({
|
|
779
|
+
token,
|
|
780
|
+
scopeUid,
|
|
781
|
+
target: { scope: "chan", channelId: channel.channelId },
|
|
782
|
+
files: localFiles,
|
|
783
|
+
});
|
|
784
|
+
payload.attachments = uploaded;
|
|
785
|
+
}
|
|
680
786
|
const sendRes = await vaultApiFetch({
|
|
681
787
|
token,
|
|
682
788
|
path: `/v1/notify/channels/${encodeURIComponent(channel.channelId)}/messages`,
|
|
@@ -692,7 +798,12 @@ async function runChannelSend(channelName, message, opts) {
|
|
|
692
798
|
? ` Mentioned ${mentions.map((m) => m.displayName).join(", ")}.`
|
|
693
799
|
: "";
|
|
694
800
|
const threadSuffix = rootEventId ? ` Reply in thread ${rootEventId}.` : "";
|
|
695
|
-
|
|
801
|
+
const attachSuffix = uploaded.length > 0
|
|
802
|
+
? ` Attached ${uploaded.length} file${uploaded.length === 1 ? "" : "s"}.`
|
|
803
|
+
: "";
|
|
804
|
+
console.log(chalk.green(`${formatChannelPostedLine(channel, channelName)}${mentionSuffix}${threadSuffix}${attachSuffix}`));
|
|
805
|
+
if (uploaded.length > 0)
|
|
806
|
+
console.log(chalk.dim(formatUploadedAttachments(uploaded)));
|
|
696
807
|
}
|
|
697
808
|
catch (err) {
|
|
698
809
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
@@ -719,14 +830,30 @@ async function runDmSend(recipient, message, opts) {
|
|
|
719
830
|
process.exit(1);
|
|
720
831
|
return;
|
|
721
832
|
}
|
|
833
|
+
if (opts.attach && opts.attach.length > 0) {
|
|
834
|
+
// A group is created on the fly, so there is no conversation to place
|
|
835
|
+
// the file in before the send. Not supported in this release.
|
|
836
|
+
console.error(chalk.red("--attach is not supported for group DMs yet. Post the file in a named channel (hq dm <channel> --attach <path>) or DM each person."));
|
|
837
|
+
process.exit(1);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
722
840
|
await runGroupSend(group, message);
|
|
723
841
|
return;
|
|
724
842
|
}
|
|
725
843
|
// A bare name or #hash addresses a named DM channel.
|
|
726
844
|
const channelName = parseChannelName(recipient);
|
|
727
845
|
if (channelName !== null) {
|
|
728
|
-
|
|
729
|
-
|
|
846
|
+
// A bare word (no '#') may name a person as well as a channel. Prefer the
|
|
847
|
+
// exact person and say so; '#name' or --channel always means the channel.
|
|
848
|
+
const person = recipient.trim().startsWith("#")
|
|
849
|
+
? null
|
|
850
|
+
: matchContactByName(await fetchContacts(await ensureCognitoToken()), channelName);
|
|
851
|
+
if (!person?.personUid) {
|
|
852
|
+
await runChannelSend(channelName, message, opts);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
console.log(chalk.dim(`Resolved '${channelName}' to person ${person.displayName || person.email || person.personUid} (${person.personUid}). Use '#${channelName}' or --channel ${channelName} to post to a channel instead.`));
|
|
856
|
+
recipient = person.personUid;
|
|
730
857
|
}
|
|
731
858
|
if (opts.thread !== undefined) {
|
|
732
859
|
console.error(chalk.red("--thread is only supported for channel messages (hq dm <channel> --thread <eventId|last> \"…\")."));
|
|
@@ -741,7 +868,10 @@ async function runDmSend(recipient, message, opts) {
|
|
|
741
868
|
let details = opts.details;
|
|
742
869
|
if (opts.detailsFile)
|
|
743
870
|
details = readFileSync(opts.detailsFile, "utf8");
|
|
744
|
-
|
|
871
|
+
// Validate the local files and every option combination before any
|
|
872
|
+
// network call, so a refused send uploads nothing.
|
|
873
|
+
const localFiles = validateAttachmentPaths(opts.attach ?? []);
|
|
874
|
+
const dryBody = buildDmBody({
|
|
745
875
|
recipient,
|
|
746
876
|
message: message ?? "",
|
|
747
877
|
prompt,
|
|
@@ -750,8 +880,31 @@ async function runDmSend(recipient, message, opts) {
|
|
|
750
880
|
inDelay: opts.in,
|
|
751
881
|
now: Date.now(),
|
|
752
882
|
audience: opts.audience,
|
|
883
|
+
// Placeholder entries only prove the option set is valid; the real
|
|
884
|
+
// entries come back from the upload below.
|
|
885
|
+
attachments: localFiles.map((f) => ({ vaultPath: "", companyUid: "", name: f.name, sizeBytes: f.sizeBytes, contentType: f.contentType })),
|
|
753
886
|
});
|
|
754
887
|
const token = await ensureCognitoToken();
|
|
888
|
+
let uploaded = [];
|
|
889
|
+
if (localFiles.length > 0) {
|
|
890
|
+
// A 1:1 attachment is stored in the SENDER's personal vault under the
|
|
891
|
+
// sorted participant pair; the server lets either member read it.
|
|
892
|
+
const callerUid = callerFromToken(token).personUid;
|
|
893
|
+
const scopeUid = personalScopeForCaller(callerUid);
|
|
894
|
+
const peerUid = await resolveDmPeerUid(token, recipient);
|
|
895
|
+
uploaded = await uploadDmAttachments({
|
|
896
|
+
token,
|
|
897
|
+
scopeUid,
|
|
898
|
+
target: { scope: "dm", uids: [scopeUid, peerUid] },
|
|
899
|
+
files: localFiles,
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
const reqBody = {
|
|
903
|
+
...dryBody,
|
|
904
|
+
...(uploaded.length > 0 ? { attachments: uploaded } : {}),
|
|
905
|
+
};
|
|
906
|
+
if (uploaded.length === 0)
|
|
907
|
+
delete reqBody.attachments;
|
|
755
908
|
const res = await vaultApiFetch({
|
|
756
909
|
token,
|
|
757
910
|
path: "/v1/notify/dm",
|
|
@@ -777,7 +930,12 @@ async function runDmSend(recipient, message, opts) {
|
|
|
777
930
|
console.log(chalk.dim("It delivers within ~60s of that time, even if you're offline."));
|
|
778
931
|
}
|
|
779
932
|
else {
|
|
780
|
-
|
|
933
|
+
const attachSuffix = uploaded.length > 0
|
|
934
|
+
? ` Attached ${uploaded.length} file${uploaded.length === 1 ? "" : "s"}.`
|
|
935
|
+
: "";
|
|
936
|
+
console.log(chalk.green(`DM sent to ${recipient} (eventId ${data.eventId}).${attachSuffix}`));
|
|
937
|
+
if (uploaded.length > 0)
|
|
938
|
+
console.log(chalk.dim(formatUploadedAttachments(uploaded)));
|
|
781
939
|
}
|
|
782
940
|
}
|
|
783
941
|
catch (err) {
|
|
@@ -1460,7 +1618,7 @@ async function resolveChannel(token, target) {
|
|
|
1460
1618
|
const byId = channels.find((c) => c.channelId === raw);
|
|
1461
1619
|
if (byId)
|
|
1462
1620
|
return byId;
|
|
1463
|
-
throw new
|
|
1621
|
+
throw new ChannelNotFoundError(`No channel named or with id '${raw}' — run \`hq channels\` to see yours.`, suggestChannels(channels, raw));
|
|
1464
1622
|
}
|
|
1465
1623
|
async function runDmChannel(target, opts) {
|
|
1466
1624
|
try {
|
|
@@ -1529,6 +1687,9 @@ async function runDmChannel(target, opts) {
|
|
|
1529
1687
|
}
|
|
1530
1688
|
}
|
|
1531
1689
|
catch (err) {
|
|
1690
|
+
if (opts.json && err instanceof ChannelNotFoundError) {
|
|
1691
|
+
console.log(JSON.stringify({ ok: false, error: err.baseMessage, suggestions: err.suggestions }, null, 2));
|
|
1692
|
+
}
|
|
1532
1693
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
1533
1694
|
process.exit(1);
|
|
1534
1695
|
}
|
|
@@ -1550,6 +1711,7 @@ export function registerDmCommand(program) {
|
|
|
1550
1711
|
.option("--at <iso>", "Schedule delivery at an ISO8601 time (store-and-forward; delivered within ~60s of the time; 1:1 DMs only)")
|
|
1551
1712
|
.option("--in <duration>", "Schedule delivery after a relative delay: 30s, 10m, 2h, 1d (1:1 DMs only)")
|
|
1552
1713
|
.option("--audience <human|agent|both>", "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both")
|
|
1714
|
+
.option("--attach <path>", "Upload a local file and attach it to the message (repeatable, up to 5 files, 25 MB each). 1:1 DMs and named channels; the file lands in the conversation's vault scope and the recipient can open it. Not available for group DMs or scheduled sends.", (value, previous = []) => [...previous, value])
|
|
1553
1715
|
.action(async (recipient, message, opts) => {
|
|
1554
1716
|
await runDmSend(recipient, message, opts);
|
|
1555
1717
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
export declare function normalizeFeedbackId(raw: string): string;
|
|
3
|
+
export interface FeedbackAttachmentEntry {
|
|
4
|
+
/** Where the attachment came from: a CLI screenshot or an email-ticket message. */
|
|
5
|
+
source: "screenshot" | "email";
|
|
6
|
+
name: string;
|
|
7
|
+
key?: string;
|
|
8
|
+
contentType?: string;
|
|
9
|
+
bytes?: number;
|
|
10
|
+
downloadUrl?: string;
|
|
11
|
+
downloadPath?: string;
|
|
12
|
+
}
|
|
13
|
+
type Row = Record<string, unknown>;
|
|
14
|
+
/** Collect every attachment the row exposes, in a stable order. */
|
|
15
|
+
export declare function collectFeedbackAttachments(item: Row): FeedbackAttachmentEntry[];
|
|
16
|
+
/** The log bundle id (`logs-<uuid>`) from the row's logBundle key, if any. */
|
|
17
|
+
export declare function logBundleIdFor(item: Row): string | undefined;
|
|
18
|
+
/** Strip every download link so `--json` output never carries a presigned URL. */
|
|
19
|
+
export declare function redactDownloadLinks(item: Row): Row;
|
|
20
|
+
/** A safe local file name: no directory parts, no leading dots, unique in `used`. */
|
|
21
|
+
export declare function safeFileName(name: string, index: number, used: Set<string>): string;
|
|
22
|
+
export declare function fetchFeedbackRow(id: string, token: string): Promise<Row>;
|
|
23
|
+
export interface DownloadResult {
|
|
24
|
+
file?: string;
|
|
25
|
+
name: string;
|
|
26
|
+
bytes?: number;
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function downloadFeedbackAttachments(opts: {
|
|
30
|
+
id: string;
|
|
31
|
+
item: Row;
|
|
32
|
+
dir: string;
|
|
33
|
+
token: string;
|
|
34
|
+
includeLogs: boolean;
|
|
35
|
+
fetchImpl?: typeof fetch;
|
|
36
|
+
}): Promise<DownloadResult[]>;
|
|
37
|
+
export declare function formatFeedbackRow(item: Row): string;
|
|
38
|
+
export declare function registerFeedbackShowCommand(feedbackCmd: Command): void;
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=feedback-show.d.ts.map
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
5
|
+
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
6
|
+
/**
|
|
7
|
+
* `hq feedback show <id>` — operator-only fetch of one feedback row and its
|
|
8
|
+
* attachments (CLI screenshots, email-ticket attachments, the log bundle).
|
|
9
|
+
*
|
|
10
|
+
* Access is enforced by hq-pro: `GET /v1/feedback/{id}` refuses non-operators
|
|
11
|
+
* with 403, and only staff callers receive download links. This command never
|
|
12
|
+
* prints a download URL or a token; with `--download` it writes the files to a
|
|
13
|
+
* local directory instead.
|
|
14
|
+
*/
|
|
15
|
+
const FEEDBACK_ID_RE = /^(?:feedback_)?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
|
|
16
|
+
const LOG_BUNDLE_ID_RE = /^logs-[0-9a-f-]{36}$/i;
|
|
17
|
+
export function normalizeFeedbackId(raw) {
|
|
18
|
+
const match = FEEDBACK_ID_RE.exec(raw.trim());
|
|
19
|
+
if (!match) {
|
|
20
|
+
throw new Error(`Invalid feedback id '${raw}'. Use feedback_<uuid> or the bare uuid.`);
|
|
21
|
+
}
|
|
22
|
+
return `feedback_${match[1].toLowerCase()}`;
|
|
23
|
+
}
|
|
24
|
+
function str(value) {
|
|
25
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
function baseName(key) {
|
|
28
|
+
const parts = key.split("/").filter(Boolean);
|
|
29
|
+
return parts[parts.length - 1] ?? key;
|
|
30
|
+
}
|
|
31
|
+
/** Collect every attachment the row exposes, in a stable order. */
|
|
32
|
+
export function collectFeedbackAttachments(item) {
|
|
33
|
+
const out = [];
|
|
34
|
+
const linked = Array.isArray(item.screenshotDownloads) ? item.screenshotDownloads : [];
|
|
35
|
+
const byKey = new Map();
|
|
36
|
+
for (const entry of linked) {
|
|
37
|
+
if (entry && typeof entry === "object" && typeof entry.key === "string") {
|
|
38
|
+
byKey.set(entry.key, entry);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const screenshots = Array.isArray(item.screenshots) ? item.screenshots : [];
|
|
42
|
+
for (const key of screenshots) {
|
|
43
|
+
if (typeof key !== "string")
|
|
44
|
+
continue;
|
|
45
|
+
const link = byKey.get(key);
|
|
46
|
+
out.push({
|
|
47
|
+
source: "screenshot",
|
|
48
|
+
name: baseName(key),
|
|
49
|
+
key,
|
|
50
|
+
...(link && str(link.downloadUrl) ? { downloadUrl: link.downloadUrl } : {}),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const messages = Array.isArray(item.messages) ? item.messages : [];
|
|
54
|
+
for (const message of messages) {
|
|
55
|
+
if (!message || typeof message !== "object")
|
|
56
|
+
continue;
|
|
57
|
+
const attachments = message.attachments;
|
|
58
|
+
if (!Array.isArray(attachments))
|
|
59
|
+
continue;
|
|
60
|
+
for (const raw of attachments) {
|
|
61
|
+
if (!raw || typeof raw !== "object")
|
|
62
|
+
continue;
|
|
63
|
+
const a = raw;
|
|
64
|
+
const key = str(a.key);
|
|
65
|
+
out.push({
|
|
66
|
+
source: "email",
|
|
67
|
+
name: str(a.filename) ?? (key ? baseName(key) : "attachment"),
|
|
68
|
+
...(key ? { key } : {}),
|
|
69
|
+
...(str(a.contentType) ? { contentType: a.contentType } : {}),
|
|
70
|
+
...(typeof a.bytes === "number" ? { bytes: a.bytes } : {}),
|
|
71
|
+
...(str(a.downloadUrl) ? { downloadUrl: a.downloadUrl } : {}),
|
|
72
|
+
...(str(a.downloadPath) ? { downloadPath: a.downloadPath } : {}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/** The log bundle id (`logs-<uuid>`) from the row's logBundle key, if any. */
|
|
79
|
+
export function logBundleIdFor(item) {
|
|
80
|
+
const bundle = item.logBundle;
|
|
81
|
+
if (!bundle || typeof bundle !== "object")
|
|
82
|
+
return undefined;
|
|
83
|
+
const key = str(bundle.key);
|
|
84
|
+
if (!key)
|
|
85
|
+
return undefined;
|
|
86
|
+
const id = baseName(key).split(".")[0] ?? "";
|
|
87
|
+
return LOG_BUNDLE_ID_RE.test(id) ? id : undefined;
|
|
88
|
+
}
|
|
89
|
+
/** Strip every download link so `--json` output never carries a presigned URL. */
|
|
90
|
+
export function redactDownloadLinks(item) {
|
|
91
|
+
return JSON.parse(JSON.stringify(item, (k, v) => (k === "downloadUrl" || k === "url" ? undefined : v)));
|
|
92
|
+
}
|
|
93
|
+
/** A safe local file name: no directory parts, no leading dots, unique in `used`. */
|
|
94
|
+
export function safeFileName(name, index, used) {
|
|
95
|
+
let base = path.basename(name).replace(/[^A-Za-z0-9._-]/g, "_").replace(/^\.+/, "");
|
|
96
|
+
if (!base)
|
|
97
|
+
base = `attachment-${index}`;
|
|
98
|
+
let candidate = `${index}-${base}`;
|
|
99
|
+
let n = 1;
|
|
100
|
+
while (used.has(candidate))
|
|
101
|
+
candidate = `${index}-${n++}-${base}`;
|
|
102
|
+
used.add(candidate);
|
|
103
|
+
return candidate;
|
|
104
|
+
}
|
|
105
|
+
async function errorText(res) {
|
|
106
|
+
const data = (await res.json().catch(() => ({})));
|
|
107
|
+
return typeof data?.error === "string" ? data.error : res.statusText;
|
|
108
|
+
}
|
|
109
|
+
export async function fetchFeedbackRow(id, token) {
|
|
110
|
+
const res = await vaultApiFetch({ path: `/v1/feedback/${id}`, token });
|
|
111
|
+
if (res.status === 401 || res.status === 403) {
|
|
112
|
+
throw new Error("hq feedback show is for HQ operators only. Your login is not allowed to read feedback rows.");
|
|
113
|
+
}
|
|
114
|
+
if (res.status === 404)
|
|
115
|
+
throw new Error(`Feedback '${id}' not found.`);
|
|
116
|
+
if (!res.ok)
|
|
117
|
+
throw new Error(`Failed to fetch feedback: ${await errorText(res)}`);
|
|
118
|
+
const data = (await res.json());
|
|
119
|
+
if (!data.item || typeof data.item !== "object")
|
|
120
|
+
throw new Error("Malformed feedback response.");
|
|
121
|
+
return data.item;
|
|
122
|
+
}
|
|
123
|
+
async function downloadUrlTo(url, file, fetchImpl) {
|
|
124
|
+
const res = await fetchImpl(url);
|
|
125
|
+
if (!res.ok)
|
|
126
|
+
throw new Error(`download failed (HTTP ${res.status})`);
|
|
127
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
128
|
+
fs.writeFileSync(file, bytes);
|
|
129
|
+
return bytes.byteLength;
|
|
130
|
+
}
|
|
131
|
+
export async function downloadFeedbackAttachments(opts) {
|
|
132
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
133
|
+
fs.mkdirSync(opts.dir, { recursive: true });
|
|
134
|
+
const used = new Set();
|
|
135
|
+
const results = [];
|
|
136
|
+
const attachments = collectFeedbackAttachments(opts.item);
|
|
137
|
+
for (const [index, a] of attachments.entries()) {
|
|
138
|
+
const file = path.join(opts.dir, safeFileName(a.name, index, used));
|
|
139
|
+
try {
|
|
140
|
+
if (a.downloadUrl) {
|
|
141
|
+
results.push({ file, name: a.name, bytes: await downloadUrlTo(a.downloadUrl, file, fetchImpl) });
|
|
142
|
+
}
|
|
143
|
+
else if (a.downloadPath && a.downloadPath.startsWith(`/v1/feedback/${opts.id}?`)) {
|
|
144
|
+
const [p, q] = a.downloadPath.split("?");
|
|
145
|
+
const query = Object.fromEntries(new URLSearchParams(q ?? ""));
|
|
146
|
+
const res = await vaultApiFetch({ path: p, query, token: opts.token });
|
|
147
|
+
if (!res.ok)
|
|
148
|
+
throw new Error(await errorText(res));
|
|
149
|
+
const data = (await res.json());
|
|
150
|
+
if (typeof data.contentBase64 !== "string")
|
|
151
|
+
throw new Error("no content returned");
|
|
152
|
+
const bytes = Buffer.from(data.contentBase64, "base64");
|
|
153
|
+
fs.writeFileSync(file, bytes);
|
|
154
|
+
results.push({ file, name: a.name, bytes: bytes.byteLength });
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
results.push({ name: a.name, error: "no download link (operator access or the attachment-download flag is off)" });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
results.push({ name: a.name, error: err instanceof Error ? err.message : String(err) });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const bundleId = opts.includeLogs ? logBundleIdFor(opts.item) : undefined;
|
|
165
|
+
if (bundleId) {
|
|
166
|
+
const name = baseName(opts.item.logBundle.key);
|
|
167
|
+
try {
|
|
168
|
+
const res = await vaultApiFetch({ path: `/v1/feedback/${opts.id}/logs/${bundleId}/view`, token: opts.token });
|
|
169
|
+
if (!res.ok)
|
|
170
|
+
throw new Error(await errorText(res));
|
|
171
|
+
const data = (await res.json());
|
|
172
|
+
if (typeof data.url !== "string")
|
|
173
|
+
throw new Error("no link returned");
|
|
174
|
+
const file = path.join(opts.dir, safeFileName(name, attachments.length, used));
|
|
175
|
+
results.push({ file, name, bytes: await downloadUrlTo(data.url, file, fetchImpl) });
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
results.push({ name, error: err instanceof Error ? err.message : String(err) });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return results;
|
|
182
|
+
}
|
|
183
|
+
export function formatFeedbackRow(item) {
|
|
184
|
+
const lines = [];
|
|
185
|
+
const field = (label, value) => {
|
|
186
|
+
if (value !== undefined && value !== null && value !== "")
|
|
187
|
+
lines.push(`${label}: ${String(value)}`);
|
|
188
|
+
};
|
|
189
|
+
field("ID", item.id);
|
|
190
|
+
field("Title", item.title);
|
|
191
|
+
field("Type", item.type);
|
|
192
|
+
field("Status", item.status);
|
|
193
|
+
field("Company", item.company ?? item.companySlug);
|
|
194
|
+
field("Reporter", item.email);
|
|
195
|
+
field("Submitted", item.submittedAt);
|
|
196
|
+
field("Linear", item.linearIssueIdentifier);
|
|
197
|
+
const attachments = collectFeedbackAttachments(item);
|
|
198
|
+
lines.push(`Attachments: ${attachments.length}`);
|
|
199
|
+
for (const a of attachments) {
|
|
200
|
+
const meta = [a.source, a.contentType, typeof a.bytes === "number" ? `${a.bytes} bytes` : undefined]
|
|
201
|
+
.filter(Boolean)
|
|
202
|
+
.join(", ");
|
|
203
|
+
const link = a.downloadUrl || a.downloadPath ? "downloadable" : "no link";
|
|
204
|
+
lines.push(` - ${a.name} (${meta}; ${link})`);
|
|
205
|
+
}
|
|
206
|
+
const bundle = item.logBundle;
|
|
207
|
+
if (bundle && typeof bundle === "object") {
|
|
208
|
+
const size = typeof bundle.sizeBytes === "number" ? `, ${bundle.sizeBytes} bytes` : "";
|
|
209
|
+
lines.push(`Log bundle: ${baseName(String(bundle.key ?? ""))}${size} (use --download <dir> --logs)`);
|
|
210
|
+
}
|
|
211
|
+
if (typeof item.body === "string")
|
|
212
|
+
lines.push("", item.body);
|
|
213
|
+
return lines.join("\n");
|
|
214
|
+
}
|
|
215
|
+
export function registerFeedbackShowCommand(feedbackCmd) {
|
|
216
|
+
feedbackCmd
|
|
217
|
+
.command("show <id>")
|
|
218
|
+
.description("Operators only: show one feedback row and fetch its attachments")
|
|
219
|
+
.option("--download <dir>", "Save screenshots and attachments into this directory")
|
|
220
|
+
.option("--logs", "With --download, also save the row's log bundle")
|
|
221
|
+
.option("--json", "Print the row as JSON (download links removed)")
|
|
222
|
+
.action(async (rawId, opts) => {
|
|
223
|
+
try {
|
|
224
|
+
const id = normalizeFeedbackId(rawId);
|
|
225
|
+
const token = await ensureCognitoToken({ interactive: false });
|
|
226
|
+
const item = await fetchFeedbackRow(id, token);
|
|
227
|
+
if (opts.json) {
|
|
228
|
+
console.log(JSON.stringify(redactDownloadLinks(item), null, 2));
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
console.log(formatFeedbackRow(item));
|
|
232
|
+
}
|
|
233
|
+
if (opts.download) {
|
|
234
|
+
const results = await downloadFeedbackAttachments({
|
|
235
|
+
id,
|
|
236
|
+
item,
|
|
237
|
+
dir: path.resolve(opts.download),
|
|
238
|
+
token,
|
|
239
|
+
includeLogs: opts.logs === true,
|
|
240
|
+
});
|
|
241
|
+
let failed = 0;
|
|
242
|
+
for (const r of results) {
|
|
243
|
+
if (r.error) {
|
|
244
|
+
failed++;
|
|
245
|
+
console.error(chalk.yellow(`Not saved: ${r.name}: ${r.error}`));
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
console.error(`Saved ${r.file} (${r.bytes} bytes)`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (failed > 0)
|
|
252
|
+
process.exitCode = 1;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
//# sourceMappingURL=feedback-show.js.map
|
|
@@ -20,6 +20,8 @@ export declare function validateFeedbackContent(title: string, body: string): vo
|
|
|
20
20
|
export declare const REQUEST_MAX_BYTES: number;
|
|
21
21
|
export interface FeedbackResult {
|
|
22
22
|
id: string;
|
|
23
|
+
/** Public tracking page (status.hqforwork.com/t/<token>); absent on older servers. */
|
|
24
|
+
publicUrl?: string;
|
|
23
25
|
}
|
|
24
26
|
export interface FeedbackSubmitOptions {
|
|
25
27
|
type: "bug" | "feature";
|
|
@@ -70,5 +72,24 @@ export declare function attachDebugLogs(requestBody: Record<string, unknown>, di
|
|
|
70
72
|
envEnabled?: typeof feedbackLogsEnabled;
|
|
71
73
|
}): void;
|
|
72
74
|
export declare function submitFeedback(opts: FeedbackSubmitOptions): Promise<FeedbackResult>;
|
|
75
|
+
/** Customer-visible states accepted by `hq feedback update --state`. */
|
|
76
|
+
export declare const PUBLIC_TICKET_STATES: readonly ["triaged", "in-progress", "shipped", "verified", "resolved"];
|
|
77
|
+
export type PublicTicketState = (typeof PUBLIC_TICKET_STATES)[number];
|
|
78
|
+
export declare function assertFeedbackId(id: string): void;
|
|
79
|
+
/** Resolve (and lazily mint) the public tracking link for a ticket. */
|
|
80
|
+
export declare function fetchFeedbackLink(opts: {
|
|
81
|
+
id: string;
|
|
82
|
+
token: string;
|
|
83
|
+
}): Promise<string>;
|
|
84
|
+
/** Post a customer-visible update; the server also mirrors it into #hq-feedback. */
|
|
85
|
+
export declare function postFeedbackUpdate(opts: {
|
|
86
|
+
id: string;
|
|
87
|
+
token: string;
|
|
88
|
+
state: PublicTicketState;
|
|
89
|
+
note: string;
|
|
90
|
+
}): Promise<{
|
|
91
|
+
publicUrl: string | null;
|
|
92
|
+
slack: string | null;
|
|
93
|
+
}>;
|
|
73
94
|
export declare function registerFeedbackCommand(program: Command): void;
|
|
74
95
|
//# sourceMappingURL=feedback.d.ts.map
|