arp-tui 0.0.4 → 0.0.6
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/dist/index.js +2125 -135
- package/npm-shrinkwrap.json +6 -5
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { parseArgs as nodeParseArgs } from "util";
|
|
|
5
5
|
import { render } from "ink";
|
|
6
6
|
|
|
7
7
|
// src/app.tsx
|
|
8
|
-
import { Box as
|
|
8
|
+
import { Box as Box9, Text as Text11, useApp, useInput, useStdin } from "ink";
|
|
9
9
|
import { useEffect, useSyncExternalStore } from "react";
|
|
10
10
|
|
|
11
11
|
// src/state/store.ts
|
|
@@ -621,7 +621,7 @@ function chooseRefreshToken(args2) {
|
|
|
621
621
|
const { memoryRt, diskRt, memorySub, diskSub, knownNewerInMemory } = args2;
|
|
622
622
|
const memNewerThanDisk = knownNewerInMemory && diskRt !== memoryRt;
|
|
623
623
|
if (memNewerThanDisk) {
|
|
624
|
-
if (
|
|
624
|
+
if (diskSub && memorySub && diskSub !== memorySub) return null;
|
|
625
625
|
return memoryRt;
|
|
626
626
|
}
|
|
627
627
|
const sameIdentity = diskRt === memoryRt || !!diskSub && diskSub === memorySub;
|
|
@@ -780,6 +780,239 @@ async function logout(instance2, log = console.error) {
|
|
|
780
780
|
return true;
|
|
781
781
|
}
|
|
782
782
|
|
|
783
|
+
// src/commands/result.ts
|
|
784
|
+
var EXIT_CODE = {
|
|
785
|
+
success: 0,
|
|
786
|
+
internal_error: 1,
|
|
787
|
+
usage: 2,
|
|
788
|
+
cancelled: 3,
|
|
789
|
+
authentication_required: 4,
|
|
790
|
+
policy_denied: 5,
|
|
791
|
+
bridge_unavailable: 6,
|
|
792
|
+
relay_unavailable: 7,
|
|
793
|
+
configured_offline: 8,
|
|
794
|
+
outcome_unknown: 9,
|
|
795
|
+
local_integrity_error: 10
|
|
796
|
+
};
|
|
797
|
+
var MAX_SAFE_MESSAGE_SCALARS = 256;
|
|
798
|
+
var SAFE_CODE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
799
|
+
function boundedText(value) {
|
|
800
|
+
const plain = value.replace(/\r\n?|\n/g, " ").replace(/[\x00-\x1f\x7f-\x9f]/g, "").normalize("NFC");
|
|
801
|
+
return Array.from(plain).slice(0, MAX_SAFE_MESSAGE_SCALARS).join("");
|
|
802
|
+
}
|
|
803
|
+
var CommandFailure = class extends Error {
|
|
804
|
+
exitCode;
|
|
805
|
+
stableCode;
|
|
806
|
+
operationId;
|
|
807
|
+
requestId;
|
|
808
|
+
constructor(symbol, stableCode, message, refs) {
|
|
809
|
+
if (!SAFE_CODE.test(stableCode)) throw new TypeError("invalid stable error code");
|
|
810
|
+
super(boundedText(message));
|
|
811
|
+
this.name = "CommandFailure";
|
|
812
|
+
this.exitCode = EXIT_CODE[symbol];
|
|
813
|
+
this.stableCode = stableCode;
|
|
814
|
+
this.operationId = refs?.operationId;
|
|
815
|
+
this.requestId = refs?.requestId;
|
|
816
|
+
}
|
|
817
|
+
toJSON() {
|
|
818
|
+
return Object.freeze({
|
|
819
|
+
schemaVersion: 1,
|
|
820
|
+
ok: false,
|
|
821
|
+
code: this.stableCode,
|
|
822
|
+
message: this.message,
|
|
823
|
+
...this.operationId ? { operationId: this.operationId } : {},
|
|
824
|
+
...this.requestId ? { requestId: this.requestId } : {}
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
var OutcomeUnknown = class extends CommandFailure {
|
|
829
|
+
operation;
|
|
830
|
+
reconcile;
|
|
831
|
+
constructor(args2) {
|
|
832
|
+
super(
|
|
833
|
+
"outcome_unknown",
|
|
834
|
+
"outcome_unknown",
|
|
835
|
+
"The relay outcome is unknown. Reconcile this operation before trying again.",
|
|
836
|
+
{ operationId: args2.operationId, requestId: args2.requestId }
|
|
837
|
+
);
|
|
838
|
+
this.name = "OutcomeUnknown";
|
|
839
|
+
this.operation = args2.operation;
|
|
840
|
+
this.reconcile = args2.reconcile;
|
|
841
|
+
}
|
|
842
|
+
toJSON() {
|
|
843
|
+
return Object.freeze({ ...super.toJSON(), operation: boundedText(this.operation), reconcile: boundedText(this.reconcile) });
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
// src/util/sanitize.ts
|
|
848
|
+
var ESCAPE_SEQ = (
|
|
849
|
+
// eslint-disable-next-line no-control-regex
|
|
850
|
+
/\x1b(?:\[[0-?]*[ -/]*[@-~]|\][\s\S]*?(?:\x07|\x1b\\)|[PX^_][\s\S]*?\x1b\\|[@-Z\\-_])/g
|
|
851
|
+
);
|
|
852
|
+
var CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
853
|
+
function sanitizeForTty(input) {
|
|
854
|
+
return input.replace(ESCAPE_SEQ, "").replace(CONTROL_CHARS, "");
|
|
855
|
+
}
|
|
856
|
+
function sanitizeLabel(input, max = 40) {
|
|
857
|
+
const clean = sanitizeForTty(input);
|
|
858
|
+
return clean.length > max ? clean.slice(0, max - 1) + "\u2026" : clean;
|
|
859
|
+
}
|
|
860
|
+
var FORMAT_CONTROLS = /[\u00ad\u034f\u061c\u115f-\u1160\u17b4-\u17b5\u180b-\u180f\u200b-\u200f\u202a-\u202e\u2060-\u206f\u3164\ufe00-\ufe0f\ufeff\uffa0\ufff0-\ufff8\u{1bca0}-\u{1bca3}\u{1d173}-\u{1d17a}\u{e0000}-\u{e0fff}]/gu;
|
|
861
|
+
function stripTerminalSequences(input) {
|
|
862
|
+
let out = "";
|
|
863
|
+
for (let i = 0; i < input.length; ) {
|
|
864
|
+
const cp = input.codePointAt(i);
|
|
865
|
+
const width = cp > 65535 ? 2 : 1;
|
|
866
|
+
const esc = cp === 27;
|
|
867
|
+
const csi = cp === 155;
|
|
868
|
+
const osc = cp === 157;
|
|
869
|
+
const stringControl = cp === 144 || cp === 152 || cp === 158 || cp === 159;
|
|
870
|
+
if (esc || csi || osc || stringControl) {
|
|
871
|
+
let kind = cp;
|
|
872
|
+
i += width;
|
|
873
|
+
if (esc && i < input.length) {
|
|
874
|
+
kind = input.codePointAt(i);
|
|
875
|
+
i += kind > 65535 ? 2 : 1;
|
|
876
|
+
}
|
|
877
|
+
if (kind === 91 || kind === 155) {
|
|
878
|
+
while (i < input.length) {
|
|
879
|
+
const n = input.codePointAt(i);
|
|
880
|
+
i += n > 65535 ? 2 : 1;
|
|
881
|
+
if (n >= 64 && n <= 126) break;
|
|
882
|
+
}
|
|
883
|
+
} else if (kind === 93 || kind === 157 || kind === 80 || kind === 88 || kind === 94 || kind === 95 || stringControl) {
|
|
884
|
+
while (i < input.length) {
|
|
885
|
+
const n = input.codePointAt(i);
|
|
886
|
+
i += n > 65535 ? 2 : 1;
|
|
887
|
+
if ((kind === 93 || kind === 157) && n === 7) break;
|
|
888
|
+
if (n === 156) break;
|
|
889
|
+
if (n === 27 && input.codePointAt(i) === 92) {
|
|
890
|
+
i++;
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
if (cp < 32 && cp !== 9 && cp !== 10 && cp !== 13 || cp >= 127 && cp <= 159) {
|
|
898
|
+
i += width;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
out += String.fromCodePoint(cp);
|
|
902
|
+
i += width;
|
|
903
|
+
}
|
|
904
|
+
return out.replace(FORMAT_CONTROLS, "");
|
|
905
|
+
}
|
|
906
|
+
function clampScalars(value, max) {
|
|
907
|
+
const points = Array.from(value);
|
|
908
|
+
return points.length <= max ? value : points.slice(0, max).join("");
|
|
909
|
+
}
|
|
910
|
+
function sanitizeRemoteMultiline(input, maxScalars = 8192) {
|
|
911
|
+
const normalizedLines = stripTerminalSequences(input).replace(/\r\n?/g, "\n").replace(/\t/g, " ").split("\n").slice(0, 64).map((line) => clampScalars(line, 512)).join("\n").normalize("NFC");
|
|
912
|
+
return clampScalars(normalizedLines, maxScalars);
|
|
913
|
+
}
|
|
914
|
+
function sanitizeRemoteSingleLine(input, maxScalars = 256) {
|
|
915
|
+
return clampScalars(
|
|
916
|
+
stripTerminalSequences(input).replace(/\r\n?|\n|\t/g, " ").normalize("NFC"),
|
|
917
|
+
maxScalars
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/api/attentionMutations.ts
|
|
922
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
923
|
+
var ATTENTION_LEVELS = /* @__PURE__ */ new Set(["info", "action_required", "critical"]);
|
|
924
|
+
function requireImmutableId(value, label) {
|
|
925
|
+
if (!UUID.test(value)) throw new CommandFailure("usage", "invalid_immutable_id", `${label} must be an immutable UUID from a list result.`);
|
|
926
|
+
return value;
|
|
927
|
+
}
|
|
928
|
+
function invalid(label) {
|
|
929
|
+
throw new CommandFailure("relay_unavailable", "invalid_relay_response", `The relay returned an invalid ${label}.`);
|
|
930
|
+
}
|
|
931
|
+
function record(value, label) {
|
|
932
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) invalid(label);
|
|
933
|
+
return value;
|
|
934
|
+
}
|
|
935
|
+
function string(value, label, maxBytes) {
|
|
936
|
+
if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > maxBytes) invalid(label);
|
|
937
|
+
return value;
|
|
938
|
+
}
|
|
939
|
+
function uuid(value, label) {
|
|
940
|
+
const id = string(value, label, 36);
|
|
941
|
+
if (!UUID.test(id)) invalid(label);
|
|
942
|
+
return id;
|
|
943
|
+
}
|
|
944
|
+
function timestamp(value, label) {
|
|
945
|
+
const stamp = string(value, label, 64);
|
|
946
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(stamp)) invalid(label);
|
|
947
|
+
return stamp;
|
|
948
|
+
}
|
|
949
|
+
function count(value, label) {
|
|
950
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 1e6) invalid(label);
|
|
951
|
+
return value;
|
|
952
|
+
}
|
|
953
|
+
function level(value) {
|
|
954
|
+
if (typeof value !== "string" || !ATTENTION_LEVELS.has(value)) invalid("attention level");
|
|
955
|
+
return value;
|
|
956
|
+
}
|
|
957
|
+
function optionalActorType(value) {
|
|
958
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
959
|
+
if (value !== "human" && value !== "bot" && value !== "system") invalid("attention attribution type");
|
|
960
|
+
return value;
|
|
961
|
+
}
|
|
962
|
+
function decodeAttention(value) {
|
|
963
|
+
const item = record(value, "attention item");
|
|
964
|
+
const raisedByType = optionalActorType(item.raisedByType);
|
|
965
|
+
const raisedBy = item.raisedBy === void 0 || item.raisedBy === null ? void 0 : sanitizeRemoteSingleLine(string(item.raisedBy, "attention attribution", 256));
|
|
966
|
+
return {
|
|
967
|
+
id: uuid(item.id, "attention id"),
|
|
968
|
+
channelId: uuid(item.channelId, "attention channel id"),
|
|
969
|
+
level: level(item.level),
|
|
970
|
+
reason: sanitizeRemoteMultiline(string(item.reason, "attention reason", 1024)),
|
|
971
|
+
count: count(item.count, "attention count"),
|
|
972
|
+
raisedAt: timestamp(item.raisedAt, "attention raised timestamp"),
|
|
973
|
+
updatedAt: timestamp(item.updatedAt, "attention updated timestamp"),
|
|
974
|
+
...raisedBy ? { raisedBy } : {},
|
|
975
|
+
...raisedByType ? { raisedByType } : {}
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
function envelope(value, field) {
|
|
979
|
+
const outer = record(value, "response envelope");
|
|
980
|
+
if (outer.ok !== true || !(field in outer)) invalid("response envelope");
|
|
981
|
+
return outer[field];
|
|
982
|
+
}
|
|
983
|
+
var AttentionMutations = class {
|
|
984
|
+
constructor(transport) {
|
|
985
|
+
this.transport = transport;
|
|
986
|
+
}
|
|
987
|
+
transport;
|
|
988
|
+
async listChannel(channelId) {
|
|
989
|
+
const id = requireImmutableId(channelId, "channel id");
|
|
990
|
+
return this.transport.request({ method: "GET", path: `/channels/${id}/attention`, operation: "attention.list-channel", decode: (value) => {
|
|
991
|
+
const items = envelope(value, "attention");
|
|
992
|
+
if (!Array.isArray(items) || items.length > 64) invalid("attention list");
|
|
993
|
+
return items.map(decodeAttention);
|
|
994
|
+
} });
|
|
995
|
+
}
|
|
996
|
+
raise(args2) {
|
|
997
|
+
const channelId = requireImmutableId(args2.channelId, "channel id");
|
|
998
|
+
return this.transport.request({ method: "POST", path: `/channels/${channelId}/attention`, operation: "attention.raise", operationId: args2.operationId, reconcile: `GET /channels/${channelId}/attention`, body: { level: args2.level, reason: args2.reason, count: 1, sourceTaskIds: [] }, decode: (value) => {
|
|
999
|
+
const attention = decodeAttention(envelope(value, "attention"));
|
|
1000
|
+
if (attention.channelId !== channelId || attention.level !== args2.level || attention.reason !== sanitizeRemoteMultiline(args2.reason)) invalid("attention receipt");
|
|
1001
|
+
return { attention };
|
|
1002
|
+
} });
|
|
1003
|
+
}
|
|
1004
|
+
ack(args2) {
|
|
1005
|
+
const channelId = requireImmutableId(args2.channelId, "channel id");
|
|
1006
|
+
return this.transport.request({ method: "POST", path: `/channels/${channelId}/attention/ack`, operation: "attention.ack", operationId: args2.operationId, reconcile: `GET /channels/${channelId}/attention`, body: args2.level ? { level: args2.level } : {}, decode: (value) => ({ acked: count(envelope(value, "acked"), "acknowledged count") }) });
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
function normalizeReason(value) {
|
|
1010
|
+
if (Buffer.byteLength(value, "utf8") > 1024) throw new CommandFailure("usage", "reason_too_long", "Attention reason must be at most 1,024 UTF-8 bytes.");
|
|
1011
|
+
const reason = sanitizeRemoteSingleLine(value, 256).trim();
|
|
1012
|
+
if (!reason) throw new CommandFailure("usage", "reason_required", "Attention reason is required.");
|
|
1013
|
+
return reason;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
783
1016
|
// src/api/clerkRefresh.ts
|
|
784
1017
|
var CACHE_TTL_MS = 45e3;
|
|
785
1018
|
var FAPI_TIMEOUT_MS = 1e4;
|
|
@@ -838,15 +1071,158 @@ function createClerkTokenSupplier(clientToken, opts) {
|
|
|
838
1071
|
};
|
|
839
1072
|
}
|
|
840
1073
|
|
|
1074
|
+
// src/api/lifecycleTransport.ts
|
|
1075
|
+
var DEFAULT_READ_TIMEOUT_MS = 15e3;
|
|
1076
|
+
var DEFAULT_MUTATION_TIMEOUT_MS = 2e4;
|
|
1077
|
+
var DEFAULT_RESPONSE_LIMIT = 64 * 1024;
|
|
1078
|
+
var DEFAULT_REQUEST_LIMIT = 64 * 1024;
|
|
1079
|
+
var MAX_JSON_DEPTH = 32;
|
|
1080
|
+
var CANONICAL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
1081
|
+
function assertJsonDepth(value, depth = 0) {
|
|
1082
|
+
if (depth > MAX_JSON_DEPTH) throw new CommandFailure("relay_unavailable", "invalid_relay_response", "The relay response exceeded the accepted JSON depth.");
|
|
1083
|
+
if (Array.isArray(value)) for (const item of value) assertJsonDepth(item, depth + 1);
|
|
1084
|
+
else if (value && typeof value === "object") for (const item of Object.values(value)) assertJsonDepth(item, depth + 1);
|
|
1085
|
+
}
|
|
1086
|
+
function requestId(headers) {
|
|
1087
|
+
const value = headers.get("x-request-id") ?? void 0;
|
|
1088
|
+
return value && /^[A-Za-z0-9._:-]{1,128}$/.test(value) ? value : void 0;
|
|
1089
|
+
}
|
|
1090
|
+
async function readBounded(response, limit) {
|
|
1091
|
+
if (!response.body) return new Uint8Array();
|
|
1092
|
+
const reader = response.body.getReader();
|
|
1093
|
+
const chunks = [];
|
|
1094
|
+
let total = 0;
|
|
1095
|
+
try {
|
|
1096
|
+
for (; ; ) {
|
|
1097
|
+
const { done, value } = await reader.read();
|
|
1098
|
+
if (done) break;
|
|
1099
|
+
total += value.byteLength;
|
|
1100
|
+
if (total > limit) throw new CommandFailure("relay_unavailable", "response_too_large", "The relay response exceeded the accepted size.", { requestId: requestId(response.headers) });
|
|
1101
|
+
chunks.push(value);
|
|
1102
|
+
}
|
|
1103
|
+
} finally {
|
|
1104
|
+
await reader.cancel().catch(() => {
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
const bytes = new Uint8Array(total);
|
|
1108
|
+
let offset = 0;
|
|
1109
|
+
for (const chunk of chunks) {
|
|
1110
|
+
bytes.set(chunk, offset);
|
|
1111
|
+
offset += chunk.byteLength;
|
|
1112
|
+
}
|
|
1113
|
+
return bytes;
|
|
1114
|
+
}
|
|
1115
|
+
var LifecycleTransport = class {
|
|
1116
|
+
constructor(cfg) {
|
|
1117
|
+
this.cfg = cfg;
|
|
1118
|
+
this.origin = new URL(cfg.relayUrl);
|
|
1119
|
+
if (this.origin.protocol !== "https:" || this.origin.username || this.origin.password || this.origin.search || this.origin.hash || this.origin.pathname !== "/") {
|
|
1120
|
+
throw new CommandFailure("local_integrity_error", "invalid_relay_origin", "The selected relay profile has an invalid origin.");
|
|
1121
|
+
}
|
|
1122
|
+
this.fetchImpl = cfg.fetch ?? globalThis.fetch;
|
|
1123
|
+
}
|
|
1124
|
+
cfg;
|
|
1125
|
+
origin;
|
|
1126
|
+
fetchImpl;
|
|
1127
|
+
async request(req) {
|
|
1128
|
+
if (!req.path.startsWith("/") || req.path.startsWith("//") || req.path.length > 2048 || /[\x00-\x20\x7f-\x9f]/u.test(req.path)) throw new CommandFailure("internal_error", "invalid_route", "The lifecycle route is invalid.");
|
|
1129
|
+
const target = new URL(req.path, this.origin);
|
|
1130
|
+
if (target.origin !== this.origin.origin) throw new CommandFailure("internal_error", "cross_origin_route", "The lifecycle route crossed the selected relay origin.");
|
|
1131
|
+
const mutation = req.method !== "GET";
|
|
1132
|
+
if (mutation && (!req.operationId || !CANONICAL_UUID.test(req.operationId) || !req.reconcile)) throw new CommandFailure("internal_error", "missing_operation_identity", "A mutation requires a canonical operation ID and reconciliation method.");
|
|
1133
|
+
const outcomeUnknown = (rid2) => new OutcomeUnknown({
|
|
1134
|
+
operation: req.operation,
|
|
1135
|
+
operationId: req.operationId,
|
|
1136
|
+
requestId: rid2,
|
|
1137
|
+
reconcile: req.reconcile
|
|
1138
|
+
});
|
|
1139
|
+
let token;
|
|
1140
|
+
try {
|
|
1141
|
+
token = await this.cfg.getToken();
|
|
1142
|
+
} catch {
|
|
1143
|
+
throw new CommandFailure("authentication_required", "authentication_required", "Sign in with `arp-tui login`.");
|
|
1144
|
+
}
|
|
1145
|
+
if (!token) throw new CommandFailure("authentication_required", "authentication_required", "Sign in with `arp-tui login`.");
|
|
1146
|
+
let serializedBody;
|
|
1147
|
+
if (req.body !== void 0) {
|
|
1148
|
+
try {
|
|
1149
|
+
serializedBody = JSON.stringify(req.body);
|
|
1150
|
+
} catch {
|
|
1151
|
+
throw new CommandFailure("internal_error", "invalid_request_body", "The lifecycle request body could not be encoded.");
|
|
1152
|
+
}
|
|
1153
|
+
if (Buffer.byteLength(serializedBody, "utf8") > DEFAULT_REQUEST_LIMIT) throw new CommandFailure("internal_error", "request_too_large", "The lifecycle request exceeded the accepted size.");
|
|
1154
|
+
}
|
|
1155
|
+
let response;
|
|
1156
|
+
try {
|
|
1157
|
+
response = await this.fetchImpl(target, {
|
|
1158
|
+
method: req.method,
|
|
1159
|
+
redirect: "manual",
|
|
1160
|
+
headers: {
|
|
1161
|
+
accept: "application/json",
|
|
1162
|
+
authorization: `Bearer ${token}`,
|
|
1163
|
+
...req.body === void 0 ? {} : { "content-type": "application/json" },
|
|
1164
|
+
...req.operationId ? { "ARP-Operation-Id": req.operationId } : {}
|
|
1165
|
+
},
|
|
1166
|
+
body: serializedBody,
|
|
1167
|
+
signal: AbortSignal.timeout(mutation ? this.cfg.mutationTimeoutMs ?? DEFAULT_MUTATION_TIMEOUT_MS : this.cfg.readTimeoutMs ?? DEFAULT_READ_TIMEOUT_MS)
|
|
1168
|
+
});
|
|
1169
|
+
} catch {
|
|
1170
|
+
if (mutation) throw outcomeUnknown();
|
|
1171
|
+
throw new CommandFailure("relay_unavailable", "relay_unavailable", "The relay did not return a definitive response.");
|
|
1172
|
+
}
|
|
1173
|
+
const rid = requestId(response.headers);
|
|
1174
|
+
if (response.status >= 300 && response.status < 400) {
|
|
1175
|
+
if (mutation) throw outcomeUnknown(rid);
|
|
1176
|
+
throw new CommandFailure("relay_unavailable", "redirect_refused", "The relay returned a redirect, which was refused.", { requestId: rid });
|
|
1177
|
+
}
|
|
1178
|
+
const contentLength = Number(response.headers.get("content-length") ?? "0");
|
|
1179
|
+
const limit = this.cfg.responseLimitBytes ?? DEFAULT_RESPONSE_LIMIT;
|
|
1180
|
+
if (Number.isFinite(contentLength) && contentLength > limit) {
|
|
1181
|
+
if (mutation) throw outcomeUnknown(rid);
|
|
1182
|
+
throw new CommandFailure("relay_unavailable", "response_too_large", "The relay response exceeded the accepted size.", { requestId: rid });
|
|
1183
|
+
}
|
|
1184
|
+
let bytes;
|
|
1185
|
+
try {
|
|
1186
|
+
bytes = await readBounded(response, limit);
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
if (mutation) throw outcomeUnknown(rid);
|
|
1189
|
+
throw error;
|
|
1190
|
+
}
|
|
1191
|
+
if (!response.ok) {
|
|
1192
|
+
if (mutation && response.status >= 500) throw outcomeUnknown(rid);
|
|
1193
|
+
const symbol = response.status === 401 ? "authentication_required" : response.status === 403 ? "policy_denied" : "relay_unavailable";
|
|
1194
|
+
const code = response.status === 401 ? "authentication_required" : response.status === 403 ? "policy_denied" : "relay_rejected";
|
|
1195
|
+
throw new CommandFailure(symbol, code, response.status === 401 ? "Sign in with `arp-tui login`." : response.status === 403 ? "The relay policy denied this operation." : "The relay rejected the request.", { operationId: req.operationId, requestId: rid });
|
|
1196
|
+
}
|
|
1197
|
+
if (!(response.headers.get("content-type") ?? "").toLowerCase().startsWith("application/json")) {
|
|
1198
|
+
if (mutation) throw outcomeUnknown(rid);
|
|
1199
|
+
throw new CommandFailure("relay_unavailable", "invalid_content_type", "The relay returned an unsupported response type.", { requestId: rid });
|
|
1200
|
+
}
|
|
1201
|
+
let parsed;
|
|
1202
|
+
try {
|
|
1203
|
+
parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
1204
|
+
assertJsonDepth(parsed);
|
|
1205
|
+
return req.decode(parsed);
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
if (mutation) throw outcomeUnknown(rid);
|
|
1208
|
+
if (error instanceof CommandFailure) throw error;
|
|
1209
|
+
throw new CommandFailure("relay_unavailable", "invalid_relay_response", "The relay returned invalid or unexpected JSON.", { requestId: rid });
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
|
|
841
1214
|
// src/api/types.ts
|
|
842
1215
|
function normalizeMember(raw, prev) {
|
|
843
1216
|
const id = String(raw?.id ?? raw?.memberId ?? raw?.userId ?? prev?.id ?? "");
|
|
844
1217
|
const rawType = raw?.type ?? raw?.memberType;
|
|
1218
|
+
const type = rawType === "bot" ? "bot" : rawType === "human" ? "human" : prev?.type ?? "human";
|
|
845
1219
|
const status = raw?.status === "active" || raw?.status === "online" || raw?.isOnline === true ? "online" : raw?.status === "away" ? "away" : raw?.status === "offline" || raw?.isOnline === false ? "offline" : prev?.status ?? "offline";
|
|
846
1220
|
return {
|
|
847
1221
|
id,
|
|
848
|
-
type
|
|
849
|
-
|
|
1222
|
+
type,
|
|
1223
|
+
// Name fallback: a bot's id IS its full agent name — never shorten it.
|
|
1224
|
+
// Only a human's uuid id gets the 8-char short form for display.
|
|
1225
|
+
name: String(raw?.name ?? raw?.displayName ?? raw?.email ?? prev?.name ?? (type === "bot" ? id : id.slice(0, 8))),
|
|
850
1226
|
role: raw?.role === "owner" ? "owner" : "member",
|
|
851
1227
|
status
|
|
852
1228
|
};
|
|
@@ -921,6 +1297,58 @@ var RelayRest = class {
|
|
|
921
1297
|
const data = await this.req("GET", "/channels");
|
|
922
1298
|
return (data.channels ?? []).map((c) => ({ ...c, members: normalizeMembers(c.members) }));
|
|
923
1299
|
}
|
|
1300
|
+
/**
|
|
1301
|
+
* GET /channels?archived=true — the caller's ARCHIVED channels only (the
|
|
1302
|
+
* default listing is active-only; ListChannelsHandler channels.go:274).
|
|
1303
|
+
*/
|
|
1304
|
+
async listArchivedChannels() {
|
|
1305
|
+
const data = await this.req("GET", "/channels?archived=true");
|
|
1306
|
+
return (data.channels ?? []).map((c) => ({ ...c, members: normalizeMembers(c.members) }));
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* POST /channels — create a channel (Braid S3). Body per CreateChannelHandler
|
|
1310
|
+
* (channels.go:66): only {name}; the creator is resolved server-side and
|
|
1311
|
+
* added as owner; visibility defaults to "private".
|
|
1312
|
+
*/
|
|
1313
|
+
async createChannel(name) {
|
|
1314
|
+
const data = await this.req("POST", "/channels", { name });
|
|
1315
|
+
const ch = data.channel ?? {};
|
|
1316
|
+
return { ...ch, members: normalizeMembers(ch?.members) };
|
|
1317
|
+
}
|
|
1318
|
+
/** POST /channels/{id}/archive — reversible retire (channels are archive-only; no delete exists). */
|
|
1319
|
+
async archiveChannel(channelId) {
|
|
1320
|
+
await this.req("POST", `/channels/${encodeURIComponent(channelId)}/archive`, {});
|
|
1321
|
+
}
|
|
1322
|
+
/** POST /channels/{id}/unarchive — restore an archived channel (idempotent). */
|
|
1323
|
+
async unarchiveChannel(channelId) {
|
|
1324
|
+
await this.req("POST", `/channels/${encodeURIComponent(channelId)}/unarchive`, {});
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* POST /channels/{id}/agents/{agentId} — add an already-registered agent to
|
|
1328
|
+
* the channel via the PORTABILITY route (AddExistingAgentToChannelHandler,
|
|
1329
|
+
* agent_add_to_channel.go:35; route router.go:364). agentId is UUID-or-name;
|
|
1330
|
+
* we always send the UUID (unambiguous). This is the ONLY add path that
|
|
1331
|
+
* works for PERSONAL agents: AddMemberHandler's org gate (channels.go:789-797)
|
|
1332
|
+
* COALESCEs a personal agent's NULL org to uuid.Nil → 404 "agent not found".
|
|
1333
|
+
* Owner-only for personal agents; 403 guest_agents_disabled when the channel
|
|
1334
|
+
* org disallows guests. Idempotent; no request body; responds {ok:true}.
|
|
1335
|
+
*/
|
|
1336
|
+
async addAgentToChannel(channelId, agentId) {
|
|
1337
|
+
await this.req(
|
|
1338
|
+
"POST",
|
|
1339
|
+
`/channels/${encodeURIComponent(channelId)}/agents/${encodeURIComponent(agentId)}`
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* DELETE /channels/{id}/members/{memberId} — memberId is the member row's
|
|
1344
|
+
* member_id: agent name for a bot, user id for a human (channels.go:951,995).
|
|
1345
|
+
*/
|
|
1346
|
+
async removeChannelMember(channelId, memberId) {
|
|
1347
|
+
await this.req(
|
|
1348
|
+
"DELETE",
|
|
1349
|
+
`/channels/${encodeURIComponent(channelId)}/members/${encodeURIComponent(memberId)}`
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
924
1352
|
/** GET /channels/{id} */
|
|
925
1353
|
async getChannel(channelId) {
|
|
926
1354
|
const data = await this.req("GET", `/channels/${encodeURIComponent(channelId)}`);
|
|
@@ -966,6 +1394,108 @@ var RelayRest = class {
|
|
|
966
1394
|
async dismissNotification(id) {
|
|
967
1395
|
await this.req("POST", `/notifications/${encodeURIComponent(id)}/dismiss`, {});
|
|
968
1396
|
}
|
|
1397
|
+
/** POST /agents — mint a personal agent + one-time connect invite (Braid S1 `agent add`) */
|
|
1398
|
+
async createPersonalAgent(name) {
|
|
1399
|
+
return this.req("POST", "/agents", { name });
|
|
1400
|
+
}
|
|
1401
|
+
/** GET /agents/mine — my agents with live presence (Braid S1 `agents`) */
|
|
1402
|
+
async listMyAgents() {
|
|
1403
|
+
const data = await this.req("GET", "/agents/mine");
|
|
1404
|
+
return data.agents ?? [];
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* POST /agents/{id}/reissue — mint a fresh one-time connect invite for an
|
|
1408
|
+
* existing personal agent (Braid S2 `agent reissue`). Owner-only; 429 when
|
|
1409
|
+
* the relay's register rate limit trips.
|
|
1410
|
+
*/
|
|
1411
|
+
async reissueAgent(agentId) {
|
|
1412
|
+
return this.req("POST", `/agents/${encodeURIComponent(agentId)}/reissue`, {});
|
|
1413
|
+
}
|
|
1414
|
+
/** POST /agents/{id}/credential/revoke — revoke the durable key family + close the live WS (Braid S2). */
|
|
1415
|
+
async revokeAgentCredential(agentId) {
|
|
1416
|
+
const data = await this.req(
|
|
1417
|
+
"POST",
|
|
1418
|
+
`/agents/${encodeURIComponent(agentId)}/credential/revoke`,
|
|
1419
|
+
{}
|
|
1420
|
+
);
|
|
1421
|
+
return data.credential ?? {};
|
|
1422
|
+
}
|
|
1423
|
+
/** DELETE /agents/{id} — permanently delete the agent (row, memberships, Clerk machine). */
|
|
1424
|
+
async deleteAgent(agentId) {
|
|
1425
|
+
await this.req("DELETE", `/agents/${encodeURIComponent(agentId)}`);
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* POST /channels/{id}/agents/invite — mint a PENDING channel-scoped agent
|
|
1429
|
+
* invite (CreateAgentInviteHandler, agent_invite.go:47; route router.go:353,
|
|
1430
|
+
* RequireChannelMembership + RequireOrgAdmin). Body requires {name}
|
|
1431
|
+
* (agent_invite.go:75-77); provisioning happens at redeem. The relay stores
|
|
1432
|
+
* only the secret's SHA-256 — the response is the ONLY copy of the secret.
|
|
1433
|
+
*/
|
|
1434
|
+
async createChannelAgentInvite(channelId, name) {
|
|
1435
|
+
return this.req(
|
|
1436
|
+
"POST",
|
|
1437
|
+
`/channels/${encodeURIComponent(channelId)}/agents/invite`,
|
|
1438
|
+
{ name }
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
/** GET /channels/{id}/agents/invites — the channel's invites, newest first (agent_invites_manage.go:17). */
|
|
1442
|
+
async listChannelAgentInvites(channelId) {
|
|
1443
|
+
const data = await this.req(
|
|
1444
|
+
"GET",
|
|
1445
|
+
`/channels/${encodeURIComponent(channelId)}/agents/invites`
|
|
1446
|
+
);
|
|
1447
|
+
return data.invites ?? [];
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* DELETE /channels/{id}/agents/invites/{inviteId} — soft-revoke a
|
|
1451
|
+
* channel-scoped invite (agent_invites_manage.go:55). Idempotent 200;
|
|
1452
|
+
* unknown/cross-channel id → 404 "invite not found".
|
|
1453
|
+
*/
|
|
1454
|
+
async revokeChannelAgentInvite(channelId, inviteId) {
|
|
1455
|
+
await this.req(
|
|
1456
|
+
"DELETE",
|
|
1457
|
+
`/channels/${encodeURIComponent(channelId)}/agents/invites/${encodeURIComponent(inviteId)}`
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* GET /agents/invites — the caller's org's channel-less ORG-kind invites,
|
|
1462
|
+
* newest first, same row shape as the channel listing (org_invites.go:20;
|
|
1463
|
+
* route router.go:236, members.manage-gated → 403 admin_required).
|
|
1464
|
+
*/
|
|
1465
|
+
async listOrgAgentInvites() {
|
|
1466
|
+
const data = await this.req("GET", "/agents/invites");
|
|
1467
|
+
return data.invites ?? [];
|
|
1468
|
+
}
|
|
1469
|
+
/** DELETE /agents/invites/{inviteId} — soft-revoke an ORG-kind invite (org_invites.go:65); 404 hides cross-org/channel-kind ids. */
|
|
1470
|
+
async revokeOrgAgentInvite(inviteId) {
|
|
1471
|
+
await this.req("DELETE", `/agents/invites/${encodeURIComponent(inviteId)}`);
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* POST /channels/{id}/humans/invite — invite a human by email
|
|
1475
|
+
* (InviteHumanHandler, humans_invite.go:26; route router.go:360, org-admin).
|
|
1476
|
+
* Hybrid Clerk+relay: sends a Clerk org invitation and adds a pending user to
|
|
1477
|
+
* the channel; an EXISTING cross-org user joins this channel only, as a guest.
|
|
1478
|
+
*/
|
|
1479
|
+
async inviteHumanToChannel(channelId, email) {
|
|
1480
|
+
return this.req(
|
|
1481
|
+
"POST",
|
|
1482
|
+
`/channels/${encodeURIComponent(channelId)}/humans/invite`,
|
|
1483
|
+
{ email }
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
/** GET /organizations/me — caller's own org id + guest-agent policy (organizations.go:16; 404 when no active org). */
|
|
1487
|
+
async getMyOrganization() {
|
|
1488
|
+
const data = await this.req("GET", "/organizations/me");
|
|
1489
|
+
return data.organization;
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* PATCH /organizations/{id} {allowGuestAgents} — admin-only, own-org-only
|
|
1493
|
+
* (PatchOrganizationHandler, organizations.go:36-59; route router.go:151,
|
|
1494
|
+
* RequireOrgAdmin → 403 admin_required; cross-org id → 404).
|
|
1495
|
+
*/
|
|
1496
|
+
async patchOrganization(orgId, patch) {
|
|
1497
|
+
await this.req("PATCH", `/organizations/${encodeURIComponent(orgId)}`, patch);
|
|
1498
|
+
}
|
|
969
1499
|
};
|
|
970
1500
|
|
|
971
1501
|
// src/state/activityFold.ts
|
|
@@ -1037,10 +1567,10 @@ function foldActivityEvents(events) {
|
|
|
1037
1567
|
|
|
1038
1568
|
// src/state/priority.ts
|
|
1039
1569
|
function channelPriority(cid, p) {
|
|
1040
|
-
const
|
|
1041
|
-
if (
|
|
1042
|
-
if (
|
|
1043
|
-
if (
|
|
1570
|
+
const level2 = p.attention[cid]?.highestLevel;
|
|
1571
|
+
if (level2 === "critical") return 0;
|
|
1572
|
+
if (level2 === "action_required") return 1;
|
|
1573
|
+
if (level2 === "info") return 2;
|
|
1044
1574
|
if ((p.unread[cid] ?? 0) > 0) return 3;
|
|
1045
1575
|
return 4;
|
|
1046
1576
|
}
|
|
@@ -1342,6 +1872,10 @@ function createStore(opts) {
|
|
|
1342
1872
|
unread: { ...config.unread ?? {} },
|
|
1343
1873
|
unreadCapNotice: null,
|
|
1344
1874
|
attention: {},
|
|
1875
|
+
channelAttention: [],
|
|
1876
|
+
attentionSelected: 0,
|
|
1877
|
+
attentionDraft: "",
|
|
1878
|
+
attentionBusy: false,
|
|
1345
1879
|
notifications: [],
|
|
1346
1880
|
notifSelected: 0,
|
|
1347
1881
|
activityByTurn: {},
|
|
@@ -1498,6 +2032,9 @@ function createStore(opts) {
|
|
|
1498
2032
|
}
|
|
1499
2033
|
const rest = new RelayRest({ relayUrl, getToken });
|
|
1500
2034
|
const ws = new RelayWs({ relayUrl, getToken });
|
|
2035
|
+
const attentionApi = () => opts.attentionApi ?? new AttentionMutations(new LifecycleTransport({ relayUrl, getToken }));
|
|
2036
|
+
let pendingAttentionLevel = null;
|
|
2037
|
+
let pendingAttentionAck = null;
|
|
1501
2038
|
function pushDivider(label) {
|
|
1502
2039
|
const id = `divider-${++dividerCounter}`;
|
|
1503
2040
|
seen.add(id);
|
|
@@ -1595,6 +2132,141 @@ function createStore(opts) {
|
|
|
1595
2132
|
const next = Math.max(0, Math.min(state.notifSelected + delta, len - 1));
|
|
1596
2133
|
set({ notifSelected: next });
|
|
1597
2134
|
}
|
|
2135
|
+
async function refreshChannelAttention() {
|
|
2136
|
+
const channelId = state.activeChannelId;
|
|
2137
|
+
if (!channelId) return;
|
|
2138
|
+
try {
|
|
2139
|
+
const items = await attentionApi().listChannel(channelId);
|
|
2140
|
+
if (state.activeChannelId !== channelId) return;
|
|
2141
|
+
setThrottled({
|
|
2142
|
+
channelAttention: items,
|
|
2143
|
+
attentionSelected: Math.max(0, Math.min(state.attentionSelected, items.length - 1))
|
|
2144
|
+
});
|
|
2145
|
+
} catch (error) {
|
|
2146
|
+
if (error instanceof CommandFailure && state.mode.startsWith("attention")) {
|
|
2147
|
+
set({ error: `${error.stableCode}: ${error.message}` });
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
function finishAttentionMutation(error) {
|
|
2152
|
+
pendingAttentionLevel = null;
|
|
2153
|
+
pendingAttentionAck = null;
|
|
2154
|
+
set({
|
|
2155
|
+
mode: "attention",
|
|
2156
|
+
attentionDraft: "",
|
|
2157
|
+
attentionBusy: false,
|
|
2158
|
+
error: error instanceof CommandFailure ? `${error.stableCode}: ${error.message}` : error ? "attention operation failed safely" : null
|
|
2159
|
+
});
|
|
2160
|
+
void refreshChannelAttention();
|
|
2161
|
+
void pollAttention();
|
|
2162
|
+
}
|
|
2163
|
+
async function performAttentionRaise(level2, reason) {
|
|
2164
|
+
const channelId = state.activeChannelId;
|
|
2165
|
+
if (!channelId || state.attentionBusy) return;
|
|
2166
|
+
set({ mode: "attention", attentionDraft: "", attentionBusy: true, error: null });
|
|
2167
|
+
const operationId = (opts.newOperationId ?? randomUUID)();
|
|
2168
|
+
try {
|
|
2169
|
+
const api = attentionApi();
|
|
2170
|
+
const before = await api.listChannel(channelId);
|
|
2171
|
+
const previousCount = before.find((item) => item.level === level2)?.count ?? 0;
|
|
2172
|
+
try {
|
|
2173
|
+
await api.raise({ channelId, level: level2, reason, operationId });
|
|
2174
|
+
} catch (error) {
|
|
2175
|
+
if (!(error instanceof OutcomeUnknown)) throw error;
|
|
2176
|
+
const current = (await api.listChannel(channelId)).find((item) => item.level === level2);
|
|
2177
|
+
if (!current || current.reason !== reason || current.count <= previousCount) throw error;
|
|
2178
|
+
}
|
|
2179
|
+
finishAttentionMutation();
|
|
2180
|
+
} catch (error) {
|
|
2181
|
+
finishAttentionMutation(error);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
async function performAttentionAck(scope) {
|
|
2185
|
+
const channelId = state.activeChannelId;
|
|
2186
|
+
if (!channelId || state.attentionBusy) return;
|
|
2187
|
+
set({ mode: "attention", attentionDraft: "", attentionBusy: true, error: null });
|
|
2188
|
+
const operationId = (opts.newOperationId ?? randomUUID)();
|
|
2189
|
+
try {
|
|
2190
|
+
const api = attentionApi();
|
|
2191
|
+
try {
|
|
2192
|
+
await api.ack({ channelId, ...scope === "all" ? {} : { level: scope }, operationId });
|
|
2193
|
+
} catch (error) {
|
|
2194
|
+
if (!(error instanceof OutcomeUnknown)) throw error;
|
|
2195
|
+
const current = await api.listChannel(channelId);
|
|
2196
|
+
const reconciled = scope === "all" ? current.length === 0 : !current.some((item) => item.level === scope);
|
|
2197
|
+
if (!reconciled) throw error;
|
|
2198
|
+
}
|
|
2199
|
+
finishAttentionMutation();
|
|
2200
|
+
} catch (error) {
|
|
2201
|
+
finishAttentionMutation(error);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
function openAttention() {
|
|
2205
|
+
if (!state.activeChannelId || state.attentionBusy) return;
|
|
2206
|
+
set({ mode: "attention", attentionDraft: "", attentionSelected: 0, error: null });
|
|
2207
|
+
void refreshChannelAttention();
|
|
2208
|
+
}
|
|
2209
|
+
function closeAttention() {
|
|
2210
|
+
if (state.attentionBusy) return;
|
|
2211
|
+
pendingAttentionLevel = null;
|
|
2212
|
+
pendingAttentionAck = null;
|
|
2213
|
+
set({
|
|
2214
|
+
mode: state.mode === "attention" ? "normal" : "attention",
|
|
2215
|
+
attentionDraft: "",
|
|
2216
|
+
error: null
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
function moveAttentionSelection(delta) {
|
|
2220
|
+
if (state.channelAttention.length === 0 || state.attentionBusy) return;
|
|
2221
|
+
set({ attentionSelected: Math.max(0, Math.min(state.attentionSelected + delta, state.channelAttention.length - 1)) });
|
|
2222
|
+
}
|
|
2223
|
+
function beginAttentionRaise() {
|
|
2224
|
+
if (state.attentionBusy) return;
|
|
2225
|
+
pendingAttentionLevel = null;
|
|
2226
|
+
pendingAttentionAck = null;
|
|
2227
|
+
set({ mode: "attention_level", attentionDraft: "", error: null });
|
|
2228
|
+
}
|
|
2229
|
+
function beginAttentionAck(all) {
|
|
2230
|
+
if (state.attentionBusy) return;
|
|
2231
|
+
if (all) pendingAttentionAck = "all";
|
|
2232
|
+
else pendingAttentionAck = state.channelAttention[state.attentionSelected]?.level ?? null;
|
|
2233
|
+
if (!pendingAttentionAck) return;
|
|
2234
|
+
set({ mode: "attention_confirm", attentionDraft: "", error: null });
|
|
2235
|
+
}
|
|
2236
|
+
function setAttentionDraft(value) {
|
|
2237
|
+
const max = state.mode === "attention_level" ? 32 : state.mode === "attention_confirm" ? 36 : 1024;
|
|
2238
|
+
if (Buffer.byteLength(value, "utf8") <= max) set({ attentionDraft: value });
|
|
2239
|
+
}
|
|
2240
|
+
function submitAttentionInput() {
|
|
2241
|
+
if (state.attentionBusy) return;
|
|
2242
|
+
if (state.mode === "attention_level") {
|
|
2243
|
+
const value = state.attentionDraft.trim();
|
|
2244
|
+
if (!ATTENTION_LEVELS.has(value)) {
|
|
2245
|
+
set({ error: "level must be info, action_required, or critical" });
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
pendingAttentionLevel = value;
|
|
2249
|
+
set({ mode: "attention_reason", attentionDraft: "", error: null });
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
if (state.mode === "attention_reason") {
|
|
2253
|
+
if (!pendingAttentionLevel) return;
|
|
2254
|
+
try {
|
|
2255
|
+
const reason = normalizeReason(state.attentionDraft);
|
|
2256
|
+
void performAttentionRaise(pendingAttentionLevel, reason);
|
|
2257
|
+
} catch (error) {
|
|
2258
|
+
if (error instanceof CommandFailure) set({ error: `${error.stableCode}: ${error.message}` });
|
|
2259
|
+
}
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
if (state.mode === "attention_confirm") {
|
|
2263
|
+
if (state.attentionDraft !== state.activeChannelId) {
|
|
2264
|
+
set({ error: "confirmation did not match the channel UUID" });
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
if (pendingAttentionAck) void performAttentionAck(pendingAttentionAck);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
1598
2270
|
function seedPresence(members) {
|
|
1599
2271
|
const presence = {};
|
|
1600
2272
|
for (const m of members) presence[m.id] = m.status === "online" ? "active" : m.status;
|
|
@@ -1811,6 +2483,16 @@ function createStore(opts) {
|
|
|
1811
2483
|
pushDivider("channel archived");
|
|
1812
2484
|
return;
|
|
1813
2485
|
}
|
|
2486
|
+
case "channel.attention.raised":
|
|
2487
|
+
case "channel.attention.updated":
|
|
2488
|
+
case "channel.attention.cleared":
|
|
2489
|
+
case "channel.attention.acked": {
|
|
2490
|
+
void pollAttention();
|
|
2491
|
+
if (frame.channelId === state.activeChannelId && state.mode.startsWith("attention")) {
|
|
2492
|
+
void refreshChannelAttention();
|
|
2493
|
+
}
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
1814
2496
|
case "activity_event": {
|
|
1815
2497
|
if (frame.channelId !== state.activeChannelId) return;
|
|
1816
2498
|
if (!frame.turnId || !frame.toolCallId) return;
|
|
@@ -2093,6 +2775,13 @@ function createStore(opts) {
|
|
|
2093
2775
|
dismissNotification: (id) => void dismissNotification(id),
|
|
2094
2776
|
markAllNotificationsRead: () => void markAllNotificationsRead(),
|
|
2095
2777
|
moveNotifSelection,
|
|
2778
|
+
openAttention,
|
|
2779
|
+
closeAttention,
|
|
2780
|
+
moveAttentionSelection,
|
|
2781
|
+
beginAttentionRaise,
|
|
2782
|
+
beginAttentionAck,
|
|
2783
|
+
setAttentionDraft,
|
|
2784
|
+
submitAttentionInput,
|
|
2096
2785
|
jumpNextActive: () => {
|
|
2097
2786
|
const s = state;
|
|
2098
2787
|
const target = nextActiveChannel(s.channels, s.activeChannelId, { attention: s.attention, unread: s.unread });
|
|
@@ -2105,22 +2794,6 @@ function createStore(opts) {
|
|
|
2105
2794
|
|
|
2106
2795
|
// src/ui/ActivityLine.tsx
|
|
2107
2796
|
import { Text } from "ink";
|
|
2108
|
-
|
|
2109
|
-
// src/util/sanitize.ts
|
|
2110
|
-
var ESCAPE_SEQ = (
|
|
2111
|
-
// eslint-disable-next-line no-control-regex
|
|
2112
|
-
/\x1b(?:\[[0-?]*[ -/]*[@-~]|\][\s\S]*?(?:\x07|\x1b\\)|[PX^_][\s\S]*?\x1b\\|[@-Z\\-_])/g
|
|
2113
|
-
);
|
|
2114
|
-
var CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
2115
|
-
function sanitizeForTty(input) {
|
|
2116
|
-
return input.replace(ESCAPE_SEQ, "").replace(CONTROL_CHARS, "");
|
|
2117
|
-
}
|
|
2118
|
-
function sanitizeLabel(input, max = 40) {
|
|
2119
|
-
const clean = sanitizeForTty(input);
|
|
2120
|
-
return clean.length > max ? clean.slice(0, max - 1) + "\u2026" : clean;
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
// src/ui/ActivityLine.tsx
|
|
2124
2797
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2125
2798
|
var MAX_SHOWN = 3;
|
|
2126
2799
|
function ActivityLine({ activities }) {
|
|
@@ -2191,8 +2864,12 @@ function fallbackTitle(call) {
|
|
|
2191
2864
|
function ActivitySurface(props) {
|
|
2192
2865
|
const { toolCalls, plan, visible, pinned } = props;
|
|
2193
2866
|
if (!visible) return null;
|
|
2867
|
+
const pinHint = pinned ? "pinned \xB7 press v to unpin" : "press v to keep open";
|
|
2194
2868
|
if (toolCalls.length === 0 && plan.length === 0) {
|
|
2195
|
-
return /* @__PURE__ */
|
|
2869
|
+
return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", paddingLeft: 1, children: [
|
|
2870
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "no activity yet" }),
|
|
2871
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: pinHint })
|
|
2872
|
+
] });
|
|
2196
2873
|
}
|
|
2197
2874
|
return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", paddingLeft: 1, children: [
|
|
2198
2875
|
toolCalls.length > 0 && /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", children: [
|
|
@@ -2208,7 +2885,7 @@ function ActivitySurface(props) {
|
|
|
2208
2885
|
pinned && call.locations && call.locations.length > 0 && /* @__PURE__ */ jsx2(Box, { flexDirection: "column", paddingLeft: 2, children: call.locations.map((loc, i) => /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
2209
2886
|
sanitizeLabel(loc.path, 60),
|
|
2210
2887
|
loc.line != null ? `:${loc.line}` : ""
|
|
2211
|
-
] }, i)) })
|
|
2888
|
+
] }, `${loc.path}:${loc.line ?? ""}#${i}`)) })
|
|
2212
2889
|
] }, call.toolCallId))
|
|
2213
2890
|
] }),
|
|
2214
2891
|
plan.length > 0 && /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", children: [
|
|
@@ -2217,14 +2894,71 @@ function ActivitySurface(props) {
|
|
|
2217
2894
|
planGlyph(entry.status),
|
|
2218
2895
|
" ",
|
|
2219
2896
|
sanitizeForTty(entry.content)
|
|
2220
|
-
] }, i))
|
|
2221
|
-
] })
|
|
2897
|
+
] }, `${entry.content}#${i}`))
|
|
2898
|
+
] }),
|
|
2899
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: pinHint })
|
|
2222
2900
|
] });
|
|
2223
2901
|
}
|
|
2224
2902
|
|
|
2225
|
-
// src/ui/
|
|
2903
|
+
// src/ui/AttentionPanel.tsx
|
|
2226
2904
|
import { Box as Box2, Text as Text3 } from "ink";
|
|
2227
2905
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2906
|
+
var LEVEL_COLOR = {
|
|
2907
|
+
info: "blue",
|
|
2908
|
+
action_required: "yellow",
|
|
2909
|
+
critical: "red"
|
|
2910
|
+
};
|
|
2911
|
+
function AttentionPanel({
|
|
2912
|
+
channelId,
|
|
2913
|
+
items,
|
|
2914
|
+
selected,
|
|
2915
|
+
mode,
|
|
2916
|
+
draft,
|
|
2917
|
+
busy
|
|
2918
|
+
}) {
|
|
2919
|
+
const prompt = mode === "attention_level" ? "level (info | action_required | critical)" : mode === "attention_reason" ? "reason" : mode === "attention_confirm" ? `type ${channelId} to confirm` : null;
|
|
2920
|
+
return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
2921
|
+
/* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
|
|
2922
|
+
"ATTENTION channel:",
|
|
2923
|
+
sanitizeForTty(channelId)
|
|
2924
|
+
] }),
|
|
2925
|
+
items.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "no active attention" }) : items.map((item, index) => /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", children: [
|
|
2926
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
2927
|
+
/* @__PURE__ */ jsxs3(Text3, { color: index === selected ? "cyan" : void 0, children: [
|
|
2928
|
+
index === selected ? ">" : " ",
|
|
2929
|
+
" "
|
|
2930
|
+
] }),
|
|
2931
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: LEVEL_COLOR[item.level], children: item.level }),
|
|
2932
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
2933
|
+
" count:",
|
|
2934
|
+
item.count,
|
|
2935
|
+
" ",
|
|
2936
|
+
item.raisedByType ?? "unknown",
|
|
2937
|
+
":",
|
|
2938
|
+
sanitizeForTty(item.raisedBy ?? "unknown")
|
|
2939
|
+
] })
|
|
2940
|
+
] }),
|
|
2941
|
+
/* @__PURE__ */ jsxs3(Text3, { wrap: "wrap", children: [
|
|
2942
|
+
" ",
|
|
2943
|
+
sanitizeForTty(item.reason)
|
|
2944
|
+
] }),
|
|
2945
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
2946
|
+
" updated ",
|
|
2947
|
+
sanitizeForTty(item.updatedAt)
|
|
2948
|
+
] })
|
|
2949
|
+
] }, item.id)),
|
|
2950
|
+
prompt ? /* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
|
|
2951
|
+
prompt,
|
|
2952
|
+
": ",
|
|
2953
|
+
sanitizeForTty(draft),
|
|
2954
|
+
"\u258C"
|
|
2955
|
+
] }) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: busy ? "working\u2026" : "j/k select \xB7 r raise \xB7 x ack selected \xB7 X ack all \xB7 Esc close" })
|
|
2956
|
+
] });
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
// src/ui/ChannelSwitcher.tsx
|
|
2960
|
+
import { Box as Box3, Text as Text4 } from "ink";
|
|
2961
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2228
2962
|
function channelLabel(ch) {
|
|
2229
2963
|
return `${channelSigil(ch.kind)}${sanitizeLabel(ch.name)}`;
|
|
2230
2964
|
}
|
|
@@ -2239,7 +2973,7 @@ function ChannelBadge({
|
|
|
2239
2973
|
if (att) {
|
|
2240
2974
|
const color = att.highestLevel === "critical" ? "red" : att.highestLevel === "action_required" ? "yellow" : "cyan";
|
|
2241
2975
|
const sigil = att.highestLevel === "critical" ? "!" : att.highestLevel === "action_required" ? "\u25B2" : "\u2022";
|
|
2242
|
-
return /* @__PURE__ */
|
|
2976
|
+
return /* @__PURE__ */ jsxs4(Text4, { color, children: [
|
|
2243
2977
|
" ",
|
|
2244
2978
|
"[",
|
|
2245
2979
|
sigil,
|
|
@@ -2247,11 +2981,11 @@ function ChannelBadge({
|
|
|
2247
2981
|
"]"
|
|
2248
2982
|
] });
|
|
2249
2983
|
}
|
|
2250
|
-
const
|
|
2251
|
-
if (
|
|
2252
|
-
return /* @__PURE__ */
|
|
2984
|
+
const count2 = unread[channelId] ?? 0;
|
|
2985
|
+
if (count2 > 0) {
|
|
2986
|
+
return /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
2253
2987
|
" (",
|
|
2254
|
-
|
|
2988
|
+
count2,
|
|
2255
2989
|
")"
|
|
2256
2990
|
] });
|
|
2257
2991
|
}
|
|
@@ -2268,11 +3002,11 @@ function ChannelSwitcher({
|
|
|
2268
3002
|
const sorted = sortChannelsByPriority(filtered, { attention, unread });
|
|
2269
3003
|
const visible = sorted.slice(0, 9);
|
|
2270
3004
|
const hidden = sorted.length - visible.length;
|
|
2271
|
-
return /* @__PURE__ */
|
|
2272
|
-
/* @__PURE__ */
|
|
3005
|
+
return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
3006
|
+
/* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
|
|
2273
3007
|
"channels",
|
|
2274
3008
|
" ",
|
|
2275
|
-
/* @__PURE__ */
|
|
3009
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
2276
3010
|
"(",
|
|
2277
3011
|
sorted.length,
|
|
2278
3012
|
"/",
|
|
@@ -2280,23 +3014,23 @@ function ChannelSwitcher({
|
|
|
2280
3014
|
")"
|
|
2281
3015
|
] })
|
|
2282
3016
|
] }),
|
|
2283
|
-
/* @__PURE__ */
|
|
2284
|
-
/* @__PURE__ */
|
|
2285
|
-
/* @__PURE__ */
|
|
2286
|
-
/* @__PURE__ */
|
|
3017
|
+
/* @__PURE__ */ jsxs4(Text4, { children: [
|
|
3018
|
+
/* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "filter: " }),
|
|
3019
|
+
/* @__PURE__ */ jsx4(Text4, { children: sanitizeLabel(filter, 40) }),
|
|
3020
|
+
/* @__PURE__ */ jsx4(Text4, { inverse: true, children: " " })
|
|
2287
3021
|
] }),
|
|
2288
|
-
visible.map((ch, i) => /* @__PURE__ */
|
|
2289
|
-
/* @__PURE__ */
|
|
3022
|
+
visible.map((ch, i) => /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
3023
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "cyan", children: [
|
|
2290
3024
|
i + 1,
|
|
2291
3025
|
" "
|
|
2292
3026
|
] }),
|
|
2293
|
-
/* @__PURE__ */
|
|
2294
|
-
/* @__PURE__ */
|
|
3027
|
+
/* @__PURE__ */ jsx4(Text4, { bold: ch.id === activeChannelId, children: channelLabel(ch) }),
|
|
3028
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
2295
3029
|
" \xB7 ",
|
|
2296
3030
|
ch.members.length,
|
|
2297
3031
|
" members"
|
|
2298
3032
|
] }),
|
|
2299
|
-
/* @__PURE__ */
|
|
3033
|
+
/* @__PURE__ */ jsx4(
|
|
2300
3034
|
ChannelBadge,
|
|
2301
3035
|
{
|
|
2302
3036
|
channelId: ch.id,
|
|
@@ -2306,36 +3040,36 @@ function ChannelSwitcher({
|
|
|
2306
3040
|
}
|
|
2307
3041
|
)
|
|
2308
3042
|
] }, ch.id)),
|
|
2309
|
-
hidden > 0 ? /* @__PURE__ */
|
|
3043
|
+
hidden > 0 ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
2310
3044
|
"\u2026",
|
|
2311
3045
|
hidden,
|
|
2312
3046
|
" more \u2014 keep typing to narrow"
|
|
2313
3047
|
] }) : null,
|
|
2314
|
-
sorted.length === 0 ? /* @__PURE__ */
|
|
3048
|
+
sorted.length === 0 ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
2315
3049
|
'no channels match "',
|
|
2316
3050
|
sanitizeLabel(filter, 20),
|
|
2317
3051
|
'"'
|
|
2318
3052
|
] }) : null,
|
|
2319
|
-
/* @__PURE__ */
|
|
3053
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: filter.length === 0 ? "type to filter \xB7 1\u20139 quick-select \xB7 n jump \xB7 Esc cancel" : "keep typing to narrow \xB7 Enter opens top match \xB7 Esc cancel" })
|
|
2320
3054
|
] });
|
|
2321
3055
|
}
|
|
2322
3056
|
|
|
2323
3057
|
// src/ui/Composer.tsx
|
|
2324
|
-
import { Box as
|
|
2325
|
-
import { jsx as
|
|
3058
|
+
import { Box as Box4, Text as Text5 } from "ink";
|
|
3059
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2326
3060
|
function Composer({ draft, focused }) {
|
|
2327
3061
|
const width = Math.max((process.stdout.columns ?? 80) - 8, 20);
|
|
2328
3062
|
const visible = draft.length > width ? "\u2026" + draft.slice(-(width - 1)) : draft;
|
|
2329
|
-
return /* @__PURE__ */
|
|
2330
|
-
/* @__PURE__ */
|
|
2331
|
-
draft.length === 0 && !focused ? /* @__PURE__ */
|
|
2332
|
-
focused ? /* @__PURE__ */
|
|
3063
|
+
return /* @__PURE__ */ jsx5(Box4, { borderStyle: "round", borderColor: focused ? "cyan" : "gray", paddingX: 1, width: "100%", children: /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
|
|
3064
|
+
/* @__PURE__ */ jsx5(Text5, { color: focused ? "cyan" : "gray", children: "> " }),
|
|
3065
|
+
draft.length === 0 && !focused ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "i to compose" }) : /* @__PURE__ */ jsx5(Text5, { children: visible }),
|
|
3066
|
+
focused ? /* @__PURE__ */ jsx5(Text5, { inverse: true, children: " " }) : null
|
|
2333
3067
|
] }) });
|
|
2334
3068
|
}
|
|
2335
3069
|
|
|
2336
3070
|
// src/ui/Header.tsx
|
|
2337
|
-
import { Box as
|
|
2338
|
-
import { jsx as
|
|
3071
|
+
import { Box as Box5, Text as Text6 } from "ink";
|
|
3072
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2339
3073
|
var STATUS_DOT = {
|
|
2340
3074
|
online: { glyph: "\u25CF", color: "green" },
|
|
2341
3075
|
away: { glyph: "\u25D0", color: "yellow" },
|
|
@@ -2352,32 +3086,32 @@ function Header({
|
|
|
2352
3086
|
members,
|
|
2353
3087
|
presence
|
|
2354
3088
|
}) {
|
|
2355
|
-
return /* @__PURE__ */
|
|
2356
|
-
/* @__PURE__ */
|
|
2357
|
-
/* @__PURE__ */
|
|
2358
|
-
/* @__PURE__ */
|
|
2359
|
-
/* @__PURE__ */
|
|
2360
|
-
/* @__PURE__ */
|
|
3089
|
+
return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", children: [
|
|
3090
|
+
/* @__PURE__ */ jsxs6(Box5, { justifyContent: "space-between", children: [
|
|
3091
|
+
/* @__PURE__ */ jsxs6(Text6, { children: [
|
|
3092
|
+
/* @__PURE__ */ jsx6(Text6, { color: "cyan", bold: true, children: "arp-tui" }),
|
|
3093
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " \u25B8 " }),
|
|
3094
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
|
|
2361
3095
|
channelSigil(channelKind),
|
|
2362
3096
|
sanitizeLabel(channelName)
|
|
2363
3097
|
] })
|
|
2364
3098
|
] }),
|
|
2365
|
-
/* @__PURE__ */
|
|
3099
|
+
/* @__PURE__ */ jsx6(Text6, { children: members.map((m) => {
|
|
2366
3100
|
const dot = STATUS_DOT[effectiveStatus(m, presence)];
|
|
2367
|
-
return /* @__PURE__ */
|
|
2368
|
-
/* @__PURE__ */
|
|
3101
|
+
return /* @__PURE__ */ jsxs6(Text6, { children: [
|
|
3102
|
+
/* @__PURE__ */ jsxs6(Text6, { color: dot.color, children: [
|
|
2369
3103
|
dot.glyph,
|
|
2370
3104
|
" "
|
|
2371
3105
|
] }),
|
|
2372
|
-
/* @__PURE__ */
|
|
3106
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: effectiveStatus(m, presence) === "offline", children: [
|
|
2373
3107
|
m.type === "bot" ? "\u2699" : "",
|
|
2374
3108
|
sanitizeLabel(m.name, 20)
|
|
2375
3109
|
] }),
|
|
2376
|
-
/* @__PURE__ */
|
|
3110
|
+
/* @__PURE__ */ jsx6(Text6, { children: " " })
|
|
2377
3111
|
] }, m.id);
|
|
2378
3112
|
}) })
|
|
2379
3113
|
] }),
|
|
2380
|
-
/* @__PURE__ */
|
|
3114
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u2500".repeat(Math.min(process.stdout.columns ?? 80, 100)) })
|
|
2381
3115
|
] });
|
|
2382
3116
|
}
|
|
2383
3117
|
|
|
@@ -2385,8 +3119,8 @@ function Header({
|
|
|
2385
3119
|
import { Static } from "ink";
|
|
2386
3120
|
|
|
2387
3121
|
// src/ui/MessageRow.tsx
|
|
2388
|
-
import { Box as
|
|
2389
|
-
import { jsx as
|
|
3122
|
+
import { Box as Box6, Text as Text7 } from "ink";
|
|
3123
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2390
3124
|
function hhmm(rfc3339) {
|
|
2391
3125
|
const d = new Date(rfc3339);
|
|
2392
3126
|
return isNaN(d.getTime()) ? "--:--" : `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
@@ -2399,7 +3133,7 @@ function TokenChip({ message }) {
|
|
|
2399
3133
|
if (message.messageType !== "agent" || !message.tokensUsed) return null;
|
|
2400
3134
|
const parts = [`${formatTokens(message.tokensUsed)} tok`];
|
|
2401
3135
|
if (message.model) parts.push(sanitizeLabel(message.model, 16));
|
|
2402
|
-
return /* @__PURE__ */
|
|
3136
|
+
return /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2403
3137
|
message.verified ? "\u2713 " : "",
|
|
2404
3138
|
parts.join(" \xB7 ")
|
|
2405
3139
|
] });
|
|
@@ -2409,41 +3143,41 @@ function MessageRow({ message }) {
|
|
|
2409
3143
|
const isSystem = message.messageType === "system";
|
|
2410
3144
|
const name = sanitizeLabel(message.agentName, 20);
|
|
2411
3145
|
if (isSystem) {
|
|
2412
|
-
return /* @__PURE__ */
|
|
3146
|
+
return /* @__PURE__ */ jsx7(Box6, { children: /* @__PURE__ */ jsxs7(Text7, { dimColor: true, italic: true, children: [
|
|
2413
3147
|
" ",
|
|
2414
3148
|
"\u2500\u2500 ",
|
|
2415
3149
|
sanitizeForTty(message.content),
|
|
2416
3150
|
" \u2500\u2500"
|
|
2417
3151
|
] }) });
|
|
2418
3152
|
}
|
|
2419
|
-
return /* @__PURE__ */
|
|
2420
|
-
/* @__PURE__ */
|
|
2421
|
-
/* @__PURE__ */
|
|
3153
|
+
return /* @__PURE__ */ jsxs7(Box6, { justifyContent: "space-between", gap: 2, children: [
|
|
3154
|
+
/* @__PURE__ */ jsx7(Box6, { flexShrink: 1, children: /* @__PURE__ */ jsxs7(Text7, { children: [
|
|
3155
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2422
3156
|
hhmm(message.createdAt),
|
|
2423
3157
|
" "
|
|
2424
3158
|
] }),
|
|
2425
|
-
/* @__PURE__ */
|
|
3159
|
+
/* @__PURE__ */ jsxs7(Text7, { color: isBot ? "magenta" : "cyan", bold: !isBot, children: [
|
|
2426
3160
|
isBot ? "\u2699" : "",
|
|
2427
3161
|
name.padEnd(12)
|
|
2428
3162
|
] }),
|
|
2429
|
-
/* @__PURE__ */
|
|
3163
|
+
/* @__PURE__ */ jsxs7(Text7, { children: [
|
|
2430
3164
|
" ",
|
|
2431
3165
|
sanitizeForTty(message.content)
|
|
2432
3166
|
] })
|
|
2433
3167
|
] }) }),
|
|
2434
|
-
/* @__PURE__ */
|
|
3168
|
+
/* @__PURE__ */ jsx7(Box6, { flexShrink: 0, children: /* @__PURE__ */ jsx7(TokenChip, { message }) })
|
|
2435
3169
|
] });
|
|
2436
3170
|
}
|
|
2437
3171
|
|
|
2438
3172
|
// src/ui/MessageList.tsx
|
|
2439
|
-
import { jsx as
|
|
3173
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
2440
3174
|
function MessageList({ messages }) {
|
|
2441
|
-
return /* @__PURE__ */
|
|
3175
|
+
return /* @__PURE__ */ jsx8(Static, { items: messages, children: (m) => /* @__PURE__ */ jsx8(MessageRow, { message: m }, m.id) });
|
|
2442
3176
|
}
|
|
2443
3177
|
|
|
2444
3178
|
// src/ui/NotificationsPanel.tsx
|
|
2445
|
-
import { Box as
|
|
2446
|
-
import { jsx as
|
|
3179
|
+
import { Box as Box7, Text as Text8 } from "ink";
|
|
3180
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2447
3181
|
function relativeAge(createdAt) {
|
|
2448
3182
|
const ms = Date.now() - new Date(createdAt).getTime();
|
|
2449
3183
|
if (!Number.isFinite(ms) || ms < 0) return "\u2014";
|
|
@@ -2461,11 +3195,11 @@ function NotificationsPanel({
|
|
|
2461
3195
|
notifSelected
|
|
2462
3196
|
}) {
|
|
2463
3197
|
const unreadCount = notifications.filter((n) => !n.readAt).length;
|
|
2464
|
-
return /* @__PURE__ */
|
|
2465
|
-
/* @__PURE__ */
|
|
3198
|
+
return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
3199
|
+
/* @__PURE__ */ jsxs8(Text8, { bold: true, children: [
|
|
2466
3200
|
"alerts",
|
|
2467
3201
|
" ",
|
|
2468
|
-
/* @__PURE__ */
|
|
3202
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2469
3203
|
"(",
|
|
2470
3204
|
unreadCount,
|
|
2471
3205
|
" unread \xB7 ",
|
|
@@ -2473,38 +3207,42 @@ function NotificationsPanel({
|
|
|
2473
3207
|
" total)"
|
|
2474
3208
|
] })
|
|
2475
3209
|
] }),
|
|
2476
|
-
notifications.length === 0 ? /* @__PURE__ */
|
|
3210
|
+
notifications.length === 0 ? /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: "no notifications" }) : notifications.map((n, i) => {
|
|
2477
3211
|
const isSelected = i === notifSelected;
|
|
2478
3212
|
const isUnread = n.readAt === null;
|
|
2479
3213
|
const type = sanitizeLabel(n.type, 20);
|
|
2480
3214
|
const ref = shortRef(sanitizeForTty(n.entityRef));
|
|
2481
3215
|
const age = relativeAge(n.createdAt);
|
|
2482
|
-
return /* @__PURE__ */
|
|
2483
|
-
isUnread ? /* @__PURE__ */
|
|
2484
|
-
/* @__PURE__ */
|
|
2485
|
-
/* @__PURE__ */
|
|
3216
|
+
return /* @__PURE__ */ jsxs8(Text8, { inverse: isSelected, children: [
|
|
3217
|
+
isUnread ? /* @__PURE__ */ jsx9(Text8, { color: "yellow", children: "\u2022 " }) : /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: " " }),
|
|
3218
|
+
/* @__PURE__ */ jsx9(Text8, { bold: isSelected, children: type }),
|
|
3219
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2486
3220
|
" ",
|
|
2487
3221
|
ref
|
|
2488
3222
|
] }),
|
|
2489
|
-
/* @__PURE__ */
|
|
3223
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2490
3224
|
" ",
|
|
2491
3225
|
age
|
|
2492
3226
|
] })
|
|
2493
3227
|
] }, n.id);
|
|
2494
3228
|
}),
|
|
2495
|
-
/* @__PURE__ */
|
|
3229
|
+
/* @__PURE__ */ jsx9(Text8, { dimColor: true, children: "J/K move \xB7 d dismiss \xB7 a mark all read \xB7 Esc close" })
|
|
2496
3230
|
] });
|
|
2497
3231
|
}
|
|
2498
3232
|
|
|
2499
3233
|
// src/ui/StatusBar.tsx
|
|
2500
|
-
import { Text as
|
|
2501
|
-
import { Fragment, jsx as
|
|
3234
|
+
import { Text as Text9 } from "ink";
|
|
3235
|
+
import { Fragment, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2502
3236
|
var MODE_LABEL = {
|
|
2503
3237
|
normal: "NORMAL",
|
|
2504
3238
|
insert: "INSERT",
|
|
2505
3239
|
switcher: "CHANNELS",
|
|
2506
3240
|
token: "TOKEN",
|
|
2507
|
-
notifications: "ALERTS"
|
|
3241
|
+
notifications: "ALERTS",
|
|
3242
|
+
attention: "ATTENTION",
|
|
3243
|
+
attention_level: "ATTENTION LEVEL",
|
|
3244
|
+
attention_reason: "ATTENTION REASON",
|
|
3245
|
+
attention_confirm: "CONFIRM ATTENTION"
|
|
2508
3246
|
};
|
|
2509
3247
|
function ageLabel(obtainedAt) {
|
|
2510
3248
|
if (!obtainedAt) return "\u2014";
|
|
@@ -2555,41 +3293,41 @@ function StatusBar({
|
|
|
2555
3293
|
color: stale ? "yellow" : "green"
|
|
2556
3294
|
};
|
|
2557
3295
|
}
|
|
2558
|
-
return /* @__PURE__ */
|
|
2559
|
-
/* @__PURE__ */
|
|
3296
|
+
return /* @__PURE__ */ jsxs9(Text9, { children: [
|
|
3297
|
+
/* @__PURE__ */ jsxs9(Text9, { bold: true, color: mode === "insert" ? "cyan" : mode === "token" ? "yellow" : "white", children: [
|
|
2560
3298
|
" ",
|
|
2561
3299
|
MODE_LABEL[mode],
|
|
2562
3300
|
" "
|
|
2563
3301
|
] }),
|
|
2564
|
-
/* @__PURE__ */
|
|
2565
|
-
/* @__PURE__ */
|
|
2566
|
-
/* @__PURE__ */
|
|
2567
|
-
/* @__PURE__ */
|
|
2568
|
-
/* @__PURE__ */
|
|
3302
|
+
/* @__PURE__ */ jsx10(Text9, { dimColor: true, children: "\u2502 " }),
|
|
3303
|
+
/* @__PURE__ */ jsx10(Text9, { color: connColor, children: connectionLabel(connection, reconnectAt, reconnectAttempt) }),
|
|
3304
|
+
/* @__PURE__ */ jsx10(Text9, { dimColor: true, children: " \u2502 " }),
|
|
3305
|
+
/* @__PURE__ */ jsx10(Text9, { color: tokenSeg.color, children: tokenSeg.text }),
|
|
3306
|
+
/* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
2569
3307
|
" \u2502 seq ",
|
|
2570
3308
|
lastSeq ?? "\u2014"
|
|
2571
3309
|
] }),
|
|
2572
|
-
notifCount > 0 ? /* @__PURE__ */
|
|
3310
|
+
notifCount > 0 ? /* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
|
|
2573
3311
|
" \u2502 alerts:",
|
|
2574
3312
|
notifCount
|
|
2575
3313
|
] }) : null,
|
|
2576
3314
|
error ? (
|
|
2577
3315
|
// BL-222: an error replaces the full hint line but must never strand
|
|
2578
3316
|
// the user — keep the exit hint visible alongside it
|
|
2579
|
-
/* @__PURE__ */
|
|
2580
|
-
/* @__PURE__ */
|
|
3317
|
+
/* @__PURE__ */ jsxs9(Fragment, { children: [
|
|
3318
|
+
/* @__PURE__ */ jsxs9(Text9, { color: "red", children: [
|
|
2581
3319
|
" \u2502 ",
|
|
2582
3320
|
sanitizeForTty(error)
|
|
2583
3321
|
] }),
|
|
2584
|
-
/* @__PURE__ */
|
|
3322
|
+
/* @__PURE__ */ jsx10(Text9, { dimColor: true, children: " \u2502 q quit" })
|
|
2585
3323
|
] })
|
|
2586
|
-
) : /* @__PURE__ */
|
|
3324
|
+
) : /* @__PURE__ */ jsx10(Text9, { dimColor: true, children: " \u2502 c channels \xB7 a alerts \xB7 t attention \xB7 n next \xB7 v watch \xB7 i compose \xB7 r reconnect \xB7 Esc normal \xB7 q quit" })
|
|
2587
3325
|
] });
|
|
2588
3326
|
}
|
|
2589
3327
|
|
|
2590
3328
|
// src/ui/TokenPrompt.tsx
|
|
2591
|
-
import { Box as
|
|
2592
|
-
import { jsx as
|
|
3329
|
+
import { Box as Box8, Text as Text10 } from "ink";
|
|
3330
|
+
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2593
3331
|
function TokenPrompt({
|
|
2594
3332
|
value,
|
|
2595
3333
|
reason,
|
|
@@ -2597,26 +3335,26 @@ function TokenPrompt({
|
|
|
2597
3335
|
jwtTemplate
|
|
2598
3336
|
}) {
|
|
2599
3337
|
const masked = value.length === 0 ? "" : value.length <= 16 ? value : `${value.slice(0, 12)}\u2026 (${value.length} chars)`;
|
|
2600
|
-
return /* @__PURE__ */
|
|
2601
|
-
/* @__PURE__ */
|
|
2602
|
-
reason ? /* @__PURE__ */
|
|
2603
|
-
/* @__PURE__ */
|
|
3338
|
+
return /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
3339
|
+
/* @__PURE__ */ jsx11(Text10, { bold: true, color: "yellow", children: "token needed" }),
|
|
3340
|
+
reason ? /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: sanitizeForTty(reason) }) : null,
|
|
3341
|
+
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
2604
3342
|
"mint one: open ",
|
|
2605
3343
|
webUrl,
|
|
2606
3344
|
" logged in, browser console:"
|
|
2607
3345
|
] }),
|
|
2608
|
-
/* @__PURE__ */
|
|
2609
|
-
/* @__PURE__ */
|
|
2610
|
-
/* @__PURE__ */
|
|
2611
|
-
/* @__PURE__ */
|
|
2612
|
-
/* @__PURE__ */
|
|
3346
|
+
/* @__PURE__ */ jsx11(Text10, { dimColor: true, children: ` await window.Clerk.session.getToken({template:'${jwtTemplate}'})` }),
|
|
3347
|
+
/* @__PURE__ */ jsxs10(Text10, { children: [
|
|
3348
|
+
/* @__PURE__ */ jsx11(Text10, { color: "yellow", children: "> " }),
|
|
3349
|
+
/* @__PURE__ */ jsx11(Text10, { children: masked }),
|
|
3350
|
+
/* @__PURE__ */ jsx11(Text10, { inverse: true, children: " " })
|
|
2613
3351
|
] }),
|
|
2614
|
-
/* @__PURE__ */
|
|
3352
|
+
/* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "paste, then Enter \xB7 Ctrl+C quits" })
|
|
2615
3353
|
] });
|
|
2616
3354
|
}
|
|
2617
3355
|
|
|
2618
3356
|
// src/app.tsx
|
|
2619
|
-
import { jsx as
|
|
3357
|
+
import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2620
3358
|
var ACTIVITY_SETTLE_MS2 = 4e3;
|
|
2621
3359
|
function cleanInput(input) {
|
|
2622
3360
|
return input.replace(/\r\n|[\r\n]/g, " ").replace(/[\x00-\x1f\x7f]/g, "");
|
|
@@ -2663,6 +3401,22 @@ function App({ store: store2 }) {
|
|
|
2663
3401
|
} else if (input === "a") store2.markAllNotificationsRead();
|
|
2664
3402
|
return;
|
|
2665
3403
|
}
|
|
3404
|
+
if (s.mode.startsWith("attention")) {
|
|
3405
|
+
if (s.mode !== "attention") {
|
|
3406
|
+
if (key.escape) store2.closeAttention();
|
|
3407
|
+
else if (key.return) store2.submitAttentionInput();
|
|
3408
|
+
else if (key.backspace || key.delete) store2.setAttentionDraft(s.attentionDraft.slice(0, -1));
|
|
3409
|
+
else if (input) store2.setAttentionDraft(s.attentionDraft + cleanInput(input));
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
if (key.escape) store2.closeAttention();
|
|
3413
|
+
else if (input === "j" || key.downArrow) store2.moveAttentionSelection(1);
|
|
3414
|
+
else if (input === "k" || key.upArrow) store2.moveAttentionSelection(-1);
|
|
3415
|
+
else if (input === "r") store2.beginAttentionRaise();
|
|
3416
|
+
else if (input === "x") store2.beginAttentionAck(false);
|
|
3417
|
+
else if (input === "X") store2.beginAttentionAck(true);
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
2666
3420
|
if (s.mode === "switcher") {
|
|
2667
3421
|
const filtered = filterChannels(s.channels, s.switcherFilter);
|
|
2668
3422
|
const sorted = sortChannelsByPriority(filtered, { attention: s.attention, unread: s.unread });
|
|
@@ -2686,6 +3440,7 @@ function App({ store: store2 }) {
|
|
|
2686
3440
|
exit();
|
|
2687
3441
|
} else if (input === "c") store2.setMode("switcher");
|
|
2688
3442
|
else if (input === "a") store2.setMode("notifications");
|
|
3443
|
+
else if (input === "t") store2.openAttention();
|
|
2689
3444
|
else if (input === "i" || key.return) store2.setMode("insert");
|
|
2690
3445
|
else if (input === "r") store2.reconnectNow();
|
|
2691
3446
|
else if (input === "n") store2.jumpNextActive();
|
|
@@ -2703,8 +3458,8 @@ function App({ store: store2 }) {
|
|
|
2703
3458
|
const activityLive = activityRows.some((r) => r.status === "pending" || r.status === "in_progress") || activityPlan.some((e) => e.status !== "completed");
|
|
2704
3459
|
const activityWithinSettle = state.activityIdleSince !== null && Date.now() - state.activityIdleSince < ACTIVITY_SETTLE_MS2;
|
|
2705
3460
|
const activityVisible = state.activityPinned || activityLive || activityWithinSettle;
|
|
2706
|
-
return /* @__PURE__ */
|
|
2707
|
-
/* @__PURE__ */
|
|
3461
|
+
return /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", children: [
|
|
3462
|
+
/* @__PURE__ */ jsx12(
|
|
2708
3463
|
Header,
|
|
2709
3464
|
{
|
|
2710
3465
|
channelName: activeChannel?.name ?? "(connecting\u2026)",
|
|
@@ -2713,9 +3468,9 @@ function App({ store: store2 }) {
|
|
|
2713
3468
|
presence: state.presence
|
|
2714
3469
|
}
|
|
2715
3470
|
),
|
|
2716
|
-
/* @__PURE__ */
|
|
2717
|
-
/* @__PURE__ */
|
|
2718
|
-
state.mode === "switcher" ? /* @__PURE__ */
|
|
3471
|
+
/* @__PURE__ */ jsx12(MessageList, { messages: state.messages }),
|
|
3472
|
+
/* @__PURE__ */ jsx12(ActivitySurface, { toolCalls: activityRows, plan: activityPlan, visible: activityVisible, pinned: state.activityPinned }),
|
|
3473
|
+
state.mode === "switcher" ? /* @__PURE__ */ jsx12(
|
|
2719
3474
|
ChannelSwitcher,
|
|
2720
3475
|
{
|
|
2721
3476
|
channels: state.channels,
|
|
@@ -2725,14 +3480,25 @@ function App({ store: store2 }) {
|
|
|
2725
3480
|
unread: state.unread
|
|
2726
3481
|
}
|
|
2727
3482
|
) : null,
|
|
2728
|
-
state.mode === "notifications" ? /* @__PURE__ */
|
|
3483
|
+
state.mode === "notifications" ? /* @__PURE__ */ jsx12(
|
|
2729
3484
|
NotificationsPanel,
|
|
2730
3485
|
{
|
|
2731
3486
|
notifications: state.notifications,
|
|
2732
3487
|
notifSelected: state.notifSelected
|
|
2733
3488
|
}
|
|
2734
3489
|
) : null,
|
|
2735
|
-
state.mode
|
|
3490
|
+
state.mode.startsWith("attention") && state.activeChannelId ? /* @__PURE__ */ jsx12(
|
|
3491
|
+
AttentionPanel,
|
|
3492
|
+
{
|
|
3493
|
+
channelId: state.activeChannelId,
|
|
3494
|
+
items: state.channelAttention,
|
|
3495
|
+
selected: state.attentionSelected,
|
|
3496
|
+
mode: state.mode,
|
|
3497
|
+
draft: state.attentionDraft,
|
|
3498
|
+
busy: state.attentionBusy
|
|
3499
|
+
}
|
|
3500
|
+
) : null,
|
|
3501
|
+
state.mode === "token" ? /* @__PURE__ */ jsx12(
|
|
2736
3502
|
TokenPrompt,
|
|
2737
3503
|
{
|
|
2738
3504
|
value: state.tokenPrompt,
|
|
@@ -2741,9 +3507,9 @@ function App({ store: store2 }) {
|
|
|
2741
3507
|
jwtTemplate: store2.instance.clerkJwtTemplate
|
|
2742
3508
|
}
|
|
2743
3509
|
) : null,
|
|
2744
|
-
/* @__PURE__ */
|
|
2745
|
-
/* @__PURE__ */
|
|
2746
|
-
/* @__PURE__ */
|
|
3510
|
+
/* @__PURE__ */ jsx12(ActivityLine, { activities: state.activities }),
|
|
3511
|
+
/* @__PURE__ */ jsx12(Composer, { draft: state.draft, focused: state.mode === "insert" }),
|
|
3512
|
+
/* @__PURE__ */ jsx12(
|
|
2747
3513
|
StatusBar,
|
|
2748
3514
|
{
|
|
2749
3515
|
mode: state.mode,
|
|
@@ -2759,7 +3525,7 @@ function App({ store: store2 }) {
|
|
|
2759
3525
|
error: state.error
|
|
2760
3526
|
}
|
|
2761
3527
|
),
|
|
2762
|
-
!interactive ? /* @__PURE__ */
|
|
3528
|
+
!interactive ? /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: "(non-interactive terminal: keys disabled \u2014 tail is read-only here)" }) : null
|
|
2763
3529
|
] });
|
|
2764
3530
|
}
|
|
2765
3531
|
|
|
@@ -2842,8 +3608,1144 @@ unknown profile command "${args2.sub}".`);
|
|
|
2842
3608
|
}
|
|
2843
3609
|
}
|
|
2844
3610
|
|
|
3611
|
+
// src/api/bridgeProcess.ts
|
|
3612
|
+
import { spawn as spawn2 } from "child_process";
|
|
3613
|
+
var DEFAULT_BRIDGE_CMD = "npx -y arp-bridge@latest";
|
|
3614
|
+
var ENROLL_TIMEOUT_MS = 12e4;
|
|
3615
|
+
var STDERR_TAIL_CHARS = 600;
|
|
3616
|
+
function resolveBridgeCmd(flag, env = process.env) {
|
|
3617
|
+
return flag ?? env["ARP_TUI_BRIDGE_CMD"] ?? DEFAULT_BRIDGE_CMD;
|
|
3618
|
+
}
|
|
3619
|
+
function splitCmd(cmd) {
|
|
3620
|
+
return cmd.trim().split(/\s+/).filter(Boolean);
|
|
3621
|
+
}
|
|
3622
|
+
function enrollArgv(bridgeCmd) {
|
|
3623
|
+
return [...splitCmd(bridgeCmd), "enroll", "--invite-stdin", "--json"];
|
|
3624
|
+
}
|
|
3625
|
+
function startArgv(bridgeCmd, name, provider) {
|
|
3626
|
+
return [...splitCmd(bridgeCmd), "start", name, ...provider ? ["--provider", provider] : []];
|
|
3627
|
+
}
|
|
3628
|
+
function enrollAgent(opts) {
|
|
3629
|
+
const [cmd, ...argv] = enrollArgv(opts.bridgeCmd);
|
|
3630
|
+
if (!cmd) {
|
|
3631
|
+
return Promise.resolve({ ok: false, exitCode: null, message: "empty bridge command", stderrTail: "" });
|
|
3632
|
+
}
|
|
3633
|
+
return new Promise((resolve) => {
|
|
3634
|
+
const child = spawn2(cmd, argv, { stdio: ["pipe", "pipe", "pipe"] });
|
|
3635
|
+
let stdout = "";
|
|
3636
|
+
let stderr = "";
|
|
3637
|
+
let settled = false;
|
|
3638
|
+
const timeoutMs = opts.timeoutMs ?? ENROLL_TIMEOUT_MS;
|
|
3639
|
+
const finish = (r) => {
|
|
3640
|
+
if (settled) return;
|
|
3641
|
+
settled = true;
|
|
3642
|
+
clearTimeout(timer);
|
|
3643
|
+
resolve(r);
|
|
3644
|
+
};
|
|
3645
|
+
const timer = setTimeout(() => {
|
|
3646
|
+
finish({
|
|
3647
|
+
ok: false,
|
|
3648
|
+
exitCode: null,
|
|
3649
|
+
message: `bridge enrollment timed out after ${Math.round(timeoutMs / 1e3)}s`,
|
|
3650
|
+
stderrTail: tail(stderr)
|
|
3651
|
+
});
|
|
3652
|
+
child.kill("SIGKILL");
|
|
3653
|
+
}, timeoutMs);
|
|
3654
|
+
child.on("error", (err) => {
|
|
3655
|
+
finish({
|
|
3656
|
+
ok: false,
|
|
3657
|
+
exitCode: null,
|
|
3658
|
+
message: `could not run the bridge (${cmd}): ${sanitizeForTty(err.message)}`,
|
|
3659
|
+
stderrTail: ""
|
|
3660
|
+
});
|
|
3661
|
+
});
|
|
3662
|
+
child.stdout.setEncoding("utf8");
|
|
3663
|
+
child.stdout.on("data", (d) => stdout += d);
|
|
3664
|
+
child.stderr.setEncoding("utf8");
|
|
3665
|
+
child.stderr.on("data", (d) => stderr += d);
|
|
3666
|
+
child.stdin.on("error", () => {
|
|
3667
|
+
});
|
|
3668
|
+
child.stdin.write(opts.connectBlob);
|
|
3669
|
+
child.stdin.end();
|
|
3670
|
+
child.on("close", (code) => {
|
|
3671
|
+
if (code !== 0) {
|
|
3672
|
+
finish({
|
|
3673
|
+
ok: false,
|
|
3674
|
+
exitCode: code,
|
|
3675
|
+
message: `bridge enroll exited ${code ?? "on a signal"}`,
|
|
3676
|
+
stderrTail: tail(stderr)
|
|
3677
|
+
});
|
|
3678
|
+
return;
|
|
3679
|
+
}
|
|
3680
|
+
const receipt = parseReceipt(stdout);
|
|
3681
|
+
if (!receipt) {
|
|
3682
|
+
finish({
|
|
3683
|
+
ok: false,
|
|
3684
|
+
exitCode: 0,
|
|
3685
|
+
message: "the bridge exited 0 but printed no parseable enrollment receipt",
|
|
3686
|
+
stderrTail: tail(stderr)
|
|
3687
|
+
});
|
|
3688
|
+
return;
|
|
3689
|
+
}
|
|
3690
|
+
finish({ ok: true, receipt });
|
|
3691
|
+
});
|
|
3692
|
+
});
|
|
3693
|
+
}
|
|
3694
|
+
function tail(stderr) {
|
|
3695
|
+
return sanitizeForTty(stderr).trim().slice(-STDERR_TAIL_CHARS);
|
|
3696
|
+
}
|
|
3697
|
+
function parseReceipt(stdout) {
|
|
3698
|
+
const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3699
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3700
|
+
const line = lines[i];
|
|
3701
|
+
if (!line.startsWith("{")) continue;
|
|
3702
|
+
try {
|
|
3703
|
+
const data = JSON.parse(line);
|
|
3704
|
+
if (data && typeof data === "object" && data["ok"] === true && typeof data["agentName"] === "string" && typeof data["agentUuid"] === "string") {
|
|
3705
|
+
return {
|
|
3706
|
+
ok: true,
|
|
3707
|
+
agentName: data["agentName"],
|
|
3708
|
+
agentUuid: data["agentUuid"],
|
|
3709
|
+
relayUrl: typeof data["relayUrl"] === "string" ? data["relayUrl"] : "",
|
|
3710
|
+
keystoreFile: typeof data["keystoreFile"] === "string" ? data["keystoreFile"] : ""
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
} catch {
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
return null;
|
|
3717
|
+
}
|
|
3718
|
+
function startAgentProcess(opts) {
|
|
3719
|
+
const [cmd, ...argv] = startArgv(opts.bridgeCmd, opts.name, opts.provider);
|
|
3720
|
+
if (!cmd) return Promise.resolve(1);
|
|
3721
|
+
return new Promise((resolve) => {
|
|
3722
|
+
const child = spawn2(cmd, argv, { stdio: "inherit" });
|
|
3723
|
+
child.on("error", () => resolve(1));
|
|
3724
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
3725
|
+
});
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
// src/commands/agent.ts
|
|
3729
|
+
import readline from "readline/promises";
|
|
3730
|
+
var AGENT_USAGE = `usage: braid agent <add|status|reissue|revoke|remove> <name> [options]
|
|
3731
|
+
|
|
3732
|
+
add <name> create a personal agent on Braid, enroll it on this machine
|
|
3733
|
+
via the local bridge, and start it (Ctrl-C stops it)
|
|
3734
|
+
status <name> detail card: presence, credential, health, service mode
|
|
3735
|
+
reissue <name> mint a fresh credential and re-enroll here \u2014 recovers an
|
|
3736
|
+
agent whose credential is broken or revoked
|
|
3737
|
+
revoke <name> revoke the agent's credential \u2014 it cannot connect until reissued
|
|
3738
|
+
remove <name> permanently delete the agent from Braid
|
|
3739
|
+
|
|
3740
|
+
--provider <p> agent CLI the bridge runs (claude-code, codex, gemini, \u2026)
|
|
3741
|
+
--no-start enroll only \u2014 prints the exact start command instead
|
|
3742
|
+
--bridge-cmd <cmd> bridge command to spawn (default: npx -y arp-bridge@latest;
|
|
3743
|
+
also ARP_TUI_BRIDGE_CMD env)
|
|
3744
|
+
--yes skip the confirmation prompt (required with --json on
|
|
3745
|
+
revoke/remove)
|
|
3746
|
+
--json machine-readable result on stdout`;
|
|
3747
|
+
async function promptOnTty(question) {
|
|
3748
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) return null;
|
|
3749
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
3750
|
+
try {
|
|
3751
|
+
return await rl.question(question);
|
|
3752
|
+
} finally {
|
|
3753
|
+
rl.close();
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
async function ensureSignedIn(deps) {
|
|
3757
|
+
const existing = deps.resolveCreds();
|
|
3758
|
+
if (existing?.token) return existing;
|
|
3759
|
+
deps.err("you're not signed in yet \u2014 opening your browser to sign in to Braid\u2026");
|
|
3760
|
+
return deps.runLogin();
|
|
3761
|
+
}
|
|
3762
|
+
function oneShotTokenSupplier(instance2, initial, refresh = refreshGrant) {
|
|
3763
|
+
let current = initial;
|
|
3764
|
+
return async () => {
|
|
3765
|
+
const refreshable = !!current.refreshToken && boundToFapi(current.clerkFapiUrl, instance2.clerkFapiUrl);
|
|
3766
|
+
if (refreshable && needsRefresh(current.expiresAt)) {
|
|
3767
|
+
const consumed = current.refreshToken;
|
|
3768
|
+
try {
|
|
3769
|
+
const tokens = await refresh({
|
|
3770
|
+
endpoints: oauthEndpoints(instance2.clerkFapiUrl),
|
|
3771
|
+
clientId: requireOAuthClientId(instance2),
|
|
3772
|
+
refreshToken: consumed
|
|
3773
|
+
});
|
|
3774
|
+
const fields = {
|
|
3775
|
+
token: tokens.accessToken,
|
|
3776
|
+
tokenType: "oauth",
|
|
3777
|
+
// a slot holding a refresh token is ALWAYS an oauth slot
|
|
3778
|
+
obtainedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3779
|
+
expiresAt: tokens.expiresAt ?? jwtExpiry(tokens.accessToken),
|
|
3780
|
+
refreshToken: tokens.refreshToken ?? consumed
|
|
3781
|
+
};
|
|
3782
|
+
try {
|
|
3783
|
+
updateCredentials(instance2.relayUrl, (onDisk) => mergeRotation(onDisk, fields, consumed));
|
|
3784
|
+
} catch {
|
|
3785
|
+
}
|
|
3786
|
+
current = { ...current, ...fields };
|
|
3787
|
+
} catch {
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
return current.token;
|
|
3791
|
+
};
|
|
3792
|
+
}
|
|
3793
|
+
async function runAgentCli(instance2, args2, overrides = {}) {
|
|
3794
|
+
const deps = {
|
|
3795
|
+
resolveCreds: () => resolveCredentials(instance2, args2.flagToken),
|
|
3796
|
+
runLogin: async () => (await login(instance2)).credentials,
|
|
3797
|
+
enroll: enrollAgent,
|
|
3798
|
+
startAgent: startAgentProcess,
|
|
3799
|
+
env: process.env,
|
|
3800
|
+
out: (line) => console.log(line),
|
|
3801
|
+
err: (line) => console.error(line),
|
|
3802
|
+
prompt: promptOnTty,
|
|
3803
|
+
...overrides
|
|
3804
|
+
};
|
|
3805
|
+
try {
|
|
3806
|
+
if (args2.cmd === "agents") return await runAgentsList(instance2, args2, deps);
|
|
3807
|
+
switch (args2.sub) {
|
|
3808
|
+
case "add":
|
|
3809
|
+
return await runAgentAdd(instance2, args2, deps);
|
|
3810
|
+
case "status":
|
|
3811
|
+
return await runAgentStatus(instance2, args2, deps);
|
|
3812
|
+
case "reissue":
|
|
3813
|
+
return await runAgentReissue(instance2, args2, deps);
|
|
3814
|
+
case "revoke":
|
|
3815
|
+
return await runAgentRevoke(instance2, args2, deps);
|
|
3816
|
+
case "remove":
|
|
3817
|
+
return await runAgentRemove(instance2, args2, deps);
|
|
3818
|
+
default:
|
|
3819
|
+
deps.err(AGENT_USAGE);
|
|
3820
|
+
if (args2.sub) deps.err(`
|
|
3821
|
+
unknown agent command "${sanitizeLabel(args2.sub)}".`);
|
|
3822
|
+
return 1;
|
|
3823
|
+
}
|
|
3824
|
+
} catch (err) {
|
|
3825
|
+
deps.err(sanitizeForTty(err instanceof Error ? err.message : String(err)));
|
|
3826
|
+
return 1;
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
function oneShotRest(instance2, creds) {
|
|
3830
|
+
return new RelayRest({
|
|
3831
|
+
relayUrl: instance2.relayUrl,
|
|
3832
|
+
getToken: oneShotTokenSupplier(instance2, creds)
|
|
3833
|
+
});
|
|
3834
|
+
}
|
|
3835
|
+
function requireName2(args2, deps) {
|
|
3836
|
+
const name = args2.name?.trim();
|
|
3837
|
+
if (!name) {
|
|
3838
|
+
deps.err(`agent ${args2.sub} requires a name \u2014 usage: braid agent ${args2.sub} <name>`);
|
|
3839
|
+
return null;
|
|
3840
|
+
}
|
|
3841
|
+
return name;
|
|
3842
|
+
}
|
|
3843
|
+
async function findMyAgent(rest, name, args2, deps) {
|
|
3844
|
+
const agents = await rest.listMyAgents();
|
|
3845
|
+
const found = agents.find((a) => a.name === name);
|
|
3846
|
+
if (found) return found;
|
|
3847
|
+
const msg = `no agent named "${sanitizeLabel(name)}"`;
|
|
3848
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
3849
|
+
const known = agents.map((a) => sanitizeLabel(a.name ?? "")).filter(Boolean);
|
|
3850
|
+
deps.err(
|
|
3851
|
+
`\u2717 ${msg}${known.length > 0 ? ` \u2014 your agents: ${known.join(", ")}` : " \u2014 you have no agents yet (run `braid agent add <name>`)"}`
|
|
3852
|
+
);
|
|
3853
|
+
return null;
|
|
3854
|
+
}
|
|
3855
|
+
async function confirmDestructive(args2, deps, name, actionLine) {
|
|
3856
|
+
if (args2.yes) return true;
|
|
3857
|
+
if (args2.json) {
|
|
3858
|
+
deps.out(JSON.stringify({ ok: false, error: "--json runs non-interactively \u2014 add --yes to confirm" }));
|
|
3859
|
+
deps.err(`\u2717 refusing: --json runs non-interactively \u2014 add --yes to confirm`);
|
|
3860
|
+
return false;
|
|
3861
|
+
}
|
|
3862
|
+
const typed = await deps.prompt(`${actionLine}
|
|
3863
|
+
type the agent name to confirm: `);
|
|
3864
|
+
if (typed === null) {
|
|
3865
|
+
deps.err("\u2717 refusing: no terminal to confirm on \u2014 re-run with --yes");
|
|
3866
|
+
return false;
|
|
3867
|
+
}
|
|
3868
|
+
if (typed.trim() !== name) {
|
|
3869
|
+
deps.err("\u2717 confirmation did not match \u2014 nothing was changed");
|
|
3870
|
+
return false;
|
|
3871
|
+
}
|
|
3872
|
+
return true;
|
|
3873
|
+
}
|
|
3874
|
+
async function enrollAndMaybeStart(args2, deps, opts) {
|
|
3875
|
+
const result = await deps.enroll({ bridgeCmd: opts.bridgeCmd, connectBlob: opts.connectBlob });
|
|
3876
|
+
if (!result.ok) {
|
|
3877
|
+
const msg = sanitizeForTty(result.message);
|
|
3878
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
3879
|
+
deps.err(`\u2717 enrollment failed: ${msg}`);
|
|
3880
|
+
if (result.stderrTail) deps.err(result.stderrTail);
|
|
3881
|
+
return 1;
|
|
3882
|
+
}
|
|
3883
|
+
const agentName = result.receipt.agentName;
|
|
3884
|
+
const started = !args2.noStart;
|
|
3885
|
+
if (args2.json) {
|
|
3886
|
+
deps.out(
|
|
3887
|
+
JSON.stringify({
|
|
3888
|
+
ok: true,
|
|
3889
|
+
agentName: sanitizeForTty(agentName),
|
|
3890
|
+
agentUuid: sanitizeForTty(result.receipt.agentUuid),
|
|
3891
|
+
enrolled: true,
|
|
3892
|
+
started
|
|
3893
|
+
})
|
|
3894
|
+
);
|
|
3895
|
+
}
|
|
3896
|
+
deps.err(opts.successLine(sanitizeLabel(agentName)));
|
|
3897
|
+
if (!started) {
|
|
3898
|
+
deps.err(` start it with: ${sanitizeForTty(startArgv(opts.bridgeCmd, agentName, args2.provider).join(" "))}`);
|
|
3899
|
+
return 0;
|
|
3900
|
+
}
|
|
3901
|
+
deps.err(`\u2192 starting ${sanitizeLabel(agentName)} now \u2014 press Ctrl-C to stop it`);
|
|
3902
|
+
return deps.startAgent({ bridgeCmd: opts.bridgeCmd, name: agentName, provider: args2.provider });
|
|
3903
|
+
}
|
|
3904
|
+
async function runAgentAdd(instance2, args2, deps) {
|
|
3905
|
+
const name = requireName2(args2, deps);
|
|
3906
|
+
if (!name) return 1;
|
|
3907
|
+
const bridgeCmd = resolveBridgeCmd(args2.bridgeCmd, deps.env);
|
|
3908
|
+
const creds = await ensureSignedIn(deps);
|
|
3909
|
+
const rest = oneShotRest(instance2, creds);
|
|
3910
|
+
let invite;
|
|
3911
|
+
try {
|
|
3912
|
+
invite = await rest.createPersonalAgent(name);
|
|
3913
|
+
} catch (err) {
|
|
3914
|
+
const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
3915
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
3916
|
+
deps.err(`\u2717 Braid could not create "${sanitizeLabel(name)}": ${msg}`);
|
|
3917
|
+
return 1;
|
|
3918
|
+
}
|
|
3919
|
+
deps.err(`\u2713 ${sanitizeLabel(name)} created on Braid \u2014 enrolling on this machine\u2026`);
|
|
3920
|
+
return enrollAndMaybeStart(args2, deps, {
|
|
3921
|
+
bridgeCmd,
|
|
3922
|
+
connectBlob: invite.connectBlob,
|
|
3923
|
+
successLine: (n) => `\u2714 ${n} is enrolled with Braid`
|
|
3924
|
+
});
|
|
3925
|
+
}
|
|
3926
|
+
async function runAgentStatus(instance2, args2, deps) {
|
|
3927
|
+
const name = requireName2(args2, deps);
|
|
3928
|
+
if (!name) return 1;
|
|
3929
|
+
const creds = await ensureSignedIn(deps);
|
|
3930
|
+
const agent = await findMyAgent(oneShotRest(instance2, creds), name, args2, deps);
|
|
3931
|
+
if (!agent) return 1;
|
|
3932
|
+
if (args2.json) {
|
|
3933
|
+
deps.out(JSON.stringify(agent, null, 2));
|
|
3934
|
+
return 0;
|
|
3935
|
+
}
|
|
3936
|
+
const row = (label, value) => deps.out(`${(label + ":").padEnd(14)}${value}`);
|
|
3937
|
+
row("agent", sanitizeLabel(agent.name ?? "", 64));
|
|
3938
|
+
row("status", agent.status === "online" ? "online" : "offline");
|
|
3939
|
+
const cred = sanitizeLabel(agent.credentialState ?? "none", 32);
|
|
3940
|
+
row(
|
|
3941
|
+
"credential",
|
|
3942
|
+
agent.credentialLastUsedAt ? `${cred} (last used ${sanitizeLabel(agent.credentialLastUsedAt, 40)})` : cred
|
|
3943
|
+
);
|
|
3944
|
+
row("health", sanitizeLabel(agent.healthState ?? "unknown", 32));
|
|
3945
|
+
if (agent.healthDetail) row("detail", sanitizeLabel(agent.healthDetail, 200));
|
|
3946
|
+
if (agent.healthFixHint) row("fix", sanitizeLabel(agent.healthFixHint, 200));
|
|
3947
|
+
row(
|
|
3948
|
+
"service mode",
|
|
3949
|
+
typeof agent.serviceMode === "boolean" ? agent.serviceMode ? "on" : "off" : sanitizeLabel(agent.serviceMode ?? "off", 20)
|
|
3950
|
+
);
|
|
3951
|
+
if (agent.runtimeVersion) row("runtime", sanitizeLabel(agent.runtimeVersion, 40));
|
|
3952
|
+
if (agent.daemonPosture) row("daemon", sanitizeLabel(agent.daemonPosture, 40));
|
|
3953
|
+
return 0;
|
|
3954
|
+
}
|
|
3955
|
+
async function runAgentReissue(instance2, args2, deps) {
|
|
3956
|
+
const name = requireName2(args2, deps);
|
|
3957
|
+
if (!name) return 1;
|
|
3958
|
+
const bridgeCmd = resolveBridgeCmd(args2.bridgeCmd, deps.env);
|
|
3959
|
+
const creds = await ensureSignedIn(deps);
|
|
3960
|
+
const rest = oneShotRest(instance2, creds);
|
|
3961
|
+
const agent = await findMyAgent(rest, name, args2, deps);
|
|
3962
|
+
if (!agent) return 1;
|
|
3963
|
+
let invite;
|
|
3964
|
+
try {
|
|
3965
|
+
invite = await rest.reissueAgent(agent.id);
|
|
3966
|
+
} catch (err) {
|
|
3967
|
+
const msg = err instanceof RelayError && err.status === 429 ? "reissue is rate-limited \u2014 wait a minute and try again" : sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
3968
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
3969
|
+
deps.err(`\u2717 Braid could not reissue "${sanitizeLabel(name)}": ${msg}`);
|
|
3970
|
+
return 1;
|
|
3971
|
+
}
|
|
3972
|
+
deps.err(`\u2713 fresh credential minted \u2014 re-enrolling ${sanitizeLabel(name)} on this machine\u2026`);
|
|
3973
|
+
return enrollAndMaybeStart(args2, deps, {
|
|
3974
|
+
bridgeCmd,
|
|
3975
|
+
connectBlob: invite.connectBlob,
|
|
3976
|
+
successLine: (n) => `\u2714 ${n} reconnected to Braid`
|
|
3977
|
+
});
|
|
3978
|
+
}
|
|
3979
|
+
async function runAgentRevoke(instance2, args2, deps) {
|
|
3980
|
+
const name = requireName2(args2, deps);
|
|
3981
|
+
if (!name) return 1;
|
|
3982
|
+
const creds = await ensureSignedIn(deps);
|
|
3983
|
+
const rest = oneShotRest(instance2, creds);
|
|
3984
|
+
const agent = await findMyAgent(rest, name, args2, deps);
|
|
3985
|
+
if (!agent) return 1;
|
|
3986
|
+
const confirmed = await confirmDestructive(
|
|
3987
|
+
args2,
|
|
3988
|
+
deps,
|
|
3989
|
+
name,
|
|
3990
|
+
`this revokes "${sanitizeLabel(name)}"'s credential and disconnects it from Braid.`
|
|
3991
|
+
);
|
|
3992
|
+
if (!confirmed) return 1;
|
|
3993
|
+
let credential;
|
|
3994
|
+
try {
|
|
3995
|
+
credential = await rest.revokeAgentCredential(agent.id);
|
|
3996
|
+
} catch (err) {
|
|
3997
|
+
const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
3998
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
3999
|
+
deps.err(`\u2717 Braid could not revoke "${sanitizeLabel(name)}": ${msg}`);
|
|
4000
|
+
return 1;
|
|
4001
|
+
}
|
|
4002
|
+
if (args2.json) {
|
|
4003
|
+
deps.out(
|
|
4004
|
+
JSON.stringify({
|
|
4005
|
+
ok: true,
|
|
4006
|
+
agentName: sanitizeForTty(name),
|
|
4007
|
+
credentialState: sanitizeForTty(credential.state ?? "revoked_owner")
|
|
4008
|
+
})
|
|
4009
|
+
);
|
|
4010
|
+
}
|
|
4011
|
+
deps.err(
|
|
4012
|
+
`\u2714 credential revoked \u2014 ${sanitizeLabel(name)} can no longer connect to Braid until you run \`braid agent reissue ${sanitizeLabel(name)}\``
|
|
4013
|
+
);
|
|
4014
|
+
return 0;
|
|
4015
|
+
}
|
|
4016
|
+
async function runAgentRemove(instance2, args2, deps) {
|
|
4017
|
+
const name = requireName2(args2, deps);
|
|
4018
|
+
if (!name) return 1;
|
|
4019
|
+
const creds = await ensureSignedIn(deps);
|
|
4020
|
+
const rest = oneShotRest(instance2, creds);
|
|
4021
|
+
const agent = await findMyAgent(rest, name, args2, deps);
|
|
4022
|
+
if (!agent) return 1;
|
|
4023
|
+
const confirmed = await confirmDestructive(
|
|
4024
|
+
args2,
|
|
4025
|
+
deps,
|
|
4026
|
+
name,
|
|
4027
|
+
`this permanently deletes agent "${sanitizeLabel(name)}" from Braid \u2014 identity, credential, and channel memberships.`
|
|
4028
|
+
);
|
|
4029
|
+
if (!confirmed) return 1;
|
|
4030
|
+
try {
|
|
4031
|
+
await rest.deleteAgent(agent.id);
|
|
4032
|
+
} catch (err) {
|
|
4033
|
+
const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
4034
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4035
|
+
deps.err(`\u2717 Braid could not remove "${sanitizeLabel(name)}": ${msg}`);
|
|
4036
|
+
return 1;
|
|
4037
|
+
}
|
|
4038
|
+
if (args2.json) deps.out(JSON.stringify({ ok: true, agentName: sanitizeForTty(name), removed: true }));
|
|
4039
|
+
deps.err(
|
|
4040
|
+
`\u2714 ${sanitizeLabel(name)} removed from Braid \u2014 its identity, credential, and channel memberships are deleted; messages it already posted stay in channel history`
|
|
4041
|
+
);
|
|
4042
|
+
return 0;
|
|
4043
|
+
}
|
|
4044
|
+
async function runAgentsList(instance2, args2, deps) {
|
|
4045
|
+
const creds = await ensureSignedIn(deps);
|
|
4046
|
+
const rest = oneShotRest(instance2, creds);
|
|
4047
|
+
let agents;
|
|
4048
|
+
try {
|
|
4049
|
+
agents = await rest.listMyAgents();
|
|
4050
|
+
} catch (err) {
|
|
4051
|
+
deps.err(
|
|
4052
|
+
`\u2717 Braid could not list your agents: ${sanitizeForTty(err instanceof Error ? err.message : String(err))}`
|
|
4053
|
+
);
|
|
4054
|
+
return 1;
|
|
4055
|
+
}
|
|
4056
|
+
if (args2.json) {
|
|
4057
|
+
deps.out(JSON.stringify(agents, null, 2));
|
|
4058
|
+
return 0;
|
|
4059
|
+
}
|
|
4060
|
+
if (agents.length === 0) {
|
|
4061
|
+
deps.out("(no agents yet \u2014 run `braid agent add <name>` to create one)");
|
|
4062
|
+
return 0;
|
|
4063
|
+
}
|
|
4064
|
+
deps.out(`${"NAME".padEnd(24)} ${"STATUS".padEnd(8)} CREDENTIAL`);
|
|
4065
|
+
for (const a of agents) {
|
|
4066
|
+
const status = a.status === "online" ? "online" : "offline";
|
|
4067
|
+
deps.out(
|
|
4068
|
+
`${sanitizeLabel(a.name ?? "", 24).padEnd(24)} ${status.padEnd(8)} ${sanitizeLabel(a.credentialState ?? "-", 20)}`
|
|
4069
|
+
);
|
|
4070
|
+
}
|
|
4071
|
+
return 0;
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
// src/commands/channel.ts
|
|
4075
|
+
var CHANNEL_USAGE = `usage: braid channels [--all] [--json]
|
|
4076
|
+
braid channel <create|archive|unarchive|members|member> \u2026
|
|
4077
|
+
|
|
4078
|
+
channels list your channels (--all includes archived)
|
|
4079
|
+
create <name> create a channel on Braid (you become its owner)
|
|
4080
|
+
archive <name-or-id> archive a channel \u2014 it leaves everyone's sidebar
|
|
4081
|
+
(reversible; y/N confirm, --yes skips)
|
|
4082
|
+
unarchive <name-or-id> restore an archived channel
|
|
4083
|
+
members <name-or-id> list a channel's members (humans and agents)
|
|
4084
|
+
member add <channel> <agent>
|
|
4085
|
+
add one of your agents to a channel
|
|
4086
|
+
member remove <channel> <agent>
|
|
4087
|
+
remove an agent from a channel (y/N confirm, --yes)
|
|
4088
|
+
|
|
4089
|
+
--all include archived channels when listing
|
|
4090
|
+
--yes skip the confirmation prompt (required with --json on
|
|
4091
|
+
archive/member remove)
|
|
4092
|
+
--json machine-readable result on stdout
|
|
4093
|
+
|
|
4094
|
+
channels are archive-only \u2014 there is no delete. archive to retire one.`;
|
|
4095
|
+
async function runChannelCli(instance2, args2, overrides = {}) {
|
|
4096
|
+
const deps = {
|
|
4097
|
+
resolveCreds: () => resolveCredentials(instance2, args2.flagToken),
|
|
4098
|
+
runLogin: async () => (await login(instance2)).credentials,
|
|
4099
|
+
out: (line) => console.log(line),
|
|
4100
|
+
err: (line) => console.error(line),
|
|
4101
|
+
prompt: promptOnTty,
|
|
4102
|
+
...overrides
|
|
4103
|
+
};
|
|
4104
|
+
try {
|
|
4105
|
+
if (args2.cmd === "channels") return await runChannelsList(instance2, args2, deps);
|
|
4106
|
+
switch (args2.sub) {
|
|
4107
|
+
case "create":
|
|
4108
|
+
return await runChannelCreate(instance2, args2, deps);
|
|
4109
|
+
case "archive":
|
|
4110
|
+
return await runChannelArchive(instance2, args2, deps);
|
|
4111
|
+
case "unarchive":
|
|
4112
|
+
return await runChannelUnarchive(instance2, args2, deps);
|
|
4113
|
+
case "members":
|
|
4114
|
+
return await runChannelMembers(instance2, args2, deps);
|
|
4115
|
+
case "member":
|
|
4116
|
+
return await runChannelMember(instance2, args2, deps);
|
|
4117
|
+
default:
|
|
4118
|
+
deps.err(CHANNEL_USAGE);
|
|
4119
|
+
if (args2.sub) deps.err(`
|
|
4120
|
+
unknown channel command "${sanitizeLabel(args2.sub)}".`);
|
|
4121
|
+
return 1;
|
|
4122
|
+
}
|
|
4123
|
+
} catch (err) {
|
|
4124
|
+
deps.err(sanitizeForTty(err instanceof Error ? err.message : String(err)));
|
|
4125
|
+
return 1;
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
async function fetchChannels(rest, scope) {
|
|
4129
|
+
const rows = [];
|
|
4130
|
+
if (scope !== "archived") {
|
|
4131
|
+
rows.push(...(await rest.listChannels()).map((c) => ({ ...c, archived: false })));
|
|
4132
|
+
}
|
|
4133
|
+
if (scope !== "active") {
|
|
4134
|
+
rows.push(...(await rest.listArchivedChannels()).map((c) => ({ ...c, archived: true })));
|
|
4135
|
+
}
|
|
4136
|
+
return rows;
|
|
4137
|
+
}
|
|
4138
|
+
async function resolveChannel(rest, ref, scope, args2, deps) {
|
|
4139
|
+
const rows = await fetchChannels(rest, scope);
|
|
4140
|
+
const byId = rows.find((c) => c.id === ref);
|
|
4141
|
+
if (byId) return byId;
|
|
4142
|
+
const byName = rows.filter((c) => sanitizeForTty(c.name ?? "") === ref);
|
|
4143
|
+
if (byName.length === 1) return byName[0];
|
|
4144
|
+
if (byName.length > 1) {
|
|
4145
|
+
const msg2 = `channel name "${sanitizeLabel(ref)}" is ambiguous \u2014 use the id`;
|
|
4146
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg2 }));
|
|
4147
|
+
deps.err(`\u2717 ${msg2}:`);
|
|
4148
|
+
for (const c of byName) {
|
|
4149
|
+
deps.err(
|
|
4150
|
+
` ${sanitizeLabel(c.name ?? "", 24).padEnd(24)} ${sanitizeLabel(c.id ?? "", 40)}${c.archived ? " (archived)" : ""}`
|
|
4151
|
+
);
|
|
4152
|
+
}
|
|
4153
|
+
return null;
|
|
4154
|
+
}
|
|
4155
|
+
const noun = scope === "archived" ? "archived channel" : "channel";
|
|
4156
|
+
const msg = `no ${noun} named "${sanitizeLabel(ref)}"`;
|
|
4157
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4158
|
+
const known = rows.map((c) => sanitizeLabel(c.name ?? "")).filter(Boolean);
|
|
4159
|
+
deps.err(
|
|
4160
|
+
`\u2717 ${msg}${known.length > 0 ? ` \u2014 your ${scope === "archived" ? "archived channels" : "channels"}: ${known.join(", ")}` : scope === "archived" ? " \u2014 you have no archived channels" : " \u2014 you have no channels yet (run `braid channel create <name>`)"}`
|
|
4161
|
+
);
|
|
4162
|
+
return null;
|
|
4163
|
+
}
|
|
4164
|
+
async function confirmSimple(args2, deps, actionLine) {
|
|
4165
|
+
if (args2.yes) return true;
|
|
4166
|
+
if (args2.json) {
|
|
4167
|
+
deps.out(JSON.stringify({ ok: false, error: "--json runs non-interactively \u2014 add --yes to confirm" }));
|
|
4168
|
+
deps.err("\u2717 refusing: --json runs non-interactively \u2014 add --yes to confirm");
|
|
4169
|
+
return false;
|
|
4170
|
+
}
|
|
4171
|
+
const typed = await deps.prompt(`${actionLine} [y/N] `);
|
|
4172
|
+
if (typed === null) {
|
|
4173
|
+
deps.err("\u2717 refusing: no terminal to confirm on \u2014 re-run with --yes");
|
|
4174
|
+
return false;
|
|
4175
|
+
}
|
|
4176
|
+
if (!/^y(es)?$/i.test(typed.trim())) {
|
|
4177
|
+
deps.err("\u2717 not confirmed \u2014 nothing was changed");
|
|
4178
|
+
return false;
|
|
4179
|
+
}
|
|
4180
|
+
return true;
|
|
4181
|
+
}
|
|
4182
|
+
function shortId(id) {
|
|
4183
|
+
return sanitizeLabel(id ?? "", 40).slice(0, 8);
|
|
4184
|
+
}
|
|
4185
|
+
async function runChannelsList(instance2, args2, deps) {
|
|
4186
|
+
const creds = await ensureSignedIn(deps);
|
|
4187
|
+
const rest = oneShotRest(instance2, creds);
|
|
4188
|
+
const rows = await fetchChannels(rest, args2.all ? "all" : "active");
|
|
4189
|
+
if (args2.json) {
|
|
4190
|
+
deps.out(JSON.stringify(rows, null, 2));
|
|
4191
|
+
return 0;
|
|
4192
|
+
}
|
|
4193
|
+
if (rows.length === 0) {
|
|
4194
|
+
deps.out("(no channels yet \u2014 run `braid channel create <name>` to make one)");
|
|
4195
|
+
return 0;
|
|
4196
|
+
}
|
|
4197
|
+
deps.out(`${"NAME".padEnd(24)} ${"ID".padEnd(10)}${args2.all ? " STATE" : ""}`);
|
|
4198
|
+
for (const c of rows) {
|
|
4199
|
+
deps.out(
|
|
4200
|
+
`${sanitizeLabel(c.name ?? "", 24).padEnd(24)} ${shortId(c.id).padEnd(10)}${args2.all ? ` ${c.archived ? "archived" : "active"}` : ""}`
|
|
4201
|
+
);
|
|
4202
|
+
}
|
|
4203
|
+
return 0;
|
|
4204
|
+
}
|
|
4205
|
+
async function runChannelCreate(instance2, args2, deps) {
|
|
4206
|
+
const name = args2.rest[0]?.trim();
|
|
4207
|
+
if (!name) {
|
|
4208
|
+
deps.err("channel create requires a name \u2014 usage: braid channel create <name>");
|
|
4209
|
+
return 1;
|
|
4210
|
+
}
|
|
4211
|
+
const creds = await ensureSignedIn(deps);
|
|
4212
|
+
const rest = oneShotRest(instance2, creds);
|
|
4213
|
+
let ch;
|
|
4214
|
+
try {
|
|
4215
|
+
ch = await rest.createChannel(name);
|
|
4216
|
+
} catch (err) {
|
|
4217
|
+
const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
4218
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4219
|
+
deps.err(`\u2717 Braid could not create channel "${sanitizeLabel(name)}": ${msg}`);
|
|
4220
|
+
return 1;
|
|
4221
|
+
}
|
|
4222
|
+
if (args2.json) {
|
|
4223
|
+
deps.out(JSON.stringify({ ok: true, id: sanitizeForTty(ch.id ?? ""), name: sanitizeForTty(ch.name ?? "") }));
|
|
4224
|
+
}
|
|
4225
|
+
deps.err(
|
|
4226
|
+
`\u2714 #${sanitizeLabel(ch.name ?? name)} created on Braid (id ${shortId(ch.id)}) \u2014 add an agent with \`braid channel member add ${sanitizeLabel(ch.name ?? name)} <agent>\``
|
|
4227
|
+
);
|
|
4228
|
+
return 0;
|
|
4229
|
+
}
|
|
4230
|
+
async function runChannelArchive(instance2, args2, deps) {
|
|
4231
|
+
const ref = args2.rest[0]?.trim();
|
|
4232
|
+
if (!ref) {
|
|
4233
|
+
deps.err("channel archive requires a name \u2014 usage: braid channel archive <name-or-id>");
|
|
4234
|
+
return 1;
|
|
4235
|
+
}
|
|
4236
|
+
const creds = await ensureSignedIn(deps);
|
|
4237
|
+
const rest = oneShotRest(instance2, creds);
|
|
4238
|
+
const ch = await resolveChannel(rest, ref, "active", args2, deps);
|
|
4239
|
+
if (!ch) return 1;
|
|
4240
|
+
const name = sanitizeLabel(ch.name ?? "");
|
|
4241
|
+
const confirmed = await confirmSimple(
|
|
4242
|
+
args2,
|
|
4243
|
+
deps,
|
|
4244
|
+
`archive #${name}? it leaves everyone's sidebar (\`braid channel unarchive ${name}\` restores it).`
|
|
4245
|
+
);
|
|
4246
|
+
if (!confirmed) return 1;
|
|
4247
|
+
try {
|
|
4248
|
+
await rest.archiveChannel(ch.id);
|
|
4249
|
+
} catch (err) {
|
|
4250
|
+
const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
4251
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4252
|
+
deps.err(`\u2717 Braid could not archive #${name}: ${msg}`);
|
|
4253
|
+
return 1;
|
|
4254
|
+
}
|
|
4255
|
+
if (args2.json) {
|
|
4256
|
+
deps.out(
|
|
4257
|
+
JSON.stringify({ ok: true, id: sanitizeForTty(ch.id), name: sanitizeForTty(ch.name ?? ""), archived: true })
|
|
4258
|
+
);
|
|
4259
|
+
}
|
|
4260
|
+
deps.err(`\u2714 #${name} archived \u2014 bring it back any time with \`braid channel unarchive ${name}\``);
|
|
4261
|
+
return 0;
|
|
4262
|
+
}
|
|
4263
|
+
async function runChannelUnarchive(instance2, args2, deps) {
|
|
4264
|
+
const ref = args2.rest[0]?.trim();
|
|
4265
|
+
if (!ref) {
|
|
4266
|
+
deps.err("channel unarchive requires a name \u2014 usage: braid channel unarchive <name-or-id>");
|
|
4267
|
+
return 1;
|
|
4268
|
+
}
|
|
4269
|
+
const creds = await ensureSignedIn(deps);
|
|
4270
|
+
const rest = oneShotRest(instance2, creds);
|
|
4271
|
+
const ch = await resolveChannel(rest, ref, "archived", args2, deps);
|
|
4272
|
+
if (!ch) return 1;
|
|
4273
|
+
const name = sanitizeLabel(ch.name ?? "");
|
|
4274
|
+
try {
|
|
4275
|
+
await rest.unarchiveChannel(ch.id);
|
|
4276
|
+
} catch (err) {
|
|
4277
|
+
const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
4278
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4279
|
+
deps.err(`\u2717 Braid could not unarchive #${name}: ${msg}`);
|
|
4280
|
+
return 1;
|
|
4281
|
+
}
|
|
4282
|
+
if (args2.json) {
|
|
4283
|
+
deps.out(
|
|
4284
|
+
JSON.stringify({ ok: true, id: sanitizeForTty(ch.id), name: sanitizeForTty(ch.name ?? ""), archived: false })
|
|
4285
|
+
);
|
|
4286
|
+
}
|
|
4287
|
+
deps.err(`\u2714 #${name} is back \u2014 restored from the archive`);
|
|
4288
|
+
return 0;
|
|
4289
|
+
}
|
|
4290
|
+
async function runChannelMembers(instance2, args2, deps) {
|
|
4291
|
+
const ref = args2.rest[0]?.trim();
|
|
4292
|
+
if (!ref) {
|
|
4293
|
+
deps.err("channel members requires a channel \u2014 usage: braid channel members <name-or-id>");
|
|
4294
|
+
return 1;
|
|
4295
|
+
}
|
|
4296
|
+
const creds = await ensureSignedIn(deps);
|
|
4297
|
+
const rest = oneShotRest(instance2, creds);
|
|
4298
|
+
const ch = await resolveChannel(rest, ref, "all", args2, deps);
|
|
4299
|
+
if (!ch) return 1;
|
|
4300
|
+
if (args2.json) {
|
|
4301
|
+
deps.out(JSON.stringify(ch.members, null, 2));
|
|
4302
|
+
return 0;
|
|
4303
|
+
}
|
|
4304
|
+
if (ch.members.length === 0) {
|
|
4305
|
+
deps.out(`(no members in #${sanitizeLabel(ch.name ?? "")})`);
|
|
4306
|
+
return 0;
|
|
4307
|
+
}
|
|
4308
|
+
deps.out(`${"NAME".padEnd(24)} ${"TYPE".padEnd(6)} ${"ROLE".padEnd(7)} STATUS`);
|
|
4309
|
+
for (const m of ch.members) {
|
|
4310
|
+
deps.out(
|
|
4311
|
+
`${sanitizeLabel(m.name, 24).padEnd(24)} ${m.type.padEnd(6)} ${sanitizeLabel(m.role, 7).padEnd(7)} ${m.status}`
|
|
4312
|
+
);
|
|
4313
|
+
}
|
|
4314
|
+
return 0;
|
|
4315
|
+
}
|
|
4316
|
+
async function runChannelMember(instance2, args2, deps) {
|
|
4317
|
+
const [action, channelRef, agentName] = args2.rest.map((s) => s?.trim());
|
|
4318
|
+
const usage = "usage: braid channel member <add|remove> <channel> <agent-name>";
|
|
4319
|
+
if (action !== "add" && action !== "remove" || !channelRef || !agentName) {
|
|
4320
|
+
deps.err(usage);
|
|
4321
|
+
if (action && action !== "add" && action !== "remove") {
|
|
4322
|
+
deps.err(`
|
|
4323
|
+
unknown member action "${sanitizeLabel(action)}".`);
|
|
4324
|
+
}
|
|
4325
|
+
return 1;
|
|
4326
|
+
}
|
|
4327
|
+
const creds = await ensureSignedIn(deps);
|
|
4328
|
+
const rest = oneShotRest(instance2, creds);
|
|
4329
|
+
const ch = await resolveChannel(rest, channelRef, "active", args2, deps);
|
|
4330
|
+
if (!ch) return 1;
|
|
4331
|
+
return action === "add" ? runMemberAdd(rest, ch, agentName, args2, deps) : runMemberRemove(rest, ch, agentName, args2, deps);
|
|
4332
|
+
}
|
|
4333
|
+
async function runMemberAdd(rest, ch, agentName, args2, deps) {
|
|
4334
|
+
const chName = sanitizeLabel(ch.name ?? "");
|
|
4335
|
+
const agent = await findMyAgent(rest, agentName, args2, deps);
|
|
4336
|
+
if (!agent) return 1;
|
|
4337
|
+
try {
|
|
4338
|
+
await rest.addAgentToChannel(ch.id, agent.id);
|
|
4339
|
+
} catch (err) {
|
|
4340
|
+
let msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
4341
|
+
if (msg.includes("guest_agents_disabled")) {
|
|
4342
|
+
msg = "this organization does not allow guest agents \u2014 enable it in org settings or ask an admin";
|
|
4343
|
+
}
|
|
4344
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4345
|
+
deps.err(`\u2717 Braid could not add "${sanitizeLabel(agentName)}" to #${chName}: ${msg}`);
|
|
4346
|
+
return 1;
|
|
4347
|
+
}
|
|
4348
|
+
if (args2.json) {
|
|
4349
|
+
deps.out(
|
|
4350
|
+
JSON.stringify({
|
|
4351
|
+
ok: true,
|
|
4352
|
+
channelId: sanitizeForTty(ch.id),
|
|
4353
|
+
channel: sanitizeForTty(ch.name ?? ""),
|
|
4354
|
+
agent: sanitizeForTty(agent.name),
|
|
4355
|
+
added: true
|
|
4356
|
+
})
|
|
4357
|
+
);
|
|
4358
|
+
}
|
|
4359
|
+
deps.err(`\u2714 ${sanitizeLabel(agent.name)} joined #${chName} \u2014 it now sees the channel and can post there`);
|
|
4360
|
+
return 0;
|
|
4361
|
+
}
|
|
4362
|
+
async function runMemberRemove(rest, ch, agentName, args2, deps) {
|
|
4363
|
+
const chName = sanitizeLabel(ch.name ?? "");
|
|
4364
|
+
const member = ch.members.find((m) => m.type === "bot" && m.id === agentName);
|
|
4365
|
+
if (!member) {
|
|
4366
|
+
const msg = `"${sanitizeLabel(agentName)}" is not a member of #${chName}`;
|
|
4367
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4368
|
+
const bots = ch.members.filter((m) => m.type === "bot").map((m) => sanitizeLabel(m.name));
|
|
4369
|
+
deps.err(`\u2717 ${msg}${bots.length > 0 ? ` \u2014 its agents: ${bots.join(", ")}` : " \u2014 it has no agents"}`);
|
|
4370
|
+
return 1;
|
|
4371
|
+
}
|
|
4372
|
+
const confirmed = await confirmSimple(
|
|
4373
|
+
args2,
|
|
4374
|
+
deps,
|
|
4375
|
+
`remove ${sanitizeLabel(agentName)} from #${chName}? it stops seeing the channel (member add restores it).`
|
|
4376
|
+
);
|
|
4377
|
+
if (!confirmed) return 1;
|
|
4378
|
+
try {
|
|
4379
|
+
await rest.removeChannelMember(ch.id, member.id);
|
|
4380
|
+
} catch (err) {
|
|
4381
|
+
const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
4382
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4383
|
+
deps.err(`\u2717 Braid could not remove "${sanitizeLabel(agentName)}" from #${chName}: ${msg}`);
|
|
4384
|
+
return 1;
|
|
4385
|
+
}
|
|
4386
|
+
if (args2.json) {
|
|
4387
|
+
deps.out(
|
|
4388
|
+
JSON.stringify({
|
|
4389
|
+
ok: true,
|
|
4390
|
+
channelId: sanitizeForTty(ch.id),
|
|
4391
|
+
channel: sanitizeForTty(ch.name ?? ""),
|
|
4392
|
+
agent: sanitizeForTty(agentName),
|
|
4393
|
+
removed: true
|
|
4394
|
+
})
|
|
4395
|
+
);
|
|
4396
|
+
}
|
|
4397
|
+
deps.err(
|
|
4398
|
+
`\u2714 ${sanitizeLabel(agentName)} removed from #${chName} \u2014 add it back any time with \`braid channel member add ${chName} ${sanitizeLabel(agentName)}\``
|
|
4399
|
+
);
|
|
4400
|
+
return 0;
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
// src/commands/invite.ts
|
|
4404
|
+
var INVITE_USAGE = `usage: braid invites [<channel>] [--all] [--json]
|
|
4405
|
+
braid invite <agent|human|revoke> \u2026
|
|
4406
|
+
|
|
4407
|
+
invites list your org's channel-less agent invites
|
|
4408
|
+
invites <channel> list a channel's agent invites
|
|
4409
|
+
(active only; --all includes revoked/expired/used)
|
|
4410
|
+
agent <channel> <name> mint an invite for a NEW agent named <name> on the
|
|
4411
|
+
channel \u2014 prints the join command ONCE (org admin)
|
|
4412
|
+
human <channel> <email> invite a human by email: they get a Braid email and
|
|
4413
|
+
land in the channel (org admin; a user from another
|
|
4414
|
+
org joins this channel only, as a guest)
|
|
4415
|
+
revoke <invite-id> revoke an org-wide invite so it cannot be redeemed
|
|
4416
|
+
(y/N confirm, --yes skips)
|
|
4417
|
+
--channel <channel> revoke a CHANNEL invite instead (the id alone
|
|
4418
|
+
targets the org-wide route)
|
|
4419
|
+
|
|
4420
|
+
--all include revoked/expired/exhausted invites when listing
|
|
4421
|
+
--yes skip the confirmation prompt (required with --json on revoke)
|
|
4422
|
+
--json machine-readable result on stdout
|
|
4423
|
+
|
|
4424
|
+
minting and managing invites needs org admin on Braid.`;
|
|
4425
|
+
async function runInviteCli(instance2, args2, overrides = {}) {
|
|
4426
|
+
const deps = {
|
|
4427
|
+
resolveCreds: () => resolveCredentials(instance2, args2.flagToken),
|
|
4428
|
+
runLogin: async () => (await login(instance2)).credentials,
|
|
4429
|
+
out: (line) => console.log(line),
|
|
4430
|
+
err: (line) => console.error(line),
|
|
4431
|
+
prompt: promptOnTty,
|
|
4432
|
+
...overrides
|
|
4433
|
+
};
|
|
4434
|
+
try {
|
|
4435
|
+
if (args2.cmd === "invites") return await runInvitesList(instance2, args2, deps);
|
|
4436
|
+
switch (args2.sub) {
|
|
4437
|
+
case "agent":
|
|
4438
|
+
return await runInviteAgent(instance2, args2, deps);
|
|
4439
|
+
case "human":
|
|
4440
|
+
return await runInviteHuman(instance2, args2, deps);
|
|
4441
|
+
case "revoke":
|
|
4442
|
+
return await runInviteRevoke(instance2, args2, deps);
|
|
4443
|
+
default:
|
|
4444
|
+
deps.err(INVITE_USAGE);
|
|
4445
|
+
if (args2.sub) deps.err(`
|
|
4446
|
+
unknown invite command "${sanitizeLabel(args2.sub)}".`);
|
|
4447
|
+
return 1;
|
|
4448
|
+
}
|
|
4449
|
+
} catch (err) {
|
|
4450
|
+
deps.err(sanitizeForTty(err instanceof Error ? err.message : String(err)));
|
|
4451
|
+
return 1;
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
function inviteErrorMessage(err) {
|
|
4455
|
+
if (err instanceof RelayError) {
|
|
4456
|
+
if (err.status === 403 && err.message === "admin_required") {
|
|
4457
|
+
return "you need to be an org admin on Braid to do this \u2014 ask an admin, or have them grant you the role";
|
|
4458
|
+
}
|
|
4459
|
+
if (err.status === 403 && err.message === "guest_agents_disabled") {
|
|
4460
|
+
return "this organization does not allow guest agents \u2014 enable it in org settings or ask an admin";
|
|
4461
|
+
}
|
|
4462
|
+
if (err.status === 402 && err.message === "seat_limit_reached") {
|
|
4463
|
+
return "the organization is out of member seats \u2014 upgrade the plan to invite more humans";
|
|
4464
|
+
}
|
|
4465
|
+
if (err.status === 402 && err.message === "guest_limit_reached") {
|
|
4466
|
+
return "the organization is at its guest limit \u2014 upgrade the plan to add more guests";
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
return sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
4470
|
+
}
|
|
4471
|
+
async function runInviteAgent(instance2, args2, deps) {
|
|
4472
|
+
const [channelRef, name] = args2.rest.map((s) => s?.trim());
|
|
4473
|
+
if (!channelRef || !name) {
|
|
4474
|
+
deps.err("usage: braid invite agent <channel> <agent-name>");
|
|
4475
|
+
return 1;
|
|
4476
|
+
}
|
|
4477
|
+
const creds = await ensureSignedIn(deps);
|
|
4478
|
+
const rest = oneShotRest(instance2, creds);
|
|
4479
|
+
const ch = await resolveChannel(rest, channelRef, "active", args2, deps);
|
|
4480
|
+
if (!ch) return 1;
|
|
4481
|
+
const chName = sanitizeLabel(ch.name ?? "");
|
|
4482
|
+
let mint;
|
|
4483
|
+
try {
|
|
4484
|
+
mint = await rest.createChannelAgentInvite(ch.id, name);
|
|
4485
|
+
} catch (err) {
|
|
4486
|
+
const msg = inviteErrorMessage(err);
|
|
4487
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4488
|
+
deps.err(`\u2717 Braid could not mint an invite for "${sanitizeLabel(name)}" on #${chName}: ${msg}`);
|
|
4489
|
+
return 1;
|
|
4490
|
+
}
|
|
4491
|
+
if (args2.json) {
|
|
4492
|
+
deps.out(JSON.stringify(mint));
|
|
4493
|
+
return 0;
|
|
4494
|
+
}
|
|
4495
|
+
deps.err(`\u2714 invite minted for agent "${sanitizeLabel(name)}" on #${chName} (invite ${shortId(mint.inviteId)})`);
|
|
4496
|
+
deps.err(" run this on the agent's machine to join:");
|
|
4497
|
+
deps.out(sanitizeForTty(mint.joinCommand));
|
|
4498
|
+
deps.err(
|
|
4499
|
+
`\u26A0 shown once \u2014 Braid keeps only a hash and this terminal will not show it again` + (mint.expiresAt ? ` (expires ${sanitizeLabel(mint.expiresAt)}` : ` (never expires`) + `${mint.oneTime ? ", single use)" : ")"}`
|
|
4500
|
+
);
|
|
4501
|
+
return 0;
|
|
4502
|
+
}
|
|
4503
|
+
function usesCell(inv) {
|
|
4504
|
+
return `${inv.uses}/${inv.maxUses === 0 ? "\u221E" : inv.maxUses}`;
|
|
4505
|
+
}
|
|
4506
|
+
async function runInvitesList(instance2, args2, deps) {
|
|
4507
|
+
const channelRef = args2.rest[0]?.trim();
|
|
4508
|
+
const creds = await ensureSignedIn(deps);
|
|
4509
|
+
const rest = oneShotRest(instance2, creds);
|
|
4510
|
+
let ch = null;
|
|
4511
|
+
let invites;
|
|
4512
|
+
try {
|
|
4513
|
+
if (channelRef) {
|
|
4514
|
+
ch = await resolveChannel(rest, channelRef, "all", args2, deps);
|
|
4515
|
+
if (!ch) return 1;
|
|
4516
|
+
invites = await rest.listChannelAgentInvites(ch.id);
|
|
4517
|
+
} else {
|
|
4518
|
+
invites = await rest.listOrgAgentInvites();
|
|
4519
|
+
}
|
|
4520
|
+
} catch (err) {
|
|
4521
|
+
const msg = inviteErrorMessage(err);
|
|
4522
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4523
|
+
deps.err(`\u2717 Braid could not list invites: ${msg}`);
|
|
4524
|
+
return 1;
|
|
4525
|
+
}
|
|
4526
|
+
if (!args2.all) invites = invites.filter((i) => i.status === "active");
|
|
4527
|
+
if (args2.json) {
|
|
4528
|
+
deps.out(JSON.stringify(invites, null, 2));
|
|
4529
|
+
return 0;
|
|
4530
|
+
}
|
|
4531
|
+
const where = ch ? `#${sanitizeLabel(ch.name ?? "")}` : "your organization (channel-less invites)";
|
|
4532
|
+
if (invites.length === 0) {
|
|
4533
|
+
deps.out(`(no ${args2.all ? "" : "active "}invites for ${where})`);
|
|
4534
|
+
return 0;
|
|
4535
|
+
}
|
|
4536
|
+
deps.err(`invites for ${where}:`);
|
|
4537
|
+
deps.out(
|
|
4538
|
+
`${"ID".padEnd(10)} ${"AGENT".padEnd(24)} ${"CHANNEL".padEnd(16)} ${"CREATED".padEnd(21)} ${"EXPIRES".padEnd(21)} ${"USES".padEnd(5)} STATUS`
|
|
4539
|
+
);
|
|
4540
|
+
for (const inv of invites) {
|
|
4541
|
+
const channelCell = ch ? sanitizeLabel(ch.name ?? "", 16) : "-";
|
|
4542
|
+
deps.out(
|
|
4543
|
+
`${shortId(inv.id).padEnd(10)} ${sanitizeLabel(inv.agentName ?? "", 24).padEnd(24)} ${channelCell.padEnd(16)} ${sanitizeLabel(inv.createdAt ?? "", 21).padEnd(21)} ${sanitizeLabel(inv.expiresAt ?? "never", 21).padEnd(21)} ${usesCell(inv).padEnd(5)} ` + sanitizeLabel(inv.status ?? "", 12)
|
|
4544
|
+
);
|
|
4545
|
+
}
|
|
4546
|
+
return 0;
|
|
4547
|
+
}
|
|
4548
|
+
async function runInviteRevoke(instance2, args2, deps) {
|
|
4549
|
+
const inviteId = args2.rest[0]?.trim();
|
|
4550
|
+
if (!inviteId) {
|
|
4551
|
+
deps.err("invite revoke requires an id \u2014 usage: braid invite revoke <invite-id> [--channel <channel>]");
|
|
4552
|
+
return 1;
|
|
4553
|
+
}
|
|
4554
|
+
const creds = await ensureSignedIn(deps);
|
|
4555
|
+
const rest = oneShotRest(instance2, creds);
|
|
4556
|
+
let ch = null;
|
|
4557
|
+
if (args2.channel) {
|
|
4558
|
+
ch = await resolveChannel(rest, args2.channel.trim(), "all", args2, deps);
|
|
4559
|
+
if (!ch) return 1;
|
|
4560
|
+
}
|
|
4561
|
+
const where = ch ? `#${sanitizeLabel(ch.name ?? "")}` : "your organization";
|
|
4562
|
+
const confirmed = await confirmSimple(
|
|
4563
|
+
args2,
|
|
4564
|
+
deps,
|
|
4565
|
+
`revoke invite ${sanitizeLabel(inviteId, 40)} on ${where}? it can no longer be redeemed (an agent that already joined stays connected).`
|
|
4566
|
+
);
|
|
4567
|
+
if (!confirmed) return 1;
|
|
4568
|
+
try {
|
|
4569
|
+
if (ch) await rest.revokeChannelAgentInvite(ch.id, inviteId);
|
|
4570
|
+
else await rest.revokeOrgAgentInvite(inviteId);
|
|
4571
|
+
} catch (err) {
|
|
4572
|
+
let msg = inviteErrorMessage(err);
|
|
4573
|
+
if (err instanceof RelayError && err.status === 404 && !ch) {
|
|
4574
|
+
msg = `invite not found org-wide \u2014 a channel invite needs --channel <channel>`;
|
|
4575
|
+
}
|
|
4576
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4577
|
+
deps.err(`\u2717 Braid could not revoke invite ${sanitizeLabel(inviteId, 40)}: ${msg}`);
|
|
4578
|
+
return 1;
|
|
4579
|
+
}
|
|
4580
|
+
if (args2.json) {
|
|
4581
|
+
deps.out(
|
|
4582
|
+
JSON.stringify({
|
|
4583
|
+
ok: true,
|
|
4584
|
+
inviteId: sanitizeForTty(inviteId),
|
|
4585
|
+
...ch ? { channelId: sanitizeForTty(ch.id), channel: sanitizeForTty(ch.name ?? "") } : {},
|
|
4586
|
+
revoked: true
|
|
4587
|
+
})
|
|
4588
|
+
);
|
|
4589
|
+
}
|
|
4590
|
+
deps.err(`\u2714 invite ${sanitizeLabel(inviteId, 40)} revoked \u2014 it can no longer be redeemed`);
|
|
4591
|
+
return 0;
|
|
4592
|
+
}
|
|
4593
|
+
function looksLikeEmail(email) {
|
|
4594
|
+
return /^[^\s@]+@[^\s@]+$/.test(email);
|
|
4595
|
+
}
|
|
4596
|
+
async function runInviteHuman(instance2, args2, deps) {
|
|
4597
|
+
const [channelRef, emailRaw] = args2.rest.map((s) => s?.trim());
|
|
4598
|
+
if (!channelRef || !emailRaw) {
|
|
4599
|
+
deps.err("usage: braid invite human <channel> <email>");
|
|
4600
|
+
return 1;
|
|
4601
|
+
}
|
|
4602
|
+
const email = emailRaw.toLowerCase();
|
|
4603
|
+
if (!looksLikeEmail(email)) {
|
|
4604
|
+
const msg = `"${sanitizeLabel(emailRaw)}" does not look like an email address`;
|
|
4605
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4606
|
+
deps.err(`\u2717 ${msg}`);
|
|
4607
|
+
return 1;
|
|
4608
|
+
}
|
|
4609
|
+
const creds = await ensureSignedIn(deps);
|
|
4610
|
+
const rest = oneShotRest(instance2, creds);
|
|
4611
|
+
const ch = await resolveChannel(rest, channelRef, "active", args2, deps);
|
|
4612
|
+
if (!ch) return 1;
|
|
4613
|
+
const chName = sanitizeLabel(ch.name ?? "");
|
|
4614
|
+
let result;
|
|
4615
|
+
try {
|
|
4616
|
+
result = await rest.inviteHumanToChannel(ch.id, email);
|
|
4617
|
+
} catch (err) {
|
|
4618
|
+
const msg = inviteErrorMessage(err);
|
|
4619
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4620
|
+
deps.err(`\u2717 Braid could not invite ${sanitizeLabel(email)} to #${chName}: ${msg}`);
|
|
4621
|
+
return 1;
|
|
4622
|
+
}
|
|
4623
|
+
if (args2.json) {
|
|
4624
|
+
deps.out(JSON.stringify(result));
|
|
4625
|
+
return 0;
|
|
4626
|
+
}
|
|
4627
|
+
if (result.note) {
|
|
4628
|
+
deps.err(`\u2714 ${sanitizeLabel(email)} \u2014 ${sanitizeLabel(result.note, 80)}`);
|
|
4629
|
+
} else if (result.guest) {
|
|
4630
|
+
deps.err(
|
|
4631
|
+
`\u2714 ${sanitizeLabel(email)} added to #${chName} as a cross-org guest \u2014 they see this channel only, not your org directory`
|
|
4632
|
+
);
|
|
4633
|
+
} else {
|
|
4634
|
+
deps.err(
|
|
4635
|
+
`\u2714 ${sanitizeLabel(email)} invited \u2014 Braid emailed them; they'll land in #${chName} when they accept`
|
|
4636
|
+
);
|
|
4637
|
+
}
|
|
4638
|
+
return 0;
|
|
4639
|
+
}
|
|
4640
|
+
|
|
4641
|
+
// src/commands/org.ts
|
|
4642
|
+
var ORG_USAGE = `usage: braid org [--json]
|
|
4643
|
+
braid org guest-agents <on|off> [--yes] [--json]
|
|
4644
|
+
|
|
4645
|
+
org show your organization: id and whether guest agents
|
|
4646
|
+
(agents owned outside the org) are allowed in
|
|
4647
|
+
guest-agents on|off allow or block guest agents org-wide \u2014 this changes
|
|
4648
|
+
live cross-org access, so it asks y/N (--yes skips;
|
|
4649
|
+
org admin only)
|
|
4650
|
+
|
|
4651
|
+
--yes skip the confirmation prompt (required with --json on guest-agents)
|
|
4652
|
+
--json machine-readable result on stdout`;
|
|
4653
|
+
async function runOrgCli(instance2, args2, overrides = {}) {
|
|
4654
|
+
const deps = {
|
|
4655
|
+
resolveCreds: () => resolveCredentials(instance2, args2.flagToken),
|
|
4656
|
+
runLogin: async () => (await login(instance2)).credentials,
|
|
4657
|
+
out: (line) => console.log(line),
|
|
4658
|
+
err: (line) => console.error(line),
|
|
4659
|
+
prompt: promptOnTty,
|
|
4660
|
+
...overrides
|
|
4661
|
+
};
|
|
4662
|
+
try {
|
|
4663
|
+
if (!args2.sub) return await runOrgShow(instance2, args2, deps);
|
|
4664
|
+
if (args2.sub === "guest-agents") return await runOrgGuestAgents(instance2, args2, deps);
|
|
4665
|
+
deps.err(ORG_USAGE);
|
|
4666
|
+
deps.err(`
|
|
4667
|
+
unknown org command "${sanitizeLabel(args2.sub)}".`);
|
|
4668
|
+
return 1;
|
|
4669
|
+
} catch (err) {
|
|
4670
|
+
deps.err(sanitizeForTty(err instanceof Error ? err.message : String(err)));
|
|
4671
|
+
return 1;
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
function orgErrorMessage(err) {
|
|
4675
|
+
if (err instanceof RelayError) {
|
|
4676
|
+
if (err.status === 403 && err.message === "admin_required") {
|
|
4677
|
+
return "you need to be an org admin on Braid to change org settings";
|
|
4678
|
+
}
|
|
4679
|
+
if (err.status === 404) {
|
|
4680
|
+
return "no active organization \u2014 sign in on Braid web and pick an organization first";
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
return sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
4684
|
+
}
|
|
4685
|
+
async function fetchMyOrg(rest, args2, deps) {
|
|
4686
|
+
try {
|
|
4687
|
+
return await rest.getMyOrganization();
|
|
4688
|
+
} catch (err) {
|
|
4689
|
+
const msg = orgErrorMessage(err);
|
|
4690
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4691
|
+
deps.err(`\u2717 Braid could not read your organization: ${msg}`);
|
|
4692
|
+
return null;
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
async function runOrgShow(instance2, args2, deps) {
|
|
4696
|
+
const creds = await ensureSignedIn(deps);
|
|
4697
|
+
const org = await fetchMyOrg(oneShotRest(instance2, creds), args2, deps);
|
|
4698
|
+
if (!org) return 1;
|
|
4699
|
+
if (args2.json) {
|
|
4700
|
+
deps.out(JSON.stringify(org, null, 2));
|
|
4701
|
+
return 0;
|
|
4702
|
+
}
|
|
4703
|
+
const row = (label, value) => deps.out(`${(label + ":").padEnd(14)}${value}`);
|
|
4704
|
+
row("org id", sanitizeLabel(org.id ?? "", 40));
|
|
4705
|
+
row("guest agents", org.allowGuestAgents ? "allowed \u2014 agents owned outside the org may join channels" : "blocked");
|
|
4706
|
+
return 0;
|
|
4707
|
+
}
|
|
4708
|
+
async function runOrgGuestAgents(instance2, args2, deps) {
|
|
4709
|
+
const value = args2.rest[0]?.trim();
|
|
4710
|
+
if (value !== "on" && value !== "off") {
|
|
4711
|
+
deps.err("usage: braid org guest-agents <on|off>");
|
|
4712
|
+
if (value) deps.err(`
|
|
4713
|
+
unknown value "${sanitizeLabel(value)}" \u2014 say on or off.`);
|
|
4714
|
+
return 1;
|
|
4715
|
+
}
|
|
4716
|
+
const allow = value === "on";
|
|
4717
|
+
const creds = await ensureSignedIn(deps);
|
|
4718
|
+
const rest = oneShotRest(instance2, creds);
|
|
4719
|
+
const org = await fetchMyOrg(rest, args2, deps);
|
|
4720
|
+
if (!org) return 1;
|
|
4721
|
+
if (org.allowGuestAgents === allow) {
|
|
4722
|
+
if (args2.json) deps.out(JSON.stringify({ ok: true, allowGuestAgents: allow, changed: false }));
|
|
4723
|
+
deps.err(`\u2714 guest agents are already ${allow ? "allowed" : "blocked"} \u2014 nothing to change`);
|
|
4724
|
+
return 0;
|
|
4725
|
+
}
|
|
4726
|
+
const confirmed = await confirmSimple(
|
|
4727
|
+
args2,
|
|
4728
|
+
deps,
|
|
4729
|
+
allow ? "allow guest agents? agents owned OUTSIDE your org can then be invited into your channels." : "block guest agents? no NEW cross-org agents or human guests can join your channels (current members are not removed)."
|
|
4730
|
+
);
|
|
4731
|
+
if (!confirmed) return 1;
|
|
4732
|
+
try {
|
|
4733
|
+
await rest.patchOrganization(org.id, { allowGuestAgents: allow });
|
|
4734
|
+
} catch (err) {
|
|
4735
|
+
const msg = orgErrorMessage(err);
|
|
4736
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4737
|
+
deps.err(`\u2717 Braid could not update the guest-agent policy: ${msg}`);
|
|
4738
|
+
return 1;
|
|
4739
|
+
}
|
|
4740
|
+
if (args2.json) deps.out(JSON.stringify({ ok: true, allowGuestAgents: allow, changed: true }));
|
|
4741
|
+
deps.err(
|
|
4742
|
+
allow ? "\u2714 guest agents allowed \u2014 cross-org agents can now be invited into your channels" : "\u2714 guest agents blocked \u2014 new cross-org agents and guests can no longer join your channels"
|
|
4743
|
+
);
|
|
4744
|
+
return 0;
|
|
4745
|
+
}
|
|
4746
|
+
|
|
2845
4747
|
// src/index.tsx
|
|
2846
|
-
import { jsx as
|
|
4748
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
2847
4749
|
function helpText(profileName2, base2, effective) {
|
|
2848
4750
|
const relayOverridden = effective.relayUrl !== base2.relayUrl;
|
|
2849
4751
|
const overrideNote = relayOverridden ? `
|
|
@@ -2852,7 +4754,7 @@ function helpText(profileName2, base2, effective) {
|
|
|
2852
4754
|
instance \u2014 a JWT minted there will NOT work against the override)` : "";
|
|
2853
4755
|
return `arp-tui \u2014 tail + post for Agent Relay Protocol channels (as the HUMAN)
|
|
2854
4756
|
|
|
2855
|
-
usage: arp-tui [tail|login|logout|status|profile] [--profile <name>] [--relay <url>] [--token <jwt>]
|
|
4757
|
+
usage: arp-tui [tail|login|logout|status|profile|agent <verb>|agents|channel <verb>|channels|invite <verb>|invites|org] [--profile <name>] [--relay <url>] [--token <jwt>]
|
|
2856
4758
|
|
|
2857
4759
|
commands:
|
|
2858
4760
|
tail live channel tail + post (default)
|
|
@@ -2861,6 +4763,34 @@ commands:
|
|
|
2861
4763
|
logout revoke the OAuth grant and delete this instance's stored tokens
|
|
2862
4764
|
status show the stored credential state for the active instance
|
|
2863
4765
|
profile manage saved instances (list|add|switch|remove; run \`arp-tui profile\`)
|
|
4766
|
+
agent add <name> create a personal agent on Braid, enroll it on this machine
|
|
4767
|
+
via the local bridge, and start it (--no-start to skip;
|
|
4768
|
+
--bridge-cmd/--provider/--json \u2014 run \`arp-tui agent\` for details)
|
|
4769
|
+
agent status|reissue|revoke|remove <name>
|
|
4770
|
+
inspect or recover an agent: detail card, fresh credential +
|
|
4771
|
+
re-enroll, credential revoke, permanent delete (revoke/remove
|
|
4772
|
+
confirm on the TTY; --yes with --json)
|
|
4773
|
+
agents list your agents: name, online/offline, credential state (--json)
|
|
4774
|
+
channels list your channels (--all includes archived; --json)
|
|
4775
|
+
channel create|archive|unarchive|members <name-or-id>
|
|
4776
|
+
channel lifecycle: create, archive (reversible; y/N confirm,
|
|
4777
|
+
--yes skips), restore, roster (run \`arp-tui channel\` for details)
|
|
4778
|
+
channel member add|remove <channel> <agent>
|
|
4779
|
+
add or remove one of your agents on a channel
|
|
4780
|
+
invites [<channel>]
|
|
4781
|
+
list agent invites: the org's channel-less invites, or one
|
|
4782
|
+
channel's (--all includes revoked/expired; --json)
|
|
4783
|
+
invite agent <channel> <name>
|
|
4784
|
+
mint a channel invite for a NEW agent \u2014 prints the join
|
|
4785
|
+
command ONCE (org admin; run \`arp-tui invite\` for details)
|
|
4786
|
+
invite human <channel> <email>
|
|
4787
|
+
invite a human by email into the channel (org admin)
|
|
4788
|
+
invite revoke <invite-id> [--channel <ch>]
|
|
4789
|
+
revoke an invite (y/N confirm, --yes skips)
|
|
4790
|
+
org show your organization: id + guest-agent policy (--json)
|
|
4791
|
+
org guest-agents on|off
|
|
4792
|
+
allow or block cross-org guest agents org-wide (org admin;
|
|
4793
|
+
y/N confirm, --yes skips)
|
|
2864
4794
|
|
|
2865
4795
|
options:
|
|
2866
4796
|
--profile <name> instance profile from ~/.arp-tui/config.json
|
|
@@ -2894,6 +4824,12 @@ function parseCliArgs(argv) {
|
|
|
2894
4824
|
"clerk-fapi": { type: "string" },
|
|
2895
4825
|
"clerk-template": { type: "string" },
|
|
2896
4826
|
"clerk-oauth-client-id": { type: "string" },
|
|
4827
|
+
provider: { type: "string" },
|
|
4828
|
+
"no-start": { type: "boolean" },
|
|
4829
|
+
"bridge-cmd": { type: "string" },
|
|
4830
|
+
channel: { type: "string" },
|
|
4831
|
+
all: { type: "boolean" },
|
|
4832
|
+
yes: { type: "boolean" },
|
|
2897
4833
|
json: { type: "boolean" },
|
|
2898
4834
|
help: { type: "boolean", short: "h" }
|
|
2899
4835
|
},
|
|
@@ -2907,6 +4843,7 @@ function parseCliArgs(argv) {
|
|
|
2907
4843
|
cmd: parsed.positionals[0] ?? "tail",
|
|
2908
4844
|
sub: parsed.positionals[1],
|
|
2909
4845
|
name: parsed.positionals[2],
|
|
4846
|
+
positionals: parsed.positionals,
|
|
2910
4847
|
profile: parsed.values.profile,
|
|
2911
4848
|
relay: parsed.values.relay,
|
|
2912
4849
|
token: parsed.values.token,
|
|
@@ -2914,6 +4851,12 @@ function parseCliArgs(argv) {
|
|
|
2914
4851
|
clerkFapi: parsed.values["clerk-fapi"],
|
|
2915
4852
|
clerkTemplate: parsed.values["clerk-template"],
|
|
2916
4853
|
clerkOAuthClientId: parsed.values["clerk-oauth-client-id"],
|
|
4854
|
+
provider: parsed.values.provider,
|
|
4855
|
+
noStart: parsed.values["no-start"] === true,
|
|
4856
|
+
bridgeCmd: parsed.values["bridge-cmd"],
|
|
4857
|
+
channel: parsed.values.channel,
|
|
4858
|
+
all: parsed.values.all === true,
|
|
4859
|
+
yes: parsed.values.yes === true,
|
|
2917
4860
|
json: parsed.values.json === true,
|
|
2918
4861
|
help: parsed.values.help === true
|
|
2919
4862
|
};
|
|
@@ -3021,6 +4964,53 @@ if (args.cmd === "login") {
|
|
|
3021
4964
|
}
|
|
3022
4965
|
}
|
|
3023
4966
|
process.exit(0);
|
|
4967
|
+
} else if (args.cmd === "agent" || args.cmd === "agents") {
|
|
4968
|
+
const code = await runAgentCli(instance, {
|
|
4969
|
+
cmd: args.cmd,
|
|
4970
|
+
sub: args.sub,
|
|
4971
|
+
name: args.name,
|
|
4972
|
+
provider: args.provider,
|
|
4973
|
+
noStart: args.noStart,
|
|
4974
|
+
bridgeCmd: args.bridgeCmd,
|
|
4975
|
+
yes: args.yes,
|
|
4976
|
+
json: args.json,
|
|
4977
|
+
flagToken: args.token
|
|
4978
|
+
});
|
|
4979
|
+
process.exit(code);
|
|
4980
|
+
} else if (args.cmd === "channel" || args.cmd === "channels") {
|
|
4981
|
+
const code = await runChannelCli(instance, {
|
|
4982
|
+
cmd: args.cmd,
|
|
4983
|
+
sub: args.sub,
|
|
4984
|
+
rest: args.positionals.slice(2),
|
|
4985
|
+
all: args.all,
|
|
4986
|
+
yes: args.yes,
|
|
4987
|
+
json: args.json,
|
|
4988
|
+
flagToken: args.token
|
|
4989
|
+
});
|
|
4990
|
+
process.exit(code);
|
|
4991
|
+
} else if (args.cmd === "invite" || args.cmd === "invites") {
|
|
4992
|
+
const code = await runInviteCli(instance, {
|
|
4993
|
+
cmd: args.cmd,
|
|
4994
|
+
sub: args.sub,
|
|
4995
|
+
// `invites [<channel>]` has no subcommand — its positionals start right after the verb
|
|
4996
|
+
rest: args.cmd === "invites" ? args.positionals.slice(1) : args.positionals.slice(2),
|
|
4997
|
+
channel: args.channel,
|
|
4998
|
+
all: args.all,
|
|
4999
|
+
yes: args.yes,
|
|
5000
|
+
json: args.json,
|
|
5001
|
+
flagToken: args.token
|
|
5002
|
+
});
|
|
5003
|
+
process.exit(code);
|
|
5004
|
+
} else if (args.cmd === "org") {
|
|
5005
|
+
const code = await runOrgCli(instance, {
|
|
5006
|
+
cmd: args.cmd,
|
|
5007
|
+
sub: args.sub,
|
|
5008
|
+
rest: args.positionals.slice(2),
|
|
5009
|
+
yes: args.yes,
|
|
5010
|
+
json: args.json,
|
|
5011
|
+
flagToken: args.token
|
|
5012
|
+
});
|
|
5013
|
+
process.exit(code);
|
|
3024
5014
|
} else if (args.cmd !== "tail") {
|
|
3025
5015
|
console.error(`unknown command "${args.cmd}" \u2014 try --help.`);
|
|
3026
5016
|
process.exit(1);
|
|
@@ -3038,4 +5028,4 @@ if (credentials?.tokenType === "dev-jwt" && credentials.token && !credentials.cl
|
|
|
3038
5028
|
);
|
|
3039
5029
|
}
|
|
3040
5030
|
var store = createStore({ instance, credentials });
|
|
3041
|
-
render(/* @__PURE__ */
|
|
5031
|
+
render(/* @__PURE__ */ jsx13(App, { store }));
|