@indigoai-us/hq-cli 5.345.188 → 5.345.190

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 (38) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/dist/command-catalog.generated.d.ts +63 -0
  3. package/dist/command-catalog.generated.js +78 -0
  4. package/dist/commands/bot.js +15 -3
  5. package/dist/commands/cloud.js +16 -2
  6. package/dist/commands/core-company-index-scope-flag.d.ts +2 -1
  7. package/dist/commands/core-company-index-scope-flag.js +2 -1
  8. package/dist/commands/core.js +0 -5
  9. package/dist/commands/dm-attach.d.ts +73 -0
  10. package/dist/commands/dm-attach.js +104 -0
  11. package/dist/commands/dm.d.ts +10 -0
  12. package/dist/commands/dm.js +114 -2
  13. package/dist/commands/files.d.ts +18 -0
  14. package/dist/commands/files.js +73 -0
  15. package/dist/commands/sources.js +15 -0
  16. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +45054 -36140
  17. package/dist/hq-cloud-sync-runner/package.json +1 -1
  18. package/dist/lib/bot/fleet-local.d.ts +43 -1
  19. package/dist/lib/bot/fleet-local.js +204 -41
  20. package/dist/lib/bot/fleet-python.d.ts +6 -0
  21. package/dist/lib/bot/fleet-python.js +24 -5
  22. package/dist/lib/bot/probe.d.ts +7 -1
  23. package/dist/lib/bot/probe.js +11 -3
  24. package/dist/lib/bot/run.d.ts +9 -0
  25. package/dist/lib/bot/run.js +114 -8
  26. package/dist/lib/bot/runtime-failure.d.ts +3 -0
  27. package/dist/lib/bot/runtime-failure.js +20 -2
  28. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  29. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  30. package/dist/lib/index-render/session-scope.d.ts +3 -3
  31. package/dist/lib/index-render/session-scope.js +11 -9
  32. package/dist/lib/index-render/shared.d.ts +6 -4
  33. package/dist/lib/index-render/shared.js +33 -4
  34. package/dist/utils/cognito-session.d.ts +4 -2
  35. package/dist/utils/cognito-session.js +27 -42
  36. package/dist/utils/hq-root-resolver.d.ts +27 -1
  37. package/dist/utils/hq-root-resolver.js +65 -33
  38. package/package.json +14 -14
@@ -1,5 +1,6 @@
1
1
  import chalk from "chalk";
2
- import { readFileSync } from "node:fs";
2
+ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
+ import { join as pathJoin } from "node:path";
3
4
  import { ensureCognitoToken } from "../utils/cognito-session.js";
