@indigoai-us/hq-cli 5.345.143 → 5.345.144
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 +71 -0
- package/dist/command-catalog.generated.d.ts +278 -6
- package/dist/command-catalog.generated.js +341 -6
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-enroll.d.ts +1 -0
- package/dist/commands/agent-enroll.js +7 -0
- package/dist/commands/agents.d.ts +25 -3
- package/dist/commands/agents.js +57 -4
- package/dist/commands/api-keys.js +66 -8
- package/dist/commands/billing-sponsor.d.ts +4 -0
- package/dist/commands/billing-sponsor.js +48 -5
- package/dist/commands/billing-usage.d.ts +59 -0
- package/dist/commands/billing-usage.js +162 -0
- package/dist/commands/billing.d.ts +9 -0
- package/dist/commands/billing.js +29 -2
- package/dist/commands/core-policy.js +76 -13
- package/dist/commands/library-add.d.ts +120 -0
- package/dist/commands/library-add.js +434 -0
- package/dist/commands/library-caption.d.ts +112 -0
- package/dist/commands/library-caption.js +393 -0
- package/dist/commands/library-link.d.ts +32 -0
- package/dist/commands/library-link.js +95 -0
- package/dist/commands/library.d.ts +72 -0
- package/dist/commands/library.js +527 -0
- package/dist/commands/meetings.d.ts +8 -0
- package/dist/commands/meetings.js +25 -2
- package/dist/commands/storage-offload.d.ts +16 -2
- package/dist/commands/storage-offload.js +55 -4
- package/dist/commands/storage.d.ts +8 -1
- package/dist/commands/storage.js +6 -2
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2410 -2063
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/billing/plan-lock.d.ts +15 -1
- package/dist/lib/billing/plan-lock.js +45 -5
- package/dist/lib/billing/usage-card.d.ts +82 -0
- package/dist/lib/billing/usage-card.js +198 -0
- package/dist/lib/billing/usage-cost.d.ts +214 -0
- package/dist/lib/billing/usage-cost.js +440 -0
- package/dist/lib/billing/usage-only.d.ts +87 -0
- package/dist/lib/billing/usage-only.js +262 -0
- package/dist/lib/billing/usage-rate-card.d.ts +51 -0
- package/dist/lib/billing/usage-rate-card.js +62 -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/plan-limit-nag.d.ts +4 -6
- package/dist/lib/plan-limit-nag.js +19 -1
- package/dist/lib/provider-launcher.d.ts +22 -0
- package/dist/lib/provider-launcher.js +65 -0
- package/dist/lib/search-index/index.d.ts +2 -0
- package/dist/lib/search-index/index.js +34 -0
- package/dist/lib/search-index/max-doc-bytes.d.ts +2 -1
- package/dist/lib/search-index/max-doc-bytes.js +2 -1
- package/dist/main.d.ts +9 -0
- package/dist/main.js +20 -3
- package/dist/utils/billing-gate.d.ts +19 -4
- package/dist/utils/billing-gate.js +25 -4
- package/dist/utils/plan-gate-error.d.ts +10 -0
- package/dist/utils/plan-gate-error.js +54 -0
- package/dist/utils/vault-api.js +4 -1
- package/package.json +2 -2
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq library search|add|tag|ls|fetch|describe|link` — the company media library.
|
|
3
|
+
* `add` and `tag` live in ./library-add.ts; AI captions (`add` and `describe`)
|
|
4
|
+
* live in ./library-caption.ts; `link` lives in ./library-link.ts; `fetch`
|
|
5
|
+
* wraps `hq storage fetch`.
|
|
6
|
+
*
|
|
7
|
+
* `hq library search <query> [--tag t] [--json]` — find library assets.
|
|
8
|
+
*
|
|
9
|
+
* A library asset is a `.hqcloud` placeholder (`vault: company`) that carries
|
|
10
|
+
* an `hq-asset` block: title, tags, description and media kind for a file whose
|
|
11
|
+
* bytes live in the company vault.
|
|
12
|
+
*
|
|
13
|
+
* Cloud first: POST /v1/search with `{ query, kind: "asset", tags }`. Under
|
|
14
|
+
* HQ_API_KEY vaultApiFetch rewrites the path to /v1/keys/search, and the server
|
|
15
|
+
* scopes results to the key's company. When the cloud cannot be reached (a
|
|
16
|
+
* transport failure, not an HTTP error), the command falls back to the local
|
|
17
|
+
* qmd index: the per-company `<co>-assets` collections (mask `**\/*.hqcloud`,
|
|
18
|
+
* see deriveCollections) searched with `--full`, so each hit's placeholder text
|
|
19
|
+
* can be parsed for its asset metadata.
|
|
20
|
+
*
|
|
21
|
+
* Wire contract (hq-pro US-007):
|
|
22
|
+
* body { query: string, kind: "asset", tags?: string[], company?: string }
|
|
23
|
+
* 200 { results: [{ title, snippet, metadata: { asset: {
|
|
24
|
+
* placeholderPath, originalPath, title, tags, mediaKind, vaultKey } } }] }
|
|
25
|
+
* 400 { code: "INVALID_SEARCH_FILTER" }
|
|
26
|
+
*/
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import { Readable } from "node:stream";
|
|
30
|
+
import chalk from "chalk";
|
|
31
|
+
import { cloudPlaceholderPathFor, isCloudPlaceholderName, parseCloudPlaceholder, } from "@indigoai-us/hq-cloud";
|
|
32
|
+
import { buildSearchArgs } from "./search.js";
|
|
33
|
+
import { ASSET_COLLECTION_MASK, deriveCollections, runQmd, } from "../lib/search-index/index.js";
|
|
34
|
+
import { findHqRoot } from "../utils/manifest.js";
|
|
35
|
+
import { networkTransportErrorCode } from "../utils/network-transport-error.js";
|
|
36
|
+
import { resolveVaultCredentialForCapability } from "../utils/resolve-vault-credential.js";
|
|
37
|
+
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
38
|
+
import { defaultLibraryIo, runLibraryAdd, runLibraryTag, } from "./library-add.js";
|
|
39
|
+
import { captionAddedAsset, extractCaptionFrames, runLibraryDescribe, runLocalClaude } from "./library-caption.js";
|
|
40
|
+
import { LIBRARY_LINK_DEFAULT_MINUTES, LIBRARY_LINK_MAX_MINUTES, runLibraryLink } from "./library-link.js";
|
|
41
|
+
/** Mirrors hq-pro's MAX_ASSET_SEARCH_TAGS / tag length bound. */
|
|
42
|
+
export const MAX_LIBRARY_SEARCH_TAGS = 20;
|
|
43
|
+
const MAX_TAG_LENGTH = 100;
|
|
44
|
+
/** Split repeatable / comma-listed `--tag` values into a validated tag list. */
|
|
45
|
+
export function normalizeTags(values) {
|
|
46
|
+
const tags = [
|
|
47
|
+
...new Set(values
|
|
48
|
+
.flatMap((value) => value.split(","))
|
|
49
|
+
.map((tag) => tag.trim())
|
|
50
|
+
.filter((tag) => tag.length > 0)),
|
|
51
|
+
];
|
|
52
|
+
if (tags.length > MAX_LIBRARY_SEARCH_TAGS) {
|
|
53
|
+
throw new Error(`At most ${MAX_LIBRARY_SEARCH_TAGS} --tag values are allowed.`);
|
|
54
|
+
}
|
|
55
|
+
const long = tags.find((tag) => tag.length > MAX_TAG_LENGTH);
|
|
56
|
+
if (long)
|
|
57
|
+
throw new Error(`Tag '${long.slice(0, 20)}…' is longer than ${MAX_TAG_LENGTH} characters.`);
|
|
58
|
+
return tags;
|
|
59
|
+
}
|
|
60
|
+
/** Case-insensitive: every requested tag must be on the asset (hq-pro assetHasTags). */
|
|
61
|
+
export function hasAllTags(assetTags, wanted) {
|
|
62
|
+
const have = new Set(assetTags.map((tag) => tag.trim().toLowerCase()));
|
|
63
|
+
return wanted.every((tag) => have.has(tag.trim().toLowerCase()));
|
|
64
|
+
}
|
|
65
|
+
function asString(value) {
|
|
66
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
67
|
+
}
|
|
68
|
+
/** Map one /v1/search result to a hit; null when it carries no asset metadata. */
|
|
69
|
+
export function hitFromCloudResult(result) {
|
|
70
|
+
if (!result || typeof result !== "object")
|
|
71
|
+
return null;
|
|
72
|
+
const record = result;
|
|
73
|
+
const metadata = record.metadata;
|
|
74
|
+
const asset = metadata?.asset;
|
|
75
|
+
if (!asset || typeof asset !== "object")
|
|
76
|
+
return null;
|
|
77
|
+
const placeholderPath = asString(asset.placeholderPath);
|
|
78
|
+
if (!placeholderPath)
|
|
79
|
+
return null;
|
|
80
|
+
return {
|
|
81
|
+
placeholderPath,
|
|
82
|
+
originalPath: asString(asset.originalPath),
|
|
83
|
+
title: asString(asset.title) ?? asString(record.title) ?? placeholderPath,
|
|
84
|
+
tags: Array.isArray(asset.tags) ? asset.tags.filter((tag) => typeof tag === "string") : [],
|
|
85
|
+
mediaKind: asString(asset.mediaKind),
|
|
86
|
+
vaultKey: asString(asset.vaultKey),
|
|
87
|
+
snippet: asString(record.snippet),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Testable core. Cloud search first; a transport failure (offline, DNS, reset)
|
|
92
|
+
* falls back to the local qmd index. HTTP errors are not a fallback: a 401 or
|
|
93
|
+
* 400 is reported, since the local index cannot answer for the vault.
|
|
94
|
+
*/
|
|
95
|
+
export async function runLibrarySearch(query, options, deps) {
|
|
96
|
+
const trimmed = query.trim();
|
|
97
|
+
if (!trimmed && options.tags.length === 0) {
|
|
98
|
+
throw new Error("Pass a query, at least one --tag, or both.");
|
|
99
|
+
}
|
|
100
|
+
const body = { query: trimmed, kind: "asset" };
|
|
101
|
+
if (options.tags.length > 0)
|
|
102
|
+
body.tags = options.tags;
|
|
103
|
+
if (options.company)
|
|
104
|
+
body.company = options.company;
|
|
105
|
+
let res;
|
|
106
|
+
try {
|
|
107
|
+
const token = await deps.getToken();
|
|
108
|
+
res = await deps.post(token, body);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const code = networkTransportErrorCode(err);
|
|
112
|
+
if (!code)
|
|
113
|
+
throw err;
|
|
114
|
+
return { source: "local", fallbackReason: code, results: deps.searchLocal(trimmed, options) };
|
|
115
|
+
}
|
|
116
|
+
const parsed = (await res.json().catch(() => ({})));
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
const msg = asString(parsed.error) ?? asString(parsed.message) ?? `HTTP ${res.status}`;
|
|
119
|
+
throw new Error(`Library search failed: ${msg}`);
|
|
120
|
+
}
|
|
121
|
+
const rows = Array.isArray(parsed.results) ? parsed.results : [];
|
|
122
|
+
let results = rows.map(hitFromCloudResult).filter((hit) => hit !== null);
|
|
123
|
+
if (options.count !== undefined)
|
|
124
|
+
results = results.slice(0, options.count);
|
|
125
|
+
return { source: "cloud", results };
|
|
126
|
+
}
|
|
127
|
+
/** A local hit, or null for a placeholder that is not a company library asset. */
|
|
128
|
+
export function hitFromPlaceholder(placeholder, tags, snippet) {
|
|
129
|
+
if (!placeholder || placeholder.vault !== "company" || !placeholder.asset)
|
|
130
|
+
return null;
|
|
131
|
+
if (!hasAllTags(placeholder.asset.tags, tags))
|
|
132
|
+
return null;
|
|
133
|
+
return {
|
|
134
|
+
placeholderPath: cloudPlaceholderPathFor(placeholder.path),
|
|
135
|
+
originalPath: placeholder.path,
|
|
136
|
+
title: placeholder.asset.title ?? path.posix.basename(placeholder.path),
|
|
137
|
+
tags: placeholder.asset.tags,
|
|
138
|
+
mediaKind: placeholder.asset.media?.kind,
|
|
139
|
+
vaultKey: placeholder.key,
|
|
140
|
+
snippet: snippet ?? placeholder.asset.description,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/** Asset collections to search, narrowed to one company when asked. */
|
|
144
|
+
export function assetCollections(collections, company) {
|
|
145
|
+
const wanted = company?.trim().toLowerCase();
|
|
146
|
+
return collections.filter((collection) => collection.mask === ASSET_COLLECTION_MASK &&
|
|
147
|
+
(!wanted || collection.name.toLowerCase() === `${wanted}-assets`));
|
|
148
|
+
}
|
|
149
|
+
const SKIP_DIRS = new Set([".git", "node_modules"]);
|
|
150
|
+
function walkPlaceholders(directory, out) {
|
|
151
|
+
let entries;
|
|
152
|
+
try {
|
|
153
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
console.error(chalk.yellow(`Skipped ${directory}: ${err.message}`));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
const child = path.join(directory, entry.name);
|
|
161
|
+
if (entry.isDirectory()) {
|
|
162
|
+
if (!SKIP_DIRS.has(entry.name))
|
|
163
|
+
walkPlaceholders(child, out);
|
|
164
|
+
}
|
|
165
|
+
else if (entry.isFile() && isCloudPlaceholderName(entry.name)) {
|
|
166
|
+
out.push(child);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Search the local qmd `<co>-assets` collections. With a query, qmd ranks
|
|
172
|
+
* hits and `--full` returns each placeholder's text (qmd normalises file
|
|
173
|
+
* paths, so the text, not the path, is the reliable record). Without a query
|
|
174
|
+
* (tags only), the collection directories are listed and filtered by tag.
|
|
175
|
+
*/
|
|
176
|
+
export function searchLocalAssets(query, options, env) {
|
|
177
|
+
const hits = [];
|
|
178
|
+
const seen = new Set();
|
|
179
|
+
const add = (hit) => {
|
|
180
|
+
if (!hit || seen.has(hit.placeholderPath))
|
|
181
|
+
return;
|
|
182
|
+
seen.add(hit.placeholderPath);
|
|
183
|
+
hits.push(hit);
|
|
184
|
+
};
|
|
185
|
+
for (const collection of assetCollections(env.collections, options.company)) {
|
|
186
|
+
if (query) {
|
|
187
|
+
const args = [
|
|
188
|
+
...buildSearchArgs(query, { collection: collection.name, count: options.count, json: true }),
|
|
189
|
+
"--full",
|
|
190
|
+
];
|
|
191
|
+
const { stdout, stderr } = env.runQmd(args);
|
|
192
|
+
let rows;
|
|
193
|
+
try {
|
|
194
|
+
rows = JSON.parse(stdout || "[]");
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
console.error(chalk.yellow(`Local search of ${collection.name} returned no usable results${stderr?.trim() ? `: ${stderr.trim()}` : "."}`));
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (!Array.isArray(rows))
|
|
201
|
+
continue;
|
|
202
|
+
for (const row of rows) {
|
|
203
|
+
const body = row?.body;
|
|
204
|
+
if (typeof body !== "string")
|
|
205
|
+
continue;
|
|
206
|
+
add(hitFromPlaceholder(parseCloudPlaceholder(body), options.tags));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
for (const file of env.listPlaceholders(collection.path)) {
|
|
211
|
+
const text = env.readFile(file);
|
|
212
|
+
if (text !== null)
|
|
213
|
+
add(hitFromPlaceholder(parseCloudPlaceholder(text), options.tags));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return options.count !== undefined ? hits.slice(0, options.count) : hits;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* `hq library ls`: every company library placeholder under `directory`
|
|
221
|
+
* (HQ-relative paths), filtered by tag. Sorted by placeholder path.
|
|
222
|
+
*/
|
|
223
|
+
export function listLibraryAssets(hqRoot, directory, tags, env = defaultFileEnv()) {
|
|
224
|
+
const hits = [];
|
|
225
|
+
const root = fs.realpathSync(hqRoot);
|
|
226
|
+
for (const file of env.listPlaceholders(directory)) {
|
|
227
|
+
const text = env.readFile(file);
|
|
228
|
+
const hit = text === null ? null : hitFromPlaceholder(parseCloudPlaceholder(text), tags);
|
|
229
|
+
if (hit)
|
|
230
|
+
hits.push({ ...hit, placeholderPath: path.relative(root, file).split(path.sep).join("/") });
|
|
231
|
+
}
|
|
232
|
+
return hits;
|
|
233
|
+
}
|
|
234
|
+
function defaultFileEnv() {
|
|
235
|
+
return {
|
|
236
|
+
readFile: (file) => {
|
|
237
|
+
try {
|
|
238
|
+
return fs.readFileSync(file, "utf8");
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
console.error(chalk.yellow(`Skipped ${file}: ${err.message}`));
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
listPlaceholders: (directory) => {
|
|
246
|
+
const out = [];
|
|
247
|
+
walkPlaceholders(directory, out);
|
|
248
|
+
return out.sort();
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function defaultLocalEnv() {
|
|
253
|
+
const hqRoot = findHqRoot();
|
|
254
|
+
return {
|
|
255
|
+
collections: deriveCollections(hqRoot),
|
|
256
|
+
runQmd: (args) => runQmd(args, { cwd: hqRoot }),
|
|
257
|
+
...defaultFileEnv(),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
export function formatLibraryResult(result) {
|
|
261
|
+
const lines = [];
|
|
262
|
+
if (result.source === "local") {
|
|
263
|
+
lines.push(chalk.yellow(`HQ cloud unreachable (${result.fallbackReason}); showing local index results.`));
|
|
264
|
+
}
|
|
265
|
+
if (result.results.length === 0) {
|
|
266
|
+
lines.push("No library assets found.");
|
|
267
|
+
return lines.join("\n");
|
|
268
|
+
}
|
|
269
|
+
for (const hit of result.results) {
|
|
270
|
+
const media = hit.mediaKind ? chalk.dim(` [${hit.mediaKind}]`) : "";
|
|
271
|
+
lines.push(`${chalk.bold(hit.title)}${media}`);
|
|
272
|
+
lines.push(` ${hit.placeholderPath}`);
|
|
273
|
+
if (hit.tags.length > 0)
|
|
274
|
+
lines.push(` tags: ${hit.tags.join(", ")}`);
|
|
275
|
+
if (hit.vaultKey)
|
|
276
|
+
lines.push(chalk.dim(` vault key: ${hit.vaultKey}`));
|
|
277
|
+
}
|
|
278
|
+
return lines.join("\n");
|
|
279
|
+
}
|
|
280
|
+
function collectTag(value, previous) {
|
|
281
|
+
return [...previous, value];
|
|
282
|
+
}
|
|
283
|
+
function formatHits(hits) {
|
|
284
|
+
return formatLibraryResult({ source: "cloud", results: [...hits] });
|
|
285
|
+
}
|
|
286
|
+
export function formatLibraryAddResult(result) {
|
|
287
|
+
return result.items
|
|
288
|
+
.map((item) => {
|
|
289
|
+
if (!item.ok)
|
|
290
|
+
return `${chalk.red("Not added")} ${item.source}: ${item.error}`;
|
|
291
|
+
const line = `${chalk.green("Added")} ${item.placeholderPath} ${chalk.dim(item.uploaded ? "(uploaded, sha256 verified)" : "(already in the vault, not uploaded again)")}`;
|
|
292
|
+
if (!item.caption)
|
|
293
|
+
return line;
|
|
294
|
+
if (item.caption.status === "captioned")
|
|
295
|
+
return `${line}\n ${chalk.dim(`AI caption added (${item.caption.captionedBy}).`)}`;
|
|
296
|
+
return `${line}\n ${chalk.yellow(item.caption.note ?? "Captioning skipped.")}`;
|
|
297
|
+
})
|
|
298
|
+
.join("\n");
|
|
299
|
+
}
|
|
300
|
+
/** GET a presigned URL. Errors never echo the URL, which carries a signature. */
|
|
301
|
+
async function downloadPresigned(url) {
|
|
302
|
+
const res = await fetch(url);
|
|
303
|
+
if (!res.ok || !res.body)
|
|
304
|
+
throw new Error(`Could not download the asset (HTTP ${res.status}).`);
|
|
305
|
+
return Readable.fromWeb(res.body);
|
|
306
|
+
}
|
|
307
|
+
/** The active company membership for `slug`, via /membership/me. */
|
|
308
|
+
async function libraryCompany(token, slug) {
|
|
309
|
+
const { companiesFromMemberships } = await import("./storage.js");
|
|
310
|
+
const res = await vaultApiFetch({ token, path: "/membership/me" });
|
|
311
|
+
if (!res.ok)
|
|
312
|
+
throw new Error(`Could not load your companies (HTTP ${res.status}). Run \`hq login\`.`);
|
|
313
|
+
const company = companiesFromMemberships(await res.json()).find((c) => c.slug === slug);
|
|
314
|
+
if (!company)
|
|
315
|
+
throw new Error(`You are not an active member of '${slug}'.`);
|
|
316
|
+
return { uid: company.uid, slug: company.slug };
|
|
317
|
+
}
|
|
318
|
+
export function registerLibraryCommand(program) {
|
|
319
|
+
const library = program
|
|
320
|
+
.command("library")
|
|
321
|
+
.description("Company media library (.hqcloud asset placeholders)");
|
|
322
|
+
library
|
|
323
|
+
.command("search")
|
|
324
|
+
.description("Search library assets in the cloud, falling back to the local index when offline")
|
|
325
|
+
.argument("[query...]", "Query terms (joined with spaces)")
|
|
326
|
+
.option("--tag <tag>", "Require a tag (repeatable or comma-separated)", collectTag, [])
|
|
327
|
+
.option("--company <slug>", "Restrict to one company")
|
|
328
|
+
.option("-n, --count <count>", "Maximum result count", (value) => Number(value))
|
|
329
|
+
.option("--json", "Print machine-readable JSON")
|
|
330
|
+
.action(async (queryParts = [], options) => {
|
|
331
|
+
try {
|
|
332
|
+
const searchOptions = {
|
|
333
|
+
tags: normalizeTags(options.tag ?? []),
|
|
334
|
+
company: options.company,
|
|
335
|
+
count: options.count,
|
|
336
|
+
};
|
|
337
|
+
const result = await runLibrarySearch(queryParts.join(" "), searchOptions, {
|
|
338
|
+
getToken: async () => (await resolveVaultCredentialForCapability("search:query", "library search", {
|
|
339
|
+
interactive: true,
|
|
340
|
+
})).token,
|
|
341
|
+
post: (token, body) => vaultApiFetch({ token, path: "/v1/search", method: "POST", body }),
|
|
342
|
+
searchLocal: (query, opts) => searchLocalAssets(query, opts, defaultLocalEnv()),
|
|
343
|
+
});
|
|
344
|
+
if (options.json) {
|
|
345
|
+
console.log(JSON.stringify(result, null, 2));
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
console.log(formatLibraryResult(result));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
console.error(chalk.red(err.message));
|
|
353
|
+
process.exitCode = 1;
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
library
|
|
357
|
+
.command("add")
|
|
358
|
+
.description("Upload files or URLs to the company vault and add .hqcloud placeholders to the library")
|
|
359
|
+
.argument("<sources...>", "Local files or http(s) URLs")
|
|
360
|
+
.option("--to <dir>", "Library folder inside companies/<co>/ (default companies/<co>/library)")
|
|
361
|
+
.option("--tags <tags>", "Tags (comma-separated, repeatable)", collectTag, [])
|
|
362
|
+
.option("--title <title>", "Asset title (default: the file name)")
|
|
363
|
+
.option("--note <text>", "Asset description")
|
|
364
|
+
.option("--company <slug>", "Company whose vault receives the files")
|
|
365
|
+
.option("--no-caption", "Skip the AI description and suggested tags for images and videos")
|
|
366
|
+
.option("--json", "Print machine-readable JSON")
|
|
367
|
+
.action(async (sources, options) => {
|
|
368
|
+
try {
|
|
369
|
+
const { token } = await resolveVaultCredentialForCapability("files:write", "library add", {
|
|
370
|
+
interactive: true,
|
|
371
|
+
});
|
|
372
|
+
const result = await runLibraryAdd(sources, {
|
|
373
|
+
company: options.company,
|
|
374
|
+
to: options.to,
|
|
375
|
+
tags: normalizeTags(options.tags ?? []),
|
|
376
|
+
title: options.title,
|
|
377
|
+
note: options.note,
|
|
378
|
+
}, {
|
|
379
|
+
hqRoot: findHqRoot(),
|
|
380
|
+
now: () => new Date(),
|
|
381
|
+
resolveCompany: (slug) => libraryCompany(token, slug),
|
|
382
|
+
presign: (body) => vaultApiFetch({ token, path: "/v1/files/presign", method: "POST", body }),
|
|
383
|
+
...defaultLibraryIo,
|
|
384
|
+
...(options.caption === false
|
|
385
|
+
? {}
|
|
386
|
+
: {
|
|
387
|
+
caption: (request) => captionAddedAsset(request, {
|
|
388
|
+
now: () => new Date(),
|
|
389
|
+
openSource: defaultLibraryIo.openSource,
|
|
390
|
+
runLocal: runLocalClaude,
|
|
391
|
+
extractFrames: (file, kind) => extractCaptionFrames(file, kind),
|
|
392
|
+
}),
|
|
393
|
+
}),
|
|
394
|
+
});
|
|
395
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatLibraryAddResult(result));
|
|
396
|
+
if (result.items.some((item) => !item.ok))
|
|
397
|
+
process.exitCode = 1;
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
console.error(chalk.red(err.message));
|
|
401
|
+
process.exitCode = 1;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
library
|
|
405
|
+
.command("tag")
|
|
406
|
+
.description("Add or remove tags on a library placeholder")
|
|
407
|
+
.argument("<placeholder>", "A .hqcloud placeholder (or the original path)")
|
|
408
|
+
.option("--add <tags>", "Tags to add (comma-separated, repeatable)", collectTag, [])
|
|
409
|
+
.option("--remove <tags>", "Tags to remove (comma-separated, repeatable)", collectTag, [])
|
|
410
|
+
.option("--json", "Print machine-readable JSON")
|
|
411
|
+
.action(async (target, options) => {
|
|
412
|
+
try {
|
|
413
|
+
const result = await runLibraryTag(findHqRoot(), target, {
|
|
414
|
+
add: normalizeTags(options.add ?? []),
|
|
415
|
+
remove: normalizeTags(options.remove ?? []),
|
|
416
|
+
});
|
|
417
|
+
console.log(options.json
|
|
418
|
+
? JSON.stringify(result, null, 2)
|
|
419
|
+
: `${result.changed ? "Updated" : "No change to"} ${result.placeholderPath}: ${result.tags.length > 0 ? result.tags.join(", ") : "(no tags)"}`);
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
console.error(chalk.red(err.message));
|
|
423
|
+
process.exitCode = 1;
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
library
|
|
427
|
+
.command("ls")
|
|
428
|
+
.description("List library placeholders in a folder (default: companies/)")
|
|
429
|
+
.argument("[dir]", "Folder to list, relative to HQ")
|
|
430
|
+
.option("--tag <tag>", "Require a tag (repeatable or comma-separated)", collectTag, [])
|
|
431
|
+
.option("--json", "Print machine-readable JSON")
|
|
432
|
+
.action(async (dir, options) => {
|
|
433
|
+
try {
|
|
434
|
+
const { containedPath } = await import("./storage-offload.js");
|
|
435
|
+
const hqRoot = findHqRoot();
|
|
436
|
+
const hits = listLibraryAssets(hqRoot, containedPath(hqRoot, dir ?? "companies"), normalizeTags(options.tag ?? []));
|
|
437
|
+
console.log(options.json ? JSON.stringify({ results: hits }, null, 2) : formatHits(hits));
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
console.error(chalk.red(err.message));
|
|
441
|
+
process.exitCode = 1;
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
library
|
|
445
|
+
.command("describe")
|
|
446
|
+
.description("Re-run the AI description and suggested tags for a library image or video")
|
|
447
|
+
.argument("<placeholder>", "A .hqcloud placeholder (or the original path)")
|
|
448
|
+
.option("--json", "Print machine-readable JSON")
|
|
449
|
+
.action(async (target, options) => {
|
|
450
|
+
try {
|
|
451
|
+
const { token } = await resolveVaultCredentialForCapability("files:read", "library describe", {
|
|
452
|
+
interactive: true,
|
|
453
|
+
});
|
|
454
|
+
const result = await runLibraryDescribe(target, {
|
|
455
|
+
hqRoot: findHqRoot(),
|
|
456
|
+
now: () => new Date(),
|
|
457
|
+
resolveCompany: (slug) => libraryCompany(token, slug),
|
|
458
|
+
presign: (body) => vaultApiFetch({ token, path: "/v1/files/presign", method: "POST", body }),
|
|
459
|
+
download: downloadPresigned,
|
|
460
|
+
runLocal: runLocalClaude,
|
|
461
|
+
extractFrames: (file, kind) => extractCaptionFrames(file, kind),
|
|
462
|
+
});
|
|
463
|
+
console.log(options.json
|
|
464
|
+
? JSON.stringify(result, null, 2)
|
|
465
|
+
: [
|
|
466
|
+
`Described ${result.placeholderPath} ${chalk.dim(`(${result.captionedBy})`)}`,
|
|
467
|
+
` ${result.description}`,
|
|
468
|
+
` tags: ${result.tags.length > 0 ? result.tags.join(", ") : "(no tags)"}`,
|
|
469
|
+
].join("\n"));
|
|
470
|
+
}
|
|
471
|
+
catch (err) {
|
|
472
|
+
console.error(chalk.red(err.message));
|
|
473
|
+
process.exitCode = 1;
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
library
|
|
477
|
+
.command("fetch")
|
|
478
|
+
.description("Download a library asset next to its placeholder (wraps hq storage fetch)")
|
|
479
|
+
.argument("<placeholder>", "A .hqcloud placeholder (or the original path)")
|
|
480
|
+
.option("--json", "Print machine-readable JSON")
|
|
481
|
+
.action(async (target, options) => {
|
|
482
|
+
try {
|
|
483
|
+
const [{ buildOffloadDeps, formatBytes }, { runFetch }] = await Promise.all([
|
|
484
|
+
import("./storage.js"),
|
|
485
|
+
import("./storage-offload.js"),
|
|
486
|
+
]);
|
|
487
|
+
const result = await runFetch(target, await buildOffloadDeps());
|
|
488
|
+
console.log(options.json
|
|
489
|
+
? JSON.stringify(result, null, 2)
|
|
490
|
+
: `Restored ${result.path} (${formatBytes(result.bytes)}), checksum verified.`);
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
console.error(chalk.red(err.message));
|
|
494
|
+
process.exitCode = 1;
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
library
|
|
498
|
+
.command("link")
|
|
499
|
+
.description("Mint a short-lived download URL for a library asset (needs files:read)")
|
|
500
|
+
.argument("<target>", "A .hqcloud placeholder, its original path, or a vault key (.hq-offload/…)")
|
|
501
|
+
.option("--company <company>", "Company uid (cmp_…), or a slug on a session; default: the placeholder's company or the key's")
|
|
502
|
+
.option("--expires <minutes>", `Link lifetime in minutes, 1-${LIBRARY_LINK_MAX_MINUTES} (default ${LIBRARY_LINK_DEFAULT_MINUTES})`, (value) => Number(value))
|
|
503
|
+
.option("--json", "Print machine-readable JSON")
|
|
504
|
+
.action(async (target, options) => {
|
|
505
|
+
try {
|
|
506
|
+
const credential = await resolveVaultCredentialForCapability("files:read", "library link", {
|
|
507
|
+
interactive: true,
|
|
508
|
+
});
|
|
509
|
+
const { token } = credential;
|
|
510
|
+
const result = await runLibraryLink(target, { company: options.company, expiresMinutes: options.expires }, {
|
|
511
|
+
hqRoot: () => findHqRoot(),
|
|
512
|
+
apiKey: credential.kind === "api-key",
|
|
513
|
+
resolveCompany: (ref) => getCompanyUid(token, ref),
|
|
514
|
+
presign: (body) => vaultApiFetch({ token, path: "/v1/files/presign", method: "POST", body }),
|
|
515
|
+
});
|
|
516
|
+
// The URL is the only stdout line (or the JSON), so scripts can capture it.
|
|
517
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : result.url);
|
|
518
|
+
console.error(chalk.dim(`Link expires ${result.expiresAt ?? `in ${Math.round(result.expiresIn / 60)} min`}. ` +
|
|
519
|
+
"Anyone with it can download the file — never post it in Slack or shared channels."));
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
console.error(chalk.red(err.message));
|
|
523
|
+
process.exitCode = 1;
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
//# sourceMappingURL=library.js.map
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { type PricingPlan } from "../lib/billing/usage-only.js";
|
|
2
3
|
/** Upper bound on list pages one short-id lookup reads (per cursor chain). */
|
|
3
4
|
export declare const SHORT_ID_MAX_PAGES = 50;
|
|
5
|
+
/**
|
|
6
|
+
* The 402 copy for meeting bots. A usage-only company (US-012: the body or the
|
|
7
|
+
* company's billing summary says `pricingPlan: "usage-only"`) has no Workforce
|
|
8
|
+
* plan to buy, so it is told to add a card. Every other company keeps the
|
|
9
|
+
* Workforce copy.
|
|
10
|
+
*/
|
|
11
|
+
export declare function meetingPaymentRequiredLines(body: Record<string, unknown>, resolvePlan?: () => Promise<PricingPlan | null>): Promise<string[]>;
|
|
4
12
|
export declare function registerMeetingsCommand(program: Command): void;
|
|
5
13
|
//# sourceMappingURL=meetings.d.ts.map
|
|
@@ -4,6 +4,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
|
4
4
|
import { vaultApiFetch, getCompanyUid, resolveCallerPersonUid, } from "../utils/vault-api.js";
|
|
5
5
|
import { isPlanGateError, planGateErrorFromPayload, } from "../utils/plan-gate-error.js";
|
|
6
6
|
import { mapWithConcurrency } from "./sync-manifest.js";
|
|
7
|
+
import { isUsageOnly, pricingPlanFromBody, resolveCompanyPricingPlan, usageOnlyCardRequiredLines, } from "../lib/billing/usage-only.js";
|
|
7
8
|
const LIVE_SNAPSHOT_STATUSES = new Set([
|
|
8
9
|
"joining",
|
|
9
10
|
"recording",
|
|
@@ -316,6 +317,27 @@ async function resolveShortId(token, prefix, query) {
|
|
|
316
317
|
: `No meeting matches ID "${prefix}". It may still be processing or attributed to a different company. Pass the full meeting id, or add --company <slug>.`));
|
|
317
318
|
process.exit(1);
|
|
318
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* The 402 copy for meeting bots. A usage-only company (US-012: the body or the
|
|
322
|
+
* company's billing summary says `pricingPlan: "usage-only"`) has no Workforce
|
|
323
|
+
* plan to buy, so it is told to add a card. Every other company keeps the
|
|
324
|
+
* Workforce copy.
|
|
325
|
+
*/
|
|
326
|
+
export async function meetingPaymentRequiredLines(body, resolvePlan = () => resolveCompanyPricingPlan()) {
|
|
327
|
+
const plan = pricingPlanFromBody(body) ?? (await resolvePlan().catch(() => null));
|
|
328
|
+
if (isUsageOnly(plan)) {
|
|
329
|
+
return usageOnlyCardRequiredLines("A meeting notetaker bot").map((line, i) => i === 0 ? chalk.red(line) : chalk.dim(line));
|
|
330
|
+
}
|
|
331
|
+
const detail = typeof body.message === "string"
|
|
332
|
+
? body.message
|
|
333
|
+
: typeof body.error === "string"
|
|
334
|
+
? body.error
|
|
335
|
+
: "Upgrade your company to HQ Workforce to record meetings.";
|
|
336
|
+
return [
|
|
337
|
+
chalk.red("Meeting notetaker bots require the $500/mo HQ Workforce plan."),
|
|
338
|
+
chalk.dim(detail),
|
|
339
|
+
];
|
|
340
|
+
}
|
|
319
341
|
async function handleApiError(res, json = false) {
|
|
320
342
|
const body = (await res.json().catch(() => ({})));
|
|
321
343
|
const planGate = planGateErrorFromPayload(res.status, body);
|
|
@@ -334,8 +356,9 @@ async function handleApiError(res, json = false) {
|
|
|
334
356
|
console.error(chalk.red(body.error ?? "Not found"));
|
|
335
357
|
}
|
|
336
358
|
else if (res.status === 402) {
|
|
337
|
-
|
|
338
|
-
|
|
359
|
+
for (const line of await meetingPaymentRequiredLines(body)) {
|
|
360
|
+
console.error(line);
|
|
361
|
+
}
|
|
339
362
|
}
|
|
340
363
|
else {
|
|
341
364
|
console.error(chalk.red(`API error (${res.status}): ${body.error ?? res.statusText}`));
|
|
@@ -96,19 +96,33 @@ export interface Pointer {
|
|
|
96
96
|
/** Set by sync when a teammate deleted the remote file. */
|
|
97
97
|
orphaned?: boolean;
|
|
98
98
|
}
|
|
99
|
-
|
|
99
|
+
/** Company-vault key for library bytes: content-addressed by sha256, relative to the company bucket. */
|
|
100
|
+
export declare function companyOffloadKey(sha256: string): string;
|
|
101
|
+
/**
|
|
102
|
+
* Render a placeholder. `assetBlock` is the fenced `hq-asset` metadata block
|
|
103
|
+
* (from hq-cloud's `formatCloudAssetBlock`) that `hq library add` appends after
|
|
104
|
+
* the footer so the placeholder is a searchable library asset.
|
|
105
|
+
*/
|
|
106
|
+
export declare function renderPointer(p: Pointer, assetBlock?: string): string;
|
|
100
107
|
/** ETags compare without the surrounding quotes S3 returns. */
|
|
101
108
|
export declare function normalizeEtag(etag: string | undefined): string;
|
|
102
109
|
/**
|
|
103
110
|
* Parse pointer text. Returns null for anything that is not a valid v1 pointer.
|
|
104
111
|
*
|
|
105
|
-
*
|
|
112
|
+
* Three shapes are valid:
|
|
106
113
|
* - an offload copy: `vault: personal`, key = offloadKey(git-blob), with a
|
|
107
114
|
* git blob id to verify against;
|
|
115
|
+
* - a library copy: `vault: company`, key = companyOffloadKey(sha256), with
|
|
116
|
+
* the sha256 to verify against (written by `hq library add`);
|
|
108
117
|
* - a vault object recorded by sync: either vault, a non-offload key, and a
|
|
109
118
|
* `remote-etag` to verify against (sha256 may be `unknown`).
|
|
110
119
|
*/
|
|
111
120
|
export declare function parsePointer(text: string): Pointer | null;
|
|
121
|
+
/**
|
|
122
|
+
* Library copy in a company vault: content-addressed under
|
|
123
|
+
* `.hq-offload/blobs/<sha256>`, verified by that sha256 (no git blob, no ETag).
|
|
124
|
+
*/
|
|
125
|
+
export declare function isCompanyOffloadCopy(p: Pointer): boolean;
|
|
112
126
|
export interface FileDigest {
|
|
113
127
|
size: number;
|
|
114
128
|
sha256: string;
|