@indigoai-us/hq-cli 5.345.154 → 5.345.155

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 CHANGED
@@ -2,6 +2,86 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.155] — 2026-10-21
6
+ - `hq secrets exec --personal` now works in a session locked to one company.
7
+ It injects your own personal-vault secrets into the command, read-only, and
8
+ appends one line per use to `workspace/audit/personal-secrets-use.jsonl`
9
+ (timestamp, session id, locked company, secret names; never values). If the
10
+ audit line cannot be written, nothing is read. In a company-locked session
11
+ every other personal-vault command (`set`, `get`, `list`, `env`, `delete`,
12
+ `share`, `generate-link`, `sandbox --personal` and the rest) is refused, as
13
+ is personal scope next to more than one locked company, API-key auth, or
14
+ another person's `prs_` scope. A `prs_`/`agt_` value passed as `--company`
15
+ is treated as personal scope and gets the same rules. Owner decision dq_20261020234444_f1b1ea4185;
16
+ feedback 5b6c67f7-59b1-410a-a4ee-b5db27fe7051.
17
+ - Lanes flag reads now retry once on a timeout, network error or 5xx, and refresh the token once on a 401. When the registry is still unreachable (timeout, network error or 5xx), the read falls back to the last value the registry confirmed within the past 24 hours (kept in the HQ state dir) before the hard-coded default, so a brief registry outage no longer turns `monitor.enabled` off. A 401 after the refresh, a 403 or other 4xx, or a missing token never uses that cached value. `flag_read_failed` events now include the HTTP status, a redacted error message, the duration, and whether the cache was used.
18
+ - `hq library add` now works on an `HQ_API_KEY` with upload rights
19
+ (`hq api-keys create --preset library-agent --allow-upload`) and no login.
20
+ `--company` takes the company's slug or its uid (`cmp_…`), and the
21
+ placeholder always goes under `companies/<slug>/library` (or a `--to` folder
22
+ inside `companies/<slug>/`), never a `companies/cmp_…` folder. On a key the
23
+ slug and uid are matched through `companies/manifest.yaml`, because a key
24
+ cannot read memberships. Before this, a key got "Could not load your
25
+ companies (HTTP 404). Run `hq login`." `hq library describe` had the same
26
+ problem and is fixed the same way. `hq library link` on a key now also
27
+ accepts `--company <slug>`, and with no `--company` it uses the
28
+ placeholder's company (`HQ_COMPANY_UID` still wins when set); before, both
29
+ failed and only `--company cmp_…` worked. A key without `files:write` now
30
+ gets "This API key cannot upload (missing files:write)" instead of the login
31
+ message. Signed-in use is unchanged, except that `--company` also accepts
32
+ the uid there.
33
+ - `hq library add` and `hq library describe` now caption a HEIC/HEIF photo from
34
+ its primary image. Before, ffmpeg's default stream was used, which for an
35
+ iPhone HEIC is a single tile of the photo or, for Portrait-mode photos, the
36
+ black-and-white depth/segmentation matte, so captions described a blurry
37
+ fragment or "a segmentation mask". The primary image is decoded with `sips`
38
+ on macOS, then ImageMagick (`magick`, or `convert` outside Windows), then by
39
+ assembling ffmpeg's primary tile grid, and each result must be a valid JPEG
40
+ of at most 1024px before it is used; otherwise the previous ffmpeg behavior
41
+ is kept. HEIC/HEIF is detected by content as well as by a `.heic`, `.heif`
42
+ or `.hif` name in any case. Photos shot upside down or sideways are turned
43
+ upright on the `sips` and ImageMagick paths, and the `sips` frame has its
44
+ EXIF/XMP removed before it is sent for captioning. `.heif` files are now
45
+ uploaded as `image/heif` and captioned. Other image formats and videos are
46
+ unchanged, and captioning still never blocks an upload.
47
+ - `hq library rm <placeholder-or-original> [more...]` (alias `remove`) removes
48
+ company library assets. It deletes the local original if one was fetched
49
+ (only when its sha256 matches the library copy), then the local placeholder,
50
+ then the placeholder's vault copy, in that order so sync cannot bring the
51
+ placeholder back midway. It previews and asks first; `--yes` skips the
52
+ question, `--dry-run` changes nothing, and `--json` prints the result as
53
+ JSON. The shared blob `.hq-offload/blobs/<sha256>` is kept by default,
54
+ because other placeholders (including teammates') may use the same bytes.
55
+ `--purge` also deletes the blob, and is refused if any other placeholder in
56
+ the local tree or the company vault points at it. The vault check lists
57
+ every placeholder you can see, so it can take a few minutes on a large
58
+ vault. Personal placeholders, plain offload placeholders and placeholders
59
+ keyed to a synced path are refused. An `HQ_API_KEY` needs files:write (and
60
+ files:read for `--purge`); it is refused before any local file is touched.
61
+ The vault is versioned, so `hq files restore <key>` undoes the vault side.
62
+ - `hq auth machine-session --output -` prints a machine identity session to a
63
+ pipe instead of a file, for example
64
+ `hq auth machine-session --output - --field refreshToken | gh secret set NAME`.
65
+ It signs in fresh in memory and writes no token cache, lock, or temp file
66
+ (the CLI may still write its usual non-secret state files). Before printing,
67
+ it checks the minted tokens against the machine identity the same way
68
+ hq-cloud does for every machine mint (client, subject, entity type and uid,
69
+ and no company-outpost claims on a non-company identity), and prints nothing
70
+ if they do not match. Without `--field` it prints the same JSON as the file
71
+ mode (`accessToken`, `idToken`, `expiresAt`) followed by a newline.
72
+ `--field <name>` prints one value raw with no trailing newline;
73
+ `refreshToken` is available only this way. A refresh token outlives the
74
+ machine secret: revoke a leaked one with a Cognito global sign-out for the
75
+ machine user or `RevokeToken`. The creds file is `HQ_MACHINE_CREDS_FILE`,
76
+ else `~/.hq-agent/machine-creds.json`; `HQ_AGENT_DIR` does not select it.
77
+ It refuses to print to a terminal unless `--force-tty` is given, and
78
+ supports hosted agent and outpost identities (not external or
79
+ company-outpost ones). `--output <path>` is unchanged. Behavior change:
80
+ `--output -` used to write a file literally named `-` in the current
81
+ directory; it now means stdout. To write a file with that name, use
82
+ `--output ./-`.
83
+ - `hq storage offload --current` can turn a library asset restored with `hq library fetch` back into a placeholder. It used to skip the file with "run hq sync first", because fetch never records the file in the sync journal. Offload now checks the company vault for the asset's blob at `.hq-offload/blobs/<sha256>`, which is the placeholder's own key, so nothing is uploaded. Before removing the local file it confirms that the blob's size and recorded sha256 match the file, and it hashes the file again. When the library placeholder sits beside the file, offload leaves the placeholder as is. When fetch removed it, offload writes back the vault's own placeholder for that path (keeping its tags and caption), or a plain library pointer when the vault no longer has one. Files the sync journal knows, and placeholders keyed by the file's own path, work as before. A file whose bytes differ from the library copy is kept, with the reason shown.
84
+
5
85
  ## [5.345.154] — 2026-10-21
6
86
  - `hq sync status` now lists local files that sit under a vault prefix you can
7
87
  only read and that were never synced. Sync will never upload these files, for
@@ -1764,17 +1764,28 @@ export declare const COMMAND_CATALOG: readonly [{
1764
1764
  readonly options: readonly [];
1765
1765
  readonly subcommands: readonly [{
1766
1766
  readonly name: "machine-session";
1767
- readonly description: "Write the existing machine identity session to a private file";
1767
+ readonly description: "Write the existing machine identity session to a private file, or to a pipe with --output -";
1768
1768
  readonly aliases: readonly [];
1769
1769
  readonly hidden: false;
1770
1770
  readonly helpOption: true;
1771
1771
  readonly helpCommand: false;
1772
1772
  readonly usage: "[options]";
1773
- readonly helpText: readonly [];
1773
+ readonly helpText: readonly [{
1774
+ readonly position: "after";
1775
+ readonly text: "\nStdout format (--output -):\n no --field one JSON line {\"accessToken\",\"idToken\",\"expiresAt\"} then a newline\n --field X the raw value of X with no trailing newline\n\nCreds come from HQ_MACHINE_CREDS_FILE, else ~/.hq-agent/machine-creds.json.\nHQ_AGENT_DIR does not select them; set HQ_MACHINE_CREDS_FILE on hosts with several agents.\n\n--field refreshToken prints a long-lived Cognito refresh token. Rotating the machine\nsecret does not revoke it; revoke a leaked one with a Cognito global sign-out for the\nmachine user (AdminUserGlobalSignOut) or RevokeToken.";
1776
+ }];
1774
1777
  readonly arguments: readonly [];
1775
1778
  readonly options: readonly [{
1776
1779
  readonly flags: "--output <path>";
1777
- readonly description: "path for the private machine session JSON";
1780
+ readonly description: "path for the private machine session JSON, or - for stdout (pipe only). Creds come from HQ_MACHINE_CREDS_FILE, else ~/.hq-agent/machine-creds.json";
1781
+ readonly hidden: false;
1782
+ }, {
1783
+ readonly flags: "--field <name>";
1784
+ readonly description: "with --output -, print one field raw with no trailing newline (accessToken, idToken, expiresAt, refreshToken)";
1785
+ readonly hidden: false;
1786
+ }, {
1787
+ readonly flags: "--force-tty";
1788
+ readonly description: "with --output -, allow printing to a terminal";
1778
1789
  readonly hidden: false;
1779
1790
  }];
1780
1791
  readonly subcommands: readonly [];
@@ -10564,8 +10575,8 @@ export declare const COMMAND_CATALOG: readonly [{
10564
10575
  readonly description: "Asset description";
10565
10576
  readonly hidden: false;
10566
10577
  }, {
10567
- readonly flags: "--company <slug>";
10568
- readonly description: "Company whose vault receives the files";
10578
+ readonly flags: "--company <company>";
10579
+ readonly description: "Company slug or uid (cmp_…) whose vault receives the files";
10569
10580
  readonly hidden: false;
10570
10581
  }, {
10571
10582
  readonly flags: "--no-caption";
@@ -10693,7 +10704,7 @@ export declare const COMMAND_CATALOG: readonly [{
10693
10704
  }];
10694
10705
  readonly options: readonly [{
10695
10706
  readonly flags: "--company <company>";
10696
- readonly description: "Company uid (cmp_…), or a slug on a session; default: the placeholder's company or the key's";
10707
+ readonly description: "Company slug or uid (cmp_…); default: the placeholder's company or the key's";
10697
10708
  readonly hidden: false;
10698
10709
  }, {
10699
10710
  readonly flags: "--expires <minutes>";
@@ -10705,6 +10716,43 @@ export declare const COMMAND_CATALOG: readonly [{
10705
10716
  readonly hidden: false;
10706
10717
  }];
10707
10718
  readonly subcommands: readonly [];
10719
+ }, {
10720
+ readonly name: "rm";
10721
+ readonly description: "Remove library assets: the local original (if fetched), the placeholder, and its vault copy. Keeps the shared blob unless --purge. Previews and asks first unless --yes";
10722
+ readonly aliases: readonly ["remove"];
10723
+ readonly hidden: false;
10724
+ readonly helpOption: true;
10725
+ readonly helpCommand: false;
10726
+ readonly usage: "[options] <targets...>";
10727
+ readonly helpText: readonly [];
10728
+ readonly arguments: readonly [{
10729
+ readonly name: "targets";
10730
+ readonly required: true;
10731
+ readonly variadic: true;
10732
+ readonly description: "Library .hqcloud placeholders (or their original paths)";
10733
+ }];
10734
+ readonly options: readonly [{
10735
+ readonly flags: "--company <company>";
10736
+ readonly description: "Company slug (or cmp_ uid); default: the targets' companies/<slug>/";
10737
+ readonly hidden: false;
10738
+ }, {
10739
+ readonly flags: "--purge";
10740
+ readonly description: "Also delete the blob, after checking no other placeholder (local or in the vault) uses it";
10741
+ readonly hidden: false;
10742
+ }, {
10743
+ readonly flags: "--dry-run";
10744
+ readonly description: "Show what would be removed without changing anything";
10745
+ readonly hidden: false;
10746
+ }, {
10747
+ readonly flags: "-y, --yes";
10748
+ readonly description: "Skip the confirmation prompt";
10749
+ readonly hidden: false;
10750
+ }, {
10751
+ readonly flags: "--json";
10752
+ readonly description: "Print machine-readable JSON (the preview goes to stderr)";
10753
+ readonly hidden: false;
10754
+ }];
10755
+ readonly subcommands: readonly [];
10708
10756
  }];
10709
10757
  }, {
10710
10758
  readonly name: "index";
@@ -2117,18 +2117,33 @@ export const COMMAND_CATALOG = [
2117
2117
  "subcommands": [
2118
2118
  {
2119
2119
  "name": "machine-session",
2120
- "description": "Write the existing machine identity session to a private file",
2120
+ "description": "Write the existing machine identity session to a private file, or to a pipe with --output -",
2121
2121
  "aliases": [],
2122
2122
  "hidden": false,
2123
2123
  "helpOption": true,
2124
2124
  "helpCommand": false,
2125
2125
  "usage": "[options]",
2126
- "helpText": [],
2126
+ "helpText": [
2127
+ {
2128
+ "position": "after",
2129
+ "text": "\nStdout format (--output -):\n no --field one JSON line {\"accessToken\",\"idToken\",\"expiresAt\"} then a newline\n --field X the raw value of X with no trailing newline\n\nCreds come from HQ_MACHINE_CREDS_FILE, else ~/.hq-agent/machine-creds.json.\nHQ_AGENT_DIR does not select them; set HQ_MACHINE_CREDS_FILE on hosts with several agents.\n\n--field refreshToken prints a long-lived Cognito refresh token. Rotating the machine\nsecret does not revoke it; revoke a leaked one with a Cognito global sign-out for the\nmachine user (AdminUserGlobalSignOut) or RevokeToken."
2130
+ }
2131
+ ],
2127
2132
  "arguments": [],
2128
2133
  "options": [
2129
2134
  {
2130
2135
  "flags": "--output <path>",
2131
- "description": "path for the private machine session JSON",
2136
+ "description": "path for the private machine session JSON, or - for stdout (pipe only). Creds come from HQ_MACHINE_CREDS_FILE, else ~/.hq-agent/machine-creds.json",
2137
+ "hidden": false
2138
+ },
2139
+ {
2140
+ "flags": "--field <name>",
2141
+ "description": "with --output -, print one field raw with no trailing newline (accessToken, idToken, expiresAt, refreshToken)",
2142
+ "hidden": false
2143
+ },
2144
+ {
2145
+ "flags": "--force-tty",
2146
+ "description": "with --output -, allow printing to a terminal",
2132
2147
  "hidden": false
2133
2148
  }
2134
2149
  ],
@@ -12811,8 +12826,8 @@ export const COMMAND_CATALOG = [
12811
12826
  "hidden": false
12812
12827
  },
12813
12828
  {
12814
- "flags": "--company <slug>",
12815
- "description": "Company whose vault receives the files",
12829
+ "flags": "--company <company>",
12830
+ "description": "Company slug or uid (cmp_…) whose vault receives the files",
12816
12831
  "hidden": false
12817
12832
  },
12818
12833
  {
@@ -12970,7 +12985,7 @@ export const COMMAND_CATALOG = [
12970
12985
  "options": [
12971
12986
  {
12972
12987
  "flags": "--company <company>",
12973
- "description": "Company uid (cmp_…), or a slug on a session; default: the placeholder's company or the key's",
12988
+ "description": "Company slug or uid (cmp_…); default: the placeholder's company or the key's",
12974
12989
  "hidden": false
12975
12990
  },
12976
12991
  {
@@ -12985,6 +13000,54 @@ export const COMMAND_CATALOG = [
12985
13000
  }
12986
13001
  ],
12987
13002
  "subcommands": []
13003
+ },
13004
+ {
13005
+ "name": "rm",
13006
+ "description": "Remove library assets: the local original (if fetched), the placeholder, and its vault copy. Keeps the shared blob unless --purge. Previews and asks first unless --yes",
13007
+ "aliases": [
13008
+ "remove"
13009
+ ],
13010
+ "hidden": false,
13011
+ "helpOption": true,
13012
+ "helpCommand": false,
13013
+ "usage": "[options] <targets...>",
13014
+ "helpText": [],
13015
+ "arguments": [
13016
+ {
13017
+ "name": "targets",
13018
+ "required": true,
13019
+ "variadic": true,
13020
+ "description": "Library .hqcloud placeholders (or their original paths)"
13021
+ }
13022
+ ],
13023
+ "options": [
13024
+ {
13025
+ "flags": "--company <company>",
13026
+ "description": "Company slug (or cmp_ uid); default: the targets' companies/<slug>/",
13027
+ "hidden": false
13028
+ },
13029
+ {
13030
+ "flags": "--purge",
13031
+ "description": "Also delete the blob, after checking no other placeholder (local or in the vault) uses it",
13032
+ "hidden": false
13033
+ },
13034
+ {
13035
+ "flags": "--dry-run",
13036
+ "description": "Show what would be removed without changing anything",
13037
+ "hidden": false
13038
+ },
13039
+ {
13040
+ "flags": "-y, --yes",
13041
+ "description": "Skip the confirmation prompt",
13042
+ "hidden": false
13043
+ },
13044
+ {
13045
+ "flags": "--json",
13046
+ "description": "Print machine-readable JSON (the preview goes to stderr)",
13047
+ "hidden": false
13048
+ }
13049
+ ],
13050
+ "subcommands": []
12988
13051
  }
12989
13052
  ]
12990
13053
  },
@@ -7,6 +7,8 @@
7
7
  * hq auth refresh — ensure a valid cached Cognito session (non-interactive)
8
8
  * hq auth status — show whether a valid session is cached + expiry
9
9
  * hq auth machine-session --output <path> — write machine tokens to a private file
10
+ * hq auth machine-session --output - [--field <name>] — print a fresh machine
11
+ * session to a pipe; nothing is written to disk
10
12
  *
11
13
  * Sign-up is owned by the onboarding web app at
12
14
  * https://onboarding.hq.computer. `hq auth login` signs an existing account
@@ -7,6 +7,8 @@
7
7
  * hq auth refresh — ensure a valid cached Cognito session (non-interactive)
8
8
  * hq auth status — show whether a valid session is cached + expiry
9
9
  * hq auth machine-session --output <path> — write machine tokens to a private file
10
+ * hq auth machine-session --output - [--field <name>] — print a fresh machine
11
+ * session to a pipe; nothing is written to disk
10
12
  *
11
13
  * Sign-up is owned by the onboarding web app at
12
14
  * https://onboarding.hq.computer. `hq auth login` signs an existing account
@@ -21,7 +23,8 @@ import * as fs from "node:fs";
21
23
  import { open, realpath } from "node:fs/promises";
22
24
  import path from "node:path";
23
25
  import chalk from "chalk";
24
- import { clearCachedTokens, loadCachedTokens, isExpiring, loadMachineCreds, CognitoAuthError, } from "@indigoai-us/hq-cloud";
26
+ import { clearCachedTokens, loadCachedTokens, isExpiring, loadMachineCreds, isExternalMachineCreds, CognitoAuthError, } from "@indigoai-us/hq-cloud";
27
+ import { authenticateMachineSession, MachineLoginError } from "../lib/daemon/machine-login.js";
25
28
  import { DEFAULT_COGNITO, refreshCachedSession, resolveCognitoTokenSource, ensureMachineSessionTokens, personTokenCacheFile, machineTokenCacheFile, loadMachineCachedTokens, } from "../utils/cognito-session.js";
26
29
  // Static (node builtins only): a lazy import here would need a resident
27
30
  // preload entry for the auth-login path.
@@ -134,15 +137,101 @@ function machineIdentityLabel() {
134
137
  const creds = loadMachineCreds();
135
138
  return creds ? `machine identity ${creds.username}` : "machine identity";
136
139
  }
140
+ const MACHINE_SESSION_FIELDS = ["accessToken", "idToken", "expiresAt", "refreshToken"];
141
+ /**
142
+ * `hq auth machine-session --output -`: mint a fresh machine session in memory
143
+ * and write it to stdout. No token cache, lock, or temp file is written. The
144
+ * full JSON has the same shape as the file mode; the refresh token is printed
145
+ * only when asked for with `--field refreshToken`.
146
+ */
147
+ async function writeMachineSessionToStdout(options) {
148
+ const fail = (message) => {
149
+ console.error(message);
150
+ process.exitCode = 1;
151
+ };
152
+ const field = options.field;
153
+ if (field !== undefined && !MACHINE_SESSION_FIELDS.includes(field)) {
154
+ fail(`Unknown --field. Use one of: ${MACHINE_SESSION_FIELDS.join(", ")}.`);
155
+ return;
156
+ }
157
+ if (process.stdout.isTTY && !options.forceTty) {
158
+ fail("Refusing to print a machine session to a terminal. Pipe it to a command, or pass --force-tty.");
159
+ return;
160
+ }
161
+ const creds = loadMachineCreds();
162
+ if (!creds) {
163
+ fail("Machine credentials are unavailable.");
164
+ return;
165
+ }
166
+ if (isExternalMachineCreds(creds) || creds.entityType === "company-outpost") {
167
+ fail("--output - supports hosted agent and outpost machine identities only.");
168
+ return;
169
+ }
170
+ let tokens;
171
+ try {
172
+ tokens = await authenticateMachineSession(creds);
173
+ }
174
+ catch (error) {
175
+ // The identity check runs on the token claims, so a creds file with no or
176
+ // a wrong entityType still cannot print a company-scoped or foreign session.
177
+ fail(error instanceof MachineLoginError && error.code === "identity_mismatch"
178
+ ? "Refusing to print a machine session: the minted tokens do not match this machine identity."
179
+ : "Could not obtain machine session.");
180
+ return;
181
+ }
182
+ let contents;
183
+ if (field === undefined) {
184
+ contents = JSON.stringify({
185
+ accessToken: tokens.accessToken,
186
+ idToken: tokens.idToken,
187
+ expiresAt: tokens.expiresAt,
188
+ }) + "\n";
189
+ }
190
+ else {
191
+ const value = tokens[field];
192
+ if (value === undefined || value === "") {
193
+ fail(`The machine sign-in did not return ${field}.`);
194
+ return;
195
+ }
196
+ contents = String(value);
197
+ }
198
+ await new Promise((resolve, reject) => {
199
+ process.stdout.write(contents, (error) => (error ? reject(error) : resolve()));
200
+ }).catch(() => fail("Could not write machine session to stdout."));
201
+ }
137
202
  export function registerAuthCommands(program) {
138
203
  const authCmd = program
139
204
  .command("auth")
140
205
  .description("Manage the local HQ Cognito session");
141
206
  authCmd
142
207
  .command("machine-session")
143
- .description("Write the existing machine identity session to a private file")
144
- .requiredOption("--output <path>", "path for the private machine session JSON")
208
+ .description("Write the existing machine identity session to a private file, or to a pipe with --output -")
209
+ .requiredOption("--output <path>", "path for the private machine session JSON, or - for stdout (pipe only). Creds come from HQ_MACHINE_CREDS_FILE, else ~/.hq-agent/machine-creds.json")
210
+ .option("--field <name>", `with --output -, print one field raw with no trailing newline (${MACHINE_SESSION_FIELDS.join(", ")})`)
211
+ .option("--force-tty", "with --output -, allow printing to a terminal")
212
+ .addHelpText("after", [
213
+ "",
214
+ "Stdout format (--output -):",
215
+ ' no --field one JSON line {"accessToken","idToken","expiresAt"} then a newline',
216
+ " --field X the raw value of X with no trailing newline",
217
+ "",
218
+ "Creds come from HQ_MACHINE_CREDS_FILE, else ~/.hq-agent/machine-creds.json.",
219
+ "HQ_AGENT_DIR does not select them; set HQ_MACHINE_CREDS_FILE on hosts with several agents.",
220
+ "",
221
+ "--field refreshToken prints a long-lived Cognito refresh token. Rotating the machine",
222
+ "secret does not revoke it; revoke a leaked one with a Cognito global sign-out for the",
223
+ "machine user (AdminUserGlobalSignOut) or RevokeToken.",
224
+ ].join("\n"))
145
225
  .action(async (options) => {
226
+ if (options.output === "-") {
227
+ await writeMachineSessionToStdout(options);
228
+ return;
229
+ }
230
+ if (options.field !== undefined || options.forceTty) {
231
+ console.error("--field and --force-tty require --output -.");
232
+ process.exitCode = 1;
233
+ return;
234
+ }
146
235
  const requestedPath = path.resolve(options.output);
147
236
  let handle;
148
237
  try {
@@ -23,8 +23,12 @@ export interface PutResponse {
23
23
  export interface LibraryAddDeps {
24
24
  hqRoot: string;
25
25
  now: () => Date;
26
- /** Resolve the slug to an active company membership. */
27
- resolveCompany: (slug: string) => Promise<LibraryCompany>;
26
+ /**
27
+ * Resolve `--company` (a slug or a cmp_ uid) to the company. `folderSlug` is
28
+ * the companies/<slug>/ of an explicit `--to`, for callers that cannot map a
29
+ * uid to its folder on their own (an HQ_API_KEY).
30
+ */
31
+ resolveCompany: (ref: string, folderSlug?: string) => Promise<LibraryCompany>;
28
32
  /** POST /v1/files/presign. */
29
33
  presign: (body: Record<string, unknown>) => Promise<Response>;
30
34
  /** Open a fresh byte stream of a source. Called once to hash and once to upload. */
@@ -29,10 +29,10 @@ import https from "node:https";
29
29
  import path from "node:path";
30
30
  import { Readable } from "node:stream";
31
31
  import { formatCloudAssetBlock, formatCloudPlaceholder, isCoveredByAny, parseCloudPlaceholder, resolvePullScope, } from "@indigoai-us/hq-cloud";
32
- import { PLACEHOLDER_SUFFIX, companyOffloadKey, containedPath, durablePlaceholderFs, renderPointer, resolvePlaceholder, } from "./storage-offload.js";
32
+ import { CONTENT_SHA256_METADATA_KEY, PLACEHOLDER_SUFFIX, companyOffloadKey, containedPath, durablePlaceholderFs, renderPointer, resolvePlaceholder, } from "./storage-offload.js";
33
33
  import { formatPlanGateError, formatUpgradeRefusal, planGateErrorFromResponse, } from "../utils/plan-gate-error.js";
34
34
  /** Metadata key hq-pro binds to `checksumSha256` (sync's content hash). */
35
- const CONTENT_SHA256_META_KEY = "hq-content-sha256";
35
+ const CONTENT_SHA256_META_KEY = CONTENT_SHA256_METADATA_KEY;
36
36
  const MEDIA_TYPES = {
37
37
  ".png": "image/png",
38
38
  ".jpg": "image/jpeg",
@@ -42,6 +42,7 @@ const MEDIA_TYPES = {
42
42
  ".svg": "image/svg+xml",
43
43
  ".avif": "image/avif",
44
44
  ".heic": "image/heic",
45
+ ".heif": "image/heif",
45
46
  ".mp4": "video/mp4",
46
47
  ".mov": "video/quicktime",
47
48
  ".webm": "video/webm",
@@ -136,6 +137,41 @@ export function resolveLibraryDir(hqRoot, options) {
136
137
  }
137
138
  return { slug, dirRel, dirAbs };
138
139
  }
140
+ /**
141
+ * The company and library folder for `add`. `--company` may be a slug or a
142
+ * cmp_ uid; the folder always uses the company's slug, so a uid never becomes
143
+ * a companies/cmp_…/ folder. A slug that disagrees with `--to` is refused
144
+ * before any lookup, as it always was.
145
+ */
146
+ async function resolveLibraryTarget(options, deps) {
147
+ const companyIsUid = options.company !== undefined && isCompanyUid(options.company);
148
+ if (!companyIsUid)
149
+ resolveLibraryDir(deps.hqRoot, options);
150
+ const folderSlug = options.to === undefined ? undefined : resolveLibraryDir(deps.hqRoot, { to: options.to }).slug;
151
+ const company = await deps.resolveCompany((options.company ?? folderSlug), folderSlug);
152
+ if (isCompanyUid(company.slug)) {
153
+ throw new Error(`No company folder is known for ${company.uid}; library folders are companies/<slug>/. ` +
154
+ "Pass --company <slug> or --to companies/<slug>/<dir>.");
155
+ }
156
+ if (folderSlug !== undefined && company.slug !== folderSlug) {
157
+ throw new Error(`--to is inside companies/${folderSlug}/, but --company ${options.company} is ${company.slug}.`);
158
+ }
159
+ return { company, ...resolveLibraryDir(deps.hqRoot, { company: company.slug, to: options.to }) };
160
+ }
161
+ function isCompanyUid(ref) {
162
+ return ref.startsWith("cmp_");
163
+ }
164
+ /**
165
+ * Capabilities an API key lacks, from hq-pro's keyed-route 403
166
+ * (`{ error, granted, missing }`). Empty for any other response.
167
+ */
168
+ function missingKeyCapabilities(status, body) {
169
+ if (status !== 403)
170
+ return [];
171
+ if (typeof body.missing === "string" && body.missing)
172
+ return [body.missing];
173
+ return Array.isArray(body.missing) ? body.missing.filter((c) => typeof c === "string") : [];
174
+ }
139
175
  async function hashStream(stream) {
140
176
  const hash = crypto.createHash("sha256");
141
177
  let size = 0;
@@ -212,8 +248,7 @@ async function uploadVerified(source, digest, grant, deps) {
212
248
  export async function runLibraryAdd(inputs, options, deps) {
213
249
  if (inputs.length === 0)
214
250
  throw new Error("Pass at least one file or URL.");
215
- const { slug, dirRel, dirAbs } = resolveLibraryDir(deps.hqRoot, options);
216
- const company = await deps.resolveCompany(slug);
251
+ const { company, slug, dirRel, dirAbs } = await resolveLibraryTarget(options, deps);
217
252
  const items = [];
218
253
  const planned = [];
219
254
  const claimed = new Set();
@@ -268,9 +303,13 @@ export async function runLibraryAdd(inputs, options, deps) {
268
303
  }
269
304
  const body = (await response.json().catch(() => ({})));
270
305
  if (!response.ok) {
271
- const msg = typeof body.error === "string" ? body.error : `HTTP ${response.status}`;
306
+ const missing = missingKeyCapabilities(response.status, body);
307
+ const msg = missing.length > 0
308
+ ? `This API key cannot upload (missing ${missing.join(", ")}). Use a key made with ` +
309
+ "`hq api-keys create --preset library-agent --allow-upload`, or unset HQ_API_KEY and run signed in."
310
+ : `Could not get an upload URL: ${typeof body.error === "string" ? body.error : `HTTP ${response.status}`}`;
272
311
  for (const p of planned)
273
- p.item.error = `Could not get an upload URL: ${msg}`;
312
+ p.item.error = msg;
274
313
  return { company: slug, items };
275
314
  }
276
315
  const rows = Array.isArray(body.results) ? body.results : [];
@@ -105,8 +105,45 @@ export declare function runLibraryDescribe(target: string, deps: LibraryDescribe
105
105
  export type RunTool = (command: string, args: string[]) => Promise<{
106
106
  stdout: string;
107
107
  }>;
108
+ /**
109
+ * HEIC / HEIF by content, or by a .heic/.heif/.hif name (any case) when the
110
+ * bytes are not a format Claude Code reads directly. AVIF (also a HEIF
111
+ * container) is not included.
112
+ */
113
+ export declare function isHeifImage(head: Buffer, name: string): boolean;
114
+ /** Width and height from a JPEG's start-of-frame marker, or null. */
115
+ export declare function jpegDimensions(bytes: Buffer): {
116
+ width: number;
117
+ height: number;
118
+ } | null;
119
+ /** A decoded frame is a non-empty JPEG within the frame byte and edge limits. */
120
+ export declare function isUsableJpegFrame(bytes: Buffer): boolean;
121
+ /** The EXIF orientation (1-8) in a JPEG's APP1 segment, or 1 when there is none. */
122
+ export declare function jpegExifOrientation(bytes: Buffer): number;
123
+ /** The JPEG without its APP1 (EXIF and XMP) segments. */
124
+ export declare function stripJpegApp1(bytes: Buffer): Buffer;
125
+ interface TileGrid {
126
+ width: number;
127
+ height: number;
128
+ left: number;
129
+ top: number;
130
+ tiles: Array<{
131
+ stream: number;
132
+ x: number;
133
+ y: number;
134
+ }>;
135
+ }
136
+ /**
137
+ * The primary tile grid from `ffprobe -show_stream_groups -of json`: the
138
+ * "Tile Grid" group marked default, else the first one. Null when the output
139
+ * does not have the expected shape (older ffmpeg has no tile grid groups).
140
+ */
141
+ export declare function primaryTileGrid(ffprobeJson: string): TileGrid | null;
142
+ /** ffmpeg filter graph that assembles a tile grid, crops it and scales it to the frame size. */
143
+ export declare function tileGridFilter(grid: TileGrid): string;
108
144
  /** Seek points at 10%, 35%, 60% and 85% of a known duration. */
109
145
  export declare function videoSeekPoints(durationSeconds: number | null): number[];
110
146
  /** Frames for captioning: the image itself when it fits, otherwise ffmpeg JPEGs. */
111
- export declare function extractCaptionFrames(file: string, mediaKind: CaptionMediaKind, run?: RunTool): Promise<Buffer[]>;
147
+ export declare function extractCaptionFrames(file: string, mediaKind: CaptionMediaKind, run?: RunTool, platform?: NodeJS.Platform): Promise<Buffer[]>;
148
+ export {};
112
149
  //# sourceMappingURL=library-caption.d.ts.map