4
5
  import { resolveCallerPersonUid, resolveDestinationCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
5
6
  import { isMachineIdentity } from "@indigoai-us/hq-cloud";
@@ -10,7 +11,7 @@ import { EMAIL_PATTERN, RECIPIENT_UID_PATTERN, PERSON_AGENT_AUDIENCE_MESSAGE, PE
10
11
  import { callerIsMachinePrincipal } from "../lib/caller-principal.js";
11
12
  export { defaultAudienceFor, recipientKindOf };
12
13
  import { dmForwardCliEnabled } from "./dm-forward-flag.js";
13
- import { attachmentScopeForChannel, formatUploadedAttachments, personalScopeForCaller, uploadDmAttachments, validateAttachmentPaths, } from "./dm-attach.js";
14
+ import { attachmentReadScopes, attachmentScopeForChannel, downloadChatAttachment, findMessageAttachments, parseChatAttachmentKey, sanitizeAttachmentName, formatUploadedAttachments, personalScopeForCaller, uploadDmAttachments, validateAttachmentPaths, } from "./dm-attach.js";
14
15
  import { ChannelNotFoundError, suggestChannels, withChannelSuggestions, } from "./channel-suggest.js";
15
16
  /**
16
17
  * Classify a recipient arg as an email or a person/agent uid. Mirrors the
@@ -1679,6 +1680,107 @@ async function runDmInbox(opts) {
1679
1680
  process.exit(1);
1680
1681
  }
1681
1682
  }
1683
+ /**
1684
+ * Where to write each downloaded attachment. One file and an `--out` that is
1685
+ * not an existing directory: write exactly there. Otherwise `--out` (default
1686
+ * cwd) is a directory and each file keeps its sanitized name. Pure apart from
1687
+ * the injected `isDir`.
1688
+ */
1689
+ export function attachmentOutputPaths(refs, out, isDir) {
1690
+ const nameOf = (r) => sanitizeAttachmentName(r.name?.trim() || r.vaultPath.split("/").pop() || "file");
1691
+ if (refs.length === 1 && out && !isDir(out))
1692
+ return [out];
1693
+ const dir = out ?? ".";
1694
+ // Two files with the same sanitized name get distinct targets: name-1.ext, name-2.ext.
1695
+ const used = new Set();
1696
+ return refs.map((r) => {
1697
+ const base = nameOf(r);
1698
+ let name = base;
1699
+ for (let n = 1; used.has(name); n++) {
1700
+ const dot = base.lastIndexOf(".");
1701
+ name = dot > 0 ? `${base.slice(0, dot)}-${n}${base.slice(dot)}` : `${base}-${n}`;
1702
+ }
1703
+ used.add(name);
1704
+ return pathJoin(dir, name);
1705
+ });
1706
+ }
1707
+ async function runDmAttachment(ref, opts) {
1708
+ try {
1709
+ const token = await ensureCognitoToken();
1710
+ const callerUid = agentCallerUid(token) ?? (await callerUidForAttach(token));
1711
+ let refs;
1712
+ if (parseChatAttachmentKey(ref)) {
1713
+ refs = [{ vaultPath: ref.trim() }];
1714
+ }
1715
+ else {
1716
+ if (agentCallerUid(token)) {
1717
+ throw new Error("Agents cannot look up an eventId in the person inbox. Pass the attachment's vault path instead.");
1718
+ }
1719
+ const res = await vaultApiFetch({
1720
+ token,
1721
+ path: "/v1/notify/inbox",
1722
+ query: { limit: opts.limit ?? "100" },
1723
+ });
1724
+ if (!res.ok) {
1725
+ const err = (await res.json().catch(() => ({})));
1726
+ throw new Error(friendlyDmError(res.status, err.code, err.error ?? err.message ?? res.statusText));
1727
+ }
1728
+ const data = (await res.json());
1729
+ const found = findMessageAttachments(data.events ?? [], ref);
1730
+ if (found === null) {
1731
+ throw new Error(`No message ${ref} in your last ${opts.limit ?? "100"} inbox messages. Raise --limit, or pass the attachment's vault path.`);
1732
+ }
1733
+ if (found.length === 0)
1734
+ throw new Error(`Message ${ref} has no attachments.`);
1735
+ refs = found;
1736
+ }
1737
+ const targets = attachmentOutputPaths(refs, opts.out, (p) => {
1738
+ try {
1739
+ return statSync(p).isDirectory();
1740
+ }
1741
+ catch {
1742
+ return false;
1743
+ }
1744
+ });
1745
+ // Check every destination before writing anything, so a clash never leaves
1746
+ // a partial download behind.
1747
+ if (!opts.force) {
1748
+ const clash = targets.find((dest) => existsSync(dest));
1749
+ if (clash) {
1750
+ throw new Error(`${clash} already exists. Pass --force to overwrite, or --out to choose another path.`);
1751
+ }
1752
+ }
1753
+ const scopes = refs.map((r) => attachmentReadScopes({
1754
+ vaultPath: r.vaultPath,
1755
+ companyUid: opts.company ?? r.companyUid,
1756
+ callerUid,
1757
+ senderUid: r.senderUid,
1758
+ }));
1759
+ for (let i = 0; i < refs.length; i++) {
1760
+ const r = refs[i];
1761
+ const dest = targets[i];
1762
+ let bytes;
1763
+ let lastErr;
1764
+ for (const scopeUid of scopes[i]) {
1765
+ try {
1766
+ bytes = await downloadChatAttachment({ token, scopeUid, vaultPath: r.vaultPath });
1767
+ break;
1768
+ }
1769
+ catch (e) {
1770
+ lastErr = e;
1771
+ }
1772
+ }
1773
+ if (!bytes)
1774
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
1775
+ writeFileSync(dest, bytes);
1776
+ console.log(chalk.green(`Saved ${dest}`) + chalk.dim(` (${bytes.length} bytes)`));
1777
+ }
1778
+ }
1779
+ catch (err) {
1780
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
1781
+ process.exit(1);
1782
+ }
1783
+ }
1682
1784
  async function runDmThread(identifier, opts) {
1683
1785
  try {
1684
1786
  const query = buildThreadQuery(identifier);
@@ -1879,6 +1981,16 @@ export function registerDmCommand(program) {
1879
1981
  .action(async (opts) => {
1880
1982
  await runDmInbox(opts);
1881
1983
  });
1984
+ dm
1985
+ .command("attachment <ref>")
1986
+ .description("Download a file attached to a DM. REF is the message eventId from `hq dm inbox` (downloads every file on it) or the attachment's vault path (chat/attachments/...). The file is read from the sender's vault; only the two people in the conversation can open a DM attachment.")
1987
+ .option("--out <path>", "Where to write: a file path for one attachment, else a directory (default: current directory)")
1988
+ .option("--company <uid>", "Vault scope the file lives in (the companyUid shown on the message). Derived from the DM pair when omitted")
1989
+ .option("--force", "Overwrite an existing file")
1990
+ .option("--limit <n>", "How many recent inbox messages to search for the eventId", "100")
1991
+ .action(async (ref, opts) => {
1992
+ await runDmAttachment(ref, opts);
1993
+ });
1882
1994
  dm
1883
1995
  .command("thread <person>")
1884
1996
  .alias("read")
@@ -242,6 +242,24 @@ export declare function runFilesRestore(params: {
242
242
  }, deps?: {
243
243
  confirm?: ConfirmFn;
244
244
  }): Promise<FilesRestoreResponse | undefined>;
245
+ export interface FilesPurgeResponse {
246
+ key: string;
247
+ purgedVersionId: string;
248
+ }
249
+ /**
250
+ * feedback_fc711fc3: permanently delete one noncurrent version. The server
251
+ * (hq-pro POST /v1/files/restore, action "purge") enforces owner-only access,
252
+ * refuses the current version and delete markers, and audits before deleting.
253
+ */
254
+ export declare function runFilesPurge(params: {
255
+ key: string;
256
+ versionId: string;
257
+ yes: boolean;
258
+ personal: boolean;
259
+ companySlug: string | undefined;
260
+ }, deps?: {
261
+ confirm?: ConfirmFn;
262
+ }): Promise<FilesPurgeResponse | undefined>;
245
263
  export declare function runFilesTrash(params: {
246
264
  prefix: string;
247
265
  cursor?: string;
@@ -731,6 +731,30 @@ export function registerFilesCommand(program) {
731
731
  process.exit(1);
732
732
  }
733
733
  });
734
+ files
735
+ .command("purge <path>")
736
+ .description("Permanently delete ONE retained (noncurrent) version of an exact vault key, for example a version that held data which must not live in HQ. Owner only; audited; cannot be undone. Delete the file first if the version is current. Use --personal for your personal vault.")
737
+ .option("--personal", "Target your own personal vault instead of a company vault (mutually exclusive with --company)")
738
+ .requiredOption("--version-id <id>", "Exact version ID to purge (see `hq files versions`)")
739
+ .option("-y, --yes", "Skip the confirmation prompt (for scripts)")
740
+ .action(async (path, opts) => {
741
+ try {
742
+ const companySlug = files.opts().company;
743
+ const personal = opts.personal === true;
744
+ assertRecoveryScope(personal, companySlug);
745
+ await runFilesPurge({
746
+ key: path,
747
+ versionId: opts.versionId,
748
+ yes: opts.yes === true,
749
+ personal,
750
+ companySlug,
751
+ });
752
+ }
753
+ catch (err) {
754
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
755
+ process.exit(1);
756
+ }
757
+ });
734
758
  files
735
759
  .command("trash")
736
760
  .description("List deleted vault keys retained as tombstones. Use --personal for your personal vault.")
@@ -1767,6 +1791,55 @@ export async function runFilesRestore(params, deps = {}) {
1767
1791
  console.log(chalk.dim("New version: " + data.newVersionId));
1768
1792
  return data;
1769
1793
  }
1794
+ /**
1795
+ * feedback_fc711fc3: permanently delete one noncurrent version. The server
1796
+ * (hq-pro POST /v1/files/restore, action "purge") enforces owner-only access,
1797
+ * refuses the current version and delete markers, and audits before deleting.
1798
+ */
1799
+ export async function runFilesPurge(params, deps = {}) {
1800
+ assertExactRecoveryKey(params.key);
1801
+ if (typeof params.versionId !== "string" || params.versionId.trim() === "") {
1802
+ throw new Error("--version-id is required and must not be empty.");
1803
+ }
1804
+ const confirm = deps.confirm ?? realConfirm;
1805
+ if (!params.yes) {
1806
+ const ok = await confirm("Permanently delete version '" + params.versionId + "' of '" + params.key + "'" +
1807
+ (params.personal ? " in your personal vault" : "") +
1808
+ "? This cannot be undone.");
1809
+ if (!ok) {
1810
+ console.log(chalk.dim("Aborted — nothing was purged."));
1811
+ return undefined;
1812
+ }
1813
+ }
1814
+ const scope = await resolveRecoveryScope(params);
1815
+ const body = scope.personal
1816
+ ? { personal: true, key: params.key }
1817
+ : { company: scope.companyUid, key: params.key };
1818
+ body.action = "purge";
1819
+ body.purgeVersionId = params.versionId;
1820
+ // Tripwire: a server without purge support rejects `versionId: null` with
1821
+ // 400 instead of ignoring `action` and restoring (re-publishing) the version.
1822
+ body.versionId = null;
1823
+ const res = await vaultApiFetch({
1824
+ token: scope.token,
1825
+ path: "/v1/files/restore",
1826
+ method: "POST",
1827
+ body,
1828
+ });
1829
+ if (!res.ok) {
1830
+ const errBody = (await res.json().catch(() => ({})));
1831
+ const err = new FilesRecoveryHttpError(res.status, errBody.message ?? errBody.error ?? res.statusText, errBody.code);
1832
+ throw new Error(formatFilesRecoveryError(err, params.key));
1833
+ }
1834
+ const data = (await res.json());
1835
+ // Belt and braces with the tripwire above: only a server that echoes the
1836
+ // purged version id has purged it.
1837
+ if (data.purgedVersionId !== params.versionId) {
1838
+ throw new Error("The server did not confirm the purge (it may not support purge yet). Check `hq files versions` before retrying.");
1839
+ }
1840
+ console.log(chalk.green("Purged version '" + data.purgedVersionId + "' of '" + params.key + "'."));
1841
+ return { key: data.key ?? params.key, purgedVersionId: data.purgedVersionId };
1842
+ }
1770
1843
  export async function runFilesTrash(params) {
1771
1844
  if (params.prefix.includes("*")) {
1772
1845
  throw new Error("Trash prefix must be literal; wildcard expansion is not supported.");
@@ -43,6 +43,7 @@ import { createPersonVaultReader } from "../lib/hot-channels/person-vault-reader
43
43
  import { S3Client } from "@aws-sdk/client-s3";
44
44
  import { resolveCanonicalPersonUid } from "./cloud.js";
45
45
  import { botsRoot as defaultBotsRoot } from "../lib/bot/paths.js";
46
+ import { ensureHotSignalSyncJob } from "../lib/bot/jobs/hot-signal-sync.js";
46
47
  import { findHotSignalSyncJobs, lastRunsPerAdapter, parseDailyAt, renderScheduleReport, rescheduleDaily, toJobView, } from "../lib/hot-channels/schedule.js";
47
48
  import { listActiveMembers } from "./members.js";
48
49
  import { runOnDemandSync, } from "../lib/hot-channels/run.js";
@@ -619,6 +620,20 @@ export async function runSchedule(options, overrides = {}) {
619
620
  parseDailyAt(options.at);
620
621
  let found = findHotSignalSyncJobs(overrides.botsRoot ?? defaultBotsRoot(), company);
621
622
  if (options.at !== undefined) {
623
+ if (found.length === 0) {
624
+ // Setting a time turns the daily sync on: register it on the personal setup bot.
625
+ const root = overrides.botsRoot ?? defaultBotsRoot();
626
+ const setupDir = path.join(root, "setup");
627
+ if (fs.existsSync(setupDir)) {
628
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
629
+ const otherBotDirs = fs
630
+ .readdirSync(root, { withFileTypes: true })
631
+ .filter((d) => d.isDirectory())
632
+ .map((d) => path.join(root, d.name));
633
+ ensureHotSignalSyncJob(setupDir, { company, timezone, otherBotDirs }, now);
634
+ found = findHotSignalSyncJobs(root, company);
635
+ }
636
+ }
622
637
  if (found.length === 0)
623
638
  throw new NoHotSignalSyncJobError(company);
624
639
  found = rescheduleDaily(found, options.at, now);