@indigoai-us/hq-cli 5.345.168 → 5.345.170
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 +23 -0
- package/dist/command-catalog.generated.d.ts +49 -4
- package/dist/command-catalog.generated.js +60 -4
- package/dist/commands/access.js +7 -2
- package/dist/commands/bot.js +4 -0
- package/dist/commands/cloud.d.ts +20 -0
- package/dist/commands/cloud.js +55 -4
- package/dist/commands/core-checkpoint-native.js +30 -10
- package/dist/commands/feedback-triage.d.ts +43 -1
- package/dist/commands/feedback-triage.js +123 -23
- package/dist/commands/feedback.d.ts +40 -1
- package/dist/commands/feedback.js +195 -7
- package/dist/commands/files.d.ts +42 -0
- package/dist/commands/files.js +223 -30
- package/dist/commands/mesh.js +6 -0
- package/dist/commands/sync-manifest.d.ts +16 -0
- package/dist/commands/sync-manifest.js +17 -1
- package/dist/lib/bot/decisions.d.ts +62 -0
- package/dist/lib/bot/decisions.js +228 -0
- package/dist/lib/bot/index.d.ts +1 -0
- package/dist/lib/bot/index.js +1 -0
- package/dist/lib/bot/run.d.ts +7 -0
- package/dist/lib/bot/run.js +104 -1
- package/dist/lib/daemon/catalog.d.ts +4 -0
- package/dist/lib/daemon/catalog.js +60 -29
- package/dist/lib/daemon/config.d.ts +7 -0
- package/dist/lib/daemon/config.js +32 -0
- package/dist/lib/daemon/run.d.ts +3 -2
- package/dist/lib/daemon/run.js +23 -3
- package/dist/lib/daemon/supervisor.d.ts +1 -0
- package/dist/lib/daemon/supervisor.js +4 -0
- package/dist/lib/daemon/sync-core-canary.d.ts +9 -0
- package/dist/lib/daemon/sync-core-canary.js +56 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +39 -1
- package/dist/lib/doctor/checks/sync-health.js +121 -3
- package/dist/lib/doctor/fix/apply.d.ts +66 -8
- package/dist/lib/doctor/fix/apply.js +278 -80
- package/dist/lib/doctor/fix/process-identity.d.ts +13 -0
- package/dist/lib/doctor/fix/process-identity.js +34 -0
- package/dist/lib/doctor/fix/refresh-sync-handoff.d.ts +201 -0
- package/dist/lib/doctor/fix/refresh-sync-handoff.js +752 -0
- package/dist/lib/doctor/fix/refresh-sync-outcome.d.ts +1 -1
- package/dist/lib/doctor/fix/refresh-sync-outcome.js +13 -0
- package/dist/lib/doctor/types.d.ts +1 -1
- package/dist/lib/doctor/types.js +3 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.js +31 -0
- package/dist/lib/work-context/organize.d.ts +7 -0
- package/dist/lib/work-context/organize.js +21 -1
- package/dist/sentry-before-send.js +14 -0
- package/dist/utils/breadcrumb-buffer.js +15 -4
- package/dist/utils/feedback-diagnostics.js +5 -2
- package/dist/utils/feedback-logs.d.ts +7 -0
- package/dist/utils/feedback-logs.js +37 -3
- package/dist/utils/scrub-diagnostics.d.ts +4 -0
- package/dist/utils/scrub-diagnostics.js +95 -0
- package/package.json +7 -7
package/dist/commands/files.js
CHANGED
|
@@ -196,6 +196,7 @@ export function registerFilesCommand(program) {
|
|
|
196
196
|
.description("Share file paths. foo/ is a private folder (children stay creator-only); quote the shared-folder glob 'foo/*' so the shell passes it literally. Without --with: mint a share-session URL and open it in the browser. With --with: grant access directly to a person, group, or @all.")
|
|
197
197
|
.option("--with <principal>", "Email address, group id, company Outpost id (out_...), or '@all' to share with every active company member")
|
|
198
198
|
.option("--permission <level>", "Permission level (only with --with): read | write")
|
|
199
|
+
.option("--exact", "With --with: grant exactly the object named by <prefix>, even when it looks like a folder (by default a bare folder name such as 'knowledge' is granted as the folder glob 'knowledge/*').")
|
|
199
200
|
.option("--full", "Grant access to the ENTIRE vault (the '*' wildcard prefix) — no need to quote a glob. Requires --with; defaults to write permission.")
|
|
200
201
|
.option("--expires <duration>", "Token expiry duration for share-session URL (e.g. 15m, 1h, 24h). Default 15m. Max 24h.")
|
|
201
202
|
.option("--no-open", "Print the share-session URL but do not launch the browser")
|
|
@@ -208,6 +209,14 @@ export function registerFilesCommand(program) {
|
|
|
208
209
|
console.error(chalk.red("--mention requires --channel <name>."));
|
|
209
210
|
process.exit(1);
|
|
210
211
|
}
|
|
212
|
+
if (opts.exact && opts.with === undefined && opts.channel === undefined) {
|
|
213
|
+
console.error(chalk.red("--exact applies only to a direct grant and requires --with <principal> (or --channel with --mention)."));
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
if (opts.exact && opts.full) {
|
|
217
|
+
console.error(chalk.red("--exact cannot be combined with --full."));
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
211
220
|
const companySlug = files.opts().company;
|
|
212
221
|
const channelTarget = opts.channel !== undefined
|
|
213
222
|
? await resolveShareChannelTarget({
|
|
@@ -258,6 +267,7 @@ export function registerFilesCommand(program) {
|
|
|
258
267
|
principal: opts.with,
|
|
259
268
|
permission: opts.permission,
|
|
260
269
|
companySlug,
|
|
270
|
+
...(opts.exact ? { exact: true } : {}),
|
|
261
271
|
...channelTarget,
|
|
262
272
|
});
|
|
263
273
|
return;
|
|
@@ -278,6 +288,7 @@ export function registerFilesCommand(program) {
|
|
|
278
288
|
principal: uid,
|
|
279
289
|
permission,
|
|
280
290
|
companySlug,
|
|
291
|
+
...(opts.exact ? { exact: true } : {}),
|
|
281
292
|
...(index === 0
|
|
282
293
|
? {
|
|
283
294
|
channelId: channelTarget.channelId,
|
|
@@ -302,14 +313,22 @@ export function registerFilesCommand(program) {
|
|
|
302
313
|
});
|
|
303
314
|
files
|
|
304
315
|
.command("unshare <prefix>")
|
|
305
|
-
.description("Remove a named file access grant, or remove an empty ACL row or grantee-less entries without --with."
|
|
316
|
+
.description("Remove a named file access grant, or remove an empty ACL row or grantee-less entries without --with. " +
|
|
317
|
+
"With --with, a bare name such as 'knowledge' first removes the exact 'knowledge' grant (rows from older " +
|
|
318
|
+
"`hq files share` versions) and, when none matches, the folder-glob 'knowledge/*' grant. Without --with, " +
|
|
319
|
+
"the prefix is used exactly as typed: pass 'knowledge/*' (quoted) to clean up the folder-glob row.")
|
|
306
320
|
.option("--with <principal>", "Person uid, agent uid, company Outpost id (out_...), email address, group id, or '@all' to remove the grant")
|
|
321
|
+
.option("--exact", "With --with: remove the grant on exactly the object named by <prefix>. By default a bare folder name such as 'knowledge' resolves to the folder glob 'knowledge/*', the same way `hq files share` resolves it.")
|
|
307
322
|
.action(async (prefix, opts) => {
|
|
308
323
|
try {
|
|
324
|
+
if (opts.exact && !opts.with) {
|
|
325
|
+
console.error(chalk.red("--exact requires --with <principal>."));
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
309
328
|
// Unshare MUTATES access with no preview. Anchored content paths remain
|
|
310
329
|
// refused; the exact company-root ACL row is the sole exception and
|
|
311
330
|
// resolves its anchor against the selected company before revoking.
|
|
312
|
-
|
|
331
|
+
let canonicalPrefix = normalizeCompanyFilePrefix(prefix, {
|
|
313
332
|
mutating: true,
|
|
314
333
|
allowCompanyRootAclRow: true,
|
|
315
334
|
...(files.opts().company !== undefined
|
|
@@ -385,40 +404,66 @@ export function registerFilesCommand(program) {
|
|
|
385
404
|
: `${cleanupMessage || `Removed empty ACL pattern '${canonicalPrefix}'. `}It now follows its parent folder's access, so anyone who can see that parent can see this folder.`));
|
|
386
405
|
return;
|
|
387
406
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
407
|
+
// A bare name may carry an exact-key row from older `hq files share`
|
|
408
|
+
// versions (DEV-5173) or the folder-glob row the current share
|
|
409
|
+
// creates. Try the exact row first, then `X/*` unless the vault says
|
|
410
|
+
// X is only a file. A revoke only reduces access, so the vault lookup
|
|
411
|
+
// never blocks it: a deleted path or a failed lookup still tries both.
|
|
412
|
+
const bare = !opts.exact &&
|
|
413
|
+
companyRootSlug === undefined &&
|
|
414
|
+
looksLikeBareFolderPrefix(canonicalPrefix) &&
|
|
415
|
+
malformedBareSharePrefix(canonicalPrefix) === null;
|
|
416
|
+
const candidates = [canonicalPrefix];
|
|
417
|
+
const tried = [];
|
|
418
|
+
let aclMissingEverywhere = true;
|
|
419
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
420
|
+
const candidate = candidates[index];
|
|
421
|
+
tried.push(candidate);
|
|
422
|
+
const res = await vaultApiFetch({
|
|
423
|
+
token,
|
|
424
|
+
path: `/files/${encodeURIComponent(companyUid)}/acl/revoke`,
|
|
425
|
+
method: "POST",
|
|
426
|
+
body: { prefix: candidate, granteeType, granteeId },
|
|
427
|
+
});
|
|
428
|
+
if (res.ok) {
|
|
429
|
+
console.log(chalk.green(`Removed grant for ${principalLabel} on '${displayAclPattern(candidate)}'`));
|
|
430
|
+
if (bare && candidate === canonicalPrefix) {
|
|
431
|
+
console.log(chalk.dim(`This removed the exact '${candidate}' grant. A grant for ${principalLabel} on ` +
|
|
432
|
+
`'${candidate}/*', if one exists, remains; remove it with ` +
|
|
433
|
+
`hq files unshare '${candidate}/*' --with ${principal}.`));
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
395
437
|
const body = await res.json().catch(() => ({}));
|
|
438
|
+
if (res.status === 404) {
|
|
439
|
+
// A 404 means ZERO rows were removed — never report a green success
|
|
440
|
+
// that implies a revoke happened. ACL_NOT_FOUND means the prefix
|
|
441
|
+
// has no ACL at all; GRANT_NOT_FOUND (or an older server with no
|
|
442
|
+
// code) means the ACL exists but no grant matched this principal.
|
|
443
|
+
if (body.code !== "ACL_NOT_FOUND")
|
|
444
|
+
aclMissingEverywhere = false;
|
|
445
|
+
if (bare && candidate === canonicalPrefix) {
|
|
446
|
+
const resolution = await resolveBareSharePrefix({
|
|
447
|
+
token,
|
|
448
|
+
companyUid,
|
|
449
|
+
prefix: canonicalPrefix,
|
|
450
|
+
});
|
|
451
|
+
// Fall back to the folder glob unless the vault confirms X is a
|
|
452
|
+
// file with nothing under it.
|
|
453
|
+
if (resolution.kind !== "unchanged")
|
|
454
|
+
candidates.push(`${canonicalPrefix}/*`);
|
|
455
|
+
}
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
396
458
|
if (res.status === 401) {
|
|
397
459
|
console.error(chalk.red("Not authenticated — please run `hq login`"));
|
|
398
460
|
}
|
|
399
461
|
else if (res.status === 403) {
|
|
400
462
|
console.error(chalk.red("Not authorized to modify this file prefix's ACL"));
|
|
401
|
-
await offerAccessLadder(
|
|
463
|
+
await offerAccessLadder(candidate, {
|
|
402
464
|
company: files.opts().company,
|
|
403
465
|
});
|
|
404
466
|
}
|
|
405
|
-
else if (res.status === 404) {
|
|
406
|
-
// A 404 means ZERO rows were removed — never report a green success
|
|
407
|
-
// that implies a revoke happened. Distinguish the two causes:
|
|
408
|
-
// ACL_NOT_FOUND — the prefix has no ACL at all (nothing was ever
|
|
409
|
-
// shared here): genuinely, benignly absent.
|
|
410
|
-
// GRANT_NOT_FOUND (or an older server with no code) — the ACL
|
|
411
|
-
// exists but no grant matched this principal:
|
|
412
|
-
// surface it as a warning so a typo'd/stale
|
|
413
|
-
// principal isn't mistaken for a real removal.
|
|
414
|
-
if (body.code === "ACL_NOT_FOUND") {
|
|
415
|
-
console.log(chalk.dim(`Nothing to remove — '${canonicalPrefix}' has no sharing grants.`));
|
|
416
|
-
}
|
|
417
|
-
else {
|
|
418
|
-
console.warn(chalk.yellow(`No matching grant for ${principalLabel} on '${canonicalPrefix}' — nothing was removed.`));
|
|
419
|
-
}
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
467
|
else if (res.status >= 500) {
|
|
423
468
|
console.error(chalk.red(`Server error: ${body.error ?? res.statusText}`));
|
|
424
469
|
}
|
|
@@ -427,7 +472,18 @@ export function registerFilesCommand(program) {
|
|
|
427
472
|
}
|
|
428
473
|
process.exit(1);
|
|
429
474
|
}
|
|
430
|
-
|
|
475
|
+
if (tried.length === 1) {
|
|
476
|
+
if (aclMissingEverywhere) {
|
|
477
|
+
console.log(chalk.dim(`Nothing to remove — '${canonicalPrefix}' has no sharing grants.`));
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
console.warn(chalk.yellow(`No matching grant for ${principalLabel} on '${canonicalPrefix}' — nothing was removed.`));
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
console.warn(chalk.yellow(`No matching grant for ${principalLabel} on ${tried.map((row) => `'${displayAclPattern(row)}'`).join(" or ")} ` +
|
|
485
|
+
`— nothing was removed. Check the grant with hq files acl, then re-run with --exact for the exact ` +
|
|
486
|
+
`'${canonicalPrefix}' row or pass '${canonicalPrefix}/*' (quoted) for the folder glob.`));
|
|
431
487
|
}
|
|
432
488
|
catch (err) {
|
|
433
489
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
@@ -811,14 +867,130 @@ async function resolveMentionPersonUid(mention, companySlug) {
|
|
|
811
867
|
console.error(chalk.red(`Could not resolve --mention '${trimmed}' to a person.`));
|
|
812
868
|
process.exit(1);
|
|
813
869
|
}
|
|
870
|
+
/**
|
|
871
|
+
* True for a prefix that might name a folder without saying so: no trailing
|
|
872
|
+
* `/` (private folder), no `/*` (shared folder glob), and not the `*`
|
|
873
|
+
* wildcard, and a last segment with no extension (a name with an extension is
|
|
874
|
+
* taken as the file it names). The vault ACL treats such a path as an exact
|
|
875
|
+
* object key, so
|
|
876
|
+
* `hq files share knowledge --with x` used to grant an object named
|
|
877
|
+
* "knowledge" and nothing under `knowledge/` (DEV-5173: members were blocked
|
|
878
|
+
* under a folder their admin had "shared"). The name alone does not decide
|
|
879
|
+
* whether it is a folder (`Makefile`, `bin/hq`); resolveBareSharePrefix asks
|
|
880
|
+
* the vault.
|
|
881
|
+
*/
|
|
882
|
+
export function looksLikeBareFolderPrefix(prefix) {
|
|
883
|
+
if (prefix === "*" || prefix.endsWith("/") || prefix.endsWith("/*"))
|
|
884
|
+
return false;
|
|
885
|
+
const last = prefix.slice(prefix.lastIndexOf("/") + 1);
|
|
886
|
+
return last.length > 0 && !last.includes(".");
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Refuse a bare prefix that would become a malformed grant if it were
|
|
890
|
+
* rewritten to a folder glob: a `*` inside a segment (`**`, `know*`) or an
|
|
891
|
+
* empty segment (`/knowledge`, `a//b`). Returns the error text, or null.
|
|
892
|
+
*/
|
|
893
|
+
export function malformedBareSharePrefix(prefix) {
|
|
894
|
+
if (!looksLikeBareFolderPrefix(prefix))
|
|
895
|
+
return null;
|
|
896
|
+
if (prefix.includes("*")) {
|
|
897
|
+
return (`'${prefix}' has a '*' inside a path segment. Only a whole trailing '/*' segment ` +
|
|
898
|
+
`(for example 'knowledge/*') or '*' alone (--full) is a glob.`);
|
|
899
|
+
}
|
|
900
|
+
if (prefix.split("/").some((segment) => segment.length === 0)) {
|
|
901
|
+
return `'${prefix}' has an empty path segment. Pass a company-relative path such as 'knowledge'.`;
|
|
902
|
+
}
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Decide what a bare prefix means by looking it up in the vault. Rewrite `X`
|
|
907
|
+
* to `X/*` only when `X/` has children and no object `X` exists. Keep `X` when
|
|
908
|
+
* only the object exists. Refuse when both exist, when neither does, or when
|
|
909
|
+
* the lookup fails; the caller exits non-zero and points to --exact or `X/*`.
|
|
910
|
+
*/
|
|
911
|
+
export async function resolveBareSharePrefix(params) {
|
|
912
|
+
const { prefix } = params;
|
|
913
|
+
if (!looksLikeBareFolderPrefix(prefix))
|
|
914
|
+
return { kind: "unchanged", prefix };
|
|
915
|
+
const malformed = malformedBareSharePrefix(prefix);
|
|
916
|
+
if (malformed)
|
|
917
|
+
return { kind: "refused", prefix, message: malformed };
|
|
918
|
+
const folderGlob = `${prefix}/*`;
|
|
919
|
+
const choose = `Re-run with --exact to use exactly the object '${prefix}', or pass '${folderGlob}' ` +
|
|
920
|
+
`(quoted) to cover everything under the folder.`;
|
|
921
|
+
const list = async (listPrefix) => {
|
|
922
|
+
const res = await vaultApiFetch({
|
|
923
|
+
token: params.token,
|
|
924
|
+
path: "/v1/files/list",
|
|
925
|
+
query: { company: params.companyUid, prefix: listPrefix },
|
|
926
|
+
});
|
|
927
|
+
if (!res.ok)
|
|
928
|
+
throw new Error(`files list failed (${res.status})`);
|
|
929
|
+
const body = (await res.json());
|
|
930
|
+
return Array.isArray(body.objects) ? body.objects : [];
|
|
931
|
+
};
|
|
932
|
+
let objectExists;
|
|
933
|
+
let folderHasChildren;
|
|
934
|
+
try {
|
|
935
|
+
// The object `X` sorts first among keys that start with `X`, so the first
|
|
936
|
+
// page holds it when it exists. Children are listed separately so sibling
|
|
937
|
+
// keys such as `X-old.md` cannot push them off the page.
|
|
938
|
+
objectExists = (await list(prefix)).some((object) => object.key === prefix);
|
|
939
|
+
folderHasChildren = (await list(`${prefix}/`)).length > 0;
|
|
940
|
+
}
|
|
941
|
+
catch (error) {
|
|
942
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
943
|
+
return {
|
|
944
|
+
kind: "refused",
|
|
945
|
+
prefix,
|
|
946
|
+
message: `Could not check whether '${prefix}' is a file or a folder (${reason}). ${choose}`,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
if (folderHasChildren && !objectExists)
|
|
950
|
+
return { kind: "folder", prefix, folderGlob };
|
|
951
|
+
if (objectExists && !folderHasChildren)
|
|
952
|
+
return { kind: "unchanged", prefix };
|
|
953
|
+
return {
|
|
954
|
+
kind: "refused",
|
|
955
|
+
prefix,
|
|
956
|
+
message: objectExists
|
|
957
|
+
? `'${prefix}' is both a file and a folder in the vault. ${choose}`
|
|
958
|
+
: `Nothing exists at '${prefix}' or under '${prefix}/' in the vault, so it is unclear ` +
|
|
959
|
+
`whether it is a file or a folder. ${choose}`,
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
/** Resolve a bare prefix for a mutating command: print the note, or exit 1 on refusal. */
|
|
963
|
+
async function resolveBareSharePrefixOrExit(params) {
|
|
964
|
+
if (params.exact)
|
|
965
|
+
return params.prefix;
|
|
966
|
+
const resolution = await resolveBareSharePrefix(params);
|
|
967
|
+
if (resolution.kind === "refused") {
|
|
968
|
+
console.error(chalk.red(resolution.message));
|
|
969
|
+
process.exit(1);
|
|
970
|
+
}
|
|
971
|
+
if (resolution.kind === "folder") {
|
|
972
|
+
console.error(chalk.yellow(`Note: '${resolution.prefix}' is a folder in the vault, so this uses the folder glob ` +
|
|
973
|
+
`'${resolution.folderGlob}' (everything under it). Pass --exact to use the exact ` +
|
|
974
|
+
`object key '${resolution.prefix}' instead.`));
|
|
975
|
+
return resolution.folderGlob;
|
|
976
|
+
}
|
|
977
|
+
return resolution.prefix;
|
|
978
|
+
}
|
|
814
979
|
async function runDirectGrant(params) {
|
|
815
|
-
|
|
980
|
+
let canonicalPrefix = normalizeCompanyFilePrefix(params.prefix, {
|
|
816
981
|
mutating: true,
|
|
817
982
|
rootAdvice: "Use --full --with <principal> to share the entire vault, or pass a bounded path.",
|
|
818
983
|
...(params.companySlug !== undefined
|
|
819
984
|
? { companySlug: params.companySlug }
|
|
820
985
|
: {}),
|
|
821
986
|
});
|
|
987
|
+
if (!params.exact) {
|
|
988
|
+
const malformed = malformedBareSharePrefix(canonicalPrefix);
|
|
989
|
+
if (malformed) {
|
|
990
|
+
console.error(chalk.red(malformed));
|
|
991
|
+
process.exit(1);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
822
994
|
if (!params.permission) {
|
|
823
995
|
console.error(chalk.red("--permission is required when --with is set (read | write)"));
|
|
824
996
|
process.exit(1);
|
|
@@ -864,6 +1036,14 @@ async function runDirectGrant(params) {
|
|
|
864
1036
|
const principalLabel = isAll ? "everyone in the company" : granteeId;
|
|
865
1037
|
const token = (await resolveVaultCredential()).token;
|
|
866
1038
|
const companyUid = await getCompanyUid(token, params.companySlug);
|
|
1039
|
+
if (!params.fullVault) {
|
|
1040
|
+
canonicalPrefix = await resolveBareSharePrefixOrExit({
|
|
1041
|
+
token,
|
|
1042
|
+
companyUid,
|
|
1043
|
+
prefix: canonicalPrefix,
|
|
1044
|
+
exact: params.exact,
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
867
1047
|
let res = await vaultApiFetch({
|
|
868
1048
|
token,
|
|
869
1049
|
path: `/files/${encodeURIComponent(companyUid)}/acl/grant`,
|
|
@@ -966,13 +1146,20 @@ async function runDirectGrant(params) {
|
|
|
966
1146
|
async function runShareSession(params) {
|
|
967
1147
|
// Validate every path before minting a session; an anchored path must never
|
|
968
1148
|
// silently become a share of the corresponding company-relative objects.
|
|
969
|
-
const
|
|
1149
|
+
const canonicalPaths = params.paths.map((prefix) => normalizeCompanyFilePrefix(prefix, {
|
|
970
1150
|
mutating: true,
|
|
971
1151
|
rootAdvice: "Use --full --with <principal> to share the entire vault, or pass a bounded path.",
|
|
972
1152
|
...(params.companySlug !== undefined
|
|
973
1153
|
? { companySlug: params.companySlug }
|
|
974
1154
|
: {}),
|
|
975
1155
|
}));
|
|
1156
|
+
for (const prefix of canonicalPaths) {
|
|
1157
|
+
const malformed = malformedBareSharePrefix(prefix);
|
|
1158
|
+
if (malformed) {
|
|
1159
|
+
console.error(chalk.red(malformed));
|
|
1160
|
+
process.exit(1);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
976
1163
|
let expiresInMs;
|
|
977
1164
|
if (params.expires !== undefined) {
|
|
978
1165
|
const parsed = parseDuration(params.expires);
|
|
@@ -988,6 +1175,12 @@ async function runShareSession(params) {
|
|
|
988
1175
|
}
|
|
989
1176
|
const token = (await resolveVaultCredential()).token;
|
|
990
1177
|
const companyUid = await getCompanyUid(token, params.companySlug);
|
|
1178
|
+
// A bare folder name in a share session would create the same exact-key row
|
|
1179
|
+
// that blocked members in DEV-5173, so resolve each path like a direct grant.
|
|
1180
|
+
const normalizedPaths = [];
|
|
1181
|
+
for (const prefix of canonicalPaths) {
|
|
1182
|
+
normalizedPaths.push(await resolveBareSharePrefixOrExit({ token, companyUid, prefix }));
|
|
1183
|
+
}
|
|
991
1184
|
let session;
|
|
992
1185
|
try {
|
|
993
1186
|
session = await mintShareSession({
|
package/dist/commands/mesh.js
CHANGED
|
@@ -2070,11 +2070,16 @@ async function runContextOrganize(opts) {
|
|
|
2070
2070
|
!opts.untracked);
|
|
2071
2071
|
if (isList) {
|
|
2072
2072
|
let fetchCandidates;
|
|
2073
|
+
let resolveCompanyUid;
|
|
2073
2074
|
let offline = Boolean(opts.offline);
|
|
2074
2075
|
if (!offline) {
|
|
2075
2076
|
try {
|
|
2076
2077
|
const token = await requireToken();
|
|
2077
2078
|
fetchCandidates = createCandidatesFetcher({ token });
|
|
2079
|
+
resolveCompanyUid = async (slug) => {
|
|
2080
|
+
const membership = await resolveActiveMembershipCompany(token, slug, AbortSignal.timeout(ONLINE_MEMBERSHIP_VALIDATION_TIMEOUT_MS));
|
|
2081
|
+
return membership?.companyUid ?? null;
|
|
2082
|
+
};
|
|
2078
2083
|
}
|
|
2079
2084
|
catch {
|
|
2080
2085
|
offline = true;
|
|
@@ -2084,6 +2089,7 @@ async function runContextOrganize(opts) {
|
|
|
2084
2089
|
sessionId,
|
|
2085
2090
|
root,
|
|
2086
2091
|
fetchCandidates,
|
|
2092
|
+
resolveCompanyUid,
|
|
2087
2093
|
offline,
|
|
2088
2094
|
});
|
|
2089
2095
|
if (opts.json || opts.machine) {
|
|
@@ -103,7 +103,23 @@ export interface RunSyncManifestOptions {
|
|
|
103
103
|
* of forcing a pass on every tick.
|
|
104
104
|
*/
|
|
105
105
|
respectThrottle?: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Print one JSON line ({@link manifestOutcomeLine}) instead of the human
|
|
108
|
+
* summary. `hq doctor --fix` reads it, because the exit code is 0 for
|
|
109
|
+
* `soft_skipped`, `throttled`, `disabled`, `locked` and
|
|
110
|
+
* `resend_full_scheduled` too, and none of those uploaded anything.
|
|
111
|
+
*/
|
|
112
|
+
json?: boolean;
|
|
113
|
+
}
|
|
114
|
+
/** The machine-readable outcome `--json` prints. Fixed tokens only, no paths. */
|
|
115
|
+
export interface ManifestOutcomeLine {
|
|
116
|
+
status: string;
|
|
117
|
+
mode?: string;
|
|
118
|
+
errorKind?: string;
|
|
119
|
+
errorStatus?: number;
|
|
106
120
|
}
|
|
121
|
+
/** Build the `--json` line for one pass. The free-text error detail is left out. */
|
|
122
|
+
export declare function manifestOutcomeLine(result: RunManifestUploadPassResult): ManifestOutcomeLine;
|
|
107
123
|
export interface RunSyncManifestOutcome {
|
|
108
124
|
result: RunManifestUploadPassResult;
|
|
109
125
|
scope: ResolvedManifestScope;
|
|
@@ -64,6 +64,15 @@ export const MANIFEST_UPLOAD_PATH = "/v1/sync-manifest/upload";
|
|
|
64
64
|
export { MANIFEST_DISABLED_ENV, manifestUploadsDisabled };
|
|
65
65
|
/** The literal `--scope` value that selects the personal (non-company) tree. */
|
|
66
66
|
export const PERSONAL_SCOPE = "personal";
|
|
67
|
+
/** Build the `--json` line for one pass. The free-text error detail is left out. */
|
|
68
|
+
export function manifestOutcomeLine(result) {
|
|
69
|
+
return {
|
|
70
|
+
status: result.status,
|
|
71
|
+
...(result.mode ? { mode: result.mode } : {}),
|
|
72
|
+
...(result.error?.kind ? { errorKind: result.error.kind } : {}),
|
|
73
|
+
...(typeof result.error?.status === "number" ? { errorStatus: result.error.status } : {}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
67
76
|
// ── Pure helpers ────────────────────────────────────────────────────────────
|
|
68
77
|
/**
|
|
69
78
|
* Resolve `--scope` into the wire scope.
|
|
@@ -301,7 +310,9 @@ export async function runSyncManifest(options, deps) {
|
|
|
301
310
|
status: "disabled",
|
|
302
311
|
scopeKey: "unknown",
|
|
303
312
|
};
|
|
304
|
-
deps.log(
|
|
313
|
+
deps.log(options.json === true
|
|
314
|
+
? JSON.stringify(manifestOutcomeLine(result))
|
|
315
|
+
: `${chalk.dim("·")} Manifest upload disabled by ${MANIFEST_DISABLED_ENV} — nothing to do.`);
|
|
305
316
|
return { result, scope: { kind: "personal" }, exitCode: 0 };
|
|
306
317
|
}
|
|
307
318
|
const scope = await resolveManifestScope(options.scopeArg, options.hqRoot, deps.resolveCompanyUid);
|
|
@@ -326,6 +337,9 @@ export async function runSyncManifest(options, deps) {
|
|
|
326
337
|
if (print) {
|
|
327
338
|
deps.log(JSON.stringify(result.chunks ?? [], null, 2));
|
|
328
339
|
}
|
|
340
|
+
else if (options.json === true) {
|
|
341
|
+
deps.log(JSON.stringify(manifestOutcomeLine(result)));
|
|
342
|
+
}
|
|
329
343
|
else {
|
|
330
344
|
deps.log(formatManifestSummary(result, scope));
|
|
331
345
|
}
|
|
@@ -342,6 +356,7 @@ export function registerSyncManifestCommand(syncCmd) {
|
|
|
342
356
|
.option("--scope <scope>", `'${PERSONAL_SCOPE}' or a company slug (defaults to the active company, else ${PERSONAL_SCOPE})`)
|
|
343
357
|
.option("--full", "Send a fresh full baseline instead of a delta")
|
|
344
358
|
.option("--print", "Build and print the chunks as JSON; upload nothing")
|
|
359
|
+
.option("--json", "Print the pass outcome as one JSON line instead of a summary")
|
|
345
360
|
.option("--respect-throttle", "Honour the 24h per-scope upload throttle instead of forcing a pass (for scheduled callers)")
|
|
346
361
|
.option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
|
|
347
362
|
.action(async (options) => {
|
|
@@ -360,6 +375,7 @@ export function registerSyncManifestCommand(syncCmd) {
|
|
|
360
375
|
full: options.full,
|
|
361
376
|
print: options.print,
|
|
362
377
|
respectThrottle: options.respectThrottle,
|
|
378
|
+
json: options.json,
|
|
363
379
|
}, createDefaultDeps());
|
|
364
380
|
if (outcome.exitCode !== 0)
|
|
365
381
|
process.exit(outcome.exitCode);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type Decision, type DecisionOption } from "../decisions/client.js";
|
|
2
|
+
export interface DecisionDmClient {
|
|
3
|
+
list(open?: boolean): Promise<Decision[]>;
|
|
4
|
+
answer(id: string, choiceId: string, source: "thread"): Promise<Decision>;
|
|
5
|
+
}
|
|
6
|
+
export interface DecisionDmInput {
|
|
7
|
+
text: string;
|
|
8
|
+
speakerUid: string | undefined;
|
|
9
|
+
rootEventId?: string;
|
|
10
|
+
ownerUid: string;
|
|
11
|
+
agentUid: string;
|
|
12
|
+
/** The owner's company uids. Null when they could not be read: refuse. */
|
|
13
|
+
companyUids: readonly string[] | null;
|
|
14
|
+
nowMs: number;
|
|
15
|
+
/** Decision ids already restated after 24 hours. */
|
|
16
|
+
restated: ReadonlySet<string>;
|
|
17
|
+
}
|
|
18
|
+
export type DecisionDmResult = {
|
|
19
|
+
kind: "none";
|
|
20
|
+
notLive?: true;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "reply";
|
|
23
|
+
text: string;
|
|
24
|
+
decisionId?: string;
|
|
25
|
+
recorded: boolean;
|
|
26
|
+
notLive?: true;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "model";
|
|
29
|
+
note: string;
|
|
30
|
+
decisionId: string;
|
|
31
|
+
restate?: string;
|
|
32
|
+
};
|
|
33
|
+
export declare const DECISIONS_NOT_LIVE_REPLY = "Decisions are not live on this HQ server yet (the decisions API returned 404), so I did not record that pick.";
|
|
34
|
+
export declare const RESTATE_AFTER_MS: number;
|
|
35
|
+
/** Same key hq-pro uses for the owner-bot DM pair. */
|
|
36
|
+
export declare function decisionTalkThreadId(ownerUid: string, botUid: string): string;
|
|
37
|
+
export declare function looksLikePick(text: string): boolean;
|
|
38
|
+
export declare function matchDecisionPick(text: string, options: readonly DecisionOption[], opts?: {
|
|
39
|
+
inCardThread?: boolean;
|
|
40
|
+
}): {
|
|
41
|
+
kind: "pick";
|
|
42
|
+
option: DecisionOption;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "ambiguous";
|
|
45
|
+
options: DecisionOption[];
|
|
46
|
+
} | {
|
|
47
|
+
kind: "none";
|
|
48
|
+
};
|
|
49
|
+
/** Full step, for the one restate after 24 hours. Never says "waiting on". */
|
|
50
|
+
export declare function restateDecision(decision: Decision): string;
|
|
51
|
+
export declare function decisionContextNote(decision: Decision): string;
|
|
52
|
+
export declare function handleDecisionDm(input: DecisionDmInput, client: DecisionDmClient): Promise<DecisionDmResult>;
|
|
53
|
+
export interface DecisionTalkState {
|
|
54
|
+
v: 1;
|
|
55
|
+
restated: Record<string, string>;
|
|
56
|
+
/** Replies decided but not yet delivered, by inbox message id. Resent on retry so a saved pick is never re-run. */
|
|
57
|
+
pending?: Record<string, string>;
|
|
58
|
+
}
|
|
59
|
+
export declare function decisionTalkStatePath(dir: string): string;
|
|
60
|
+
export declare function readDecisionTalkState(dir: string): DecisionTalkState;
|
|
61
|
+
export declare function writeDecisionTalkState(dir: string, state: DecisionTalkState): void;
|
|
62
|
+
//# sourceMappingURL=decisions.d.ts.map
|