@indigoai-us/hq-cli 5.345.49 → 5.345.51
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 +15 -0
- package/dist/command-catalog.generated.d.ts +56 -11
- package/dist/command-catalog.generated.js +67 -11
- package/dist/commands/core-hq-session-native.js +2 -2
- package/dist/commands/core-resolve-company-native.js +2 -2
- package/dist/commands/db-destroy.js +13 -11
- package/dist/commands/db-dump.js +3 -3
- package/dist/commands/db-remote-deps.d.ts +3 -0
- package/dist/commands/db-remote-deps.js +13 -0
- package/dist/commands/db-sql.js +4 -3
- package/dist/commands/db-usage.js +4 -3
- package/dist/commands/dm-forward-flag.d.ts +4 -0
- package/dist/commands/dm-forward-flag.js +7 -0
- package/dist/commands/dm.d.ts +8 -0
- package/dist/commands/dm.js +219 -27
- package/dist/commands/lanes.js +52 -11
- package/dist/commands/secrets.js +2 -2
- package/dist/commands/workers.js +2 -2
- package/dist/lib/bot/daemon.d.ts +1 -1
- package/dist/lib/bot/daemon.js +35 -29
- package/dist/lib/daemon/hq-cloud.d.ts +2 -2
- package/dist/lib/daemon/hq-cloud.js +38 -9
- package/dist/lib/daemon/install.js +34 -3
- package/dist/lib/daemon/run.js +45 -19
- package/dist/lib/db/app-ref.d.ts +21 -0
- package/dist/lib/db/app-ref.js +45 -0
- package/dist/lib/doctor/checks/bots.js +3 -8
- package/dist/lib/lanes/keep-alive-audit.d.ts +5 -1
- package/dist/lib/lanes/keep-alive-audit.js +85 -3
- package/dist/lib/lanes/outpost-dispatch.d.ts +1 -0
- package/dist/lib/lanes/outpost-dispatch.js +2 -0
- package/dist/lib/mesh/live/daemon/install.js +28 -15
- package/dist/lib/service-manager/index.d.ts +3 -0
- package/dist/lib/service-manager/index.js +22 -6
- package/dist/lib/service-manager/launchd.d.ts +40 -3
- package/dist/lib/service-manager/launchd.js +106 -5
- package/dist/lib/service-manager/migration-state.d.ts +12 -0
- package/dist/lib/service-manager/migration-state.js +46 -0
- package/dist/lib/service-manager/migration.d.ts +22 -0
- package/dist/lib/service-manager/migration.js +85 -0
- package/dist/lib/service-manager/startup-migration.d.ts +24 -0
- package/dist/lib/service-manager/startup-migration.js +27 -0
- package/dist/lib/service-manager/types.d.ts +3 -0
- package/dist/main.js +22 -0
- package/dist/native-fast-route.d.ts +4 -0
- package/dist/native-fast-route.js +20 -0
- package/dist/utils/resident-package-preload.js +2 -1
- package/package.json +1 -1
package/dist/commands/dm.js
CHANGED
|
@@ -5,6 +5,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
|
|
|
5
5
|
import { isMachineIdentity } from "@indigoai-us/hq-cloud";
|
|
6
6
|
import { peekIdToken } from "../utils/id-token.js";
|
|
7
7
|
import { EMAIL_PATTERN, RECIPIENT_UID_PATTERN } from "../lib/notify-recipient.js";
|
|
8
|
+
import { dmForwardCliEnabled } from "./dm-forward-flag.js";
|
|
8
9
|
/**
|
|
9
10
|
* Classify a recipient arg as an email or a person/agent uid. Mirrors the
|
|
10
11
|
* email/uid heuristic used by `hq members`. Returns null for neither.
|
|
@@ -346,6 +347,32 @@ function friendlyDmError(status, code, fallback) {
|
|
|
346
347
|
return `Server error: ${fallback}`;
|
|
347
348
|
return fallback;
|
|
348
349
|
}
|
|
350
|
+
function friendlyForwardError(code) {
|
|
351
|
+
switch (code) {
|
|
352
|
+
case "FORWARD_SOURCE_NOT_FOUND":
|
|
353
|
+
return "Source message not found or you cannot read it.";
|
|
354
|
+
case "INVALID_FORWARD_OF":
|
|
355
|
+
return "Invalid source message reference; provide a valid eventId and --from value.";
|
|
356
|
+
case "INVALID_FILE_ACCESS":
|
|
357
|
+
return "Invalid file access choice; use --grant-files or --omit-files.";
|
|
358
|
+
case "FORWARD_FILE_ACCESS_REQUIRED":
|
|
359
|
+
return "Some forwarded files are not readable by the recipient.";
|
|
360
|
+
case "FORWARD_FILES_NOT_ALLOWED":
|
|
361
|
+
return "Files cannot be granted for this channel or cross-company forward.";
|
|
362
|
+
case "CROSS_COMPANY_ACK_REQUIRED":
|
|
363
|
+
return "Warning: this forward crosses company boundaries; review the destination and retry with --cross-company.";
|
|
364
|
+
case "CROSS_COMPANY_FORBIDDEN":
|
|
365
|
+
return "Cross-company forwarding requires owner or admin access in both companies.";
|
|
366
|
+
case "FORWARD_FILE_SHARE_FORBIDDEN":
|
|
367
|
+
return "You do not have permission to share one or more forwarded files.";
|
|
368
|
+
case "FORWARD_FILE_GRANT_FAILED":
|
|
369
|
+
return "The recipient could not be granted access to a forwarded file; no message was sent.";
|
|
370
|
+
case "RECIPIENT_NOT_FOUND":
|
|
371
|
+
return "Recipient not found or not reachable.";
|
|
372
|
+
default:
|
|
373
|
+
return "Message forwarding failed; check the source and destination, then try again.";
|
|
374
|
+
}
|
|
375
|
+
}
|
|
349
376
|
/**
|
|
350
377
|
* Resolve a connection request from a pending-requests list by a caller-supplied
|
|
351
378
|
* identifier (email, personUid, or pairKey). Pure → unit-testable.
|
|
@@ -529,6 +556,21 @@ export async function fetchChannelMembers(token, channelId) {
|
|
|
529
556
|
const data = (await res.json());
|
|
530
557
|
return data.members ?? [];
|
|
531
558
|
}
|
|
559
|
+
/** Resolve structured mentions in a channel message body, failing before post on invalid names. */
|
|
560
|
+
async function resolveChannelBodyMentions(token, channelId, body) {
|
|
561
|
+
const tokens = parseMentionTokens(body);
|
|
562
|
+
if (tokens.length === 0)
|
|
563
|
+
return { ok: true, body, mentions: [] };
|
|
564
|
+
const members = await fetchChannelMembers(token, channelId);
|
|
565
|
+
const resolved = resolveMentions(tokens, members);
|
|
566
|
+
if (!resolved.ok)
|
|
567
|
+
return resolved;
|
|
568
|
+
return {
|
|
569
|
+
ok: true,
|
|
570
|
+
body: stripMentionQuotes(body),
|
|
571
|
+
mentions: resolved.mentions,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
532
574
|
/** Fetch a channel history page (GET /v1/notify/channels/{id}/messages). Newest-first. */
|
|
533
575
|
async function fetchChannelMessages(token, channelId, query = {}) {
|
|
534
576
|
const res = await vaultApiFetch({
|
|
@@ -546,9 +588,9 @@ async function fetchChannelMessages(token, channelId, query = {}) {
|
|
|
546
588
|
/**
|
|
547
589
|
* Channel DM path: `hq dm vyg-dev "msg"`, `hq dm '#vyg-dev' "msg"`, or
|
|
548
590
|
* `hq dm --channel vyg-dev "msg"`. Resolves the caller's channel by name via
|
|
549
|
-
* GET /v1/notify/channels, then posts the message into it. Scheduling
|
|
550
|
-
*
|
|
551
|
-
*
|
|
591
|
+
* GET /v1/notify/channels, then posts the message into it. Scheduling remains
|
|
592
|
+
* a 1:1-DM feature; prompt and details are supported for channel sends while
|
|
593
|
+
* the dm.forward-cli kill switch is enabled.
|
|
552
594
|
*/
|
|
553
595
|
async function runChannelSend(channelName, message, opts) {
|
|
554
596
|
try {
|
|
@@ -557,12 +599,17 @@ async function runChannelSend(channelName, message, opts) {
|
|
|
557
599
|
console.error(chalk.red(`A message body is required: hq dm ${channelName} "<message>" (or hq dm --channel ${channelName} "<message>").`));
|
|
558
600
|
process.exit(1);
|
|
559
601
|
}
|
|
560
|
-
const
|
|
561
|
-
opts.
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
602
|
+
const hasChannelContext = opts.prompt !== undefined || opts.promptFile !== undefined ||
|
|
603
|
+
opts.details !== undefined || opts.detailsFile !== undefined;
|
|
604
|
+
if (hasChannelContext && !dmForwardCliEnabled()) {
|
|
605
|
+
console.error(chalk.red("Channel --details/--prompt support is disabled by the dm.forward-cli kill switch."));
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
if (opts.promptFile === "" || opts.detailsFile === "") {
|
|
609
|
+
console.error(chalk.red("--prompt-file and --details-file require a non-empty path."));
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
const unsupported = [opts.at ? "--at" : null, opts.in ? "--in" : null].filter(Boolean);
|
|
566
613
|
if (unsupported.length > 0) {
|
|
567
614
|
console.error(chalk.red(`${unsupported.join(", ")} ${unsupported.length === 1 ? "is" : "are"} only supported for 1:1 DMs, not channel messages.`));
|
|
568
615
|
process.exit(1);
|
|
@@ -599,21 +646,16 @@ async function runChannelSend(channelName, message, opts) {
|
|
|
599
646
|
// and posts the text literally.
|
|
600
647
|
let mentions = [];
|
|
601
648
|
if (opts.mentions !== false) {
|
|
602
|
-
const
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
console.error(chalk.red(resolved.error));
|
|
608
|
-
process.exit(1);
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
mentions = resolved.mentions;
|
|
649
|
+
const resolved = await resolveChannelBodyMentions(token, channel.channelId, body);
|
|
650
|
+
if (!resolved.ok) {
|
|
651
|
+
console.error(chalk.red(resolved.error));
|
|
652
|
+
process.exit(1);
|
|
653
|
+
return;
|
|
612
654
|
}
|
|
655
|
+
body = resolved.body;
|
|
656
|
+
mentions = resolved.mentions;
|
|
613
657
|
}
|
|
614
658
|
// Send the human-readable form: the quotes only existed for parsing.
|
|
615
|
-
if (mentions.length > 0)
|
|
616
|
-
body = stripMentionQuotes(body);
|
|
617
659
|
let audience;
|
|
618
660
|
if (opts.audience !== undefined && opts.audience !== "") {
|
|
619
661
|
if (!DM_AUDIENCE_VALUES.includes(opts.audience)) {
|
|
@@ -622,6 +664,12 @@ async function runChannelSend(channelName, message, opts) {
|
|
|
622
664
|
audience = opts.audience;
|
|
623
665
|
}
|
|
624
666
|
const payload = { body };
|
|
667
|
+
const prompt = opts.promptFile ? readFileSync(opts.promptFile, "utf8") : opts.prompt;
|
|
668
|
+
const details = opts.detailsFile ? readFileSync(opts.detailsFile, "utf8") : opts.details;
|
|
669
|
+
if (prompt !== undefined)
|
|
670
|
+
payload.prompt = prompt;
|
|
671
|
+
if (details !== undefined)
|
|
672
|
+
payload.details = details;
|
|
625
673
|
if (mentions.length > 0)
|
|
626
674
|
payload.mentions = mentions;
|
|
627
675
|
if (rootEventId)
|
|
@@ -736,6 +784,130 @@ async function runDmSend(recipient, message, opts) {
|
|
|
736
784
|
process.exit(1);
|
|
737
785
|
}
|
|
738
786
|
}
|
|
787
|
+
async function resolveForwardSourceConversation(token, source) {
|
|
788
|
+
if (source.includes(",")) {
|
|
789
|
+
throw new Error("--from accepts one person or channel, not a group recipient list.");
|
|
790
|
+
}
|
|
791
|
+
const channelName = parseChannelName(source);
|
|
792
|
+
if (channelName !== null) {
|
|
793
|
+
const channel = await resolveChannel(token, channelName);
|
|
794
|
+
return { conversationId: channel.channelId };
|
|
795
|
+
}
|
|
796
|
+
const recipient = detectRecipient(source);
|
|
797
|
+
if (!recipient) {
|
|
798
|
+
throw new Error("--from must be a person email, person/agent UID, or channel name/ID.");
|
|
799
|
+
}
|
|
800
|
+
if (recipient.toPersonUid)
|
|
801
|
+
return { conversationId: recipient.toPersonUid };
|
|
802
|
+
const res = await vaultApiFetch({ token, path: "/v1/notify/contacts" });
|
|
803
|
+
if (!res.ok)
|
|
804
|
+
throw new Error("Could not resolve the source person; use their person UID.");
|
|
805
|
+
const data = (await res.json());
|
|
806
|
+
const peer = (data.contacts ?? []).find((contact) => contact.email?.trim().toLowerCase() === recipient.toEmail);
|
|
807
|
+
if (!peer?.personUid)
|
|
808
|
+
throw new Error("Source person not found in your contacts; use their person UID.");
|
|
809
|
+
return { conversationId: peer.personUid };
|
|
810
|
+
}
|
|
811
|
+
async function runDmForward(eventId, opts) {
|
|
812
|
+
try {
|
|
813
|
+
if (!dmForwardCliEnabled()) {
|
|
814
|
+
console.error(chalk.red("Message forwarding is disabled by the dm.forward-cli kill switch."));
|
|
815
|
+
process.exit(1);
|
|
816
|
+
}
|
|
817
|
+
if (opts.grantFiles && opts.omitFiles) {
|
|
818
|
+
console.error(chalk.red("Choose only one of --grant-files or --omit-files."));
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
if (opts.note !== undefined && opts.noteFile !== undefined) {
|
|
822
|
+
console.error(chalk.red("Choose only one of --note or --note-file."));
|
|
823
|
+
process.exit(1);
|
|
824
|
+
}
|
|
825
|
+
if (!eventId.trim()) {
|
|
826
|
+
console.error(chalk.red("A source eventId is required."));
|
|
827
|
+
process.exit(1);
|
|
828
|
+
}
|
|
829
|
+
if (opts.to.includes(",")) {
|
|
830
|
+
console.error(chalk.red("--to accepts one recipient or channel, not a group recipient list."));
|
|
831
|
+
process.exit(1);
|
|
832
|
+
}
|
|
833
|
+
const note = opts.noteFile !== undefined
|
|
834
|
+
? readFileSync(opts.noteFile, "utf8")
|
|
835
|
+
: opts.note;
|
|
836
|
+
const token = await ensureCognitoToken();
|
|
837
|
+
const source = await resolveForwardSourceConversation(token, opts.from);
|
|
838
|
+
const destinationChannelName = parseChannelName(opts.to);
|
|
839
|
+
const fileAccess = opts.grantFiles ? "grant" : opts.omitFiles ? "omit" : undefined;
|
|
840
|
+
const shared = {
|
|
841
|
+
...(note !== undefined ? { body: note } : {}),
|
|
842
|
+
forwardOf: { conversationId: source.conversationId, eventId: eventId.trim() },
|
|
843
|
+
...(fileAccess ? { fileAccess } : {}),
|
|
844
|
+
...(opts.crossCompany ? { acknowledgeCrossCompany: true } : {}),
|
|
845
|
+
};
|
|
846
|
+
let path;
|
|
847
|
+
let payload;
|
|
848
|
+
let destinationLabel;
|
|
849
|
+
if (destinationChannelName !== null) {
|
|
850
|
+
const channel = await resolveChannel(token, destinationChannelName);
|
|
851
|
+
path = `/v1/notify/channels/${encodeURIComponent(channel.channelId)}/messages`;
|
|
852
|
+
payload = { ...shared };
|
|
853
|
+
if (note !== undefined) {
|
|
854
|
+
const resolved = await resolveChannelBodyMentions(token, channel.channelId, note);
|
|
855
|
+
if (!resolved.ok) {
|
|
856
|
+
console.error(chalk.red(resolved.error));
|
|
857
|
+
process.exit(1);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
payload.body = resolved.body;
|
|
861
|
+
if (resolved.mentions.length > 0)
|
|
862
|
+
payload.mentions = resolved.mentions;
|
|
863
|
+
}
|
|
864
|
+
destinationLabel = `#${channel.name ?? destinationChannelName}`;
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
const recipient = detectRecipient(opts.to);
|
|
868
|
+
if (!recipient) {
|
|
869
|
+
console.error(chalk.red("--to must be a person email, person/agent UID, or channel name/ID."));
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
path = "/v1/notify/dm";
|
|
873
|
+
payload = { ...recipient, ...shared };
|
|
874
|
+
destinationLabel = opts.to;
|
|
875
|
+
}
|
|
876
|
+
const res = await vaultApiFetch({ token, path, method: "POST", body: payload });
|
|
877
|
+
if (!res.ok) {
|
|
878
|
+
const err = (await res.json().catch(() => ({})));
|
|
879
|
+
const code = typeof err.code === "string" ? err.code : undefined;
|
|
880
|
+
if (code === "FORWARD_FILE_ACCESS_REQUIRED") {
|
|
881
|
+
console.error(chalk.yellow(friendlyForwardError(code)));
|
|
882
|
+
const files = Array.isArray(err.files) ? err.files : [];
|
|
883
|
+
for (const item of files) {
|
|
884
|
+
if (!item || typeof item !== "object")
|
|
885
|
+
continue;
|
|
886
|
+
const file = item;
|
|
887
|
+
const label = typeof file.name === "string" ? file.name : "forwarded file";
|
|
888
|
+
const location = typeof file.vaultPath === "string" ? file.vaultPath :
|
|
889
|
+
typeof file.path === "string" ? file.path : undefined;
|
|
890
|
+
console.error(chalk.dim(` - ${label}${location ? ` (${location})` : ""}`));
|
|
891
|
+
}
|
|
892
|
+
console.error(chalk.dim("Retry with --grant-files or --omit-files."));
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
console.error(chalk.red(friendlyForwardError(code)));
|
|
896
|
+
}
|
|
897
|
+
process.exit(1);
|
|
898
|
+
}
|
|
899
|
+
const data = (await res.json().catch(() => ({})));
|
|
900
|
+
if (typeof data.eventId !== "string" || !data.eventId.trim()) {
|
|
901
|
+
console.error(chalk.red("Forward succeeded without returning an eventId."));
|
|
902
|
+
process.exit(1);
|
|
903
|
+
}
|
|
904
|
+
console.log(chalk.green(`Message forwarded to ${destinationLabel}. New eventId: ${data.eventId}`));
|
|
905
|
+
}
|
|
906
|
+
catch (err) {
|
|
907
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
908
|
+
process.exit(1);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
739
911
|
async function runDmRequests() {
|
|
740
912
|
try {
|
|
741
913
|
const token = await ensureCognitoToken();
|
|
@@ -871,7 +1043,11 @@ export function formatThreadMessage(m, nowMs) {
|
|
|
871
1043
|
const who = m.direction === "out" ? "you" : senderLabel(m);
|
|
872
1044
|
const when = chalk.dim(formatRelativeTime(m.createdAt, nowMs));
|
|
873
1045
|
const audienceTag = m.audience === "agent" ? chalk.dim(" [agent]") : "";
|
|
874
|
-
|
|
1046
|
+
const id = m.eventId ? ` ${chalk.dim(m.eventId)}` : "";
|
|
1047
|
+
const forwarded = m.forwardedFrom?.senderName
|
|
1048
|
+
? `\n ${chalk.dim(`Forwarded from ${m.forwardedFrom.senderName}`)}`
|
|
1049
|
+
: "";
|
|
1050
|
+
return `${arrow} ${chalk.bold(who)} ${when}${audienceTag}${id}${forwarded}\n ${firstLine(m.body)}${formatAttachments(m)}`;
|
|
875
1051
|
}
|
|
876
1052
|
/** Render one channel/group message line. */
|
|
877
1053
|
export function formatChannelMessage(m, nowMs, opts = {}) {
|
|
@@ -886,7 +1062,10 @@ export function formatChannelMessage(m, nowMs, opts = {}) {
|
|
|
886
1062
|
(senderLabel(m).toLowerCase() === "hq"
|
|
887
1063
|
? "System notification (no text provided)."
|
|
888
1064
|
: "(no message text)");
|
|
889
|
-
|
|
1065
|
+
const forwarded = m.forwardedFrom?.senderName
|
|
1066
|
+
? `${bodyIndent}${chalk.dim(`Forwarded from ${m.forwardedFrom.senderName}`)}\n`
|
|
1067
|
+
: "";
|
|
1068
|
+
return `${indent}${who} ${when}${idPart}\n${forwarded}${bodyIndent}${readableBody}${formatAttachments(m, bodyIndent)}`;
|
|
890
1069
|
}
|
|
891
1070
|
/**
|
|
892
1071
|
* Read the caller's person/agent uid and email from a JWT we already hold.
|
|
@@ -1330,16 +1509,29 @@ export function registerDmCommand(program) {
|
|
|
1330
1509
|
.option("--channel <name>", "Post the message to one of your DM channels by name (e.g. --channel vyg-dev)")
|
|
1331
1510
|
.option("--no-mentions", "Do not resolve `@name` in a channel message into structured mentions — post the text literally")
|
|
1332
1511
|
.option("--thread <eventId>", 'Reply in a channel thread. Pass a root eventId from `hq dm channel`, or `last` for your most recent top-level message in that channel')
|
|
1333
|
-
.option("--prompt <text>", "Agent-context prompt the recipient can one-click copy into their agent (1:1 DMs
|
|
1334
|
-
.option("--prompt-file <path>", "Read the agent prompt from a file (1:1 DMs
|
|
1335
|
-
.option("--details <text>", "Longer detail shown in the recipient's DM detail window (1:1 DMs
|
|
1336
|
-
.option("--details-file <path>", "Read the details from a file (1:1 DMs
|
|
1512
|
+
.option("--prompt <text>", "Agent-context prompt the recipient can one-click copy into their agent (1:1 DMs; channel sends also supported)")
|
|
1513
|
+
.option("--prompt-file <path>", "Read the agent prompt from a file (1:1 DMs; channel sends also supported)")
|
|
1514
|
+
.option("--details <text>", "Longer detail shown in the recipient's DM detail window (1:1 DMs; channel sends also supported)")
|
|
1515
|
+
.option("--details-file <path>", "Read the details from a file (1:1 DMs; channel sends also supported)")
|
|
1337
1516
|
.option("--at <iso>", "Schedule delivery at an ISO8601 time (store-and-forward; delivered within ~60s of the time; 1:1 DMs only)")
|
|
1338
1517
|
.option("--in <duration>", "Schedule delivery after a relative delay: 30s, 10m, 2h, 1d (1:1 DMs only)")
|
|
1339
1518
|
.option("--audience <human|agent|both>", "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both")
|
|
1340
1519
|
.action(async (recipient, message, opts) => {
|
|
1341
1520
|
await runDmSend(recipient, message, opts);
|
|
1342
1521
|
});
|
|
1522
|
+
dm
|
|
1523
|
+
.command("forward <eventId>")
|
|
1524
|
+
.description("Forward a DM or channel message through the existing send routes.")
|
|
1525
|
+
.requiredOption("--from <person|channel>", "Source person email/UID or channel name/ID")
|
|
1526
|
+
.requiredOption("--to <person|agent|channel>", "Destination person email/UID, agent UID, or channel name/ID")
|
|
1527
|
+
.option("--note <text>", "Optional note to add above the forwarded message")
|
|
1528
|
+
.option("--note-file <path>", "Read the optional forward note from a file")
|
|
1529
|
+
.option("--grant-files", "Grant the recipient access to files they cannot currently open")
|
|
1530
|
+
.option("--omit-files", "Omit files the recipient cannot currently open")
|
|
1531
|
+
.option("--cross-company", "Confirm forwarding across company boundaries")
|
|
1532
|
+
.action(async (eventId, opts) => {
|
|
1533
|
+
await runDmForward(eventId, opts);
|
|
1534
|
+
});
|
|
1343
1535
|
dm
|
|
1344
1536
|
.command("inbox")
|
|
1345
1537
|
.description("List your recent incoming direct messages.")
|
package/dist/commands/lanes.js
CHANGED
|
@@ -947,6 +947,7 @@ function laneListQueryFlags(query) {
|
|
|
947
947
|
...(query.laneUlids?.length ? ["--lane-ulid"] : []),
|
|
948
948
|
...(query.active ? ["--active"] : []),
|
|
949
949
|
...(query.hierarchy?.length ? ["--hierarchy"] : []),
|
|
950
|
+
...(query.senior !== undefined ? ["--senior"] : []),
|
|
950
951
|
...(query.snapshot ? ["--snapshot"] : []),
|
|
951
952
|
];
|
|
952
953
|
}
|
|
@@ -1404,6 +1405,7 @@ function normalizedLaneListQuery(query) {
|
|
|
1404
1405
|
...(laneUlids === undefined ? {} : { laneUlids }),
|
|
1405
1406
|
...(query.active ? { active: true } : {}),
|
|
1406
1407
|
...(hierarchy === undefined ? {} : { hierarchy }),
|
|
1408
|
+
...(query.senior === undefined ? {} : { senior: query.senior }),
|
|
1407
1409
|
...(query.snapshot ? { snapshot: true } : {}),
|
|
1408
1410
|
...(query.strictSnapshot ? { strictSnapshot: true } : {}),
|
|
1409
1411
|
...(query.includeForeignHostRows ? { includeForeignHostRows: true } : {}),
|
|
@@ -1503,6 +1505,48 @@ async function readOutpostLaneRecords(ctx, outpostId, requestedQuery = {}, dispa
|
|
|
1503
1505
|
}
|
|
1504
1506
|
return invocationMemo.getOrCreate(outpostId, query, () => readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOptions));
|
|
1505
1507
|
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Read the owner rows the hire-grant context for `laneId` is built from.
|
|
1510
|
+
*
|
|
1511
|
+
* The owner's `--hierarchy` selector also returns every lane that shares the
|
|
1512
|
+
* target's company and project, so on a store with hundreds of lanes in one
|
|
1513
|
+
* project the response reaches the Outpost exec output limit and the read
|
|
1514
|
+
* fails before a single row is parsed. When that happens, and the target has a
|
|
1515
|
+
* lane senior, assemble the rows the context builder needs from bounded reads
|
|
1516
|
+
* every owner CLI supports: the target and its senior by id, the senior's
|
|
1517
|
+
* direct children (the target's siblings) and the target's own children by
|
|
1518
|
+
* `--senior`. The owner rejects `--senior` together with `--snapshot`, so the
|
|
1519
|
+
* two `--senior` reads use the plain JSON list; the dispatched command sets
|
|
1520
|
+
* HQ_LANES_REMOTE_READ=1, which keeps that list from refreshing remote
|
|
1521
|
+
* mirrors, and the view-only fields it adds are stripped before parsing. A
|
|
1522
|
+
* session-senior target needs same-company profile rows that no bounded
|
|
1523
|
+
* filter selects, so its oversized read stays an error.
|
|
1524
|
+
*/
|
|
1525
|
+
async function readOutpostLaneHierarchy(ctx, outpostId, laneId) {
|
|
1526
|
+
let tooLarge;
|
|
1527
|
+
try {
|
|
1528
|
+
return await readOutpostLaneRecords(ctx, outpostId, { hierarchy: [laneId], snapshot: true });
|
|
1529
|
+
}
|
|
1530
|
+
catch (err) {
|
|
1531
|
+
if (!isLanesError(err) || err.error !== "outpost_lane_list_too_large")
|
|
1532
|
+
throw err;
|
|
1533
|
+
tooLarge = err;
|
|
1534
|
+
}
|
|
1535
|
+
const targets = await readOutpostLaneRecords(ctx, outpostId, { laneIds: [laneId], snapshot: true });
|
|
1536
|
+
const target = targets.find((lane) => lane.lane_id === laneId);
|
|
1537
|
+
if (target === undefined)
|
|
1538
|
+
return targets;
|
|
1539
|
+
if (target.senior.kind !== "lane" || !target.senior.id) {
|
|
1540
|
+
throw new LanesError(tooLarge.error, `${tooLarge.message}; lane ${laneId} has a ${target.senior.kind} senior, whose hierarchy cannot be read in bounded parts`, { ...tooLarge.details, lane_id: laneId, senior: target.senior });
|
|
1541
|
+
}
|
|
1542
|
+
const seniorId = target.senior.id;
|
|
1543
|
+
const [seniorRows, siblings, children] = await Promise.all([
|
|
1544
|
+
readOutpostLaneRecords(ctx, outpostId, { laneIds: [seniorId], snapshot: true }),
|
|
1545
|
+
readOutpostLaneRecords(ctx, outpostId, { senior: `lane:${seniorId}` }),
|
|
1546
|
+
readOutpostLaneRecords(ctx, outpostId, { senior: `lane:${laneId}` }),
|
|
1547
|
+
]);
|
|
1548
|
+
return mergeOwnerLaneRecords([...targets, ...seniorRows, ...siblings], children);
|
|
1549
|
+
}
|
|
1506
1550
|
/** Refresh this host's selected Outpost mirrors in one bounded owner query per Outpost. */
|
|
1507
1551
|
async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds, options = {}) {
|
|
1508
1552
|
const result = {
|
|
@@ -1829,9 +1873,12 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1829
1873
|
for (const outpostId of lookupOutpostIds) {
|
|
1830
1874
|
let ownerLanes;
|
|
1831
1875
|
try {
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1876
|
+
// The hierarchy read is shared with the grant context built after
|
|
1877
|
+
// resolution, so one owner round trip serves both whenever the Outpost
|
|
1878
|
+
// exec output limit can carry the owner's response.
|
|
1879
|
+
ownerLanes = hostlessUlidLookup
|
|
1880
|
+
? await readOutpostLaneRecords(ctx, outpostId, { laneUlids: [requestedUlid], snapshot: true })
|
|
1881
|
+
: await readOutpostLaneHierarchy(ctx, outpostId, laneId);
|
|
1835
1882
|
probedOutpostIds.push(outpostId);
|
|
1836
1883
|
}
|
|
1837
1884
|
catch (err) {
|
|
@@ -4665,20 +4712,14 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
4665
4712
|
let grantContext;
|
|
4666
4713
|
let grantHierarchy;
|
|
4667
4714
|
if (action === "grant") {
|
|
4668
|
-
const ownerHierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await
|
|
4669
|
-
hierarchy: [lane.lane_id],
|
|
4670
|
-
snapshot: true,
|
|
4671
|
-
}));
|
|
4715
|
+
const ownerHierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneHierarchy(ctx, outpostId, lane.lane_id));
|
|
4672
4716
|
const hierarchy = await hierarchyWithVerifiedRemoteSenior(ctx, ctx.hqRoot(), lane, ownerHierarchy);
|
|
4673
4717
|
grantHierarchy = hierarchy;
|
|
4674
4718
|
grantContext = createOutpostHireGrantContext(lane, hierarchy);
|
|
4675
4719
|
}
|
|
4676
4720
|
else if ((action === "resume" && lane.senior.kind === "lane") ||
|
|
4677
4721
|
(action === "message" && (lane.senior.kind === "lane" || lane.senior.kind === "session"))) {
|
|
4678
|
-
const ownerHierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await
|
|
4679
|
-
hierarchy: [lane.lane_id],
|
|
4680
|
-
snapshot: true,
|
|
4681
|
-
}));
|
|
4722
|
+
const ownerHierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneHierarchy(ctx, outpostId, lane.lane_id));
|
|
4682
4723
|
const hierarchy = await hierarchyWithVerifiedRemoteSenior(ctx, ctx.hqRoot(), lane, ownerHierarchy);
|
|
4683
4724
|
grantContext = createOutpostHireGrantContext(lane, hierarchy, action === "message" ? options.supersededMessageCallerId : undefined, action === "message" ? options.supersededMessageSessionId : undefined);
|
|
4684
4725
|
}
|
package/dist/commands/secrets.js
CHANGED
|
@@ -145,7 +145,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
|
145
145
|
import { removeCacheEntry, clearAllCache } from "../utils/secrets-cache.js";
|
|
146
146
|
import { isHqCliChild, releaseChildWrapperPackageUseLeaseAfterStart } from "../utils/child-wrapper-lease.js";
|
|
147
147
|
import { resolveDefaultHqRoot } from "../utils/hq-root-resolver.js";
|
|
148
|
-
import {
|
|
148
|
+
import { fastFlagEnabledDefaultOn } from "../native-fast-route.js";
|
|
149
149
|
import { MULTI_COMPANY_SESSION_LOCK_FLAG } from "../lib/cli-flag-keys.js";
|
|
150
150
|
import { resolveSessionId } from "../lib/core-utils/session-id.js";
|
|
151
151
|
import { secretsExecChildExitCode } from "../utils/child-wrapper-exit.js";
|
|
@@ -1249,7 +1249,7 @@ export function registerSecretsCommand(program) {
|
|
|
1249
1249
|
process.exit(1);
|
|
1250
1250
|
}
|
|
1251
1251
|
const keys = parseSecretNameList(_opts.only);
|
|
1252
|
-
const multiCompanyLockEnabled = await
|
|
1252
|
+
const multiCompanyLockEnabled = await fastFlagEnabledDefaultOn(MULTI_COMPANY_SESSION_LOCK_FLAG);
|
|
1253
1253
|
const sessionLock = multiCompanyLockEnabled ? readSessionCompanyLock(process.env) : undefined;
|
|
1254
1254
|
const parentScope = secrets.opts();
|
|
1255
1255
|
const inheritedCompany = parentScope.company;
|
package/dist/commands/workers.js
CHANGED
|
@@ -7,7 +7,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
|
7
7
|
import { vaultApiFetch, getCompanyUid } from "./secrets.js";
|
|
8
8
|
import { GROUP_ID_PATTERN, EMAIL_PATTERN, toSharedFolderGlob } from "./_patterns.js";
|
|
9
9
|
import { findHqRoot } from "../utils/manifest.js";
|
|
10
|
-
import {
|
|
10
|
+
import { fastFlagEnabledDefaultOn } from "../native-fast-route.js";
|
|
11
11
|
import { MULTI_COMPANY_SESSION_LOCK_FLAG } from "../lib/cli-flag-keys.js";
|
|
12
12
|
import { resolveSessionId } from "../lib/core-utils/session-id.js";
|
|
13
13
|
export { readWorkerRegistry } from "../lib/workers-registry/read.js";
|
|
@@ -198,7 +198,7 @@ async function runWorkersShare(workerId, opts) {
|
|
|
198
198
|
}
|
|
199
199
|
async function runWorkersList(opts) {
|
|
200
200
|
const hqRoot = findHqRoot();
|
|
201
|
-
const enabled = await
|
|
201
|
+
const enabled = await fastFlagEnabledDefaultOn(MULTI_COMPANY_SESSION_LOCK_FLAG);
|
|
202
202
|
const lockedCompanies = resolveActiveCompanies(hqRoot, enabled);
|
|
203
203
|
if (enabled && opts.company && denyWorkerCompanyOutsideLock(opts.company, lockedCompanies))
|
|
204
204
|
return;
|
package/dist/lib/bot/daemon.d.ts
CHANGED
|
@@ -64,7 +64,7 @@ export interface BotDaemonDeps {
|
|
|
64
64
|
platform?: BotDaemonPlatform;
|
|
65
65
|
launchctl?: LaunchctlExec;
|
|
66
66
|
uid?: number;
|
|
67
|
-
fsImpl?: Pick<typeof fs, "existsSync" | "writeFileSync" | "mkdirSync" | "unlinkSync"
|
|
67
|
+
fsImpl?: Pick<typeof fs, "existsSync" | "readFileSync" | "writeFileSync" | "mkdirSync" | "unlinkSync"> & Partial<Pick<typeof fs, "chmodSync" | "renameSync">>;
|
|
68
68
|
/** Blocking sleep, injectable for tests. */
|
|
69
69
|
sleepMs?: (ms: number) => void;
|
|
70
70
|
/**
|
package/dist/lib/bot/daemon.js
CHANGED
|
@@ -15,7 +15,7 @@ import * as path from "node:path";
|
|
|
15
15
|
import { spawn } from "node:child_process";
|
|
16
16
|
import { resolveHqBinary, resolveNodeBinary, preferStableHqBinary } from "../mesh/live/daemon/install.js";
|
|
17
17
|
import { botLogPath, launchdLabel } from "./paths.js";
|
|
18
|
-
import { reloadLaunchd } from "../service-manager/launchd.js";
|
|
18
|
+
import { discoverLaunchdIdentity, installLaunchdPlist, launchdProgramArguments, launchdPlistProgramArguments as parsePlistArguments, parseLaunchdCommand, reloadLaunchd } from "../service-manager/launchd.js";
|
|
19
19
|
import { nodeBinaryProblem, preferStableNodeBinary } from "../node-binary.js";
|
|
20
20
|
export function detectBotPlatform(platform = process.platform) {
|
|
21
21
|
if (platform === "darwin")
|
|
@@ -66,6 +66,12 @@ function escapeXml(value) {
|
|
|
66
66
|
.replace(/"/g, """);
|
|
67
67
|
}
|
|
68
68
|
export function renderBotLaunchdPlist(p) {
|
|
69
|
+
const identity = discoverLaunchdIdentity(p.home);
|
|
70
|
+
const args = launchdProgramArguments({
|
|
71
|
+
nodeBinary: p.nodeBinary,
|
|
72
|
+
hqBinary: p.hqBinary,
|
|
73
|
+
launchdLauncherBinary: identity.launcherBinary,
|
|
74
|
+
}, ["bot", "run", p.name]);
|
|
69
75
|
const envRows = [
|
|
70
76
|
["HOME", p.home],
|
|
71
77
|
["PATH", p.pathEntries.join(":")],
|
|
@@ -80,13 +86,9 @@ export function renderBotLaunchdPlist(p) {
|
|
|
80
86
|
<dict>
|
|
81
87
|
<key>Label</key>
|
|
82
88
|
<string>${escapeXml(p.label)}</string>
|
|
83
|
-
<key>ProgramArguments</key>
|
|
89
|
+
${identity.associatedBundleIdentifier ? ` <key>AssociatedBundleIdentifiers</key>\n <array>\n <string>${escapeXml(identity.associatedBundleIdentifier)}</string>\n </array>\n` : ""} <key>ProgramArguments</key>
|
|
84
90
|
<array>
|
|
85
|
-
<string>${escapeXml(
|
|
86
|
-
<string>${escapeXml(p.hqBinary)}</string>
|
|
87
|
-
<string>bot</string>
|
|
88
|
-
<string>run</string>
|
|
89
|
-
<string>${escapeXml(p.name)}</string>
|
|
91
|
+
${args.map((arg) => ` <string>${escapeXml(arg)}</string>`).join("\n")}
|
|
90
92
|
</array>
|
|
91
93
|
<key>WorkingDirectory</key>
|
|
92
94
|
<string>${escapeXml(p.hqRoot ?? p.botDir)}</string>
|
|
@@ -190,16 +192,13 @@ function launchdLoaded(p, deps) {
|
|
|
190
192
|
return deps.launchctl(["print", `${launchdDomain(deps.uid)}/${p.label}`]).ok;
|
|
191
193
|
}
|
|
192
194
|
export function launchdPlistProgramArguments(plist) {
|
|
193
|
-
|
|
194
|
-
if (!block)
|
|
195
|
-
return [];
|
|
196
|
-
return [...block[1].matchAll(/<string>([^<]*)<\/string>/g)].map((m) => m[1]);
|
|
195
|
+
return parsePlistArguments(plist);
|
|
197
196
|
}
|
|
198
197
|
/** The hq script path baked into an installed unit, or null if none. */
|
|
199
198
|
export function installedDaemonHqBinary(p, io = fs) {
|
|
200
199
|
if (io.existsSync(p.plistPath)) {
|
|
201
200
|
const args = launchdPlistProgramArguments(io.readFileSync(p.plistPath, "utf8"));
|
|
202
|
-
return args
|
|
201
|
+
return parseLaunchdCommand(args)?.hq ?? null;
|
|
203
202
|
}
|
|
204
203
|
if (io.existsSync(p.systemdUnitPath)) {
|
|
205
204
|
const unit = io.readFileSync(p.systemdUnitPath, "utf8");
|
|
@@ -212,7 +211,7 @@ export function installedDaemonHqBinary(p, io = fs) {
|
|
|
212
211
|
export function installedDaemonNodeBinary(p, io = fs) {
|
|
213
212
|
if (io.existsSync(p.plistPath)) {
|
|
214
213
|
const args = launchdPlistProgramArguments(io.readFileSync(p.plistPath, "utf8"));
|
|
215
|
-
return args
|
|
214
|
+
return parseLaunchdCommand(args)?.node ?? null;
|
|
216
215
|
}
|
|
217
216
|
if (io.existsSync(p.systemdUnitPath)) {
|
|
218
217
|
const unit = io.readFileSync(p.systemdUnitPath, "utf8");
|
|
@@ -383,19 +382,27 @@ export function installBotDaemon(p, deps = {}) {
|
|
|
383
382
|
io.mkdirSync(path.dirname(p.logPath), { recursive: true, mode: 0o700 });
|
|
384
383
|
if (platform === "darwin") {
|
|
385
384
|
io.mkdirSync(path.dirname(p.plistPath), { recursive: true });
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
385
|
+
const reloadState = { result: null };
|
|
386
|
+
const written = installLaunchdPlist(p.plistPath, p.label, renderBotLaunchdPlist(p), {
|
|
387
|
+
files: {
|
|
388
|
+
existsSync: io.existsSync,
|
|
389
|
+
readFileSync: (file) => io.readFileSync(file, "utf8"),
|
|
390
|
+
writeFileSync: io.writeFileSync,
|
|
391
|
+
...(io.chmodSync ? { chmodSync: io.chmodSync } : {}),
|
|
392
|
+
...(io.renameSync ? { renameSync: io.renameSync } : {}),
|
|
393
|
+
unlinkSync: io.unlinkSync,
|
|
394
|
+
},
|
|
395
|
+
activate: Boolean(deps.launchctl),
|
|
396
|
+
isLoaded: () => launchdLoaded(p, deps),
|
|
397
|
+
...(deps.launchctl ? { reload: () => { reloadState.result = rebootstrapLaunchd(p, deps); return reloadState.result; } } : {}),
|
|
398
|
+
});
|
|
399
|
+
let loaded = written.loaded;
|
|
400
|
+
if (deps.launchctl && reloadState.result) {
|
|
401
|
+
const r = reloadState.result;
|
|
402
|
+
loaded = r.loaded;
|
|
403
|
+
if (!r?.ok) {
|
|
404
|
+
const detail = `launchctl bootstrap failed after ${r?.bootstrapAttempts ?? 0} attempts: ${(r?.error ?? "").slice(0, 200)}`;
|
|
405
|
+
if (r?.loaded) {
|
|
399
406
|
// The previous definition survived the bootout; keep it running.
|
|
400
407
|
deps.launchctl(["kickstart", "-k", `${launchdDomain(deps.uid)}/${p.label}`]);
|
|
401
408
|
}
|
|
@@ -403,14 +410,13 @@ export function installBotDaemon(p, deps = {}) {
|
|
|
403
410
|
platform,
|
|
404
411
|
action: "install",
|
|
405
412
|
installed: true,
|
|
406
|
-
loaded: r
|
|
413
|
+
loaded: r?.loaded ?? false,
|
|
407
414
|
dest: p.plistPath,
|
|
408
|
-
message: r
|
|
415
|
+
message: r?.loaded
|
|
409
416
|
? `Wrote ${p.plistPath} but ${detail}. The previous definition of ${p.label} is still loaded and was restarted; the rewritten plist is not active yet.`
|
|
410
417
|
: `Wrote ${p.plistPath} but ${detail}. ${p.label} is NOT loaded. Load it with: launchctl bootstrap ${launchdDomain(deps.uid)} ${p.plistPath}`,
|
|
411
418
|
};
|
|
412
419
|
}
|
|
413
|
-
loaded = true;
|
|
414
420
|
}
|
|
415
421
|
return {
|
|
416
422
|
platform,
|
|
@@ -14,8 +14,6 @@ export interface HqCloudCopy {
|
|
|
14
14
|
}
|
|
15
15
|
export interface PickedHqCloud extends HqCloudCopy {
|
|
16
16
|
source: "bundled" | "managed";
|
|
17
|
-
/** Set when a version is pinned but that version is not the installed managed copy. */
|
|
18
|
-
pinnedVersionMissing?: string;
|
|
19
17
|
}
|
|
20
18
|
export type ManagedHqCloud = PickedHqCloud & {
|
|
21
19
|
source: "managed";
|
|
@@ -85,6 +83,8 @@ export interface UpdateHqCloudDeps {
|
|
|
85
83
|
run?: (file: string, args: string[], shell: boolean, env: NodeJS.ProcessEnv) => NpmRunResult;
|
|
86
84
|
bundled?: HqCloudCopy | null;
|
|
87
85
|
platform?: NodeJS.Platform;
|
|
86
|
+
/** Test seam for bounded install retry delays. */
|
|
87
|
+
sleep?: (ms: number) => void;
|
|
88
88
|
}
|
|
89
89
|
/** Install or update the updater's copy, as this (the login) user. */
|
|
90
90
|
export declare function updateManagedHqCloud(daemonRoot: string, env: NodeJS.ProcessEnv, deps?: UpdateHqCloudDeps): HqCloudUpdateOutcome;
|